@sentry/junior-dashboard 0.233.0 → 0.234.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/app.js +89 -15
  2. package/dist/client.js +55 -52
  3. package/dist/index.js +89 -15
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +3 -3
  6. package/src/client/App.tsx +359 -345
  7. package/src/client/api.ts +17 -1
  8. package/src/client/components/ActorAvatar.tsx +37 -0
  9. package/src/client/components/Button.tsx +12 -10
  10. package/src/client/components/Field.tsx +14 -1
  11. package/src/client/components/FormNotice.tsx +40 -0
  12. package/src/client/components/ParticipantAvatarStack.tsx +6 -14
  13. package/src/client/components/README.md +11 -2
  14. package/src/client/components/Select.tsx +18 -0
  15. package/src/client/components/StatusChip.tsx +15 -12
  16. package/src/client/components/TextInput.tsx +25 -5
  17. package/src/client/http.ts +31 -3
  18. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +71 -54
  19. package/src/client/pages/automations/AutomationEditPage.tsx +172 -0
  20. package/src/client/pages/automations/AutomationEditor.tsx +397 -0
  21. package/src/client/pages/automations/AutomationEditorLoading.tsx +18 -0
  22. package/src/client/pages/automations/AutomationEventFields.tsx +264 -0
  23. package/src/client/pages/automations/AutomationFilters.tsx +171 -0
  24. package/src/client/pages/automations/AutomationFormSection.tsx +22 -0
  25. package/src/client/pages/automations/AutomationLifecycle.tsx +146 -0
  26. package/src/client/pages/automations/AutomationOutcomeFields.tsx +177 -0
  27. package/src/client/pages/automations/AutomationRow.tsx +358 -0
  28. package/src/client/pages/automations/AutomationScheduleFields.tsx +328 -0
  29. package/src/client/pages/automations/AutomationsPage.tsx +157 -408
  30. package/src/client/pages/automations/AutomationsPageLayout.tsx +16 -5
  31. package/src/client/pages/automations/README.md +19 -0
  32. package/src/client/pages/automations/automationDraft.ts +110 -0
  33. package/src/client/pages/automations/automationOutcomes.ts +23 -0
  34. package/src/client/pages/dev/ComponentsPage.tsx +50 -4
  35. package/src/client/searchParams.ts +4 -2
  36. package/src/client.tsx +7 -4
  37. package/src/mock-reporting/automation-collection.ts +101 -0
  38. package/src/mock-reporting/fixtures.ts +9 -3
  39. package/src/mock-reporting/routes.ts +14 -9
@@ -11,7 +11,11 @@ import { SecondaryNavigation } from "../../components/layout/SecondaryNavigation
11
11
  const RESERVED_TASK_SEGMENTS = new Set(["list", "runs"]);
12
12
 
13
13
  function isTasksListPath(pathname: string): boolean {
14
- if (pathname === "/automations/list") return true;
14
+ if (
15
+ pathname === "/automations/list" ||
16
+ /^\/automations\/(scheduled|event)\/[^/]+\/edit$/.test(pathname)
17
+ )
18
+ return true;
15
19
  const match = pathname.match(/^\/automations\/([^/]+)$/);
16
20
  return Boolean(match?.[1] && !RESERVED_TASK_SEGMENTS.has(match[1]));
17
21
  }
@@ -22,14 +26,21 @@ const taskNavigationItems = [
22
26
  { label: "Runs", to: "/automations/runs" },
23
27
  ];
24
28
 
29
+ /** Keep Automation navigation consistent across lists, details, and editing. */
30
+ export function AutomationsNavigation() {
31
+ return (
32
+ <SecondaryNavigation
33
+ ariaLabel="Automations navigation"
34
+ items={taskNavigationItems}
35
+ />
36
+ );
37
+ }
38
+
25
39
  /** Place the shared secondary navigation above one Automations page. */
