Skip to content

Commit fa850f4

Browse files
committed
fix(org): give each loading state the shape of what replaces it
Subscription, Seats, Single sign-on, Sources and tool permissions held bare rows or a heading the loaded page lacks. Home's workspace rows lost their detail line while settling and the activity chart its range control, so the page moved when the read answered. A settling list now marks itself busy.
1 parent 5a01d0f commit fa850f4

11 files changed

Lines changed: 68 additions & 27 deletions

File tree

‎apps/sim/platform/embedded/app/(shell)/_components/section-parts/section-parts.tsx‎

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import type { ReactNode } from 'react'
44
import Link from 'next/link'
55
import { Button } from '@/platform/design-system/components/button/button'
6-
import { SettingsSection } from '@/platform/design-system/components/settings-section/settings-section'
6+
import {
7+
SettingsSection,
8+
SettingsSurface,
9+
} from '@/platform/design-system/components/settings-section/settings-section'
710
import { LoadingRows } from '@/platform/design-system/components/status/query-boundary'
811
import { InlineNotice } from '@/platform/design-system/components/status/status'
912
import styles from '@/platform/embedded/app/(shell)/_components/section-parts/section-parts.module.css'
@@ -154,15 +157,23 @@ export function DraftConflictNotice({ onKeepMine, onTakeLatest }: DraftConflictN
154157
)
155158
}
156159

