Wrap admin checklist checks in suspense

This commit is contained in:
Sam Becker 2024-06-12 00:01:40 -05:00
parent 95746b750d
commit f8e13d7212
7 changed files with 91 additions and 88 deletions

View File

@ -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,
};
});

View File

@ -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
View File

@ -0,0 +1,3 @@
import { kv } from '@vercel/kv';
export const testKvConnection = () => kv.get('test');

View File

@ -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>
);
}

View File

@ -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

View 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,
}} />
);
}

View File

@ -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,