fix(ui): repair rebased storybook types

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-20 21:20:02 +00:00
1 parent 1880c9d21e
commit d1f27903a9
4 files changed
+4 -566

No files matched your search

@@ -108,7 +108,9 @@ describe("GatewayActivityPanel", () => {
function clickButton(text: string) {
const button = [...container.querySelectorAll("button")].find((item) => item.textContent?.includes(text));
expect(button, `button containing ${text}`).toBeTruthy();
return act(async () => button!.dispatchEvent(new MouseEvent("click", { bubbles: true })));
return act(async () => {
button!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
}
it("filters by gateway and expands redacted call details", async () => {
@@ -36,6 +36,7 @@ function event(overrides: Partial<ToolGatewayActivityEvent> = {}): ToolGatewayAc
},
createdAt: new Date(Date.now() - 5 * MIN).toISOString(),
...overrides,
invocation: overrides.invocation ?? null,
};
}
@@ -1,319 +0,0 @@
import { useEffect, useMemo, useRef } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ToolCatalogEntry, ToolConnection, ToolPolicy } from "@paperclipai/shared";
import { queryKeys } from "@/lib/queryKeys";
import { PoliciesTab } from "@/pages/tools/PoliciesTab";
const COMPANY = "company-storybook";
function makeTool(
id: string,
toolName: string,
cap: "read" | "write" | "destructive",
title: string,
applicationId: string,
connectionId: string,
): ToolCatalogEntry {
return {
id,
companyId: COMPANY,
applicationId,
connectionId,
entryKind: "tool",
name: toolName,
toolName,
title,
description: title,
inputSchema: null,
outputSchema: null,
annotations: null,
riskLevel: cap,
isReadOnly: cap === "read",
isWrite: cap !== "read",
isDestructive: cap === "destructive",
status: "active",
addedAt: new Date("2026-06-01T00:00:00Z"),
version: null,
versionHash: null,
schemaHash: null,
firstSeenAt: new Date("2026-06-01T00:00:00Z"),
lastSeenAt: new Date("2026-06-01T00:00:00Z"),
reviewedAt: null,
reviewedByAgentId: null,
reviewedByUserId: null,
createdAt: new Date("2026-06-01T00:00:00Z"),
updatedAt: new Date("2026-06-01T00:00:00Z"),
} as ToolCatalogEntry;
}
const GMAIL: ToolCatalogEntry[] = [
makeTool("g-list", "gmail.list_messages", "read", "List messages", "app-gmail", "conn-gmail"),
makeTool("g-read", "gmail.get_message", "read", "Read a message", "app-gmail", "conn-gmail"),
makeTool("g-send", "gmail.send_message", "write", "Send a message", "app-gmail", "conn-gmail"),
makeTool("g-draft", "gmail.create_draft", "write", "Create a draft", "app-gmail", "conn-gmail"),
makeTool("g-trash", "gmail.trash_message", "destructive", "Move to trash", "app-gmail", "conn-gmail"),
makeTool("g-delete", "gmail.delete_message", "destructive", "Permanently delete", "app-gmail", "conn-gmail"),
];
const SLACK: ToolCatalogEntry[] = [
makeTool("s-list", "slack.list_channels", "read", "List channels", "app-slack", "conn-slack"),
makeTool("s-post", "slack.post_message", "write", "Post a message", "app-slack", "conn-slack"),
makeTool("s-archive", "slack.archive_channel", "destructive", "Archive a channel", "app-slack", "conn-slack"),
];
const CATALOG: ToolCatalogEntry[] = [...GMAIL, ...SLACK];
const CONNECTIONS: ToolConnection[] = [
{
id: "conn-gmail",
companyId: COMPANY,
applicationId: "app-gmail",
name: "Gmail",
uid: "gmail/gmail",
connectionKind: "managed",
ownership: "customer",
transport: "mcp_remote",
authKind: "oauth",
status: "active",
transportConfig: {},
credentialSecretRefs: [],
healthStatus: "ok",
healthCheckedAt: null,
lastError: null,
enabled: true,
createdByAgentId: null,
createdByUserId: null,
createdAt: new Date("2026-06-01T00:00:00Z"),
updatedAt: new Date("2026-06-01T00:00:00Z"),
} as ToolConnection,
{
id: "conn-slack",
companyId: COMPANY,
applicationId: "app-slack",
name: "Slack",
uid: "slack/slack",
connectionKind: "managed",
ownership: "customer",
transport: "mcp_remote",
authKind: "oauth",
status: "active",
transportConfig: {},
credentialSecretRefs: [],
healthStatus: "ok",
healthCheckedAt: null,
lastError: null,
enabled: true,
createdByAgentId: null,
createdByUserId: null,
createdAt: new Date("2026-06-01T00:00:00Z"),
updatedAt: new Date("2026-06-01T00:00:00Z"),
} as ToolConnection,
];
const AGENTS = [
{ id: "agent-fable", name: "Fable" },
{ id: "agent-sage", name: "Sage" },
{ id: "agent-atlas", name: "Atlas" },
];
const PROJECTS = [
{ id: "project-launch", name: "Launch" },
{ id: "project-support", name: "Support" },
];
function rule(partial: Partial<ToolPolicy> & { id: string; policyType: ToolPolicy["policyType"]; priority: number }): ToolPolicy {
return {
id: partial.id,
companyId: COMPANY,
name: partial.name ?? partial.id,
description: partial.description ?? null,
policyType: partial.policyType,
priority: partial.priority,
enabled: partial.enabled ?? true,
selectors: partial.selectors ?? {},
conditions: partial.conditions ?? null,
config: partial.config ?? null,
createdByAgentId: null,
createdByUserId: null,
createdAt: new Date("2026-06-01T00:00:00Z"),
updatedAt: new Date("2026-06-11T00:00:00Z"),
};
}
const POLICIES: ToolPolicy[] = [
rule({
id: "p1",
name: "Block destructive actions everywhere",
policyType: "block",
priority: 50,
selectors: { riskLevel: "destructive" },
}),
rule({
id: "p2",
name: "Ask first when Fable sends mail",
policyType: "require_approval",
priority: 100,
selectors: { agentId: "agent-fable", toolName: "gmail.send_message" },
}),
rule({
id: "p3",
name: "Limit Slack posts",
policyType: "rate_limit",
priority: 150,
selectors: { toolName: "slack.post_message" },
config: { rateLimit: { limit: 25, windowSeconds: 3600, keyBy: ["agent", "tool"] } },
}),
rule({
id: "p4",
name: "Allow Sage to use Gmail",
policyType: "allow",
priority: 200,
selectors: { agentId: "agent-sage", applicationId: "app-gmail" },
}),
rule({
id: "p5",
name: "Block critical actions",
policyType: "block",
priority: 300,
selectors: { riskLevel: "critical" },
}),
];
const TRUST_RULES: ToolPolicy[] = [
rule({
id: "trust-1",
policyType: "trust_rule",
priority: 1000,
selectors: { agentId: "agent-fable", toolName: "gmail.send_message" },
}),
rule({
id: "trust-2",
policyType: "trust_rule",
priority: 1000,
selectors: { agentId: "agent-sage", toolName: "slack.post_message" },
}),
];
const AUDIT_HITS = [
{ policyId: "p1", count: 14 },
{ policyId: "p2", count: 6 },
{ policyId: "p3", count: 22 },
{ policyId: "p4", count: 3 },
{ policyId: "p5", count: 1 },
];
function buildAudit() {
const now = Date.now();
const rows: Array<{ createdAt: string; details: { matchedPolicyIds: string[] } }> = [];
for (const hit of AUDIT_HITS) {
for (let i = 0; i < hit.count; i += 1) {
rows.push({
createdAt: new Date(now - i * 60_000).toISOString(),
details: { matchedPolicyIds: [hit.policyId] },
});
}
}
return rows;
}
function seededClient({
policies,
trustRules,
}: {
policies: ToolPolicy[];
trustRules: ToolPolicy[];
}) {
const c = new QueryClient({
defaultOptions: {
queries: { staleTime: Infinity, gcTime: Infinity, retry: false, refetchOnMount: false },
},
});
c.setQueryData(queryKeys.tools.policies(COMPANY), { policies });
c.setQueryData(queryKeys.tools.trustRules(COMPANY), { trustRules });
c.setQueryData(queryKeys.tools.audit(COMPANY, 250), buildAudit());
c.setQueryData(queryKeys.agents.list(COMPANY), AGENTS);
c.setQueryData(queryKeys.projects.list(COMPANY), PROJECTS);
c.setQueryData(queryKeys.tools.applications(COMPANY), {
applications: [
{ id: "app-gmail", name: "Gmail" },
{ id: "app-slack", name: "Slack" },
],
});
c.setQueryData(queryKeys.tools.connections(COMPANY), { connections: CONNECTIONS });
c.setQueryData(queryKeys.tools.catalog("conn-gmail"), { catalog: GMAIL });
c.setQueryData(queryKeys.tools.catalog("conn-slack"), { catalog: SLACK });
return c;
}
function findButtonByText(label: string): HTMLButtonElement | null {
const buttons = Array.from(document.querySelectorAll<HTMLButtonElement>("button"));
return buttons.find((b) => (b.textContent ?? "").trim() === label) ?? null;
}
function PoliciesHost({
policies = POLICIES,
trustRules = TRUST_RULES,
autoClick,
}: {
policies?: ToolPolicy[];
trustRules?: ToolPolicy[];
autoClick?: "new-rule" | "test-rule";
}) {
const client = useMemo(() => seededClient({ policies, trustRules }), [policies, trustRules]);
const triggered = useRef(false);
useEffect(() => {
if (!autoClick || triggered.current) return;
let cancelled = false;
const tryClick = (attempt: number) => {
if (cancelled || triggered.current) return;
const label = autoClick === "new-rule" ? "New rule" : "Test a rule";
const button = findButtonByText(label);
if (button) {
triggered.current = true;
button.click();
return;
}
if (attempt < 30) window.setTimeout(() => tryClick(attempt + 1), 50);
};
tryClick(0);
return () => {
cancelled = true;
};
}, [autoClick]);
return (
<QueryClientProvider client={client}>
<div className="mx-auto max-w-6xl p-6">
<PoliciesTab companyId={COMPANY} />
</div>
</QueryClientProvider>
);
}
const meta: Meta = {
title: "Tools/Rules (PAP-11049)",
parameters: { layout: "fullscreen" },
};
export default meta;
type Story = StoryObj;
export const IndexPopulated: Story = {
name: "Rules index — populated",
render: () => <PoliciesHost />,
};
export const Empty: Story = {
name: "Rules — empty state",
render: () => <PoliciesHost policies={[]} trustRules={[]} />,
};
export const BuilderNewRule: Story = {
name: "Rule builder — after New rule",
render: () => <PoliciesHost autoClick="new-rule" />,
};
export const TestRuleSlideover: Story = {
name: "Test a rule — slide-over",
render: () => <PoliciesHost autoClick="test-rule" />,
};
@@ -1,246 +0,0 @@
import { useMemo } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type {
ToolConnection,
ToolRuntimeAlertRecommendation,
ToolRuntimeHealthSummary,
ToolRuntimeSlot,
} from "@paperclipai/shared";
import { TooltipProvider } from "@/components/ui/tooltip";
import { queryKeys } from "@/lib/queryKeys";
import { RuntimeTab } from "@/pages/tools/RuntimeTab";
const COMPANY = "company-storybook";
function slot(overrides: Partial<ToolRuntimeSlot> = {}): ToolRuntimeSlot {
return {
id: "slot-gmail",
companyId: COMPANY,
applicationId: "app-gmail",
connectionId: "conn-gmail",
projectWorkspaceId: null,
executionWorkspaceId: null,
issueId: null,
ownerScopeType: "company",
ownerScopeId: null,
runtimeKind: "local_stdio",
slotKey: "gmail-stdio-local",
status: "running",
reuseKey: null,
workspaceScope: null,
credentialScopeHash: null,
provider: null,
providerRef: null,
processId: 41832,
commandTemplateKey: "gmail",
healthStatus: "healthy",
lastHealthCheckAt: null,
idleExpiresAt: null,
startedAt: new Date("2026-06-13T10:46:00Z"),
stoppedAt: null,
lastUsedAt: new Date("2026-06-13T12:55:00Z"),
lastError: null,
metadata: null,
createdAt: new Date("2026-06-13T10:46:00Z"),
updatedAt: new Date("2026-06-13T13:00:00Z"),
...overrides,
} as ToolRuntimeSlot;
}
function connection(overrides: Partial<ToolConnection> = {}): ToolConnection {
return {
id: "conn-gmail",
companyId: COMPANY,
applicationId: "app-gmail",
name: "Gmail",
connectionKind: "managed",
transport: "local_stdio",
status: "active",
transportConfig: {},
credentialSecretRefs: [],
healthStatus: "healthy",
healthCheckedAt: null,
lastError: null,
enabled: true,
createdByAgentId: null,
createdByUserId: null,
createdAt: new Date("2026-06-13T10:46:00Z"),
updatedAt: new Date("2026-06-13T13:00:00Z"),
...overrides,
} as ToolConnection;
}
const SLOTS: ToolRuntimeSlot[] = [
slot(),
slot({
id: "slot-sheets",
applicationId: "app-sheets",
connectionId: "conn-sheets",
slotKey: "sheets-stdio-local",
commandTemplateKey: "google_sheets",
processId: 41877,
lastUsedAt: new Date("2026-06-13T12:51:00Z"),
}),
slot({
id: "slot-slack",
applicationId: "app-slack",
connectionId: "conn-slack",
runtimeKind: "remote_session",
slotKey: "slack-remote",
commandTemplateKey: null,
providerRef: "slack",
processId: null,
lastUsedAt: new Date("2026-06-13T12:42:00Z"),
}),
slot({
id: "slot-github",
applicationId: "app-github",
connectionId: "conn-github",
slotKey: "github-stdio-local",
commandTemplateKey: "github",
processId: 41901,
lastUsedAt: new Date("2026-06-13T12:59:00Z"),
}),
];
const CONNECTIONS: ToolConnection[] = [
connection(),
connection({ id: "conn-sheets", applicationId: "app-sheets", name: "Google Sheets" }),
connection({ id: "conn-slack", applicationId: "app-slack", name: "Slack", transport: "mcp_remote" }),
connection({ id: "conn-github", applicationId: "app-github", name: "GitHub" }),
];
function alert(overrides: Partial<ToolRuntimeAlertRecommendation> = {}): ToolRuntimeAlertRecommendation {
return {
name: "mcp_runtime_stuck_running_slot",
severity: "critical",
status: "firing",
threshold: "Any running slot with no progress for 5 minutes.",
observed: "1 stuck running slot(s).",
description: "A runtime slot is running but has not recorded progress inside the supervisor stuck-slot window.",
firstResponderAction: "Inspect recent audit events and active tool calls; restart the slot only after confirming no healthy call is still in progress.",
runbookSection: "runbook.md#stuck-running-slot",
...overrides,
};
}
function health(overrides: Partial<ToolRuntimeHealthSummary> = {}): ToolRuntimeHealthSummary {
return {
status: "ok",
generatedAt: new Date("2026-06-13T13:00:00Z"),
runbookPath: "docs/runbooks/mcp-runtime.md",
metrics: {
windowStartedAt: new Date("2026-06-13T12:00:00Z"),
windowEndedAt: new Date("2026-06-13T13:00:00Z"),
activeSlots: 4,
startingSlots: 0,
runningSlots: 4,
idleSlots: 0,
failedSlots: 0,
stoppedSlots: 0,
stuckStartingSlots: 0,
stuckRunningSlots: 0,
capacityDeferralsLastHour: 0,
restartAttemptsLastHour: 0,
restartSuppressionsLastHour: 0,
idleEvictionsLastHour: 0,
toolCallsLastHour: 128,
toolTimeoutsLastHour: 0,
toolFailuresLastHour: 0,
timeoutRateLastHour: 0,
failureRateLastHour: 0,
averageToolLatencyMsLastHour: 1200,
p95ToolLatencyMsLastHour: 2100,
missingSecretFailuresLastHour: 0,
auditWriteFailuresLastHour: 0,
activeConnections: 4,
disabledConnections: 0,
degradedConnections: 0,
remoteHttpConnections: 1,
localStdioConnections: 3,
},
supportMatrix: {
remoteHttp: { supported: true, note: "" },
localStdio: { supported: true, note: "" },
},
alerts: [],
recommendations: [],
...overrides,
};
}
function Seeded({
slots,
connections,
summary,
}: {
slots: ToolRuntimeSlot[];
connections: ToolConnection[];
summary: ToolRuntimeHealthSummary;
}) {
const client = useMemo(() => {
const c = new QueryClient({
defaultOptions: { queries: { staleTime: Infinity, gcTime: Infinity, retry: false, refetchOnMount: false } },
});
c.setQueryData(queryKeys.tools.runtimeSlots(COMPANY), { runtimeSlots: slots });
c.setQueryData(queryKeys.tools.runtimeHealth(COMPANY), summary);
c.setQueryData(queryKeys.tools.connections(COMPANY), { connections });
return c;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<QueryClientProvider client={client}>
<TooltipProvider>
<div className="mx-auto max-w-5xl p-6">
<RuntimeTab companyId={COMPANY} />
</div>
</TooltipProvider>
</QueryClientProvider>
);
}
const meta: Meta = {
title: "Tools/Health (runtime)",
parameters: { layout: "fullscreen" },
};
export default meta;
type Story = StoryObj;
export const AllGood: Story = {
name: "All good",
render: () => <Seeded slots={SLOTS} connections={CONNECTIONS} summary={health()} />,
};
export const NeedsAttention: Story = {
name: "Needs attention",
render: () => (
<Seeded
slots={[
slot({ healthStatus: "error", lastError: "MCP request timed out after 30000ms" }),
...SLOTS.slice(1),
]}
connections={[connection({ healthStatus: "degraded" }), ...CONNECTIONS.slice(1)]}
summary={health({
status: "degraded",
metrics: {
...health().metrics,
runningSlots: 3,
degradedConnections: 1,
toolFailuresLastHour: 11,
toolTimeoutsLastHour: 3,
timeoutRateLastHour: 12,
averageToolLatencyMsLastHour: 2400,
p95ToolLatencyMsLastHour: 5200,
},
alerts: [alert(), alert({ name: "mcp_runtime_connection_health_degraded", observed: "1 degraded connection(s), 0 disabled connection(s)." })],
})}
/>
),
};
export const RowExpanded: Story = {
name: "Row expanded (use the chevron)",
render: () => <Seeded slots={SLOTS} connections={CONNECTIONS} summary={health()} />,
};