@sentry/junior-dashboard 0.233.0 → 0.235.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 +178 -29
- package/dist/client.js +53 -50
- package/dist/index.js +178 -29
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- package/src/assets/junior-avatar.png +0 -0
- package/src/assets/junior-favicon.png +0 -0
- package/src/assets.ts +1 -0
- package/src/client/App.tsx +372 -345
- package/src/client/api.ts +60 -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 +19 -6
- package/src/client/components/SegmentedNav.tsx +167 -0
- package/src/client/components/SegmentedTabs.tsx +24 -5
- 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/components/layout/SecondaryNavigation.tsx +40 -83
- package/src/client/http.ts +31 -3
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +78 -68
- package/src/client/pages/automations/AutomationEditPage.tsx +171 -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 +179 -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 +343 -0
- package/src/client/pages/automations/AutomationScheduleFields.tsx +328 -0
- package/src/client/pages/automations/AutomationsPage.tsx +186 -439
- package/src/client/pages/automations/AutomationsPageLayout.tsx +19 -7
- package/src/client/pages/automations/README.md +19 -0
- package/src/client/pages/automations/automationDraft.ts +110 -0
- package/src/client/pages/automations/automationFormat.ts +43 -0
- package/src/client/pages/automations/automationOutcomes.ts +23 -0
- package/src/client/pages/code/CodeChangeRow.tsx +41 -0
- package/src/client/pages/code/CodePage.tsx +18 -93
- package/src/client/pages/code/CodeSummaryCards.tsx +53 -0
- package/src/client/pages/code/RepositoryPage.tsx +227 -0
- package/src/client/pages/code/RepositorySections.tsx +108 -0
- package/src/client/pages/code/RepositoryTabs.tsx +146 -0
- package/src/client/pages/code/codeRepositoryRoutes.ts +18 -0
- package/src/client/pages/dev/ComponentsPage.tsx +55 -26
- package/src/client/pages/dev/SegmentedFixtures.tsx +58 -0
- package/src/client/pages/memory/MemoryPageLayout.tsx +12 -13
- package/src/client/pages/system/SystemNavigation.tsx +6 -2
- package/src/client/pages/system/SystemPageLayout.tsx +4 -6
- 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/code-repository.ts +77 -0
- package/src/mock-reporting/fixtures.ts +9 -3
- package/src/mock-reporting/routes.ts +39 -17
- package/src/shell.ts +21 -5
|
@@ -24,20 +24,19 @@ function isMemoriesLibraryPath(pathname: string): boolean {
|
|
|
24
24
|
/** Place the shared secondary navigation above one Memories page. */
|
|
25
25
|
export function MemoryPageLayout(props: { children: ReactNode }) {
|
|
26
26
|
return (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
/>
|
|
27
|
+
<SecondaryNavigation
|
|
28
|
+
ariaLabel="Memory navigation"
|
|
29
|
+
items={[
|
|
30
|
+
{ end: true, label: "Overview", to: MEMORY_BASE_PATH },
|
|
31
|
+
{
|
|
32
|
+
isActive: isMemoriesLibraryPath,
|
|
33
|
+
label: "Memories",
|
|
34
|
+
to: MEMORY_LIBRARY_PATH,
|
|
35
|
+
},
|
|
36
|
+
]}
|
|
37
|
+
>
|
|
39
38
|
<PageLayout className="gap-6 sm:gap-8">{props.children}</PageLayout>
|
|
40
|
-
|
|
39
|
+
</SecondaryNavigation>
|
|
41
40
|
);
|
|
42
41
|
}
|
|
43
42
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
1
3
|
import { SecondaryNavigation } from "../../components/layout/SecondaryNavigation";
|
|
2
4
|
import { systemPluginsPath } from "./SystemPlugins";
|
|
3
5
|
|
|
@@ -10,11 +12,13 @@ const systemNavigationItems = [
|
|
|
10
12
|
];
|
|
11
13
|
|
|
12
14
|
/** Render the stable secondary navigation shared by System pages. */
|
|
13
|
-
export function SystemNavigation() {
|
|
15
|
+
export function SystemNavigation(props: { children: ReactNode }) {
|
|
14
16
|
return (
|
|
15
17
|
<SecondaryNavigation
|
|
16
18
|
ariaLabel="System navigation"
|
|
17
19
|
items={systemNavigationItems}
|
|
18
|
-
|
|
20
|
+
>
|
|
21
|
+
{props.children}
|
|
22
|
+
</SecondaryNavigation>
|
|
19
23
|
);
|
|
20
24
|
}
|
|
@@ -8,10 +8,9 @@ import { SystemNavigation } from "./SystemNavigation";
|
|
|
8
8
|
/** Place the shared secondary navigation above one System page. */
|
|
9
9
|
export function SystemPageLayout(props: { children: ReactNode }) {
|
|
10
10
|
return (
|
|
11
|
-
|
|
12
|
-
<SystemNavigation />
|
|
11
|
+
<SystemNavigation>
|
|
13
12
|
<PageLayout>{props.children}</PageLayout>
|
|
14
|
-
|
|
13
|
+
</SystemNavigation>
|
|
15
14
|
);
|
|
16
15
|
}
|
|
17
16
|
|
|
@@ -23,9 +22,8 @@ export function SystemRouteLoading(props: {
|
|
|
23
22
|
variant?: PageContentSkeletonVariant;
|
|
24
23
|
}) {
|
|
25
24
|
return (
|
|
26
|
-
|
|
27
|
-
<SystemNavigation />
|
|
25
|
+
<SystemNavigation>
|
|
28
26
|
<PageRouteLoading {...props} />
|
|
29
|
-
|
|
27
|
+
</SystemNavigation>
|
|
30
28
|
);
|
|
31
29
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CodeRepositoryReport,
|
|
3
|
+
ConversationFeed,
|
|
4
|
+
} from "@sentry/junior/api/schema";
|
|
5
|
+
|
|
6
|
+
import { readMockCodeOverview } from "./fixtures";
|
|
7
|
+
|
|
8
|
+
const MOCK_WORKSPACES: Record<string, CodeRepositoryReport["workspaces"]> = {
|
|
9
|
+
"getsentry/junior": [{ id: "workspace-junior", name: "junior" }],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Build one mock repository report from the mock Code overview. */
|
|
13
|
+
export function readMockCodeRepository(
|
|
14
|
+
repositoryId: string,
|
|
15
|
+
): CodeRepositoryReport | undefined {
|
|
16
|
+
const overview = readMockCodeOverview();
|
|
17
|
+
const repository = overview.repositories.find(
|
|
18
|
+
(entry) => entry.id === repositoryId,
|
|
19
|
+
);
|
|
20
|
+
if (!repository) return undefined;
|
|
21
|
+
return {
|
|
22
|
+
activityDays: overview.activityDays,
|
|
23
|
+
changes: overview.changes.filter(
|
|
24
|
+
(change) => change.repository === repository.name,
|
|
25
|
+
),
|
|
26
|
+
generatedAt: overview.generatedAt,
|
|
27
|
+
repository: {
|
|
28
|
+
id: repository.id,
|
|
29
|
+
name: repository.name,
|
|
30
|
+
provider: repository.provider,
|
|
31
|
+
...(repository.url ? { url: repository.url } : undefined),
|
|
32
|
+
},
|
|
33
|
+
summary: {
|
|
34
|
+
closed: repository.closed,
|
|
35
|
+
created: repository.created,
|
|
36
|
+
merged: repository.merged,
|
|
37
|
+
open: repository.open,
|
|
38
|
+
...(repository.mergeRate === undefined
|
|
39
|
+
? undefined
|
|
40
|
+
: { mergeRate: repository.mergeRate }),
|
|
41
|
+
...(repository.medianCostUsd === undefined
|
|
42
|
+
? undefined
|
|
43
|
+
: {
|
|
44
|
+
// Mock rows have no total cost; estimate it from the median.
|
|
45
|
+
costUsd: repository.medianCostUsd * repository.created,
|
|
46
|
+
medianCostUsd: repository.medianCostUsd,
|
|
47
|
+
}),
|
|
48
|
+
...(overview.summary.medianMergeTimeMs === undefined
|
|
49
|
+
? undefined
|
|
50
|
+
: { medianMergeTimeMs: overview.summary.medianMergeTimeMs }),
|
|
51
|
+
},
|
|
52
|
+
windowEnd: overview.windowEnd,
|
|
53
|
+
windowStart: overview.windowStart,
|
|
54
|
+
workspaces: MOCK_WORKSPACES[repository.name] ?? [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Keep mock conversations whose code change annotations name the repository.
|
|
60
|
+
* The real API links conversations through recorded code changes.
|
|
61
|
+
*/
|
|
62
|
+
export function filterMockConversationsByRepository(
|
|
63
|
+
feed: ConversationFeed,
|
|
64
|
+
repositoryId: string,
|
|
65
|
+
): ConversationFeed {
|
|
66
|
+
const name = readMockCodeRepository(repositoryId)?.repository.name;
|
|
67
|
+
return {
|
|
68
|
+
...feed,
|
|
69
|
+
conversations: name
|
|
70
|
+
? feed.conversations.filter((conversation) =>
|
|
71
|
+
conversation.sidebarAnnotations?.some((annotation) =>
|
|
72
|
+
annotation.key.startsWith(`${name}#`),
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
: [],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -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 {
|
|
@@ -17,6 +18,8 @@ import {
|
|
|
17
18
|
conversationStatsReportSchema,
|
|
18
19
|
codeOverviewReportSchema,
|
|
19
20
|
codePersonReportSchema,
|
|
21
|
+
codeRepositoryParamsSchema,
|
|
22
|
+
codeRepositoryReportSchema,
|
|
20
23
|
locationDetailReportSchema,
|
|
21
24
|
locationDirectoryReportSchema,
|
|
22
25
|
locationParamsSchema,
|
|
@@ -26,9 +29,14 @@ import {
|
|
|
26
29
|
automationExecutionListSchema,
|
|
27
30
|
automationListSchema,
|
|
28
31
|
automationParamsSchema,
|
|
32
|
+
automationSummarySchema,
|
|
29
33
|
automationRunListSchema,
|
|
30
34
|
} from "@sentry/junior/api/schema";
|
|
31
35
|
import { mockChartPng } from "./chart-png";
|
|
36
|
+
import {
|
|
37
|
+
filterMockConversationsByRepository,
|
|
38
|
+
readMockCodeRepository,
|
|
39
|
+
} from "./code-repository";
|
|
32
40
|
import {
|
|
33
41
|
readMockConversationDetail,
|
|
34
42
|
readMockConversationEvents,
|
|
@@ -62,6 +70,16 @@ export function createMockReportingApi(): Hono<{
|
|
|
62
70
|
app.get("/code", () =>
|
|
63
71
|
jsonResponse(codeOverviewReportSchema, readMockCodeOverview()),
|
|
64
72
|
);
|
|
73
|
+
app.get("/code/repositories/:repositoryId", (c) => {
|
|
74
|
+
const params = codeRepositoryParamsSchema.safeParse(c.req.param());
|
|
75
|
+
if (!params.success) {
|
|
76
|
+
return errorResponse("Invalid route parameters.", 400);
|
|
77
|
+
}
|
|
78
|
+
const report = readMockCodeRepository(params.data.repositoryId);
|
|
79
|
+
return report
|
|
80
|
+
? jsonResponse(codeRepositoryReportSchema, report)
|
|
81
|
+
: errorResponse("Repository not found.", 404);
|
|
82
|
+
});
|
|
65
83
|
app.get("/plugin-reports", () =>
|
|
66
84
|
jsonResponse(pluginOperationalReportFeedSchema, readMockPluginReports()),
|
|
67
85
|
);
|
|
@@ -126,17 +144,18 @@ export function createMockReportingApi(): Hono<{
|
|
|
126
144
|
if (!query.success) {
|
|
127
145
|
return errorResponse("Invalid query parameters.", 400);
|
|
128
146
|
}
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
query.data.status
|
|
132
|
-
|
|
147
|
+
const repositoryId = query.data.codeRepositoryId;
|
|
148
|
+
const readFeed = (status: "active" | "archived") => {
|
|
149
|
+
const feed = readMockConversationFeed(query.data.actorEmail, status);
|
|
150
|
+
return repositoryId
|
|
151
|
+
? filterMockConversationsByRepository(feed, repositoryId)
|
|
152
|
+
: feed;
|
|
153
|
+
};
|
|
154
|
+
const report = readFeed(query.data.status);
|
|
133
155
|
if (!query.data.q) {
|
|
134
156
|
return jsonResponse(conversationFeedSchema, report);
|
|
135
157
|
}
|
|
136
|
-
const archived =
|
|
137
|
-
query.data.actorEmail,
|
|
138
|
-
"archived",
|
|
139
|
-
);
|
|
158
|
+
const archived = readFeed("archived");
|
|
140
159
|
const search = query.data.q.toLowerCase();
|
|
141
160
|
const conversations = new Map(
|
|
142
161
|
[...report.conversations, ...archived.conversations].map(
|
|
@@ -253,15 +272,10 @@ export function createMockReportingApi(): Hono<{
|
|
|
253
272
|
});
|
|
254
273
|
app.get("/automations", (c) => {
|
|
255
274
|
const report = readMockAutomationList();
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
? report.automations.filter((automation) =>
|
|
261
|
-
automation.title.toLowerCase().includes(query),
|
|
262
|
-
)
|
|
263
|
-
: report.automations,
|
|
264
|
-
});
|
|
275
|
+
return jsonResponse(
|
|
276
|
+
automationListSchema,
|
|
277
|
+
mockAutomationCollection(report, new URL(c.req.url).searchParams),
|
|
278
|
+
);
|
|
265
279
|
});
|
|
266
280
|
app.get("/automations/runs", () => {
|
|
267
281
|
const automations = readMockAutomationList().automations;
|
|
@@ -284,6 +298,14 @@ export function createMockReportingApi(): Hono<{
|
|
|
284
298
|
truncated: false,
|
|
285
299
|
});
|
|
286
300
|
});
|
|
301
|
+
app.get("/automations/:id", (c) => {
|
|
302
|
+
const automation = readMockAutomationList().automations.find(
|
|
303
|
+
(automation) => automation.id === c.req.param("id"),
|
|
304
|
+
);
|
|
305
|
+
return automation
|
|
306
|
+
? jsonResponse(automationSummarySchema, automation)
|
|
307
|
+
: errorResponse("Automation not found.", 404);
|
|
308
|
+
});
|
|
287
309
|
app.get("/automations/:kind/:id/executions", (c) => {
|
|
288
310
|
const params = automationParamsSchema.safeParse(c.req.param());
|
|
289
311
|
if (!params.success) {
|
package/src/shell.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import {
|
|
4
4
|
dashboardAvatarHeaderAsset,
|
|
5
|
+
dashboardFaviconAsset,
|
|
5
6
|
dashboardClientAsset,
|
|
6
7
|
dashboardClientChunks,
|
|
7
8
|
dashboardInstallIconAsset,
|
|
@@ -120,6 +121,7 @@ export function dashboardPagePaths(
|
|
|
120
121
|
const paths: Array<{ nested?: boolean; path: string }> = [
|
|
121
122
|
{ path: basePath },
|
|
122
123
|
{
|
|
124
|
+
nested: true,
|
|
123
125
|
path: basePath === "/" ? "/code" : `${basePath}/code`,
|
|
124
126
|
},
|
|
125
127
|
{
|
|
@@ -298,12 +300,26 @@ export function renderDashboard(basePath: string, agentName: string): Response {
|
|
|
298
300
|
);
|
|
299
301
|
}
|
|
300
302
|
|
|
301
|
-
|
|
303
|
+
function readDashboardFavicon(): ArrayBuffer {
|
|
304
|
+
if (dashboardFaviconAsset) {
|
|
305
|
+
return Uint8Array.from(Buffer.from(dashboardFaviconAsset, "base64")).buffer;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const assetUrl = new URL("./assets/junior-favicon.png", import.meta.url);
|
|
309
|
+
if (!existsSync(assetUrl)) {
|
|
310
|
+
throw new Error("Junior dashboard favicon was not found");
|
|
311
|
+
}
|
|
312
|
+
return Uint8Array.from(readFileSync(assetUrl)).buffer;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Serve the head-only dashboard favicon, which stays readable at 16px. */
|
|
302
316
|
export function renderFavicon(): Response {
|
|
303
|
-
return new Response(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
317
|
+
return new Response(readDashboardFavicon(), {
|
|
318
|
+
headers: {
|
|
319
|
+
"cache-control": "public, max-age=0, must-revalidate",
|
|
320
|
+
"content-type": "image/png",
|
|
321
|
+
},
|
|
322
|
+
});
|
|
307
323
|
}
|
|
308
324
|
|
|
309
325
|
/** Render a browser-readable access denied page for blocked dashboard users. */
|