History

Fix toolkit loading skeleton

Rhys Sullivan committed 4 months ago

Commit 07cbee21 parent 2836ce22 files · +149 −45Browse files

Unified · Split

2 files
  1. e2e/selfhost/toolkits-ui.test.ts +3 −0
  2. packages/plugins/toolkits/src/page.tsx +146 −45
e2e/selfhost/toolkits-ui.test.ts+3−0
@@ -56,6 +56,9 @@ scenario( · 55 unmodified lines
56 await page.getByRole("heading", { name: "Toolkits" }).waitFor();56 await page.getByRole("heading", { name: "Toolkits" }).waitFor();
57 await page.getByRole("heading", { name: "Workspace" }).waitFor();57 await page.getByRole("heading", { name: "Workspace" }).waitFor();
58 await page.getByRole("heading", { name: "Personal" }).waitFor();58 await page.getByRole("heading", { name: "Personal" }).waitFor();
59 await page.getByRole("button", { name: "Add workspace toolkit" }).waitFor();
60 await page.getByRole("button", { name: "Add personal toolkit" }).waitFor();
61 expect(await page.locator('main [data-slot="skeleton"]').count()).toBe(0);
59 expect(await page.getByLabel("New toolkit").count()).toBe(0);62 expect(await page.getByLabel("New toolkit").count()).toBe(0);
60 });63 });
61 64
packages/plugins/toolkits/src/page.tsx+146−45
@@ -1114,15 +1114,110 @@ function ToolkitWorkspace(props: { · 1113 unmodified lines
1114 );1114 );
1115}1115}
1116 1116
1117function ToolkitsPageSkeleton() {1117function ToolkitTileSkeleton(props: { index: number }) {
1118 return (1118 return (
1119 <div className="flex min-h-0 flex-1 overflow-hidden">1119 <div
1120 <div className="flex-1 p-4">1120 className="flex min-h-36 min-w-0 flex-col justify-between rounded-md border border-border/60 bg-card/35 p-3"
1121 <Skeleton className="h-8 w-full" />1121 style={toolkitCardStyle}
1122 <div className="grid grid-cols-1 gap-3 pt-4 sm:grid-cols-2 md:grid-cols-3">1122 aria-hidden="true"
1123 {Array.from({ length: 6 }).map((_, index) => (1123 >
1124 <Skeleton key={index} className="h-36 w-full rounded-md" />1124 <div className="flex min-w-0 items-start gap-3">
1125 ))}1125 <Skeleton className="size-9 shrink-0 rounded-md" />
1126 <div className="min-w-0 flex-1 space-y-2 pt-0.5">
1127 <Skeleton className="h-4" style={{ width: `${52 + ((props.index * 17) % 28)}%` }} />
1128 <Skeleton className="h-3" style={{ width: `${34 + ((props.index * 11) % 24)}%` }} />
1129 </div>
1130 </div>
1131 <div className="space-y-2">
1132 <Skeleton className="h-3 w-24" />
1133 <div className="flex min-w-0 items-center gap-1.5">
1134 <Skeleton className="h-5 w-14 rounded-full" />
1135 <Skeleton className="h-5 w-20 rounded-full" />
1136 </div>
1137 </div>
1138 </div>
1139 );
1140}
1141
1142function ToolkitSectionSkeleton(props: { title: string; offset: number }) {
1143 return (
1144 <section className="space-y-3" aria-label={`Loading ${props.title.toLowerCase()} toolkits`}>
1145 <div className="border-b border-border/60 pb-2">
1146 <h2 className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">
1147 {props.title}
1148 </h2>
1149 </div>
1150
1151 <div className="grid content-start grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3">
1152 {Array.from({ length: 3 }).map((_, index) => (
1153 <ToolkitTileSkeleton key={index} index={props.offset + index} />
1154 ))}
1155 </div>
1156 </section>
1157 );
1158}
1159
1160function ToolkitGridSkeleton() {
1161 return (
1162 <div className="min-h-0 flex-1 overflow-y-auto">
1163 <div className="w-full space-y-7 px-4 py-4" style={toolkitGridContainerStyle}>
1164 <ToolkitSectionSkeleton title="Workspace" offset={0} />
1165 <ToolkitSectionSkeleton title="Personal" offset={3} />
1166 </div>
1167 </div>
1168 );
1169}
1170
1171function ToolkitDetailSkeleton() {
1172 return (
1173 <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
1174 <div className="shrink-0 border-b border-border/60 bg-background/95 px-4 py-3">
1175 <Skeleton className="mb-3 h-7 w-20 rounded-md" />
1176 <div className="space-y-2">
1177 <div className="flex min-w-0 items-center gap-2">
1178 <Skeleton className="h-4 w-40" />
1179 <Skeleton className="h-5 w-16 rounded-full" />
1180 <Skeleton className="h-3 w-12" />
1181 </div>
1182 <Skeleton className="h-6 w-full max-w-md" />
1183 </div>
1184 </div>
1185
1186 <div className="flex min-h-0 flex-1 overflow-hidden">
1187 <div
1188 className="flex min-h-0 shrink-0 flex-col border-r border-border/60"
1189 style={toolkitToolTreeStyle}
1190 >
1191 <div className="border-b border-border/60 px-3 py-3">
1192 <div className="flex items-center justify-between gap-3">
1193 <div className="space-y-2">
1194 <Skeleton className="h-3 w-28" />
1195 <Skeleton className="h-3 w-14" />
1196 </div>
1197 <Skeleton className="h-8 w-16 rounded-md" />
1198 </div>
1199 </div>
1200 <div className="space-y-1 p-3">
1201 {Array.from({ length: 9 }).map((_, index) => (
1202 <div key={index} className="flex items-center gap-2 rounded-md px-2 py-1.5">
1203 <Skeleton className="size-4 shrink-0 rounded" />
1204 <Skeleton className="h-3.5" style={{ width: `${48 + ((index * 13) % 38)}%` }} />
1205 </div>
1206 ))}
1207 </div>
1208 </div>
1209 <div className="flex min-w-0 flex-1 flex-col gap-5 overflow-hidden p-6">
1210 <div className="space-y-2">
1211 <Skeleton className="h-5 w-52" />
1212 <Skeleton className="h-4 w-full max-w-lg" />
1213 <Skeleton className="h-4 w-full max-w-sm" />
1214 </div>
1215 <div className="space-y-3">
1216 <Skeleton className="h-4 w-24" />
1217 <Skeleton className="h-24 w-full rounded-md" />
1218 <Skeleton className="h-4 w-28" />
1219 <Skeleton className="h-24 w-full rounded-md" />
1220 </div>
1126 </div>1221 </div>
1127 </div>1222 </div>
1128 </div>1223 </div>
@@ -1193,7 +1288,7 @@ function ToolkitDetailView(props: { · 64 unmodified lines
1193 return <div className="p-6 text-sm text-destructive">Failed to load toolkit</div>;1288 return <div className="p-6 text-sm text-destructive">Failed to load toolkit</div>;
1194 }1289 }
1195 if (!AsyncResult.isSuccess(policies) || !AsyncResult.isSuccess(connections)) {1290 if (!AsyncResult.isSuccess(policies) || !AsyncResult.isSuccess(connections)) {
1196 return <ToolkitsPageSkeleton />;1291 return <ToolkitDetailSkeleton />;
1197 }1292 }
1198 1293
1199 return (1294 return (
@@ -1234,6 +1329,10 @@ export function ToolkitsPage(props: PluginPageProps) { · 34 unmodified lines
1234 const integrationRows = AsyncResult.isSuccess(integrations)1329 const integrationRows = AsyncResult.isSuccess(integrations)
1235 ? (integrations.value as readonly Integration[])1330 ? (integrations.value as readonly Integration[])
1236 : [];1331 : [];
1332 const toolkitsReady = AsyncResult.isSuccess(toolkits);
1333 const toolkitsFailed = AsyncResult.isFailure(toolkits);
1334 const toolsReady = AsyncResult.isSuccess(tools);
1335 const toolsFailed = AsyncResult.isFailure(tools);
1237 const navigateToIndex = () =>1336 const navigateToIndex = () =>
1238 navigate({1337 navigate({
1239 to: "/{-$orgSlug}/plugins/$pluginId/$",1338 to: "/{-$orgSlug}/plugins/$pluginId/$",
@@ -1270,46 +1369,48 @@ export function ToolkitsPage(props: PluginPageProps) { · 30 unmodified lines
1270 </div>1369 </div>
1271 ) : null}1370 ) : null}
1272 1371
1273 {!AsyncResult.isSuccess(toolkits) || !AsyncResult.isSuccess(tools) ? (1372 {!toolkitsReady ? (
1274 AsyncResult.isFailure(toolkits) || AsyncResult.isFailure(tools) ? (1373 toolkitsFailed ? (
1275 <div className="p-6 text-sm text-destructive">Failed to load toolkits</div>1374 <div className="p-6 text-sm text-destructive">Failed to load toolkits</div>
1276 ) : (1375 ) : (
1277 <ToolkitsPageSkeleton />1376 <ToolkitGridSkeleton />
1377 )
1378 ) : selectedToolkit ? (
1379 toolsFailed ? (
1380 <div className="p-6 text-sm text-destructive">Failed to load toolkit tools</div>
1381 ) : !toolsReady ? (
1382 <ToolkitDetailSkeleton />
1383 ) : (
1384 <ToolkitDetailView
1385 toolkit={selectedToolkit}
1386 tools={toolRows}
1387 integrations={integrationRows}
1388 integrationPlugins={integrationPlugins}
1389 orgSlug={params.orgSlug}
1390 onBack={() => void navigateToIndex()}
1391 onRemoveToolkit={removeToolkitHandler}
1392 />
1278 )1393 )
1394 ) : selectedToolkitSlug !== null ? (
1395 <div className="flex min-h-0 flex-1 flex-col p-6">
1396 <Button
1397 type="button"
1398 variant="ghost"
1399 size="xs"
1400 onClick={() => void navigateToIndex()}
1401 className="mb-4 w-fit -ml-1 text-muted-foreground"
1402 >
1403 <ArrowLeftIcon className="size-3.5" />
1404 Toolkits
1405 </Button>
1406 <div className="text-sm text-muted-foreground">Toolkit not found</div>
1407 </div>
1279 ) : (1408 ) : (
1280 <>1409 <ToolkitGrid
1281 {selectedToolkit ? (1410 pluginId={props.pluginId}
1282 <ToolkitDetailView1411 toolkits={toolkitRows}
1283 toolkit={selectedToolkit}1412 onCreate={createToolkitHandler}
1284 tools={toolRows}1413 />
1285 integrations={integrationRows}
1286 integrationPlugins={integrationPlugins}
1287 orgSlug={params.orgSlug}
1288 onBack={() => void navigateToIndex()}
1289 onRemoveToolkit={removeToolkitHandler}
1290 />
1291 ) : selectedToolkitSlug !== null ? (
1292 <div className="flex min-h-0 flex-1 flex-col p-6">
1293 <Button
1294 type="button"
1295 variant="ghost"
1296 size="xs"
1297 onClick={() => void navigateToIndex()}
1298 className="mb-4 w-fit -ml-1 text-muted-foreground"
1299 >
1300 <ArrowLeftIcon className="size-3.5" />
1301 Toolkits
1302 </Button>
1303 <div className="text-sm text-muted-foreground">Toolkit not found</div>
1304 </div>
1305 ) : (
1306 <ToolkitGrid
1307 pluginId={props.pluginId}
1308 toolkits={toolkitRows}
1309 onCreate={createToolkitHandler}
1310 />
1311 )}
1312 </>
1313 )}1414 )}
1314 </div>1415 </div>
1315 );1416 );