Wrap admin checklist checks in suspense
This commit is contained in:
parent
95746b750d
commit
f8e13d7212
@ -1,6 +1,7 @@
|
||||
'use server';
|
||||
|
||||
import { runAuthenticatedAdminServerAction } from '@/auth';
|
||||
import { testKvConnection } from '@/services/kv';
|
||||
import { testOpenAiConnection } from '@/services/openai';
|
||||
import { testDatabaseConnection } from '@/services/postgres';
|
||||
import { testStorageConnection } from '@/services/storage';
|
||||
@ -11,7 +12,9 @@ const scanForError = (
|
||||
promise: () => Promise<any>
|
||||
): Promise<string> =>
|
||||
shouldCheck
|
||||
? promise().then(() => '').catch(error => error.message)
|
||||
? promise()
|
||||
.then(() => '')
|
||||
.catch(error => error.message)
|
||||
: Promise.resolve('');
|
||||
|
||||
export const testConnectionsAction = async () =>
|
||||
@ -19,22 +22,26 @@ export const testConnectionsAction = async () =>
|
||||
const {
|
||||
hasDatabase,
|
||||
hasStorageProvider,
|
||||
hasVercelKv,
|
||||
isAiTextGenerationEnabled,
|
||||
} = CONFIG_CHECKLIST_STATUS;
|
||||
|
||||
const [
|
||||
databaseError,
|
||||
storageError,
|
||||
kvError,
|
||||
aiError,
|
||||
] = await Promise.all([
|
||||
scanForError(hasDatabase, testDatabaseConnection),
|
||||
scanForError(hasStorageProvider, testStorageConnection),
|
||||
scanForError(hasVercelKv, testKvConnection),
|
||||
scanForError(isAiTextGenerationEnabled, testOpenAiConnection),
|
||||
]);
|
||||
|
||||
return {
|
||||
databaseError,
|
||||
storageError,
|
||||
kvError,
|
||||
aiError,
|
||||
};
|
||||
});
|
||||
|
||||
@ -13,7 +13,7 @@ export default function ChecklistRow({
|
||||
}: {
|
||||
title: string
|
||||
status: boolean
|
||||
isPending: boolean
|
||||
isPending?: boolean
|
||||
optional?: boolean
|
||||
experimental?: boolean
|
||||
children: ReactNode
|
||||
|
||||
3
src/services/kv.ts
Normal file
3
src/services/kv.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { kv } from '@vercel/kv';
|
||||
|
||||
export const testKvConnection = () => kv.get('test');
|
||||
@ -1,21 +1,20 @@
|
||||
import { generateAuthSecret } from '@/auth';
|
||||
import SiteChecklistClient from './SiteChecklistClient';
|
||||
import { Suspense } from 'react';
|
||||
import { CONFIG_CHECKLIST_STATUS } from '@/site/config';
|
||||
import { testConnectionsAction } from '@/admin/actions';
|
||||
import SiteChecklistServer from './SiteChecklistServer';
|
||||
import SiteChecklistClient from './SiteChecklistClient';
|
||||
|
||||
export default async function SiteChecklist({
|
||||
export default function SiteChecklist({
|
||||
simplifiedView,
|
||||
}: {
|
||||
simplifiedView?: boolean
|
||||
}) {
|
||||
const secret = await generateAuthSecret();
|
||||
const connectionErrors = await testConnectionsAction().catch(() => ({}));
|
||||
return (
|
||||
<SiteChecklistClient {...{
|
||||
<Suspense fallback={<SiteChecklistClient {...{
|
||||
...CONFIG_CHECKLIST_STATUS,
|
||||
...connectionErrors,
|
||||
isTestingConnections: true,
|
||||
simplifiedView,
|
||||
secret,
|
||||
}} />
|
||||
}} /> }>
|
||||
<SiteChecklistServer />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
import {
|
||||
ComponentProps,
|
||||
ReactNode,
|
||||
useTransition,
|
||||
} from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import ChecklistRow from '../components/ChecklistRow';
|
||||
import { FiExternalLink } from 'react-icons/fi';
|
||||
@ -15,7 +13,6 @@ import {
|
||||
BiData,
|
||||
BiLockAlt,
|
||||
BiPencil,
|
||||
BiRefresh,
|
||||
} from 'react-icons/bi';
|
||||
import InfoBlock from '@/components/InfoBlock';
|
||||
import Checklist from '@/components/Checklist';
|
||||
@ -27,13 +24,14 @@ import { HiSparkles } from 'react-icons/hi';
|
||||
import LoaderButton from '@/components/primitives/LoaderButton';
|
||||
import { testConnectionsAction } from '@/admin/actions';
|
||||
import ErrorNote from '@/components/ErrorNote';
|
||||
import Spinner from '@/components/Spinner';
|
||||
|
||||
export default function SiteChecklistClient({
|
||||
// Config checklist
|
||||
hasDatabase,
|
||||
isPostgresSSLEnabled,
|
||||
isPostgresSslEnabled,
|
||||
hasVercelPostgres,
|
||||
hasVercelKV,
|
||||
hasVercelKv,
|
||||
hasStorageProvider,
|
||||
hasVercelBlobStorage,
|
||||
hasCloudflareR2Storage,
|
||||
@ -66,29 +64,18 @@ export default function SiteChecklistClient({
|
||||
// Connection status
|
||||
databaseError,
|
||||
storageError,
|
||||
kvError,
|
||||
aiError,
|
||||
// Component props
|
||||
simplifiedView,
|
||||
showRefreshButton,
|
||||
isTestingConnections,
|
||||
secret,
|
||||
}: ConfigChecklistStatus &
|
||||
Partial<Awaited<ReturnType<typeof testConnectionsAction>>> & {
|
||||
simplifiedView?: boolean
|
||||
showRefreshButton?: boolean
|
||||
secret: string
|
||||
isTestingConnections?: boolean
|
||||
secret?: string
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
const [isPendingPage, startTransitionPage] = useTransition();
|
||||
const [isPendingSecret, startTransitionSecret] = useTransition();
|
||||
|
||||
const refreshPage = () => {
|
||||
startTransitionPage(router.refresh);
|
||||
};
|
||||
const refreshSecret = () => {
|
||||
startTransitionSecret(router.refresh);
|
||||
};
|
||||
|
||||
const renderLink = (href: string, text: string, external = true) =>
|
||||
<>
|
||||
<a {...{
|
||||
@ -110,18 +97,21 @@ export default function SiteChecklistClient({
|
||||
</>}
|
||||
</>;
|
||||
|
||||
const renderCopyButton = (label: string, text: string, subtle?: boolean) =>
|
||||
const renderCopyButton = (label: string, text?: string, subtle?: boolean) =>
|
||||
<LoaderButton
|
||||
icon={<BiCopy size={15} />}
|
||||
className={clsx(
|
||||
'translate-y-[2px]',
|
||||
subtle && 'text-gray-300 dark:text-gray-700',
|
||||
)}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(text);
|
||||
toastSuccess(`${label} copied to clipboard`);
|
||||
}}
|
||||
onClick={text
|
||||
? () => {
|
||||
navigator.clipboard.writeText(text);
|
||||
toastSuccess(`${label} copied to clipboard`);
|
||||
}
|
||||
: undefined}
|
||||
styleAs="link"
|
||||
disabled={!text}
|
||||
/>;
|
||||
|
||||
const renderEnvVar = (
|
||||
@ -179,9 +169,11 @@ export default function SiteChecklistClient({
|
||||
icon={<BiData size={16} />}
|
||||
>
|
||||
<ChecklistRow
|
||||
title="Setup database"
|
||||
title={hasDatabase && isTestingConnections
|
||||
? 'Testing database connection'
|
||||
: 'Setup database'}
|
||||
status={hasDatabase}
|
||||
isPending={isPendingPage}
|
||||
isPending={hasDatabase && isTestingConnections}
|
||||
>
|
||||
{databaseError &&
|
||||
renderConnectionError('Database', databaseError)}
|
||||
@ -202,18 +194,21 @@ export default function SiteChecklistClient({
|
||||
renderSubStatus('checked', <>
|
||||
Postgres-compatible: connected
|
||||
{' '}
|
||||
(SSL {isPostgresSSLEnabled ? 'enabled' : 'disabled'})
|
||||
(SSL {isPostgresSslEnabled ? 'enabled' : 'disabled'})
|
||||
</>)}
|
||||
</ChecklistRow>
|
||||
<ChecklistRow
|
||||
title={!hasStorageProvider
|
||||
? 'Setup storage (one of the following)'
|
||||
: hasMultipleStorageProviders
|
||||
// eslint-disable-next-line max-len
|
||||
? `Setup storage (new uploads go to: ${labelForStorage(currentStorage)})`
|
||||
: 'Setup storage'}
|
||||
title={
|
||||
hasStorageProvider && isTestingConnections
|
||||
? 'Testing storage connection'
|
||||
: !hasStorageProvider
|
||||
? 'Setup storage (one of the following)'
|
||||
: hasMultipleStorageProviders
|
||||
// eslint-disable-next-line max-len
|
||||
? `Setup storage (new uploads go to: ${labelForStorage(currentStorage)})`
|
||||
: 'Setup storage'}
|
||||
status={hasStorageProvider}
|
||||
isPending={isPendingPage}
|
||||
isPending={hasStorageProvider && isTestingConnections}
|
||||
>
|
||||
{storageError &&
|
||||
renderConnectionError('Storage', storageError)}
|
||||
@ -258,9 +253,11 @@ export default function SiteChecklistClient({
|
||||
icon={<BiLockAlt size={16} />}
|
||||
>
|
||||
<ChecklistRow
|
||||
title="Setup auth"
|
||||
title={!hasAuthSecret && isTestingConnections
|
||||
? 'Generating secret'
|
||||
: 'Setup auth'}
|
||||
status={hasAuthSecret}
|
||||
isPending={isPendingPage}
|
||||
isPending={!hasAuthSecret && isTestingConnections}
|
||||
>
|
||||
Store auth secret in environment variable:
|
||||
{!hasAuthSecret &&
|
||||
@ -269,16 +266,9 @@ export default function SiteChecklistClient({
|
||||
<div className={clsx(
|
||||
'flex flex-nowrap items-center gap-2 leading-none -mx-1',
|
||||
)}>
|
||||
<span>{secret}</span>
|
||||
{secret ? <span>{secret}</span> : <Spinner />}
|
||||
<div className="flex items-center gap-0.5 translate-y-[-2px]">
|
||||
{renderCopyButton('Secret', secret)}
|
||||
<LoaderButton
|
||||
icon={<BiRefresh size={18} />}
|
||||
onClick={refreshSecret}
|
||||
isLoading={isPendingSecret}
|
||||
spinnerColor="text"
|
||||
styleAs="link"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</InfoBlock>
|
||||
@ -288,7 +278,6 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title="Setup admin user"
|
||||
status={hasAdminUser}
|
||||
isPending={isPendingPage}
|
||||
>
|
||||
Store admin email/password
|
||||
{' '}
|
||||
@ -307,7 +296,6 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title="Add title"
|
||||
status={hasTitle}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
>
|
||||
Store in environment variable (used in page titles):
|
||||
@ -316,7 +304,6 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title="Add custom domain"
|
||||
status={hasDomain}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
>
|
||||
Store in environment variable (displayed in top-right nav):
|
||||
@ -331,9 +318,11 @@ export default function SiteChecklistClient({
|
||||
optional
|
||||
>
|
||||
<ChecklistRow
|
||||
title="Add OpenAI Secret Key"
|
||||
title={isAiTextGenerationEnabled && isTestingConnections
|
||||
? 'Testing OpenAI connection'
|
||||
: 'Add OpenAI Secret Key'}
|
||||
status={isAiTextGenerationEnabled}
|
||||
isPending={isPendingPage}
|
||||
isPending={isAiTextGenerationEnabled && isTestingConnections}
|
||||
optional
|
||||
>
|
||||
{aiError &&
|
||||
@ -344,11 +333,15 @@ export default function SiteChecklistClient({
|
||||
{renderEnvVars(['OPENAI_SECRET_KEY'])}
|
||||
</ChecklistRow>
|
||||
<ChecklistRow
|
||||
title="Enable Rate Limiting"
|
||||
status={hasVercelKV}
|
||||
isPending={isPendingPage}
|
||||
title={hasVercelKv && isTestingConnections
|
||||
? 'Testing KV connection'
|
||||
: 'Enable Rate Limiting'}
|
||||
status={hasVercelKv}
|
||||
isPending={hasVercelKv && isTestingConnections}
|
||||
optional
|
||||
>
|
||||
{kvError &&
|
||||
renderConnectionError('Vercel KV', kvError)}
|
||||
{renderLink(
|
||||
// eslint-disable-next-line max-len
|
||||
'https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database',
|
||||
@ -361,7 +354,6 @@ export default function SiteChecklistClient({
|
||||
// eslint-disable-next-line max-len
|
||||
title={`Auto-generated fields: ${aiTextAutoGeneratedFields.join(', ')}`}
|
||||
status={hasAiTextAutoGeneratedFields}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
>
|
||||
Comma-separated fields to auto-generate when
|
||||
@ -378,7 +370,6 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title="Pro mode"
|
||||
status={isProModeEnabled}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
>
|
||||
Set environment variable to {'"1"'} to enable
|
||||
@ -388,7 +379,6 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title="Static Optimization"
|
||||
status={isStaticallyOptimized}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
experimental
|
||||
>
|
||||
@ -408,7 +398,6 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title="Photo Matting"
|
||||
status={arePhotosMatted}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
>
|
||||
Set environment variable to {'"1"'} to constrain the size
|
||||
@ -419,7 +408,6 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title="Image Blur"
|
||||
status={isBlurEnabled}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
>
|
||||
Set environment variable to {'"1"'} to prevent
|
||||
@ -429,7 +417,6 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title="Geo privacy"
|
||||
status={isGeoPrivacyEnabled}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
>
|
||||
Set environment variable to {'"1"'} to disable
|
||||
@ -439,7 +426,6 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title="Priority order"
|
||||
status={isPriorityOrderEnabled}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
>
|
||||
Set environment variable to {'"1"'} to prevent
|
||||
@ -449,7 +435,6 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title="Public API"
|
||||
status={isPublicApiEnabled}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
>
|
||||
Set environment variable to {'"1"'} to enable
|
||||
@ -459,7 +444,6 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title="Show repo link"
|
||||
status={showRepoLink}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
>
|
||||
Set environment variable to {'"1"'} to hide footer link:
|
||||
@ -468,7 +452,6 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title="Show social"
|
||||
status={showSocial}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
>
|
||||
Set environment variable to {'"1"'} to hide
|
||||
@ -479,7 +462,6 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title="Show Fujifilm simulations"
|
||||
status={showFilmSimulations}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
>
|
||||
Set environment variable to {'"1"'} to prevent
|
||||
@ -490,7 +472,6 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title="Show EXIF data"
|
||||
status={showExifInfo}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
>
|
||||
Set environment variable to {'"1"'} to hide EXIF data:
|
||||
@ -499,7 +480,6 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title={`Grid aspect ratio: ${gridAspectRatio}`}
|
||||
status={hasGridAspectRatio}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
>
|
||||
Set environment variable to any number to enforce aspect ratio
|
||||
@ -510,7 +490,6 @@ export default function SiteChecklistClient({
|
||||
<ChecklistRow
|
||||
title="Legacy OG text alignment"
|
||||
status={isOgTextBottomAligned}
|
||||
isPending={isPendingPage}
|
||||
optional
|
||||
>
|
||||
Set environment variable to {'"BOTTOM"'} to
|
||||
@ -519,12 +498,6 @@ export default function SiteChecklistClient({
|
||||
</ChecklistRow>
|
||||
</Checklist>
|
||||
</>}
|
||||
{showRefreshButton &&
|
||||
<div className="py-4 space-y-4">
|
||||
<button onClick={refreshPage}>
|
||||
Check
|
||||
</button>
|
||||
</div>}
|
||||
<div className="px-11 text-dim">
|
||||
Changes to environment variables require a redeploy
|
||||
or reboot of local dev server
|
||||
|
||||
21
src/site/SiteChecklistServer.tsx
Normal file
21
src/site/SiteChecklistServer.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { generateAuthSecret } from '@/auth';
|
||||
import SiteChecklistClient from './SiteChecklistClient';
|
||||
import { CONFIG_CHECKLIST_STATUS } from '@/site/config';
|
||||
import { testConnectionsAction } from '@/admin/actions';
|
||||
|
||||
export default async function SiteChecklistServer({
|
||||
simplifiedView,
|
||||
}: {
|
||||
simplifiedView?: boolean
|
||||
}) {
|
||||
const secret = await generateAuthSecret();
|
||||
const connectionErrors = await testConnectionsAction().catch(() => ({}));
|
||||
return (
|
||||
<SiteChecklistClient {...{
|
||||
...CONFIG_CHECKLIST_STATUS,
|
||||
...connectionErrors,
|
||||
simplifiedView,
|
||||
secret,
|
||||
}} />
|
||||
);
|
||||
}
|
||||
@ -148,12 +148,12 @@ export const HIGH_DENSITY_GRID = GRID_ASPECT_RATIO <= 1;
|
||||
|
||||
export const CONFIG_CHECKLIST_STATUS = {
|
||||
hasDatabase: HAS_DATABASE,
|
||||
isPostgresSSLEnabled: POSTGRES_SSL_ENABLED,
|
||||
isPostgresSslEnabled: POSTGRES_SSL_ENABLED,
|
||||
hasVercelPostgres: (
|
||||
/\/verceldb\?/.test(process.env.POSTGRES_URL ?? '') ||
|
||||
/\.vercel-storage\.com\//.test(process.env.POSTGRES_URL ?? '')
|
||||
),
|
||||
hasVercelKV: HAS_VERCEL_KV,
|
||||
hasVercelKv: HAS_VERCEL_KV,
|
||||
hasVercelBlobStorage: HAS_VERCEL_BLOB_STORAGE,
|
||||
hasCloudflareR2Storage: HAS_CLOUDFLARE_R2_STORAGE,
|
||||
hasAwsS3Storage: HAS_AWS_S3_STORAGE,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user