@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.
- package/dist/app.js +89 -15
- package/dist/client.js +55 -52
- package/dist/index.js +89 -15
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- package/src/client/App.tsx +359 -345
- package/src/client/api.ts +17 -1
- package/src/client/components/ActorAvatar.tsx +37 -0
- package/src/client/components/Button.tsx +12 -10
- package/src/client/components/Field.tsx +14 -1
- package/src/client/components/FormNotice.tsx +40 -0
- package/src/client/components/ParticipantAvatarStack.tsx +6 -14
- package/src/client/components/README.md +11 -2
- package/src/client/components/Select.tsx +18 -0
- package/src/client/components/StatusChip.tsx +15 -12
- package/src/client/components/TextInput.tsx +25 -5
- package/src/client/http.ts +31 -3
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +71 -54
- package/src/client/pages/automations/AutomationEditPage.tsx +172 -0
- package/src/client/pages/automations/AutomationEditor.tsx +397 -0
- package/src/client/pages/automations/AutomationEditorLoading.tsx +18 -0
- package/src/client/pages/automations/AutomationEventFields.tsx +264 -0
- package/src/client/pages/automations/AutomationFilters.tsx +171 -0
- package/src/client/pages/automations/AutomationFormSection.tsx +22 -0
- package/src/client/pages/automations/AutomationLifecycle.tsx +146 -0
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +177 -0
- package/src/client/pages/automations/AutomationRow.tsx +358 -0
- package/src/client/pages/automations/AutomationScheduleFields.tsx +328 -0
- package/src/client/pages/automations/AutomationsPage.tsx +157 -408
- package/src/client/pages/automations/AutomationsPageLayout.tsx +16 -5
- package/src/client/pages/automations/README.md +19 -0
- package/src/client/pages/automations/automationDraft.ts +110 -0
- package/src/client/pages/automations/automationOutcomes.ts +23 -0
- package/src/client/pages/dev/ComponentsPage.tsx +50 -4
- package/src/client/searchParams.ts +4 -2
- package/src/client.tsx +7 -4
- package/src/mock-reporting/automation-collection.ts +101 -0
- package/src/mock-reporting/fixtures.ts +9 -3
- 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 (
|
|
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
|
-
<
|
|
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
|
|
567
|
-
<div className="flex flex-wrap items-center gap-
|
|
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="
|
|
575
|
-
|
|
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 {
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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) {
|