26
40
  export function AutomationsPageLayout(props: { children: ReactNode }) {
27
41
  return (
28
42
  <>
29
- <SecondaryNavigation
30
- ariaLabel="Automations navigation"
31
- items={taskNavigationItems}
32
- />
43
+ <AutomationsNavigation />
33
44
  <PageLayout>{props.children}</PageLayout>
34
45
  </>
35
46
  );
@@ -0,0 +1,19 @@
1
+ # Automation editor
2
+
3
+ Core owns validation, creator access, revision checks, Schedule previews, and
4
+ the Event catalog. Preview requests cannot save or run work. Completed scheduled
5
+ Automations remain read-only.
6
+
7
+ The editor sends only changed fields. Keep unsupported triggers and stored
8
+ Destinations intact unless the user replaces them. Outcomes keep their order.
9
+ New messages can target only the current Destination or the creator.
10
+
11
+ Query refreshes must not replace an unsaved draft. After a stale save, the user
12
+ can keep their changed fields or load the latest saved version. Unchanged fields
13
+ come from that version; core checks its revision again on save.
14
+
15
+ The data router blocks navigation with unsaved changes. Browser unload uses the
16
+ native prompt. Save and Cancel keep the list query string.
17
+
18
+ Browser behavior lives in `e2e/automation-editor.spec.ts`. API and persistence
19
+ coverage lives in core's `tests/integration/api/automation-edits.test.ts`.
@@ -0,0 +1,110 @@
1
+ import type {
2
+ AutomationEdit,
3
+ AutomationScheduleIntent,
4
+ AutomationUpdate,
5
+ } from "@sentry/junior/api/schema";
6
+
7
+ export const weekdays = [
8
+ "sunday",
9
+ "monday",
10
+ "tuesday",
11
+ "wednesday",
12
+ "thursday",
13
+ "friday",
14
+ "saturday",
15
+ ] as const;
16
+ export type AutomationScheduleDraft =
17
+ | Extract<AutomationScheduleIntent, { kind: "recurring" }>
18
+ | {
19
+ kind: "one_off";
20
+ timezone: string;
21
+ timing: { type: "at"; date: string; time: string };
22
+ };
23
+
24
+ export type AutomationDraft = {
25
+ title: string;
26
+ instruction: string;
27
+ credentialMode: AutomationEdit["credentialMode"];
28
+ outcomes: NonNullable<AutomationUpdate["outcomes"]>;
29
+ schedule?: AutomationScheduleDraft;
30
+ trigger?: Extract<AutomationEdit, { kind: "event" }>["trigger"];
31
+ };
32
+
33
+ /** Keep stored values intact until a user edits that field. */
34
+ export function createAutomationDraft(value: AutomationEdit): AutomationDraft {
35
+ return {
36
+ title: value.title ?? "",
37
+ instruction: value.instruction,
38
+ credentialMode: value.credentialMode,
39
+ outcomes: value.outcomes,
40
+ trigger: value.kind === "event" ? value.trigger : undefined,
41
+ };
42
+ }
43
+
44
+ /** Populate calendar controls without changing the saved start date. */
45
+ export function scheduleDraft(
46
+ value: Extract<AutomationEdit, { kind: "scheduled" }>,
47
+ ): AutomationScheduleDraft {
48
+ const recurrence = value.schedule.recurrence;
49
+ if (value.schedule.kind === "recurring" && recurrence) {
50
+ const { time, weekdays: days, ...fields } = recurrence;
51
+ return {
52
+ kind: "recurring",
53
+ ...fields,
54
+ timezone: value.schedule.timezone,
55
+ time: `${String(time.hour).padStart(2, "0")}:${String(time.minute).padStart(2, "0")}`,
56
+ weekdays: days?.map((day) => weekdays[day]),
57
+ };
58
+ }
59
+ if (value.nextRunAtMs) {
60
+ const parts = new Intl.DateTimeFormat("en-CA", {
61
+ timeZone: value.schedule.timezone,
62
+ year: "numeric",
63
+ month: "2-digit",
64
+ day: "2-digit",
65
+ hour: "2-digit",
66
+ minute: "2-digit",
67
+ hourCycle: "h23",
68
+ }).formatToParts(value.nextRunAtMs);
69
+ const part = (name: string) =>
70
+ parts.find((p) => p.type === name)?.value ?? "";
71
+ return {
72
+ kind: "one_off",
73
+ timezone: value.schedule.timezone,
74
+ timing: {
75
+ type: "at",
76
+ date: `${part("year")}-${part("month")}-${part("day")}`,
77
+ time: `${part("hour")}:${part("minute")}`,
78
+ },
79
+ };
80
+ }
81
+ return {
82
+ kind: "recurring",
83
+ frequency: "daily",
84
+ time: "09:00",
85
+ timezone: value.schedule.timezone,
86
+ };
87
+ }
88
+
89
+ /** Keep metadata edits from replacing unchanged triggers or outcomes. */
90
+ export function automationDraftChanges(
91
+ original: AutomationEdit,
92
+ draft: AutomationDraft,
93
+ ): Partial<AutomationDraft> {
94
+ const changes: Partial<AutomationDraft> = {};
95
+ if (draft.title !== (original.title ?? "")) changes.title = draft.title;
96
+ if (draft.instruction !== original.instruction)
97
+ changes.instruction = draft.instruction;
98
+ if (draft.credentialMode !== original.credentialMode)
99
+ changes.credentialMode = draft.credentialMode;
100
+ if (JSON.stringify(draft.outcomes) !== JSON.stringify(original.outcomes))
101
+ changes.outcomes = draft.outcomes;
102
+ if (original.kind === "scheduled" && draft.schedule)
103
+ changes.schedule = draft.schedule;
104
+ if (
105
+ original.kind === "event" &&
106
+ JSON.stringify(draft.trigger) !== JSON.stringify(original.trigger)
107
+ )
108
+ changes.trigger = draft.trigger;
109
+ return changes;
110
+ }
@@ -0,0 +1,23 @@
1
+ import type {
2
+ AutomationSummary,
3
+ AutomationUpdate,
4
+ } from "@sentry/junior/api/schema";
5
+
6
+ type Outcome = NonNullable<AutomationUpdate["outcomes"]>[number];
7
+
8
+ /** Describe a retained Destination without guessing a recipient from a private id. */
9
+ export function automationOutcomeLabel(
10
+ outcome: Outcome,
11
+ destination: AutomationSummary["destination"],
12
+ ): string {
13
+ if (typeof outcome.destination === "string")
14
+ return outcome.destination === "task_creator"
15
+ ? "Creator · Direct message"
16
+ : `${destination.label} · ${destination.visibility}`;
17
+ if (
18
+ outcome.destination.channelId === destination.channelId &&
19
+ outcome.destination.teamId === destination.teamId
20
+ )
21
+ return `${destination.label} · ${destination.visibility}`;
22
+ return `Saved destination · ${outcome.destination.channelId}`;
23
+ }
@@ -1,3 +1,5 @@
1
+ import { FormNotice } from "../../components/FormNotice";
2
+ import { Select } from "../../components/Select";
1
3
  import { ObjectFixtures } from "./ObjectFixtures";
2
4
  import { ChartFixtures } from "./ChartFixtures";
3
5
  import {
@@ -33,6 +35,7 @@ import { DashboardHeader } from "../../components/layout/DashboardHeader";
33
35
  import { PageHeader } from "../../components/layout/PageHeader";
34
36
  import { MetricList, MetricValue } from "../../components/Metric";
35
37
  import { PageContentSkeleton } from "../../components/PageContentSkeleton";
38
+ import { ActorAvatar } from "../../components/ActorAvatar";
36
39
  import { ParticipantAvatarStack } from "../../components/ParticipantAvatarStack";
37
40
  import { StatCard } from "../../components/metrics/StatCard";
38
41
  import { StatusChip } from "../../components/StatusChip";
@@ -308,6 +311,34 @@ function FoundationsGalleryPage() {
308
311
  sectionId="foundations"
309
312
  title="Foundations"
310
313
  >
314
+ <Fixture title="Editor controls">
315
+ <Field
316
+ label="Instruction"
317
+ htmlFor="gallery-instruction"
318
+ error="Enter an instruction."
319
+ >
320
+ <TextArea
321
+ prose
322
+ id="gallery-instruction"
323
+ aria-invalid
324
+ aria-describedby="gallery-instruction-error"
325
+ />
326
+ </Field>
327
+ <Field label="Repeat" htmlFor="gallery-repeat">
328
+ <TextInput
329
+ size="comfortable"
330
+ aria-label="Resource identifier"
331
+ defaultValue="getsentry/junior#42"
332
+ />
333
+ <Select id="gallery-repeat">
334
+ <option>Every week</option>
335
+ </Select>
336
+ </Field>
337
+ <Button tone="primary">Save changes</Button>
338
+ <FormNotice title="This automation changed while you were editing.">
339
+ Your edits are still here. Review the latest version before saving.
340
+ </FormNotice>
341
+ </Fixture>
311
342
  <Fixture title="Conversation navigation">
312
343
  <DashboardChromeProvider>
313
344
  <DashboardHeader
@@ -563,19 +594,34 @@ function FoundationsGalleryPage() {
563
594
  />
564
595
  </div>
565
596
  </Fixture>
566
- <Fixture title="Status chips">
567
- <div className="flex flex-wrap items-center gap-2">
597
+ <Fixture title="Status labels">
598
+ <div className="flex flex-wrap items-center gap-x-5 gap-y-3">
568
599
  <StatusChip tone="neutral">private</StatusChip>
569
600
  <StatusChip tone="success">completed</StatusChip>
570
601
  <StatusChip tone="danger">failed</StatusChip>
571
602
  <StatusChip tone="warning">blocked</StatusChip>
572
603
  <StatusChip tone="info">preference</StatusChip>
573
604
  <StatusChip tone="accent">knowledge</StatusChip>
574
- <StatusChip size="compact" tone="success">
575
- public
605
+ <StatusChip size="compact" tone="warning">
606
+ Trigger unavailable
576
607
  </StatusChip>
577
608
  </div>
578
609
  </Fixture>
610
+ <Fixture title="Actor avatars">
611
+ <div className="flex items-center gap-4">
612
+ <ActorAvatar
613
+ name="Ada Lovelace"
614
+ imageUrl="/_junior/dashboard/avatar.png"
615
+ size="detail"
616
+ />
617
+ <ActorAvatar name="Grace Hopper" size="detail" />
618
+ <ActorAvatar
619
+ name="Alan Turing"
620
+ imageUrl="/missing-avatar.png"
621
+ size="list"
622
+ />
623
+ </div>
624
+ </Fixture>
579
625
  <Fixture title="Participant avatars">
580
626
  <div className="flex flex-wrap items-center gap-8">
581
627
  <ParticipantAvatarStack
@@ -39,9 +39,10 @@ export function parseSearchParamEnum<T extends string>(
39
39
  /** Read and write one debounced free-text query param (default `q`). */
40
40
  export function useDebouncedSearchParam(
41
41
  key = "q",
42
- options: { delayMs?: number } = {},
42
+ options: { delayMs?: number; resetPage?: boolean } = {},
43
43
  ) {
44
44
  const delayMs = options.delayMs ?? 250;
45
+ const resetPage = options.resetPage ?? false;
45
46
  const [searchParams, setSearchParams] = useSearchParams();
46
47
  const committed = searchParams.get(key)?.trim() ?? "";
47
48
  const [value, setValue] = useState(committed);
@@ -58,6 +59,7 @@ export function useDebouncedSearchParam(
58
59
  setSearchParams(
59
60
  (current) => {
60
61
  const next = new URLSearchParams(current);
62
+ if (resetPage) next.delete("page");
61
63
  if (normalized) next.set(key, normalized);
62
64
  else next.delete(key);
63
65
  return next;
@@ -66,7 +68,7 @@ export function useDebouncedSearchParam(
66
68
  );
67
69
  }, delayMs);
68
70
  return () => window.clearTimeout(timeout);
69
- }, [committed, delayMs, key, setSearchParams, value]);
71
+ }, [committed, delayMs, key, resetPage, setSearchParams, value]);
70
72
 
71
73
  return [value, setValue, committed] as const;
72
74
  }
package/src/client.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
2
  import { Component, type ErrorInfo, type ReactNode } from "react";
3
3
  import { createRoot } from "react-dom/client";
4
- import { BrowserRouter } from "react-router";
4
+ import { createBrowserRouter, RouterProvider } from "react-router";
5
5
 
6
6
  import { DashboardShell } from "./client/App";
7
7
  import { getDashboardAgentName } from "./client/agentName";
@@ -79,12 +79,15 @@ const queryClient = new QueryClient({
79
79
  },
80
80
  });
81
81
 
82
+ const router = createBrowserRouter(
83
+ [{ path: "*", element: <DashboardShell /> }],
84
+ { basename: window.__JUNIOR_DASHBOARD_BASE_PATH__ ?? "/" },
85
+ );
86
+
82
87
  createRoot(root).render(
83
88
  <DashboardErrorBoundary>
84
89
  <QueryClientProvider client={queryClient}>
85
- <BrowserRouter basename={window.__JUNIOR_DASHBOARD_BASE_PATH__ ?? "/"}>
86
- <DashboardShell />
87
- </BrowserRouter>
90
+ <RouterProvider router={router} />
88
91
  </QueryClientProvider>
89
92
  </DashboardErrorBoundary>,
90
93
  );
@@ -0,0 +1,101 @@
1
+ import {
2
+ automationListQuerySchema,
3
+ type AutomationList,
4
+ type AutomationSummary,
5
+ } from "@sentry/junior/api/schema";
6
+
7
+ function creator(automation: AutomationSummary) {
8
+ return automation.createdByEmail ?? automation.createdBy;
9
+ }
10
+ function destination(automation: AutomationSummary) {
11
+ return `${automation.destination.teamId}:${automation.destination.channelId}`;
12
+ }
13
+
14
+ /** Apply web collection controls to local preview and browser fixtures. */
15
+ export function mockAutomationCollection(
16
+ report: Pick<
17
+ AutomationList,
18
+ "automations" | "executionDays" | "executionHours" | "executionSixHours"
19
+ >,
20
+ params: URLSearchParams = new URLSearchParams(),
21
+ ): AutomationList {
22
+ const input = automationListQuerySchema.parse(Object.fromEntries(params));
23
+ const all = report.automations;
24
+ const filtered = all
25
+ .filter((automation) => {
26
+ const search =
27
+ `${automation.title} ${automation.instruction} ${automation.kind === "event" ? automation.resource : ""}`.toLowerCase();
28
+ return (
29
+ (input.scope !== "mine" || automation.ownedByViewer) &&
30
+ (input.scope !== "public" ||
31
+ automation.destination.visibility === "public") &&
32
+ (input.scope !== "attention" ||
33
+ (automation.status !== "paused" &&
34
+ automation.status !== "completed" &&
35
+ (automation.status === "blocked" ||
36
+ (automation.kind === "event" && !automation.triggerAvailable) ||
37
+ automation.lastRunStatus === "failed" ||
38
+ automation.lastRunStatus === "blocked"))) &&
39
+ (input.type === "all" || automation.kind === input.type) &&
40
+ (input.state === "all" ||
41
+ (input.state === "unavailable"
42
+ ? automation.kind === "event" && !automation.triggerAvailable
43
+ : automation.status === input.state)) &&
44
+ (!input.creator || creator(automation) === input.creator) &&
45
+ (!input.destination || destination(automation) === input.destination) &&
46
+ (!input.q || search.includes(input.q.toLowerCase()))
47
+ );
48
+ })
49
+ .sort((a, b) => {
50
+ const order =
51
+ input.sort === "title"
52
+ ? a.title.localeCompare(b.title)
53
+ : input.sort === "oldest"
54
+ ? a.createdAt.localeCompare(b.createdAt)
55
+ : b.createdAt.localeCompare(a.createdAt);
56
+ return order || b.id.localeCompare(a.id);
57
+ });
58
+ const page = Math.min(
59
+ input.page,
60
+ Math.max(1, Math.ceil(filtered.length / input.pageSize)),
61
+ );
62
+ return {
63
+ ...report,
64
+ automations: filtered.slice(
65
+ (page - 1) * input.pageSize,
66
+ page * input.pageSize,
67
+ ),
68
+ total: filtered.length,
69
+ page,
70
+ pageSize: input.pageSize,
71
+ counts: {
72
+ all: all.length,
73
+ mine: all.filter((automation) => automation.ownedByViewer).length,
74
+ public: all.filter(
75
+ (automation) => automation.destination.visibility === "public",
76
+ ).length,
77
+ private: all.filter(
78
+ (automation) => automation.destination.visibility === "private",
79
+ ).length,
80
+ },
81
+ creators: [
82
+ ...new Map(
83
+ all.map((automation) => [
84
+ creator(automation),
85
+ { value: creator(automation), label: automation.createdBy },
86
+ ]),
87
+ ).values(),
88
+ ],
89
+ destinations: [
90
+ ...new Map(
91
+ all.map((automation) => [
92
+ destination(automation),
93
+ {
94
+ value: destination(automation),
95
+ label: automation.destination.label,
96
+ },
97
+ ]),
98
+ ).values(),
99
+ ],
100
+ };
101
+ }
@@ -1,3 +1,4 @@
1
+ import { mockAutomationCollection } from "./automation-collection";
1
2
  /** Deterministic reporting fixtures for local dashboard development and QA. */
2
3
  import type {
3
4
  ActorDirectoryReport,
@@ -2669,8 +2670,10 @@ function mockTasks(): AutomationSummary[] {
2669
2670
  lastConversationId: "scheduler:daily-ops-digest",
2670
2671
  lastRunAt: "2026-08-06T16:00:00.000Z",
2671
2672
  nextRunAt: "2026-08-10T16:00:00.000Z",
2673
+ credentialMode: "creator",
2672
2674
  ownedByViewer: true,
2673
2675
  runs: { 1: 1, 7: 3, 30: 12, 90: 48 },
2676
+ timezone: "America/Los_Angeles",
2674
2677
  schedule: "Every Monday at 9:00 AM",
2675
2678
  status: "active",
2676
2679
  outcomes: [
@@ -2698,6 +2701,7 @@ function mockTasks(): AutomationSummary[] {
2698
2701
  kind: "event",
2699
2702
  lastConversationId: "agent-dispatch:event-1",
2700
2703
  lastRunAt: "2026-08-05T18:30:00.000Z",
2704
+ credentialMode: "creator",
2701
2705
  ownedByViewer: true,
2702
2706
  resource: "Issue · ACME-42",
2703
2707
  runs: { 1: 0, 7: 1, 30: 4, 90: 7 },
@@ -2706,6 +2710,7 @@ function mockTasks(): AutomationSummary[] {
2706
2710
  title: "Closed issue summary",
2707
2711
  totalRuns: 7,
2708
2712
  triggerAvailable: true,
2713
+ status: "active",
2709
2714
  },
2710
2715
  {
2711
2716
  createdAt: "2026-07-30T16:00:00.000Z",
@@ -2721,6 +2726,7 @@ function mockTasks(): AutomationSummary[] {
2721
2726
  id: "event-2",
2722
2727
  instruction: "Notify responders when the incident changes",
2723
2728
  kind: "event",
2729
+ credentialMode: "creator",
2724
2730
  ownedByViewer: false,
2725
2731
  resource: "Incident · INC-17",
2726
2732
  runs: { 1: 0, 7: 0, 30: 0, 90: 0 },
@@ -2734,13 +2740,14 @@ function mockTasks(): AutomationSummary[] {
2734
2740
  title: "Incident change alerts",
2735
2741
  totalRuns: 0,
2736
2742
  triggerAvailable: false,
2743
+ status: "active",
2737
2744
  },
2738
2745
  ];
2739
2746
  }
2740
2747
 
2741
2748
  /** Build mock Tasks list for local dashboard development. */
2742
2749
  export function readMockAutomationList(nowMs = NOW_MS): AutomationList {
2743
- return {
2750
+ return mockAutomationCollection({
2744
2751
  executionDays: mockAutomationExecutionDays(nowMs),
2745
2752
  executionHours: trailingMetricHours(nowMs, (date) => ({
2746
2753
  costUsd: 0,
@@ -2759,8 +2766,7 @@ export function readMockAutomationList(nowMs = NOW_MS): AutomationList {
2759
2766
  (date) => ({ costUsd: 0, date, event: 0, scheduled: 0 }),
2760
2767
  ),
2761
2768
  automations: mockTasks(),
2762
- truncated: false,
2763
- };
2769
+ });
2764
2770
  }
2765
2771
 
2766
2772
  function mockStatusDays(
@@ -1,3 +1,4 @@
1
+ import { mockAutomationCollection } from "./automation-collection";
1
2
  import { Hono } from "hono";
2
3
  import { jsonResponse, type JuniorApiVariables } from "@sentry/junior/api";
3
4
  import {
@@ -26,6 +27,7 @@ import {
26
27
  automationExecutionListSchema,
27
28
  automationListSchema,
28
29
  automationParamsSchema,
30
+ automationSummarySchema,
29
31
  automationRunListSchema,
30
32
  } from "@sentry/junior/api/schema";
31
33
  import { mockChartPng } from "./chart-png";
@@ -253,15 +255,10 @@ export function createMockReportingApi(): Hono<{
253
255
  });
254
256
  app.get("/automations", (c) => {
255
257
  const report = readMockAutomationList();
256
- const query = c.req.query("q")?.trim().toLowerCase();
257
- return jsonResponse(automationListSchema, {
258
- ...report,
259
- automations: query
260
- ? report.automations.filter((automation) =>
261
- automation.title.toLowerCase().includes(query),
262
- )
263
- : report.automations,
264
- });
258
+ return jsonResponse(
259
+ automationListSchema,
260
+ mockAutomationCollection(report, new URL(c.req.url).searchParams),
261
+ );
265
262
  });
266
263
  app.get("/automations/runs", () => {
267
264
  const automations = readMockAutomationList().automations;
@@ -284,6 +281,14 @@ export function createMockReportingApi(): Hono<{
284
281
  truncated: false,
285
282
  });
286
283
  });
284
+ app.get("/automations/:id", (c) => {
285
+ const automation = readMockAutomationList().automations.find(
286
+ (automation) => automation.id === c.req.param("id"),
287
+ );
288
+ return automation
289
+ ? jsonResponse(automationSummarySchema, automation)
290
+ : errorResponse("Automation not found.", 404);
291
+ });
287
292
  app.get("/automations/:kind/:id/executions", (c) => {
288
293
  const params = automationParamsSchema.safeParse(c.req.param());
289
294
  if (!params.success) {