fix(settings): align skeleton loading states with actual page layouts (#3967)

* fix(settings): align skeleton loading states with actual page layouts

* lint

* fix(settings): address PR feedback — deduplicate skeleton, fix import order, remove inline comments
This commit is contained in:
Waleed
2026-04-04 19:18:46 -07:00
committed by GitHub
parent d0baf5b1df
commit f9a7c4538e
13 changed files with 257 additions and 67 deletions

View File

@@ -16,7 +16,9 @@ import { CredentialsSkeleton } from '@/app/workspace/[workspaceId]/settings/comp
import { CustomToolsSkeleton } from '@/app/workspace/[workspaceId]/settings/components/custom-tools/custom-tool-skeleton'
import { GeneralSkeleton } from '@/app/workspace/[workspaceId]/settings/components/general/general-skeleton'
import { InboxSkeleton } from '@/app/workspace/[workspaceId]/settings/components/inbox/inbox-skeleton'
import { IntegrationsSkeleton } from '@/app/workspace/[workspaceId]/settings/components/integrations/integrations-skeleton'
import { McpSkeleton } from '@/app/workspace/[workspaceId]/settings/components/mcp/mcp-skeleton'
import { RecentlyDeletedSkeleton } from '@/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted-skeleton'
import { SkillsSkeleton } from '@/app/workspace/[workspaceId]/settings/components/skills/skill-skeleton'
import { WorkflowMcpServersSkeleton } from '@/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers-skeleton'
import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation'
@@ -52,7 +54,7 @@ const Integrations = dynamic(
import('@/app/workspace/[workspaceId]/settings/components/integrations/integrations').then(
(m) => m.Integrations
),
{ loading: () => <CredentialsSkeleton /> }
{ loading: () => <IntegrationsSkeleton /> }
)
const Credentials = dynamic(
() =>
@@ -145,7 +147,7 @@ const RecentlyDeleted = dynamic(
import(
'@/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted'
).then((m) => m.RecentlyDeleted),
{ loading: () => <SettingsSectionSkeleton /> }
{ loading: () => <RecentlyDeletedSkeleton /> }
)
const AccessControl = dynamic(
() => import('@/ee/access-control/components/access-control').then((m) => m.AccessControl),

View File

@@ -1,5 +1,9 @@
import { Skeleton } from '@/components/emcn'
/**
* Skeleton component for admin settings loading state.
* Matches the exact layout structure of the Admin component.
*/
export function AdminSkeleton() {
return (
<div className='flex h-full flex-col gap-6'>
@@ -7,6 +11,9 @@ export function AdminSkeleton() {
<Skeleton className='h-[14px] w-[120px]' />
<Skeleton className='h-[20px] w-[36px] rounded-full' />
</div>
<div className='h-px bg-[var(--border-secondary)]' />
<div className='flex flex-col gap-2'>
<Skeleton className='h-[14px] w-[340px]' />
<div className='flex gap-2'>
@@ -14,9 +21,51 @@ export function AdminSkeleton() {
<Skeleton className='h-9 w-[80px] rounded-md' />
</div>
</div>
<div className='flex flex-col gap-2'>
<div className='h-px bg-[var(--border-secondary)]' />
<div className='flex flex-col gap-3'>
<Skeleton className='h-[14px] w-[120px]' />
<Skeleton className='h-[200px] w-full rounded-lg' />
<div className='flex gap-2'>
<Skeleton className='h-9 flex-1 rounded-md' />
<Skeleton className='h-9 w-[80px] rounded-md' />
</div>
<div className='flex flex-col gap-0.5'>
<div className='flex items-center gap-3 border-[var(--border-secondary)] border-b px-3 py-2'>
<Skeleton className='h-[12px] w-[200px]' />
<Skeleton className='h-[12px] flex-1' />
<Skeleton className='h-[12px] w-[80px]' />
<Skeleton className='h-[12px] w-[80px]' />
<Skeleton className='h-[12px] w-[250px]' />
</div>
{Array.from({ length: 5 }).map((_, i) => (
<div
key={i}
className='flex items-center gap-3 border-[var(--border-secondary)] border-b px-3 py-2 last:border-b-0'
>
<Skeleton className='h-[14px] w-[200px]' />
<Skeleton className='h-[14px] flex-1' />
<Skeleton className='h-[20px] w-[50px] rounded-full' />
<Skeleton className='h-[20px] w-[50px] rounded-full' />
<div className='flex w-[250px] justify-end gap-1'>
<Skeleton className='h-[28px] w-[80px] rounded-md' />
<Skeleton className='h-[28px] w-[64px] rounded-md' />
<Skeleton className='h-[28px] w-[40px] rounded-md' />
</div>
</div>
))}
</div>
<div className='flex items-center justify-between'>
<Skeleton className='h-[14px] w-[160px]' />
<div className='flex gap-1'>
<Skeleton className='h-[28px] w-[64px] rounded-md' />
<Skeleton className='h-[28px] w-[48px] rounded-md' />
</div>
</div>
</div>
</div>
)

View File

@@ -25,12 +25,31 @@ export function ApiKeysSkeleton() {
return (
<div className='flex h-full flex-col gap-4.5'>
<div className='flex items-center gap-2'>
<Skeleton className='h-[30px] flex-1 rounded-lg' />
<Skeleton className='h-[30px] w-[80px] rounded-md' />
<Skeleton className='h-[38px] flex-1 rounded-lg' />
<Skeleton className='h-[38px] w-[90px] rounded-md' />
</div>
<div className='flex flex-col gap-2'>
<ApiKeySkeleton />
<ApiKeySkeleton />
<div className='min-h-0 flex-1 overflow-y-auto'>
<div className='flex flex-col gap-4.5'>
<div className='flex flex-col gap-2'>
<Skeleton className='h-5 w-[80px]' />
<Skeleton className='h-5 w-[180px]' />
</div>
<div className='flex flex-col gap-2'>
<Skeleton className='h-5 w-[60px]' />
<ApiKeySkeleton />
<ApiKeySkeleton />
</div>
</div>
</div>
<div className='mt-6 flex items-center justify-between'>
<div className='flex items-center gap-2'>
<Skeleton className='h-5 w-[170px]' />
<Skeleton className='h-3 w-3 rounded-full' />
</div>
<Skeleton className='h-5 w-9 rounded-full' />
</div>
</div>
)

View File

@@ -158,14 +158,14 @@ export function ApiKeys() {
<div ref={scrollContainerRef} className='min-h-0 flex-1 overflow-y-auto'>
{isLoading ? (
<div className='flex flex-col gap-4.5'>
{/* Workspace section header */}
<div className='flex flex-col gap-2'>
<Skeleton className='h-5 w-[70px]' />
<div className='text-[var(--text-muted)] text-sm'>
<Skeleton className='h-5 w-[140px]' />
</div>
<Skeleton className='h-5 w-[80px]' />
<Skeleton className='h-5 w-[180px]' />
</div>
{/* Personal section header + keys */}
<div className='flex flex-col gap-2'>
<Skeleton className='h-5 w-[55px]' />
<Skeleton className='h-5 w-[60px]' />
<ApiKeySkeleton />
<ApiKeySkeleton />
</div>
@@ -310,6 +310,15 @@ export function ApiKeys() {
</div>
{/* Allow Personal API Keys Toggle - Fixed at bottom */}
{isLoading && canManageWorkspaceKeys && (
<div className='mt-6 flex items-center justify-between'>
<div className='flex items-center gap-2'>
<Skeleton className='h-5 w-[170px]' />
<Skeleton className='h-3 w-3 rounded-full' />
</div>
<Skeleton className='h-5 w-9 rounded-full' />
</div>
)}
{!isLoading && canManageWorkspaceKeys && (
<Tooltip.Provider delayDuration={150}>
<div className='mt-6 flex items-center justify-between'>

View File

@@ -9,11 +9,14 @@ export function BYOKKeySkeleton() {
<div className='flex items-center gap-3'>
<Skeleton className='h-9 w-9 flex-shrink-0 rounded-md' />
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
<Skeleton className='h-[14px] w-[100px]' />
<Skeleton className='h-[13px] w-[200px]' />
<Skeleton className='h-[16px] w-[100px]' />
<Skeleton className='h-[14px] w-[200px]' />
</div>
</div>
<Skeleton className='h-[32px] w-[72px] rounded-md' />
<div className='flex flex-shrink-0 items-center gap-2'>
<Skeleton className='h-[32px] w-[72px] rounded-md' />
<Skeleton className='h-[32px] w-[64px] rounded-md' />
</div>
</div>
)
}

View File

@@ -1,46 +1,70 @@
import { Skeleton } from '@/components/emcn'
const GRID_COLS = 'grid grid-cols-[minmax(0,1fr)_8px_minmax(0,1fr)_auto] items-center'
const GRID_COLS = 'grid grid-cols-[minmax(0,1fr)_8px_minmax(0,1fr)_auto_auto] items-center'
const COL_SPAN_ALL = 'col-span-5'
/**
* Skeleton component for a single secret row in the grid layout.
* Skeleton for a single integration credential row.
*/
export function CredentialSkeleton() {
return (
<div className={GRID_COLS}>
<Skeleton className='h-9 rounded-md' />
<div />
<Skeleton className='h-9 rounded-md' />
<div className='ml-2 flex items-center gap-0'>
<Skeleton className='h-9 w-9 rounded-md' />
<Skeleton className='h-9 w-9 rounded-md' />
<div className='flex items-center justify-between gap-3'>
<div className='flex min-w-0 items-center gap-2.5'>
<Skeleton className='h-8 w-8 flex-shrink-0 rounded-md' />
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
<Skeleton className='h-4 w-[120px] rounded' />
<Skeleton className='h-3.5 w-[160px] rounded' />
</div>
</div>
<div className='flex flex-shrink-0 items-center gap-1'>
<Skeleton className='h-9 w-[60px] rounded-md' />
<Skeleton className='h-9 w-[88px] rounded-md' />
</div>
</div>
)
}
/**
* Skeleton for the Secrets section shown during dynamic import loading.
* Skeleton for a single secret row matching the credentials grid layout.
*/
function CredentialRowSkeleton() {
return (
<div className='contents'>
<Skeleton className='h-9 rounded-md' />
<div />
<Skeleton className='h-9 rounded-md' />
<Skeleton className='ml-2 h-9 w-[60px] rounded-md' />
<Skeleton className='h-9 w-9 rounded-md' />
</div>
)
}
/**
* Skeleton for the Credentials (Secrets) page shown during dynamic import loading.
*/
export function CredentialsSkeleton() {
return (
<div className='flex h-full flex-col gap-4'>
<div className='flex items-center gap-2'>
<Skeleton className='h-[30px] flex-1 rounded-lg' />
<Skeleton className='h-[30px] w-[56px] rounded-md' />
<Skeleton className='h-[30px] w-[50px] rounded-md' />
</div>
<div className='flex flex-col gap-2'>
<Skeleton className='h-5 w-[70px]' />
<div className='text-[var(--text-muted)] text-small'>
<Skeleton className='h-5 w-[160px]' />
<div className='min-h-0 flex-1 overflow-y-auto'>
<div className='flex flex-col gap-4'>
<div className={`${GRID_COLS} gap-y-2`}>
<Skeleton className={`${COL_SPAN_ALL} h-5 w-[70px]`} />
<CredentialRowSkeleton />
<CredentialRowSkeleton />
<div className={`${COL_SPAN_ALL} h-[8px]`} />
<Skeleton className={`${COL_SPAN_ALL} h-5 w-[55px]`} />
<CredentialRowSkeleton />
<CredentialRowSkeleton />
</div>
</div>
</div>
<div className='flex flex-col gap-2'>
<Skeleton className='h-5 w-[55px]' />
<CredentialSkeleton />
<CredentialSkeleton />
</div>
</div>
)
}

View File

@@ -7,11 +7,11 @@ export function CustomToolSkeleton() {
return (
<div className='flex items-center justify-between gap-3'>
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
<Skeleton className='h-[14px] w-[100px]' />
<Skeleton className='h-[13px] w-[200px]' />
<Skeleton className='h-4 w-[100px]' />
<Skeleton className='h-3.5 w-[200px]' />
</div>
<div className='flex flex-shrink-0 items-center gap-2'>
<Skeleton className='h-[30px] w-[40px] rounded-sm' />
<Skeleton className='h-[30px] w-[52px] rounded-sm' />
<Skeleton className='h-[30px] w-[54px] rounded-sm' />
</div>
</div>

View File

@@ -11,10 +11,10 @@ export function GeneralSkeleton() {
<Skeleton className='h-9 w-9 rounded-full' />
<div className='flex flex-1 flex-col justify-center gap-[1px]'>
<div className='flex items-center gap-2'>
<Skeleton className='h-5 w-24' />
<Skeleton className='h-4 w-24' />
<Skeleton className='h-[10.5px] w-[10.5px]' />
</div>
<Skeleton className='h-5 w-40' />
<Skeleton className='h-3.5 w-40' />
</div>
</div>

View File

@@ -8,7 +8,7 @@ export function InboxTaskSkeleton() {
<div className='flex flex-col gap-1 rounded-lg border border-[var(--border)] p-3'>
<div className='flex items-center justify-between'>
<Skeleton className='h-[14px] w-[200px]' />
<Skeleton className='h-[14px] w-[50px]' />
<Skeleton className='h-[12px] w-[50px]' />
</div>
<div className='flex items-center justify-between'>
<Skeleton className='h-[12px] w-[140px]' />
@@ -20,36 +20,64 @@ export function InboxTaskSkeleton() {
}
/**
* Skeleton for the full Inbox section shown during dynamic import loading.
* Skeleton for the full Inbox section shown while data is loading.
*/
export function InboxSkeleton() {
return (
<div className='flex h-full flex-col gap-4.5'>
<Skeleton className='h-[32px] w-full rounded-lg' />
<Skeleton className='h-[20px] w-[140px] rounded-sm' />
<Skeleton className='h-[40px] w-full rounded-lg' />
<div className='flex flex-col gap-1.5'>
<Skeleton className='h-[14px] w-[100px]' />
<Skeleton className='h-[13px] w-[200px]' />
<Skeleton className='h-[40px] w-full rounded-lg' />
<div className='flex items-center justify-between'>
<div className='flex flex-col gap-0.5'>
<Skeleton className='h-[14px] w-[140px]' />
<Skeleton className='h-[13px] w-[260px]' />
</div>
<Skeleton className='h-[20px] w-[36px] rounded-full' />
</div>
<div className='flex flex-col gap-1.5'>
<Skeleton className='h-[14px] w-[120px]' />
<Skeleton className='h-[13px] w-[250px]' />
<Skeleton className='h-[80px] w-full rounded-lg' />
<div className='border-[var(--border)] border-t' />
<div className='flex flex-col gap-6'>
<div className='flex flex-col gap-1.5'>
<Skeleton className='h-[14px] w-[90px]' />
<div className='flex items-center justify-between'>
<Skeleton className='h-[13px] w-[200px]' />
<div className='flex items-center gap-1.5'>
<Skeleton className='h-[12px] w-[12px] rounded-sm' />
<Skeleton className='h-[12px] w-[12px] rounded-sm' />
</div>
</div>
<Skeleton className='h-9 w-full rounded-md' />
</div>
<div className='flex flex-col gap-1.5'>
<Skeleton className='h-[14px] w-[110px]' />
<Skeleton className='h-[13px] w-[260px]' />
<div className='mt-1 overflow-hidden rounded-lg border border-[var(--border)]'>
<div className='px-3 py-2.5'>
<Skeleton className='h-[14px] w-[180px]' />
</div>
<div className='border-[var(--border)] border-t px-3 py-2.5'>
<Skeleton className='h-[14px] w-[160px]' />
</div>
</div>
<Skeleton className='mt-1 h-[32px] w-[100px] rounded-md' />
</div>
</div>
<div className='border-[var(--border)] border-t pt-4'>
<Skeleton className='h-[14px] w-[40px]' />
<Skeleton className='mt-0.5 h-[13px] w-[220px]' />
</div>
<div className='flex items-center gap-2'>
<Skeleton className='h-[30px] flex-1 rounded-lg' />
<Skeleton className='h-[30px] w-[100px] rounded-md' />
</div>
<div className='flex flex-col gap-1'>
<InboxTaskSkeleton />
<InboxTaskSkeleton />
<InboxTaskSkeleton />
<div className='flex flex-col gap-3'>
<div className='flex items-center gap-2'>
<Skeleton className='h-[32px] flex-1 rounded-lg' />
<Skeleton className='h-[32px] w-[100px] rounded-md' />
</div>
<div className='flex flex-col gap-1'>
<InboxTaskSkeleton />
<InboxTaskSkeleton />
<InboxTaskSkeleton />
</div>
</div>
</div>
)

View File

@@ -0,0 +1,23 @@
import { Skeleton } from '@/components/emcn'
import { CredentialSkeleton } from '@/app/workspace/[workspaceId]/settings/components/credentials/credential-skeleton'
/**
* Skeleton for the Integrations section shown during dynamic import loading.
*/
export function IntegrationsSkeleton() {
return (
<div className='flex h-full flex-col gap-4.5'>
<div className='flex items-center gap-2'>
<Skeleton className='h-[30px] flex-1 rounded-lg' />
<Skeleton className='h-[30px] w-[100px] rounded-md' />
</div>
<div className='min-h-0 flex-1 overflow-y-auto'>
<div className='flex flex-col gap-2'>
<CredentialSkeleton />
<CredentialSkeleton />
<CredentialSkeleton />
</div>
</div>
</div>
)
}

View File

@@ -9,10 +9,10 @@ export function McpServerSkeleton() {
<div className='flex items-center justify-between gap-3'>
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
<div className='flex items-center gap-1.5'>
<Skeleton className='h-[14px] w-[100px]' />
<Skeleton className='h-[13px] w-[80px]' />
<Skeleton className='h-4 w-[100px]' />
<Skeleton className='h-3.5 w-[80px]' />
</div>
<Skeleton className='h-[13px] w-[120px]' />
<Skeleton className='h-3.5 w-[120px]' />
</div>
<div className='flex flex-shrink-0 items-center gap-1'>
<Skeleton className='h-[30px] w-[60px] rounded-sm' />

View File

@@ -5,7 +5,7 @@ import { Skeleton } from '@/components/emcn'
*/
export function DeletedItemSkeleton() {
return (
<div className='flex items-center gap-3 px-2 py-2'>
<div className='flex items-center gap-3 rounded-md px-2 py-2'>
<Skeleton className='h-[14px] w-[14px] shrink-0 rounded-[3px]' />
<div className='flex min-w-0 flex-1 flex-col gap-0.5'>
<Skeleton className='h-[14px] w-[120px]' />

View File

@@ -0,0 +1,33 @@
import { Skeleton } from '@/components/emcn'
import { DeletedItemSkeleton } from '@/app/workspace/[workspaceId]/settings/components/recently-deleted/deleted-item-skeleton'
/**
* Skeleton component for the entire Recently Deleted settings section.
* Renders placeholder UI for the search bar, sort dropdown, tabs, and item list.
*/
export function RecentlyDeletedSkeleton() {
return (
<div className='flex h-full flex-col gap-4.5'>
<div className='flex items-center gap-2'>
<Skeleton className='h-[30px] flex-1 rounded-lg' />
<Skeleton className='h-[30px] w-[190px] shrink-0 rounded-lg' />
</div>
<div className='relative flex gap-4 border-[var(--border)] border-b px-4'>
<Skeleton className='mb-2 h-[20px] w-[32px] rounded-sm' />
<Skeleton className='mb-2 h-[20px] w-[72px] rounded-sm' />
<Skeleton className='mb-2 h-[20px] w-[52px] rounded-sm' />
<Skeleton className='mb-2 h-[20px] w-[112px] rounded-sm' />
<Skeleton className='mb-2 h-[20px] w-[40px] rounded-sm' />
</div>
<div className='min-h-0 flex-1 overflow-y-auto'>
<div className='flex flex-col gap-2'>
<DeletedItemSkeleton />
<DeletedItemSkeleton />
<DeletedItemSkeleton />
</div>
</div>
</div>
)
}