157-
interface SectionSkeletonProps {
158-
title: string
159-
rows?: number
160-
}
161-
162-
export function SectionSkeleton({ title, rows }: SectionSkeletonProps) {
160+
type SectionSkeletonProps = { rows?: number } & (
161+
| { title: string; label?: never }
162+
/** A section that opens with a card and no heading, like a Subscription's plan. */
163+
| { title?: never; label: string }
164+
)
165+
166+
/** A read's place in the page, inside the heading and card it will fill. */
167+
export function SectionSkeleton({ title, label, rows }: SectionSkeletonProps) {
168+
if (title)
169+
return (
170+
<SettingsSection title={title}>
171+
<LoadingRows label={`Loading ${title}`} rows={rows} />
172+
</SettingsSection>
173+
)
163174
return (
164-
<SettingsSection title={title}>
165-
<LoadingRows label={`Loading ${title}`} rows={rows} />
166-
</SettingsSection>
175+
<SettingsSurface>
176+
<LoadingRows label={label ?? 'Loading'} rows={rows} />
177+
</SettingsSurface>
167178
)
168179
}

‎apps/sim/platform/embedded/app/(shell)/home/_components/activity-chart/activity-chart.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export function HomeActivityChart({
114114
</span>
115115
)}
116116
</div>
117-
{(ready || days !== ACTIVITY_WINDOW_DAYS[0]) && (
117+
{(state.status !== 'error' || days !== ACTIVITY_WINDOW_DAYS[0]) && (
118118
<RangePicker
119119
label='Range'
120120
options={RANGE_LABELS}
@@ -125,7 +125,7 @@ export function HomeActivityChart({
125125
/>
126126
)}
127127
</header>
128-
{state.status === 'loading' && <SkeletonBlock label={`Loading ${title}`} />}
128+
{state.status === 'loading' && <SkeletonBlock label={`Loading ${title}`} shape='chart' />}
129129
{state.status === 'error' && (
130130
<FailureNotice
131131
intent='warning'

‎apps/sim/platform/embedded/app/(shell)/home/_components/home-workspaces/home-workspaces.tsx‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ interface HomeWorkspacesProps {
2525
onRetry: () => void
2626
}
2727

28-
/** Shown while a row's activity is read, since an empty slot would claim the workspace is idle. */
29-
const SETTLING_METADATA = '…'
28+
/**
29+
* Stands in for a row's detail while its activity is read: an empty slot would claim the workspace
30+
* is idle, and the line holds the row at the height the answer will give it.
31+
*/
32+
const SETTLING_DETAIL = '…'
3033

3134
function metadataOf(item: HomeWorkspaceItem, asOf: number) {
3235
if (item.status) return STATUS_LABEL[item.status]
@@ -64,14 +67,14 @@ export function HomeWorkspaces({ state, count, asOf, onRetry }: HomeWorkspacesPr
6467
onRetry={onRetry}
6568
/>
6669
)}
67-
<div className={styles.viewport}>
70+
<div className={styles.viewport} aria-busy={state.settling || undefined}>
6871
<RowList dividers>
6972
{state.items.slice(0, visibleCount).map((item) => (
7073
<ActivityRow
7174
key={item.id}
7275
title={item.name}
73-
detail={item.detail}
74-
metadata={state.settling ? SETTLING_METADATA : metadataOf(item, asOf)}
76+
detail={state.settling ? SETTLING_DETAIL : item.detail}
77+
metadata={state.settling ? undefined : metadataOf(item, asOf)}
7578
metadataLeading={item.status === 'running' ? <Spinner /> : undefined}
7679
render={<SettingsGuardedLink href={item.href} />}
7780
/>

‎apps/sim/platform/embedded/app/(shell)/home/_components/widget-skeleton/widget-skeleton.module.css‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
border-radius: var(--ui-control-radius);
4141
background: var(--control-muted-fill);
4242
}
43+
/* The total, plot and ticks together, so the sections below stay put when the chart arrives. */
44+
.block[data-shape="chart"] {
45+
block-size: calc(var(--spacing-chart-plot) + var(--spacing-56) + var(--spacing-32));
46+
}
4347
.lane {
4448
display: flex;
4549
gap: var(--ui-list-gap);

‎apps/sim/platform/embedded/app/(shell)/home/_components/widget-skeleton/widget-skeleton.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,17 @@ export function WidgetSkeleton({
5050
)
5151
}
5252

53+
interface SkeletonBlockProps extends SkeletonPlaceholderProps {
54+
/** A chart's height, where the block sits under the chart's own heading. */
55+
shape?: 'chart'
56+
}
57+
5358
/** The same fill for a chart or schedule, whose shape has no rows. */
54-
export function SkeletonBlock({ label, silent }: SkeletonPlaceholderProps) {
59+
export function SkeletonBlock({ label, silent, shape }: SkeletonBlockProps) {
5560
return (
5661
<div
5762
className={styles.block}
63+
data-shape={shape}
5864
role={silent ? undefined : 'status'}
5965
aria-label={silent ? undefined : label}
6066
aria-hidden={silent || undefined}

‎apps/sim/platform/embedded/app/(shell)/integrations/_components/tool-permissions/tool-permissions-panel.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import { integrationViewParsers } from '@/lib/organizations/integrations-search-
55
import { useDebouncedSearchSetter } from '@/hooks/use-debounced-search-setter'
66
import { QueryBoundary } from '@/platform/design-system/components'
77
import { ProviderHeading } from '@/platform/embedded/app/(shell)/integrations/_components/provider-heading/provider-heading'
8-
import { ToolPermissions } from '@/platform/embedded/app/(shell)/integrations/_components/tool-permissions/tool-permissions'
8+
import {
9+
ToolPermissions,
10+
ToolPermissionsSkeleton,
11+
} from '@/platform/embedded/app/(shell)/integrations/_components/tool-permissions/tool-permissions'
912
import { useToolPermissions } from '@/platform/embedded/lib/data/integrations'
1013

1114
interface ToolPermissionsPanelProps {
@@ -31,6 +34,7 @@ export function ToolPermissionsPanel({
3134
query={{ ...query, data: undefined }}
3235
failure='Could not load tool permissions'
3336
loadingLabel='Loading tool permissions'
37+
loading={<ToolPermissionsSkeleton />}
3438
>
3539
{() => null}
3640
</QueryBoundary>

‎apps/sim/platform/embedded/app/(shell)/integrations/_components/tool-permissions/tool-permissions.tsx‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SectionSearch } from '@/platform/design-system/components'
1+
import { LoadingRows, SectionSearch } from '@/platform/design-system/components'
22
import { EmptyState } from '@/platform/design-system/components/empty-state/empty-state'
33
import { CountedLabel } from '@/platform/design-system/components/overflow-text/counted-label'
44
import { SegmentedControl } from '@/platform/design-system/components/segmented-control/segmented-control'
@@ -25,6 +25,18 @@ interface ToolPermissionsProps {
2525
onChange: (change: ToolPermissionChange) => void
2626
}
2727

28+
/** The section's heading over rows, as its read will fill it. */
29+
export function ToolPermissionsSkeleton() {
30+
return (
31+
<section className={styles.section} aria-label='Tool permissions'>
32+
<div className={styles.intro}>
33+
<h2 data-type='T093'>Tool permissions</h2>
34+
</div>
35+
<LoadingRows label='Loading tool permissions' />
36+
</section>
37+
)
38+
}
39+
2840
export function ToolPermissions({
2941
permissions,
3042
query,

‎apps/sim/platform/embedded/app/(shell)/settings/_components/sections/billing/billing-view.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function UsageLimit({ field }: { field: ReturnType<typeof useBillingSection>['us
4848

4949
export function BillingView({ organization }: Pick<SettingsSectionProps, 'organization'>) {
5050
const billing = useBillingSection(organization)
51-
if (billing.status === 'pending') return <SectionSkeleton title='Plan' rows={1} />
51+
if (billing.status === 'pending') return <SectionSkeleton label='Loading plan' rows={1} />
5252
if (billing.status === 'error')
5353
return <FailureNotice message={billing.error} onRetry={billing.retry} />
5454
return (

‎apps/sim/platform/embedded/app/(shell)/settings/_components/sections/integrations/integrations-section.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import {
3535
ConfirmDialog,
3636
EmptyState,
3737
FailureNotice,
38-
LoadingRows,
3938
SectionSearch,
4039
} from '@/platform/design-system/components'
4140
import {
@@ -46,6 +45,7 @@ import { IntegrationMark } from '@/platform/embedded/app/(shell)/_components/int
4645
import {
4746
ActionRow,
4847
Muted,
48+
SectionSkeleton,
4949
SectionStack,
5050
SectionToolbar,
5151
} from '@/platform/embedded/app/(shell)/_components/section-parts/section-parts'
@@ -179,7 +179,7 @@ export default function IntegrationsSection({ organization: context }: SettingsS
179179
}}
180180
/>
181181
) : !policies.data || !secrets.data ? (
182-
<LoadingRows label='Loading sources' />
182+
<SectionSkeleton title='Added sources' />
183183
) : added.length === 0 && !secretSource && !search && !assistantVisible ? (
184184
<EmptyState title='No sources yet. Add a source to get started.' />
185185
) : (

‎apps/sim/platform/embedded/app/(shell)/settings/_components/sections/members/seats-overview.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { getErrorMessage } from '@sim/utils/errors'
22
import Link from 'next/link'
33
import { getOrganizationSettingsHref } from '@/components/settings/navigation'
44
import { useOrganization } from '@/lib/organizations/provider'
5-
import { Button, FailureNotice, LoadingRows, StatusTag } from '@/platform/design-system/components'
5+
import { Button, FailureNotice, StatusTag } from '@/platform/design-system/components'
66
import {
77
SettingsRow,
88
SettingsSection,
99
} from '@/platform/design-system/components/settings-section/settings-section'
10+
import { SectionSkeleton } from '@/platform/embedded/app/(shell)/_components/section-parts/section-parts'
1011
import styles from '@/platform/embedded/app/(shell)/settings/_components/sections/members/members.module.css'
1112
import type { useMembers } from '@/platform/embedded/lib/data/members'
1213
import { plural } from '@/platform/shared/plural'
@@ -17,7 +18,7 @@ interface SeatsOverviewProps {
1718

1819
export function SeatsOverview({ seats }: SeatsOverviewProps) {
1920
const organizationId = useOrganization().organization.id
20-
if (seats.status === 'pending') return <LoadingRows label='Loading seats' rows={1} />
21+
if (seats.status === 'pending') return <SectionSkeleton title='Seats' rows={1} />
2122
if (seats.status === 'error')
2223
return (
2324
<FailureNotice

0 commit comments

Comments
 (0)