@sentry/junior-dashboard 0.259.0 → 0.260.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/README.md +5 -3
- package/dist/app.js +88 -37
- package/dist/chunks/github-light-QHW3AEMR.js +1 -0
- package/dist/client.js +86 -50
- package/dist/index.js +88 -37
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- package/src/client/App.tsx +0 -1
- package/src/client/code.tsx +10 -4
- package/src/client/components/ProfileMenu.tsx +3 -0
- package/src/client/components/README.md +12 -2
- package/src/client/components/ThemeSwitcher.tsx +48 -0
- package/src/client/components/charts/ActivityChart.tsx +6 -7
- package/src/client/components/charts/InputCacheChart.tsx +1 -1
- package/src/client/components/charts/SystemMetricCharts.tsx +3 -3
- package/src/client/components/charts/WorkspaceUsageChart.tsx +1 -1
- package/src/client/components/charts/cache-input.ts +11 -3
- package/src/client/components/layout/DashboardHeader.tsx +2 -9
- package/src/client/conversations/ConversationBrief.tsx +35 -9
- package/src/client/conversations/ConversationHomeList.tsx +7 -1
- package/src/client/conversations/ConversationMeta.tsx +19 -7
- package/src/client/conversations/ConversationTranscript.tsx +8 -11
- package/src/client/conversations/EventDetails.tsx +9 -5
- package/src/client/conversations/README.md +6 -0
- package/src/client/conversations/TranscriptContextEventView.tsx +5 -1
- package/src/client/conversations/TranscriptEventView.tsx +48 -9
- package/src/client/conversations/TranscriptMessageView.tsx +6 -1
- package/src/client/conversations/TranscriptStructuredEventView.tsx +4 -2
- package/src/client/conversations/TranscriptText.tsx +28 -20
- package/src/client/conversations/conversationActivityPreview.ts +6 -0
- package/src/client/conversations/eventTranscript.ts +4 -1
- package/src/client/conversations/transcriptMentions.tsx +27 -10
- package/src/client/pages/automations/AutomationCostChart.tsx +2 -2
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +4 -1
- package/src/client/pages/automations/AutomationExecutionChart.tsx +2 -2
- package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +3 -3
- package/src/client/pages/code/CodeActivityChart.tsx +4 -4
- package/src/client/pages/dev/ComponentsPage.tsx +7 -3
- package/src/client/pages/dev/ConversationFixture.tsx +29 -0
- package/src/client/pages/locations/LocationActivityChart.tsx +14 -4
- package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +14 -9
- package/src/client/pages/memory/MemoryCostChart.tsx +5 -5
- package/src/client/pages/memory/MemoryDetailsDrawer.tsx +9 -2
- package/src/client/pages/memory/MemoryTimeline.tsx +2 -2
- package/src/client/pages/people/ContributionGrid.tsx +8 -8
- package/src/client/pages/people/PeopleActivityChart.tsx +23 -10
- package/src/client/pages/system/BaseSandboxCard.tsx +76 -0
- package/src/client/pages/system/ConversationActivityChart.tsx +2 -2
- package/src/client/pages/system/PluginBarChart.tsx +12 -6
- package/src/client/pages/system/SandboxSizeSummary.tsx +47 -0
- package/src/client/pages/system/WorkspaceDetails.tsx +11 -1
- package/src/client/pages/system/WorkspaceEditor.tsx +19 -0
- package/src/client/pages/system/WorkspaceFormPage.tsx +28 -19
- package/src/client/pages/system/WorkspaceList.tsx +6 -1
- package/src/client/pages/system/WorkspacesPage.tsx +18 -12
- package/src/client/pages/system/workspaceDraft.ts +8 -0
- package/src/client/theme.ts +52 -0
- package/src/client/types.ts +8 -1
- package/src/mock-reporting/fixtures.ts +48 -35
- package/src/shell.ts +4 -1
- package/src/tailwind.css +109 -0
- package/src/theme.ts +49 -0
- package/src/client/pages/system/BaselineSnapshotCard.tsx +0 -18
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { BaselineSnapshotReport } from "@sentry/junior/api/schema";
|
|
3
|
+
|
|
4
|
+
import { Card } from "../../components/layout/Card";
|
|
5
|
+
import { formatMs, formatRelativeTime, formatTime } from "../../format";
|
|
6
|
+
import { formatSandboxSize } from "./SandboxSizeSummary";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Show the base Sandbox: the default size and the baseline snapshot.
|
|
10
|
+
* Workspaces inherit the base setup and use the default size unless they set
|
|
11
|
+
* vCPUs.
|
|
12
|
+
*/
|
|
13
|
+
export function BaseSandboxCard(props: {
|
|
14
|
+
defaultVcpus: number | null;
|
|
15
|
+
snapshot: BaselineSnapshotReport | null;
|
|
16
|
+
}) {
|
|
17
|
+
const snapshot = props.snapshot;
|
|
18
|
+
return (
|
|
19
|
+
<Card as="section" className="mb-0" padding="md" variant="section">
|
|
20
|
+
<div aria-labelledby="base-sandbox-heading" className="grid gap-4">
|
|
21
|
+
<div className="min-w-0 grid gap-1">
|
|
22
|
+
<h2
|
|
23
|
+
className="m-0 text-base font-semibold text-dashboard-text"
|
|
24
|
+
id="base-sandbox-heading"
|
|
25
|
+
>
|
|
26
|
+
Base Sandbox
|
|
27
|
+
</h2>
|
|
28
|
+
<p className="m-0 text-sm leading-relaxed text-dashboard-text-muted">
|
|
29
|
+
The base snapshot is inherited by all Workspaces.
|
|
30
|
+
</p>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<dl className="m-0 grid grid-cols-[max-content_minmax(0,1fr)] gap-x-6 gap-y-2 text-sm">
|
|
34
|
+
<Fact label="Size">{formatSandboxSize(props.defaultVcpus)}</Fact>
|
|
35
|
+
{snapshot ? (
|
|
36
|
+
<>
|
|
37
|
+
{snapshot.dependencyCount == null ? null : (
|
|
38
|
+
<Fact label="Dependencies">{snapshot.dependencyCount}</Fact>
|
|
39
|
+
)}
|
|
40
|
+
<Fact label="Build time">
|
|
41
|
+
{formatMs(snapshot.buildDurationMs)}
|
|
42
|
+
</Fact>
|
|
43
|
+
<Fact label="Generated">
|
|
44
|
+
<span title={formatTime(snapshot.generatedAt)}>
|
|
45
|
+
{formatRelativeTime(snapshot.generatedAt)}
|
|
46
|
+
</span>
|
|
47
|
+
</Fact>
|
|
48
|
+
<Fact label="Snapshot">
|
|
49
|
+
<span className="break-all font-mono text-xs">
|
|
50
|
+
{snapshot.id}
|
|
51
|
+
</span>
|
|
52
|
+
</Fact>
|
|
53
|
+
</>
|
|
54
|
+
) : (
|
|
55
|
+
<Fact label="Snapshot">
|
|
56
|
+
<span className="text-dashboard-text-muted">
|
|
57
|
+
Not built yet. The first Sandbox without a Workspace builds it.
|
|
58
|
+
</span>
|
|
59
|
+
</Fact>
|
|
60
|
+
)}
|
|
61
|
+
</dl>
|
|
62
|
+
</div>
|
|
63
|
+
</Card>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Fact(props: { children: ReactNode; label: string }) {
|
|
68
|
+
return (
|
|
69
|
+
<div className="contents">
|
|
70
|
+
<dt className="text-dashboard-text-muted">{props.label}</dt>
|
|
71
|
+
<dd className="m-0 min-w-0 tabular-nums text-dashboard-text">
|
|
72
|
+
{props.children}
|
|
73
|
+
</dd>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -65,7 +65,7 @@ export function ConversationActivityChart(props: {
|
|
|
65
65
|
>
|
|
66
66
|
<g tabIndex={0}>
|
|
67
67
|
<rect
|
|
68
|
-
fill="
|
|
68
|
+
fill="var(--color-cyan-400)"
|
|
69
69
|
height={renderedHeight}
|
|
70
70
|
opacity={day.conversations ? 0.78 : 0.08}
|
|
71
71
|
rx="2"
|
|
@@ -89,7 +89,7 @@ export function ConversationActivityChart(props: {
|
|
|
89
89
|
format={formatActivityChartAverage}
|
|
90
90
|
layout={layout}
|
|
91
91
|
maximum={maximum}
|
|
92
|
-
stroke="
|
|
92
|
+
stroke="var(--color-cyan-400)"
|
|
93
93
|
unit={timeRangeBucketAverageUnit(bucketUnit)}
|
|
94
94
|
/>
|
|
95
95
|
<ActivityChartDateLabels
|
|
@@ -15,12 +15,18 @@ import { formatCostSummary } from "../../format";
|
|
|
15
15
|
|
|
16
16
|
type Widget = NonNullable<PluginOperationalReport["widgets"]>[number];
|
|
17
17
|
|
|
18
|
-
const colors = [
|
|
18
|
+
const colors = [
|
|
19
|
+
"var(--color-cyan-300)",
|
|
20
|
+
"var(--color-emerald-300)",
|
|
21
|
+
"var(--color-amber-400)",
|
|
22
|
+
"var(--color-rose-400)",
|
|
23
|
+
"var(--color-violet-400)",
|
|
24
|
+
] as const;
|
|
19
25
|
const toneColors = {
|
|
20
|
-
danger: "
|
|
21
|
-
good: "
|
|
22
|
-
neutral: "
|
|
23
|
-
warning: "
|
|
26
|
+
danger: "var(--color-rose-400)",
|
|
27
|
+
good: "var(--color-emerald-300)",
|
|
28
|
+
neutral: "var(--color-cyan-300)",
|
|
29
|
+
warning: "var(--color-amber-400)",
|
|
24
30
|
} as const;
|
|
25
31
|
|
|
26
32
|
function seriesColor(series: Widget["series"][number], index: number): string {
|
|
@@ -97,7 +103,7 @@ export function PluginBarChart(props: {
|
|
|
97
103
|
return (
|
|
98
104
|
<g key={index}>
|
|
99
105
|
<line
|
|
100
|
-
stroke="
|
|
106
|
+
stroke="var(--color-dashboard-border-subtle)"
|
|
101
107
|
strokeDasharray="3 5"
|
|
102
108
|
x1={layout.left}
|
|
103
109
|
x2={layout.width - layout.right}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Cpu } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { Detail, DetailList } from "../../components/DetailList";
|
|
4
|
+
import { Card } from "../../components/layout/Card";
|
|
5
|
+
|
|
6
|
+
/** Vercel Sandbox gives each vCPU 2 GB of memory. */
|
|
7
|
+
const MEMORY_GB_PER_VCPU = 2;
|
|
8
|
+
|
|
9
|
+
/** Format a Sandbox vCPU count with its memory. Null means the Vercel default. */
|
|
10
|
+
export function formatSandboxSize(vcpus: number | null): string {
|
|
11
|
+
if (vcpus === null) return "Vercel default";
|
|
12
|
+
return `${vcpus} vCPU${vcpus === 1 ? "" : "s"} · ${vcpus * MEMORY_GB_PER_VCPU} GB`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Show one Sandbox size in the shared System summary surface. */
|
|
16
|
+
export function SandboxSizeSummary(props: {
|
|
17
|
+
description: string;
|
|
18
|
+
headingId: string;
|
|
19
|
+
title: string;
|
|
20
|
+
value: string;
|
|
21
|
+
}) {
|
|
22
|
+
return (
|
|
23
|
+
<Card as="section" className="mb-0" padding="md" variant="section">
|
|
24
|
+
<div aria-labelledby={props.headingId} className="grid gap-4">
|
|
25
|
+
<div className="min-w-0 grid gap-1">
|
|
26
|
+
<h2
|
|
27
|
+
className="m-0 text-base font-semibold text-dashboard-text"
|
|
28
|
+
id={props.headingId}
|
|
29
|
+
>
|
|
30
|
+
{props.title}
|
|
31
|
+
</h2>
|
|
32
|
+
<p className="m-0 text-sm leading-relaxed text-dashboard-text-muted">
|
|
33
|
+
{props.description}
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
<DetailList>
|
|
37
|
+
<Detail label="Size">
|
|
38
|
+
<span className="inline-flex items-center gap-2">
|
|
39
|
+
<Cpu aria-hidden="true" size={14} />
|
|
40
|
+
{props.value}
|
|
41
|
+
</span>
|
|
42
|
+
</Detail>
|
|
43
|
+
</DetailList>
|
|
44
|
+
</div>
|
|
45
|
+
</Card>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -6,16 +6,19 @@ import { WorkspaceUsageChart } from "../../components/charts/WorkspaceUsageChart
|
|
|
6
6
|
import { workspaceUsageDays } from "../../components/charts/workspaceUsage";
|
|
7
7
|
import type { TimeRangeDays } from "../../components/controls/TimeRangeSelector";
|
|
8
8
|
import { Card } from "../../components/layout/Card";
|
|
9
|
+
import { formatSandboxSize, SandboxSizeSummary } from "./SandboxSizeSummary";
|
|
9
10
|
import { SnapshotSummary } from "./SnapshotSummary";
|
|
10
11
|
|
|
11
12
|
/** Load Workspace usage stats and render snapshot plus switch volume. */
|
|
12
13
|
export function WorkspaceDetails(props: {
|
|
14
|
+
defaultVcpus: number | null;
|
|
13
15
|
range: TimeRangeDays;
|
|
14
16
|
workspace: WorkspaceReport;
|
|
15
17
|
}) {
|
|
16
18
|
const statsQuery = useStatsData();
|
|
17
19
|
return (
|
|
18
20
|
<WorkspaceDetailsContent
|
|
21
|
+
defaultVcpus={props.defaultVcpus}
|
|
19
22
|
error={Boolean(statsQuery.error)}
|
|
20
23
|
loading={statsQuery.isPending}
|
|
21
24
|
range={props.range}
|
|
@@ -25,8 +28,9 @@ export function WorkspaceDetails(props: {
|
|
|
25
28
|
);
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
/** Present snapshot metadata and switch volume for one Workspace recipe. */
|
|
31
|
+
/** Present snapshot metadata, Sandbox size, and switch volume for one Workspace recipe. */
|
|
29
32
|
export function WorkspaceDetailsContent(props: {
|
|
33
|
+
defaultVcpus: number | null;
|
|
30
34
|
error: boolean;
|
|
31
35
|
loading: boolean;
|
|
32
36
|
range: TimeRangeDays;
|
|
@@ -42,6 +46,12 @@ export function WorkspaceDetailsContent(props: {
|
|
|
42
46
|
snapshot={props.workspace.snapshot}
|
|
43
47
|
title="Current snapshot"
|
|
44
48
|
/>
|
|
49
|
+
<SandboxSizeSummary
|
|
50
|
+
description="Sandboxes for this Workspace start with this size."
|
|
51
|
+
headingId="workspace-sandbox-size-heading"
|
|
52
|
+
title="Sandbox size"
|
|
53
|
+
value={formatSandboxSize(props.workspace.vcpus ?? props.defaultVcpus)}
|
|
54
|
+
/>
|
|
45
55
|
{!props.stats ? (
|
|
46
56
|
<Card padding="sm">
|
|
47
57
|
<EmptyTelemetry>
|
|
@@ -6,6 +6,7 @@ import { Field } from "../../components/Field";
|
|
|
6
6
|
import { InlineError } from "../../components/InlineError";
|
|
7
7
|
import { TextArea, TextInput } from "../../components/TextInput";
|
|
8
8
|
import { Card } from "../../components/layout/Card";
|
|
9
|
+
import { formatSandboxSize } from "./SandboxSizeSummary";
|
|
9
10
|
import {
|
|
10
11
|
createRepoDraft,
|
|
11
12
|
type RepoDraft,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
type WorkspaceEditorProps = {
|
|
16
17
|
busy: boolean;
|
|
17
18
|
canSave: boolean;
|
|
19
|
+
defaultVcpus: number | null;
|
|
18
20
|
draft: WorkspaceDraft;
|
|
19
21
|
error?: string;
|
|
20
22
|
editing: boolean;
|
|
@@ -200,6 +202,23 @@ export function WorkspaceEditor(props: WorkspaceEditorProps) {
|
|
|
200
202
|
/>
|
|
201
203
|
</Field>
|
|
202
204
|
|
|
205
|
+
<Field
|
|
206
|
+
help="Sandbox vCPUs for this Workspace. Each vCPU adds 2 GB of memory. Leave empty to use the app default."
|
|
207
|
+
htmlFor="workspace-vcpus"
|
|
208
|
+
label="vCPUs"
|
|
209
|
+
>
|
|
210
|
+
<TextInput
|
|
211
|
+
autoComplete="off"
|
|
212
|
+
id="workspace-vcpus"
|
|
213
|
+
inputMode="numeric"
|
|
214
|
+
onChange={(event) =>
|
|
215
|
+
props.onChange({ ...props.draft, vcpus: event.target.value })
|
|
216
|
+
}
|
|
217
|
+
placeholder={formatSandboxSize(props.defaultVcpus)}
|
|
218
|
+
value={props.draft.vcpus}
|
|
219
|
+
/>
|
|
220
|
+
</Field>
|
|
221
|
+
|
|
203
222
|
{props.error ? <InlineError>{props.error}</InlineError> : null}
|
|
204
223
|
|
|
205
224
|
<div className="flex flex-wrap items-center gap-3 border-t border-dashboard-border-subtle pt-5">
|
|
@@ -2,9 +2,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { useNavigate, useParams } from "react-router";
|
|
4
4
|
import {
|
|
5
|
+
workspaceListSchema,
|
|
5
6
|
workspaceSchema,
|
|
6
|
-
type
|
|
7
|
-
type WorkspaceReport,
|
|
7
|
+
type WorkspaceListReport,
|
|
8
8
|
} from "@sentry/junior/api/schema";
|
|
9
9
|
|
|
10
10
|
import { EmptyTelemetry } from "../../components/EmptyTelemetry";
|
|
@@ -16,10 +16,7 @@ import { fetchDashboardJson, post, put } from "../../http";
|
|
|
16
16
|
import { SystemPageLayout } from "./SystemPageLayout";
|
|
17
17
|
import { WorkspaceDetails } from "./WorkspaceDetails";
|
|
18
18
|
import { WorkspaceEditor } from "./WorkspaceEditor";
|
|
19
|
-
import {
|
|
20
|
-
readWorkspaceApiError,
|
|
21
|
-
workspacesQueryKey,
|
|
22
|
-
} from "./WorkspacesPage";
|
|
19
|
+
import { readWorkspaceApiError, workspacesQueryKey } from "./WorkspacesPage";
|
|
23
20
|
import {
|
|
24
21
|
canSaveWorkspaceDraft,
|
|
25
22
|
createWorkspaceDraft,
|
|
@@ -56,6 +53,14 @@ export function WorkspaceFormPage() {
|
|
|
56
53
|
),
|
|
57
54
|
retry: false,
|
|
58
55
|
});
|
|
56
|
+
// Shares the list cache so the default size is usually loaded already.
|
|
57
|
+
const workspacesQuery = useQuery({
|
|
58
|
+
queryKey: workspacesQueryKey,
|
|
59
|
+
queryFn: ({ signal }) =>
|
|
60
|
+
fetchDashboardJson(workspaceListSchema, "/api/workspaces", signal),
|
|
61
|
+
retry: false,
|
|
62
|
+
});
|
|
63
|
+
const defaultVcpus = workspacesQuery.data?.defaultVcpus ?? null;
|
|
59
64
|
|
|
60
65
|
// Seed the edit draft once per Workspace id. editWorkspaceDraft assigns fresh
|
|
61
66
|
// repo keys, so calling it during every query-driven render remounts fields.
|
|
@@ -87,18 +92,17 @@ export function WorkspaceFormPage() {
|
|
|
87
92
|
},
|
|
88
93
|
onSuccess: async (workspace) => {
|
|
89
94
|
await queryClient.cancelQueries({ queryKey: workspacesQueryKey });
|
|
90
|
-
queryClient.setQueryData<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}));
|
|
95
|
+
queryClient.setQueryData<WorkspaceListReport>(
|
|
96
|
+
workspacesQueryKey,
|
|
97
|
+
(current) =>
|
|
98
|
+
current && {
|
|
99
|
+
...current,
|
|
100
|
+
workspaces: [
|
|
101
|
+
workspace,
|
|
102
|
+
...current.workspaces.filter((item) => item.id !== workspace.id),
|
|
103
|
+
].sort((left, right) => left.name.localeCompare(right.name)),
|
|
104
|
+
},
|
|
105
|
+
);
|
|
102
106
|
navigate("/system/workspaces");
|
|
103
107
|
},
|
|
104
108
|
});
|
|
@@ -135,7 +139,11 @@ export function WorkspaceFormPage() {
|
|
|
135
139
|
/>
|
|
136
140
|
|
|
137
141
|
{editing && workspaceQuery.data ? (
|
|
138
|
-
<WorkspaceDetails
|
|
142
|
+
<WorkspaceDetails
|
|
143
|
+
defaultVcpus={defaultVcpus}
|
|
144
|
+
range={range}
|
|
145
|
+
workspace={workspaceQuery.data}
|
|
146
|
+
/>
|
|
139
147
|
) : null}
|
|
140
148
|
|
|
141
149
|
{workspaceQuery.error ? (
|
|
@@ -150,6 +158,7 @@ export function WorkspaceFormPage() {
|
|
|
150
158
|
) : draft ? (
|
|
151
159
|
<WorkspaceEditor
|
|
152
160
|
busy={saveMutation.isPending}
|
|
161
|
+
defaultVcpus={defaultVcpus}
|
|
153
162
|
canSave={canSaveWorkspaceDraft(draft, saveMutation.isPending)}
|
|
154
163
|
draft={draft}
|
|
155
164
|
editing={editing}
|
|
@@ -6,9 +6,11 @@ import { Button } from "../../components/Button";
|
|
|
6
6
|
import { EmptyTelemetry } from "../../components/EmptyTelemetry";
|
|
7
7
|
import { SelectableRow } from "../../components/SelectableRow";
|
|
8
8
|
import { Card } from "../../components/layout/Card";
|
|
9
|
+
import { formatSandboxSize } from "./SandboxSizeSummary";
|
|
9
10
|
|
|
10
11
|
type WorkspaceListProps = {
|
|
11
12
|
busy: boolean;
|
|
13
|
+
defaultVcpus: number | null;
|
|
12
14
|
onDelete(workspace: WorkspaceReport): void;
|
|
13
15
|
workspaces: WorkspaceReport[];
|
|
14
16
|
};
|
|
@@ -29,6 +31,7 @@ export function WorkspaceList(props: WorkspaceListProps) {
|
|
|
29
31
|
{props.workspaces.map((workspace) => (
|
|
30
32
|
<WorkspaceListItem
|
|
31
33
|
busy={props.busy}
|
|
34
|
+
defaultVcpus={props.defaultVcpus}
|
|
32
35
|
key={workspace.id}
|
|
33
36
|
onDelete={props.onDelete}
|
|
34
37
|
workspace={workspace}
|
|
@@ -42,6 +45,7 @@ export function WorkspaceList(props: WorkspaceListProps) {
|
|
|
42
45
|
|
|
43
46
|
function WorkspaceListItem(props: {
|
|
44
47
|
busy: boolean;
|
|
48
|
+
defaultVcpus: number | null;
|
|
45
49
|
onDelete(workspace: WorkspaceReport): void;
|
|
46
50
|
workspace: WorkspaceReport;
|
|
47
51
|
}) {
|
|
@@ -72,7 +76,8 @@ function WorkspaceListItem(props: {
|
|
|
72
76
|
</span>
|
|
73
77
|
<span className="font-mono text-xs text-dashboard-text-muted">
|
|
74
78
|
{workspace.repos.length} repo
|
|
75
|
-
{workspace.repos.length === 1 ? "" : "s"}
|
|
79
|
+
{workspace.repos.length === 1 ? "" : "s"} ·{" "}
|
|
80
|
+
{formatSandboxSize(workspace.vcpus ?? props.defaultVcpus)}
|
|
76
81
|
</span>
|
|
77
82
|
</span>
|
|
78
83
|
<span className="mt-1.5 block truncate font-mono text-xs text-dashboard-text-muted">
|
|
@@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
2
2
|
import { Plus } from "lucide-react";
|
|
3
3
|
import {
|
|
4
4
|
workspaceListSchema,
|
|
5
|
-
type
|
|
5
|
+
type WorkspaceListReport,
|
|
6
6
|
type WorkspaceReport,
|
|
7
7
|
} from "@sentry/junior/api/schema";
|
|
8
8
|
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
deleteDashboardResource,
|
|
18
18
|
fetchDashboardJson,
|
|
19
19
|
} from "../../http";
|
|
20
|
-
import {
|
|
20
|
+
import { BaseSandboxCard } from "./BaseSandboxCard";
|
|
21
21
|
import { SystemPageLayout, SystemRouteLoading } from "./SystemPageLayout";
|
|
22
22
|
import { WorkspaceList } from "./WorkspaceList";
|
|
23
23
|
|
|
@@ -55,15 +55,16 @@ export function WorkspacesPage() {
|
|
|
55
55
|
},
|
|
56
56
|
onSuccess: async (workspace) => {
|
|
57
57
|
await queryClient.cancelQueries({ queryKey: workspacesQueryKey });
|
|
58
|
-
queryClient.setQueryData<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
58
|
+
queryClient.setQueryData<WorkspaceListReport>(
|
|
59
|
+
workspacesQueryKey,
|
|
60
|
+
(current) =>
|
|
61
|
+
current && {
|
|
62
|
+
...current,
|
|
63
|
+
workspaces: current.workspaces.filter(
|
|
64
|
+
(item) => item.id !== workspace.id,
|
|
65
|
+
),
|
|
66
|
+
},
|
|
67
|
+
);
|
|
67
68
|
},
|
|
68
69
|
});
|
|
69
70
|
|
|
@@ -80,6 +81,7 @@ export function WorkspacesPage() {
|
|
|
80
81
|
|
|
81
82
|
const workspaces = workspacesQuery.data?.workspaces ?? [];
|
|
82
83
|
const baselineSnapshot = workspacesQuery.data?.baselineSnapshot ?? null;
|
|
84
|
+
const defaultVcpus = workspacesQuery.data?.defaultVcpus ?? null;
|
|
83
85
|
return (
|
|
84
86
|
<SystemPageLayout>
|
|
85
87
|
<PageHeader
|
|
@@ -111,11 +113,15 @@ export function WorkspacesPage() {
|
|
|
111
113
|
) : null}
|
|
112
114
|
|
|
113
115
|
{!workspacesQuery.error ? (
|
|
114
|
-
<
|
|
116
|
+
<BaseSandboxCard
|
|
117
|
+
defaultVcpus={defaultVcpus}
|
|
118
|
+
snapshot={baselineSnapshot}
|
|
119
|
+
/>
|
|
115
120
|
) : null}
|
|
116
121
|
|
|
117
122
|
<WorkspaceList
|
|
118
123
|
busy={deleteMutation.isPending}
|
|
124
|
+
defaultVcpus={defaultVcpus}
|
|
119
125
|
onDelete={(workspace) => {
|
|
120
126
|
if (
|
|
121
127
|
window.confirm(
|
|
@@ -9,6 +9,8 @@ export type RepoDraft = {
|
|
|
9
9
|
export type WorkspaceDraft = {
|
|
10
10
|
name: string;
|
|
11
11
|
setupScript: string;
|
|
12
|
+
/** Text input value. Empty means the app default. */
|
|
13
|
+
vcpus: string;
|
|
12
14
|
repos: RepoDraft[];
|
|
13
15
|
};
|
|
14
16
|
|
|
@@ -24,6 +26,7 @@ export function createWorkspaceDraft(): WorkspaceDraft {
|
|
|
24
26
|
return {
|
|
25
27
|
name: "",
|
|
26
28
|
setupScript: "",
|
|
29
|
+
vcpus: "",
|
|
27
30
|
repos: [createRepoDraft()],
|
|
28
31
|
};
|
|
29
32
|
}
|
|
@@ -32,6 +35,7 @@ export function editWorkspaceDraft(workspace: WorkspaceReport): WorkspaceDraft {
|
|
|
32
35
|
return {
|
|
33
36
|
name: workspace.name,
|
|
34
37
|
setupScript: workspace.setupScript,
|
|
38
|
+
vcpus: workspace.vcpus === null ? "" : String(workspace.vcpus),
|
|
35
39
|
repos:
|
|
36
40
|
workspace.repos.length > 0
|
|
37
41
|
? workspace.repos.map((repo) => ({
|
|
@@ -48,6 +52,7 @@ export function workspaceDraftBody(draft: WorkspaceDraft) {
|
|
|
48
52
|
return {
|
|
49
53
|
name: draft.name.trim(),
|
|
50
54
|
setupScript: draft.setupScript,
|
|
55
|
+
vcpus: draft.vcpus.trim() ? Number(draft.vcpus.trim()) : null,
|
|
51
56
|
repos: draft.repos.map((repo) => ({
|
|
52
57
|
provider: repo.provider.trim(),
|
|
53
58
|
repo: repo.repo.trim(),
|
|
@@ -60,6 +65,9 @@ export function canSaveWorkspaceDraft(
|
|
|
60
65
|
busy: boolean,
|
|
61
66
|
): boolean {
|
|
62
67
|
if (busy || !draft.name.trim()) return false;
|
|
68
|
+
if (draft.vcpus.trim() && !/^[1-9]\d*$/.test(draft.vcpus.trim())) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
63
71
|
if (draft.repos.some((repo) => !repo.provider.trim() || !repo.repo.trim())) {
|
|
64
72
|
return false;
|
|
65
73
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DASHBOARD_THEME_CHANGE_EVENT,
|
|
5
|
+
DASHBOARD_THEME_STORAGE_KEY,
|
|
6
|
+
type DashboardThemeChoice,
|
|
7
|
+
} from "../theme";
|
|
8
|
+
|
|
9
|
+
function readThemeChoice(): DashboardThemeChoice {
|
|
10
|
+
try {
|
|
11
|
+
const choice = window.localStorage.getItem(DASHBOARD_THEME_STORAGE_KEY);
|
|
12
|
+
return choice === "light" || choice === "dark" ? choice : "system";
|
|
13
|
+
} catch {
|
|
14
|
+
return "system";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function subscribe(onChange: () => void): () => void {
|
|
19
|
+
window.addEventListener(DASHBOARD_THEME_CHANGE_EVENT, onChange);
|
|
20
|
+
window.addEventListener("storage", onChange);
|
|
21
|
+
return () => {
|
|
22
|
+
window.removeEventListener(DASHBOARD_THEME_CHANGE_EVENT, onChange);
|
|
23
|
+
window.removeEventListener("storage", onChange);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Save a theme choice for this browser. The shell theme script applies it
|
|
29
|
+
* and keeps following the system color scheme for `system`.
|
|
30
|
+
*/
|
|
31
|
+
function saveThemeChoice(choice: DashboardThemeChoice): void {
|
|
32
|
+
try {
|
|
33
|
+
if (choice === "system") {
|
|
34
|
+
window.localStorage.removeItem(DASHBOARD_THEME_STORAGE_KEY);
|
|
35
|
+
} else {
|
|
36
|
+
window.localStorage.setItem(DASHBOARD_THEME_STORAGE_KEY, choice);
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// Storage is blocked, so the dashboard keeps following the system.
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
window.dispatchEvent(new Event(DASHBOARD_THEME_CHANGE_EVENT));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Read and update this browser's dashboard theme choice. */
|
|
46
|
+
export function useDashboardThemeChoice(): [
|
|
47
|
+
DashboardThemeChoice,
|
|
48
|
+
(choice: DashboardThemeChoice) => void,
|
|
49
|
+
] {
|
|
50
|
+
const choice = useSyncExternalStore(subscribe, readThemeChoice);
|
|
51
|
+
return [choice, saveThemeChoice];
|
|
52
|
+
}
|
package/src/client/types.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
} from "@sentry/junior/api/schema";
|
|
10
10
|
import type {
|
|
11
11
|
ActorIdentity,
|
|
12
|
+
ConversationReportMessageEvent,
|
|
12
13
|
ConversationStatsReport,
|
|
13
14
|
ConversationSummaryReport,
|
|
14
15
|
ConversationTurnFailureCode,
|
|
@@ -51,6 +52,8 @@ export type TranscriptViewSubagentPart = {
|
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
export type TranscriptViewContextEventPart = {
|
|
55
|
+
/** People mentioned in the summary, resolved by the API. */
|
|
56
|
+
mentions?: ActorIdentity[];
|
|
54
57
|
event:
|
|
55
58
|
| {
|
|
56
59
|
createdAt: string;
|
|
@@ -81,6 +84,8 @@ export type TranscriptViewContextEventPart = {
|
|
|
81
84
|
};
|
|
82
85
|
|
|
83
86
|
export type TranscriptViewStructuredEventPart = {
|
|
87
|
+
/** People mentioned in the preview and details, resolved by the API. */
|
|
88
|
+
mentions?: ActorIdentity[];
|
|
84
89
|
name: string;
|
|
85
90
|
namespace: string;
|
|
86
91
|
presentation: ConversationEventPresentation;
|
|
@@ -120,9 +125,11 @@ export type TranscriptViewMessage = {
|
|
|
120
125
|
eventObjectType?: ObjectType;
|
|
121
126
|
/** Short summary supplied by the Event publisher. */
|
|
122
127
|
trustedSummary?: string;
|
|
128
|
+
/** Every Event when several arrived together in one message. */
|
|
129
|
+
events?: ConversationReportMessageEvent[];
|
|
123
130
|
/** Whether the source message addressed Junior directly. */
|
|
124
131
|
explicitMention?: boolean;
|
|
125
|
-
/** People mentioned in the
|
|
132
|
+
/** People mentioned in the event text, resolved by the API. */
|
|
126
133
|
mentions?: ActorIdentity[];
|
|
127
134
|
/** Whether a non-mention message was used as input to a turn. */
|
|
128
135
|
context?: boolean;
|