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
5959 await page.getByRole("button", { name: "Add workspace toolkit" }).waitFor();
6060 await page.getByRole("button", { name: "Add personal toolkit" }).waitFor();
6161 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);
6268 expect(await page.getByLabel("New toolkit").count()).toBe(0);
6369 });
6470
packages/plugins/toolkits/src/page.tsx+90−52
@@ -163,17 +163,6 @@ const toolkitUrlFor = (orgSlug: string | undefined, slug: string): string => { · 162 unmodified lines
163163
164164 const identityPattern = (displayPattern: string): string => displayPattern;
165165
166−const 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−
174−const toolkitScopeLabel = (toolkit: ToolkitResponse): string =>
175− toolkit.owner === "org" ? "Workspace tools" : "Workspace + personal tools";
176−
177166 const compareToolkitRows = (a: ToolkitResponse, b: ToolkitResponse): number => {
178167 if (a.updatedAt !== b.updatedAt) return b.updatedAt - a.updatedAt;
179168 return a.name.localeCompare(b.name);
@@ -192,10 +181,7 @@ const toolkitShelfStyle = { minHeight: "28.5rem" }; · 12 unmodified lines
192181 const toolkitGridContainerStyle = { maxWidth: "80rem" };
193182 const toolkitToolTreeStyle = { width: "24rem" };
194183
195−const ownerAccentClass = (owner: Owner): string =>
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";
184+const neutralToolkitIconClass = "border-border/70 bg-muted/30 text-muted-foreground";
199185
200186 type ToolkitConnectionGroup = {
201187 readonly id: string;
@@ -409,9 +395,77 @@ const configuredConnectionPatterns = ( · 207 unmodified lines
409395 ...policies.filter((policy) => legacyPolicyIds.has(policy.id)).map((policy) => policy.pattern),
410396 ]);
411397
398+function 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+
434+const connectionCountLabel = (count: number): string =>
435+ count === 0 ? "No connections" : `${count} ${count === 1 ? "connection" : "connections"}`;
436+
412437 function ToolkitTile(props: { pluginId: string; toolkit: ToolkitResponse }) {
413438 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+ : [];
415469 return (
416470 <Link
417471 to="/{-$orgSlug}/plugins/$pluginId/$"
@@ -421,31 +475,18 @@ function ToolkitTile(props: { pluginId: string; toolkit: ToolkitResponse }) { · 3 unmodified lines
421475 style={toolkitCardStyle}
422476 >
423477 <div className="flex min-w-0 items-start gap-3">
424− <div
425− className={cn(
426− "flex size-9 shrink-0 items-center justify-center rounded-md border",
427− ownerAccentClass(toolkit.owner),
428− )}
429− >
430− <BoxIcon className="size-4" />
431− </div>
478+ {tileDataReady ? (
479+ <ToolkitConnectionIconStack connections={configuredConnections} />
480+ ) : (
481+ <Skeleton className="size-9 shrink-0 rounded-md" />
482+ )}
432483 <div className="min-w-0 flex-1">
433484 <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">
435− {toolkit.slug}
485+ <div className="mt-1 truncate text-xs text-muted-foreground">
486+ {tileDataReady ? connectionCountLabel(configuredConnections.length) : "Loading"}
436487 </div>
437488 </div>
438489 </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>
449490 </Link>
450491 );
451492 }
@@ -541,7 +582,7 @@ function AddToolkitCard(props: { owner: Owner; onClick: () => void }) { · 89 unmodified lines
541582 <span
542583 className={cn(
543584 "flex size-12 items-center justify-center rounded-md border transition-[border-color,background-color,color,transform]",
544− ownerAccentClass(props.owner),
585+ neutralToolkitIconClass,
545586 "group-hover:scale-105",
546587 )}
547588 >
@@ -1117,23 +1158,14 @@ function ToolkitWorkspace(props: { · 569 unmodified lines
11171158 function ToolkitTileSkeleton(props: { index: number }) {
11181159 return (
11191160 <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"
11211162 style={toolkitCardStyle}
11221163 aria-hidden="true"
11231164 >
1124− <div className="flex min-w-0 items-start gap-3">
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>
1165+ <Skeleton className="size-9 shrink-0 rounded-md" />
1166+ <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)}%` }} />
1168+ <Skeleton className="h-3" style={{ width: `${34 + ((props.index * 11) % 24)}%` }} />
11371169 </div>
11381170 </div>
11391171 );
@@ -1369,7 +1401,13 @@ export function ToolkitsPage(props: PluginPageProps) { · 229 unmodified lines
13691401 </div>
13701402 ) : null}
13711403
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 ? (
13731411 toolkitsFailed ? (
13741412 <div className="p-6 text-sm text-destructive">Failed to load toolkits</div>
13751413 ) : (

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