History

Refine toolkit card loading layout

Rhys Sullivan committed 4 months ago

Commit d1fb1251 parent ef7c6232 files · +96 −52Browse files

Unified · Split

2 files
  1. e2e/selfhost/toolkits-ui.test.ts +6 −0
  2. packages/plugins/toolkits/src/page.tsx +90 −52
e2e/selfhost/toolkits-ui.test.ts+6−0
@@ -59,6 +59,12 @@ scenario( · 58 unmodified lines
59 await page.getByRole("button", { name: "Add workspace toolkit" }).waitFor();59 await page.getByRole("button", { name: "Add workspace toolkit" }).waitFor();
60 await page.getByRole("button", { name: "Add personal toolkit" }).waitFor();60 await page.getByRole("button", { name: "Add personal toolkit" }).waitFor();
61 expect(await page.locator('main [data-slot="skeleton"]').count()).toBe(0);61 expect(await page.locator('main [data-slot="skeleton"]').count()).toBe(0);
62 const seededCard = page.getByRole("link", {
63 name: `Open toolkit ${prefix}-workspace-a`,
64 });
65 await seededCard.waitFor();
66 expect(await seededCard.getByText("/mcp/toolkits").count()).toBe(0);
67 expect(await seededCard.getByText("Workspace tools").count()).toBe(0);
62 expect(await page.getByLabel("New toolkit").count()).toBe(0);68 expect(await page.getByLabel("New toolkit").count()).toBe(0);
63 });69 });
64 70
packages/plugins/toolkits/src/page.tsx+90−52
@@ -163,17 +163,6 @@ const toolkitUrlFor = (orgSlug: string | undefined, slug: string): string => { · 162 unmodified lines
163 163
164const identityPattern = (displayPattern: string): string => displayPattern;164const identityPattern = (displayPattern: string): string => displayPattern;
165 165
166const formatUpdatedAt = (value: number): string =>
167 new Intl.DateTimeFormat(undefined, {
168 month: "short",
169 day: "numeric",
170 hour: "numeric",
171 minute: "2-digit",
172 }).format(new Date(value));
173
174const toolkitScopeLabel = (toolkit: ToolkitResponse): string =>
175 toolkit.owner === "org" ? "Workspace tools" : "Workspace + personal tools";
176
177const compareToolkitRows = (a: ToolkitResponse, b: ToolkitResponse): number => {166const compareToolkitRows = (a: ToolkitResponse, b: ToolkitResponse): number => {
178 if (a.updatedAt !== b.updatedAt) return b.updatedAt - a.updatedAt;167 if (a.updatedAt !== b.updatedAt) return b.updatedAt - a.updatedAt;
179 return a.name.localeCompare(b.name);168 return a.name.localeCompare(b.name);
@@ -192,10 +181,7 @@ const toolkitShelfStyle = { minHeight: "28.5rem" }; · 12 unmodified lines
192const toolkitGridContainerStyle = { maxWidth: "80rem" };181const toolkitGridContainerStyle = { maxWidth: "80rem" };
193const toolkitToolTreeStyle = { width: "24rem" };182const toolkitToolTreeStyle = { width: "24rem" };
194 183
195const ownerAccentClass = (owner: Owner): string =>184const neutralToolkitIconClass = "border-border/70 bg-muted/30 text-muted-foreground";
196 owner === "org"
197 ? "border-sky-500/25 bg-sky-500/10 text-sky-700 dark:text-sky-300"
198 : "border-emerald-500/25 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300";
199 185
200type ToolkitConnectionGroup = {186type ToolkitConnectionGroup = {
201 readonly id: string;187 readonly id: string;
@@ -409,9 +395,77 @@ const configuredConnectionPatterns = ( · 207 unmodified lines
409 ...policies.filter((policy) => legacyPolicyIds.has(policy.id)).map((policy) => policy.pattern),395 ...policies.filter((policy) => legacyPolicyIds.has(policy.id)).map((policy) => policy.pattern),
410 ]);396 ]);
411 397
398function ToolkitConnectionIconStack(props: { connections: readonly ConfiguredConnectionView[] }) {
399 const visibleConnections = props.connections.slice(0, 3);
400 if (visibleConnections.length === 0) {
401 return (
402 <div
403 className={cn(
404 "flex size-9 shrink-0 items-center justify-center rounded-md border",
405 neutralToolkitIconClass,
406 )}
407 >
408 <BoxIcon className="size-4" />
409 </div>
410 );
411 }
412
413 return (
414 <div className="flex size-9 shrink-0 items-center justify-center rounded-md border border-border/70 bg-muted/20">
415 <div className="flex -space-x-2">
416 {visibleConnections.map((connection) => (
417 <span
418 key={connection.id}
419 className="flex size-5 items-center justify-center rounded-sm border border-background bg-card shadow-xs"
420 >
421 <IntegrationFavicon
422 icon={connection.icon}
423 sourceId={connection.sourceId}
424 url={connection.url}
425 size={14}
426 />
427 </span>
428 ))}
429 </div>
430 </div>
431 );
432}
433
434const connectionCountLabel = (count: number): string =>
435 count === 0 ? "No connections" : `${count} ${count === 1 ? "connection" : "connections"}`;
436
412function ToolkitTile(props: { pluginId: string; toolkit: ToolkitResponse }) {437function ToolkitTile(props: { pluginId: string; toolkit: ToolkitResponse }) {
413 const toolkit = props.toolkit;438 const toolkit = props.toolkit;
414 const scope = toolkitScopeLabel(toolkit);439 const tools = useAtomValue(toolsAllAtom);
440 const integrations = useAtomValue(integrationsOptimisticAtom);
441 const connections = useAtomValue(toolkitConnectionsAtom(toolkit.id));
442 const integrationPlugins = useIntegrationPlugins();
443 const visibleTools = useMemo(
444 () =>
445 AsyncResult.isSuccess(tools)
446 ? (tools.value as readonly ToolRow[]).filter((tool) =>
447 toolCanAppearInToolkit(toolkit, tool),
448 )
449 : [],
450 [toolkit, tools],
451 );
452 const connectionGroups = useMemo(() => buildConnectionGroups(visibleTools), [visibleTools]);
453 const connectionRows = AsyncResult.isSuccess(connections) ? connections.value.connections : [];
454 const integrationRows = AsyncResult.isSuccess(integrations)
455 ? (integrations.value as readonly Integration[])
456 : [];
457 const tileDataReady =
458 AsyncResult.isSuccess(connections) &&
459 AsyncResult.isSuccess(tools) &&
460 AsyncResult.isSuccess(integrations);
461 const configuredConnections = tileDataReady
462 ? configuredConnectionViews(
463 connectionRows,
464 connectionGroups,
465 integrationRows,
466 integrationPlugins,
467 )
468 : [];
415 return (469 return (
416 <Link470 <Link
417 to="/{-$orgSlug}/plugins/$pluginId/$"471 to="/{-$orgSlug}/plugins/$pluginId/$"
@@ -421,31 +475,18 @@ function ToolkitTile(props: { pluginId: string; toolkit: ToolkitResponse }) { · 3 unmodified lines
421 style={toolkitCardStyle}475 style={toolkitCardStyle}
422 >476 >
423 <div className="flex min-w-0 items-start gap-3">477 <div className="flex min-w-0 items-start gap-3">
424 <div478 {tileDataReady ? (
425 className={cn(479 <ToolkitConnectionIconStack connections={configuredConnections} />
426 "flex size-9 shrink-0 items-center justify-center rounded-md border",480 ) : (
427 ownerAccentClass(toolkit.owner),481 <Skeleton className="size-9 shrink-0 rounded-md" />
428 )}482 )}
429 >
430 <BoxIcon className="size-4" />
431 </div>
432 <div className="min-w-0 flex-1">483 <div className="min-w-0 flex-1">
433 <div className="truncate text-sm font-semibold text-foreground">{toolkit.name}</div>484 <div className="truncate text-sm font-semibold text-foreground">{toolkit.name}</div>
434 <div className="mt-1 truncate font-mono text-[11px] text-muted-foreground">485 <div className="mt-1 truncate text-xs text-muted-foreground">
435 {toolkit.slug}486 {tileDataReady ? connectionCountLabel(configuredConnections.length) : "Loading"}
436 </div>487 </div>
437 </div>488 </div>
438 </div>489 </div>
439
440 <div className="mt-5 min-w-0 space-y-3">
441 <code className="block truncate rounded-md border border-border/60 bg-muted/30 px-2 py-1.5 font-mono text-[11px] text-muted-foreground transition-colors group-hover:border-border">
442 /mcp/toolkits/{toolkit.slug}
443 </code>
444 <div className="flex min-w-0 items-center justify-between gap-3 text-xs text-muted-foreground">
445 <span className="truncate">{scope}</span>
446 <span className="shrink-0 tabular-nums">{formatUpdatedAt(toolkit.updatedAt)}</span>
447 </div>
448 </div>
449 </Link>490 </Link>
450 );491 );
451}492}
@@ -541,7 +582,7 @@ function AddToolkitCard(props: { owner: Owner; onClick: () => void }) { · 89 unmodified lines
541 <span582 <span
542 className={cn(583 className={cn(
543 "flex size-12 items-center justify-center rounded-md border transition-[border-color,background-color,color,transform]",584 "flex size-12 items-center justify-center rounded-md border transition-[border-color,background-color,color,transform]",
544 ownerAccentClass(props.owner),585 neutralToolkitIconClass,
545 "group-hover:scale-105",586 "group-hover:scale-105",
546 )}587 )}
547 >588 >
@@ -1117,23 +1158,14 @@ function ToolkitWorkspace(props: { · 569 unmodified lines
1117function ToolkitTileSkeleton(props: { index: number }) {1158function ToolkitTileSkeleton(props: { index: number }) {
1118 return (1159 return (
1119 <div1160 <div
1120 className="flex min-h-36 min-w-0 flex-col justify-between rounded-md border border-border/60 bg-card/35 p-3"1161 className="flex min-h-36 min-w-0 items-start gap-3 rounded-md border border-border/60 bg-card/35 p-3.5"
1121 style={toolkitCardStyle}1162 style={toolkitCardStyle}
1122 aria-hidden="true"1163 aria-hidden="true"
1123 >1164 >
1124 <div className="flex min-w-0 items-start gap-3">1165 <Skeleton className="size-9 shrink-0 rounded-md" />
1125 <Skeleton className="size-9 shrink-0 rounded-md" />1166 <div className="min-w-0 flex-1 space-y-2 pt-0.5">
1126 <div className="min-w-0 flex-1 space-y-2 pt-0.5">1167 <Skeleton className="h-4" style={{ width: `${52 + ((props.index * 17) % 28)}%` }} />
1127 <Skeleton className="h-4" style={{ width: `${52 + ((props.index * 17) % 28)}%` }} />1168 <Skeleton className="h-3" style={{ width: `${34 + ((props.index * 11) % 24)}%` }} />
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>1169 </div>
1138 </div>1170 </div>
1139 );1171 );
@@ -1369,7 +1401,13 @@ export function ToolkitsPage(props: PluginPageProps) { · 229 unmodified lines
1369 </div>1401 </div>
1370 ) : null}1402 ) : null}
1371 1403
1372 {!toolkitsReady ? (1404 {selectedToolkitSlug !== null && !toolkitsReady ? (
1405 toolkitsFailed ? (
1406 <div className="p-6 text-sm text-destructive">Failed to load toolkit</div>
1407 ) : (
1408 <ToolkitDetailSkeleton />
1409 )
1410 ) : !toolkitsReady ? (
1373 toolkitsFailed ? (1411 toolkitsFailed ? (
1374 <div className="p-6 text-sm text-destructive">Failed to load toolkits</div>1412 <div className="p-6 text-sm text-destructive">Failed to load toolkits</div>
1375 ) : (1413 ) : (

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t