@sentry/junior-dashboard 0.263.0 → 0.265.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 (197) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +2 -5
  3. package/dist/app.js +119 -35
  4. package/dist/client.js +148 -135
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.js +119 -35
  7. package/dist/tailwind.css +1 -1
  8. package/package.json +9 -4
  9. package/src/api/schema.ts +0 -1
  10. package/src/app.ts +5 -20
  11. package/src/client/App.tsx +29 -31
  12. package/src/client/api.ts +7 -0
  13. package/src/client/components/BottomSheet.tsx +26 -18
  14. package/src/client/components/CopyButton.tsx +83 -0
  15. package/src/client/components/DetailsPanel.tsx +133 -0
  16. package/src/client/components/Drawer.tsx +34 -33
  17. package/src/client/components/FilterBar.tsx +16 -31
  18. package/src/client/components/Metric.tsx +33 -23
  19. package/src/client/components/PageContentSkeleton.tsx +1 -1
  20. package/src/client/components/Pagination.tsx +1 -1
  21. package/src/client/components/ParticipantAvatarStack.tsx +40 -36
  22. package/src/client/components/ProfileMenu.tsx +27 -93
  23. package/src/client/components/README.md +101 -64
  24. package/src/client/components/SegmentedNav.tsx +1 -1
  25. package/src/client/components/ShimmerText.tsx +2 -6
  26. package/src/client/components/ThemeSwitcher.tsx +19 -15
  27. package/src/client/components/charts/ActivityChart.tsx +173 -288
  28. package/src/client/components/charts/CacheWrites.tsx +63 -50
  29. package/src/client/components/charts/InputCacheChart.tsx +137 -131
  30. package/src/client/components/charts/README.md +4 -4
  31. package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
  32. package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
  33. package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
  34. package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
  35. package/src/client/components/layout/DashboardHeader.tsx +44 -90
  36. package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
  37. package/src/client/components/layout/VisualViewportShell.tsx +2 -2
  38. package/src/client/components/ui/accordion.tsx +77 -0
  39. package/src/client/components/ui/alert-dialog.tsx +89 -0
  40. package/src/client/components/ui/button.tsx +85 -0
  41. package/src/client/components/ui/chart.tsx +74 -0
  42. package/src/client/components/ui/checkbox.tsx +26 -0
  43. package/src/client/components/ui/collapsible.tsx +22 -0
  44. package/src/client/components/ui/field.tsx +49 -0
  45. package/src/client/components/ui/input.tsx +35 -0
  46. package/src/client/components/ui/native-select.tsx +57 -0
  47. package/src/client/components/ui/popover.tsx +67 -0
  48. package/src/client/components/ui/radio-group.tsx +32 -0
  49. package/src/client/components/ui/sheet.tsx +151 -0
  50. package/src/client/components/ui/skeleton.tsx +19 -0
  51. package/src/client/components/ui/table.tsx +80 -0
  52. package/src/client/components/ui/tabs.tsx +75 -0
  53. package/src/client/components/ui/textarea.tsx +17 -0
  54. package/src/client/components/ui/toast.tsx +215 -0
  55. package/src/client/components/ui/toggle-group.tsx +36 -0
  56. package/src/client/components/ui/toggle.tsx +37 -0
  57. package/src/client/components/ui/tooltip.tsx +168 -0
  58. package/src/client/components/ui/trace.tsx +96 -0
  59. package/src/client/components/ui/useSheet.ts +74 -0
  60. package/src/client/conversations/ChatLayout.tsx +7 -3
  61. package/src/client/conversations/ConversationBrief.tsx +17 -6
  62. package/src/client/conversations/ConversationChildAgents.tsx +87 -58
  63. package/src/client/conversations/ConversationComposer.tsx +26 -12
  64. package/src/client/conversations/ConversationDetails.tsx +198 -95
  65. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  66. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  67. package/src/client/conversations/ConversationHeader.tsx +8 -3
  68. package/src/client/conversations/ConversationHeaderActions.tsx +90 -82
  69. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  70. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  71. package/src/client/conversations/ConversationMeta.tsx +111 -92
  72. package/src/client/conversations/ConversationPage.tsx +241 -110
  73. package/src/client/conversations/ConversationSidebar.tsx +48 -29
  74. package/src/client/conversations/ConversationUsageBreakdown.tsx +16 -3
  75. package/src/client/conversations/ConversationWatches.tsx +165 -0
  76. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  77. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  78. package/src/client/conversations/DetailsList.tsx +124 -0
  79. package/src/client/conversations/EventDetails.tsx +42 -8
  80. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  81. package/src/client/conversations/ObjectCard.tsx +20 -8
  82. package/src/client/conversations/PendingMailboxStack.tsx +52 -33
  83. package/src/client/conversations/README.md +93 -19
  84. package/src/client/conversations/ShareConversationButton.tsx +28 -3
  85. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  86. package/src/client/conversations/ToolFrame.tsx +36 -52
  87. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  88. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  89. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  90. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  91. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  92. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  93. package/src/client/conversations/TranscriptMessageContextView.tsx +8 -2
  94. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  95. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  96. package/src/client/conversations/TranscriptSubagentView.tsx +3 -3
  97. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  98. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  99. package/src/client/conversations/TranscriptTurnContextView.tsx +99 -27
  100. package/src/client/conversations/TranscriptView.tsx +63 -27
  101. package/src/client/conversations/eventLog.ts +57 -0
  102. package/src/client/conversations/eventTime.ts +60 -0
  103. package/src/client/conversations/queries.ts +29 -0
  104. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  105. package/src/client/format.ts +5 -0
  106. package/src/client/lib/utils.ts +1 -0
  107. package/src/client/pages/ConnectionsPage.tsx +2 -2
  108. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  109. package/src/client/pages/SettingsPage.tsx +9 -10
  110. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  111. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  112. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  113. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  114. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  115. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  116. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  117. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  118. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  119. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  120. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  121. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  122. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  123. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  124. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  125. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  126. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  127. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  128. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  129. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  130. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  131. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  132. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  133. package/src/client/pages/automations/README.md +57 -0
  134. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  135. package/src/client/pages/automations/automationFormat.ts +37 -23
  136. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  137. package/src/client/pages/code/CodePage.tsx +52 -55
  138. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  139. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  140. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  141. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  142. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  143. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  144. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  145. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  146. package/src/client/pages/memory/memoryRecord.ts +1 -1
  147. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  148. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  149. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  150. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  151. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  152. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  153. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  154. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  155. package/src/client/pages/system/README.md +23 -0
  156. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  157. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  158. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  159. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  160. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  161. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  162. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  163. package/src/client/pages/system/sandboxSize.ts +8 -0
  164. package/src/client/pages/system/workspaceQueries.ts +53 -0
  165. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  166. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  167. package/src/client/styles.ts +4 -0
  168. package/src/client.tsx +7 -1
  169. package/src/mock-reporting/automation-collection.ts +4 -2
  170. package/src/mock-reporting/fixtures.ts +23 -7
  171. package/src/mock-reporting/routes.ts +12 -0
  172. package/src/mock-reporting/watches.ts +94 -0
  173. package/src/shell.ts +4 -9
  174. package/src/tailwind.css +77 -10
  175. package/src/client/components/Button.tsx +0 -118
  176. package/src/client/components/Field.tsx +0 -53
  177. package/src/client/components/SegmentedTabs.tsx +0 -119
  178. package/src/client/components/Select.tsx +0 -52
  179. package/src/client/components/Sheet.LICENSE +0 -21
  180. package/src/client/components/Sheet.tsx +0 -141
  181. package/src/client/components/Skeleton.tsx +0 -17
  182. package/src/client/components/TextInput.tsx +0 -58
  183. package/src/client/components/Tooltip.tsx +0 -216
  184. package/src/client/components/charts/useChartLayout.ts +0 -27
  185. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -91
  186. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  187. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  188. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  189. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  190. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  191. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  192. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  193. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  194. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  195. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  196. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  197. package/src/client/pages/system/SnapshotSummary.tsx +0 -78
@@ -1,67 +1,182 @@
1
+ import { Pencil, Trash2 } from "lucide-react";
2
+ import { Link } from "react-router";
1
3
  import type { StatsReport, WorkspaceReport } from "@sentry/junior/api/schema";
2
4
 
3
5
  import { useStatsData } from "../../api";
6
+ import { buttonVariants, Button } from "../../components/ui/button";
7
+ import { Skeleton } from "../../components/ui/skeleton";
8
+ import {
9
+ Tooltip,
10
+ TooltipContent,
11
+ TooltipTrigger,
12
+ } from "../../components/ui/tooltip";
13
+ import { Detail, DetailList } from "../../components/DetailList";
14
+ import { DetailsPanel } from "../../components/DetailsPanel";
4
15
  import { EmptyTelemetry } from "../../components/EmptyTelemetry";
16
+ import { PageContentSkeleton } from "../../components/PageContentSkeleton";
5
17
  import { WorkspaceUsageChart } from "../../components/charts/WorkspaceUsageChart";
6
18
  import { workspaceUsageDays } from "../../components/charts/workspaceUsage";
7
19
  import type { TimeRangeDays } from "../../components/controls/TimeRangeSelector";
8
- import { Card } from "../../components/layout/Card";
9
- import { formatSandboxSize, SandboxSizeSummary } from "./SandboxSizeSummary";
10
- import { SnapshotSummary } from "./SnapshotSummary";
20
+ import {
21
+ formatMs,
22
+ formatRelativeTime,
23
+ formatTime,
24
+ workspacePath,
25
+ } from "../../format";
26
+ import { formatSandboxSize } from "./sandboxSize";
27
+ import { useWorkspaceData } from "./workspaceQueries";
11
28
 
12
- /** Load Workspace usage stats and render snapshot plus switch volume. */
29
+ // Workspace switch counters are daily-only (`junior_stats.date`).
30
+ const USAGE_RANGE: TimeRangeDays = 30;
31
+
32
+ /**
33
+ * Show one Workspace beside the list with Edit and Delete in the header. The
34
+ * list item renders at once; the detail read adds the current snapshot.
35
+ */
13
36
  export function WorkspaceDetails(props: {
14
37
  defaultVcpus: number | null;
15
- range: TimeRangeDays;
16
- workspace: WorkspaceReport;
38
+ listError: boolean;
39
+ onClose(): void;
40
+ onDelete(workspace: WorkspaceReport, source: HTMLButtonElement): void;
41
+ workspace: WorkspaceReport | undefined;
42
+ workspaceId: string;
17
43
  }) {
44
+ const detail = useWorkspaceData(props.workspaceId);
18
45
  const statsQuery = useStatsData();
46
+ const workspace = detail.data ?? props.workspace;
47
+ const missing = !workspace && (detail.isError || props.listError);
19
48
  return (
20
- <WorkspaceDetailsContent
21
- defaultVcpus={props.defaultVcpus}
22
- error={Boolean(statsQuery.error)}
23
- loading={statsQuery.isPending}
24
- range={props.range}
25
- stats={statsQuery.data}
26
- workspace={props.workspace}
27
- />
49
+ <DetailsPanel
50
+ actions={
51
+ workspace ? (
52
+ <>
53
+ <Tooltip>
54
+ <TooltipTrigger
55
+ render={
56
+ <Link
57
+ aria-label={`Edit ${workspace.name}`}
58
+ className={buttonVariants({ size: "icon" })}
59
+ to={`${workspacePath(workspace.id)}/edit`}
60
+ >
61
+ <Pencil aria-hidden="true" size={15} />
62
+ </Link>
63
+ }
64
+ />
65
+ <TooltipContent size="compact">Edit Workspace</TooltipContent>
66
+ </Tooltip>
67
+ <Tooltip>
68
+ <TooltipTrigger
69
+ render={
70
+ <Button
71
+ aria-label={`Delete ${workspace.name}`}
72
+ onClick={(event) =>
73
+ props.onDelete(workspace, event.currentTarget)
74
+ }
75
+ size="icon"
76
+ >
77
+ <Trash2 aria-hidden="true" size={15} />
78
+ </Button>
79
+ }
80
+ />
81
+ <TooltipContent size="compact">Delete Workspace</TooltipContent>
82
+ </Tooltip>
83
+ </>
84
+ ) : undefined
85
+ }
86
+ label="Workspace details"
87
+ onClose={props.onClose}
88
+ sheetTitle={workspace?.name}
89
+ title={
90
+ <h2 className="m-0 break-words font-display text-base font-medium text-dashboard-text">
91
+ {workspace?.name ?? "Workspace details"}
92
+ </h2>
93
+ }
94
+ >
95
+ <div className="p-4 [overflow-wrap:anywhere]">
96
+ {workspace ? (
97
+ <WorkspaceDetailsContent
98
+ defaultVcpus={props.defaultVcpus}
99
+ error={Boolean(statsQuery.error)}
100
+ loading={statsQuery.isPending}
101
+ snapshot={
102
+ detail.data ? "ready" : detail.isError ? "error" : "loading"
103
+ }
104
+ stats={statsQuery.data}
105
+ workspace={workspace}
106
+ />
107
+ ) : missing ? (
108
+ <EmptyTelemetry>
109
+ Workspace not found. It may have been deleted.
110
+ </EmptyTelemetry>
111
+ ) : (
112
+ <PageContentSkeleton
113
+ label="Loading Workspace details"
114
+ variant="panel"
115
+ />
116
+ )}
117
+ </div>
118
+ </DetailsPanel>
28
119
  );
29
120
  }
30
121
 
31
- /** Present snapshot metadata, Sandbox size, and switch volume for one Workspace recipe. */
122
+ /** Present repositories, Sandbox size, snapshot, setup, and switch volume for one Workspace. */
32
123
  export function WorkspaceDetailsContent(props: {
33
124
  defaultVcpus: number | null;
34
125
  error: boolean;
35
126
  loading: boolean;
36
- range: TimeRangeDays;
127
+ snapshot: "error" | "loading" | "ready";
37
128
  stats: StatsReport | undefined;
38
129
  workspace: WorkspaceReport;
39
130
  }) {
131
+ const { workspace } = props;
40
132
  return (
41
133
  <div className="grid min-w-0 gap-4">
42
- <SnapshotSummary
43
- description="New Sandboxes start from this prepared snapshot."
44
- emptyDescription="No snapshot yet. The first successful prepare creates one."
45
- headingId="workspace-snapshot-heading"
46
- snapshot={props.workspace.snapshot}
47
- title="Current snapshot"
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
- />
134
+ <DetailList>
135
+ <Detail label="Repositories">
136
+ {workspace.repos.length === 0 ? (
137
+ <span className="text-dashboard-text-muted">None</span>
138
+ ) : (
139
+ <ul className="m-0 grid list-none gap-1.5 p-0">
140
+ {workspace.repos.map((repo) => (
141
+ <li className="min-w-0" key={`${repo.provider}:${repo.repo}`}>
142
+ <span className="block font-mono text-xs text-dashboard-text">
143
+ {repo.provider}:{repo.repo}
144
+ </span>
145
+ <span className="block font-mono text-xs text-dashboard-text-muted">
146
+ {repo.checkoutPath}
147
+ </span>
148
+ </li>
149
+ ))}
150
+ </ul>
151
+ )}
152
+ </Detail>
153
+ <Detail label="Sandbox size">
154
+ {formatSandboxSize(workspace.vcpus ?? props.defaultVcpus)}
155
+ {workspace.vcpus === null ? (
156
+ <span className="text-dashboard-text-muted"> · default</span>
157
+ ) : null}
158
+ </Detail>
159
+ <Detail label="Snapshot">
160
+ <WorkspaceSnapshot state={props.snapshot} workspace={workspace} />
161
+ </Detail>
162
+ <Detail label="Setup script">
163
+ {workspace.setupScript.trim() ? (
164
+ <pre className="m-0 max-h-48 overflow-auto whitespace-pre-wrap rounded-md bg-dashboard-fill-soft p-2 font-mono text-xs leading-relaxed text-dashboard-text">
165
+ {workspace.setupScript}
166
+ </pre>
167
+ ) : (
168
+ <span className="text-dashboard-text-muted">None</span>
169
+ )}
170
+ </Detail>
171
+ </DetailList>
55
172
  {!props.stats ? (
56
- <Card padding="sm">
57
- <EmptyTelemetry>
58
- {props.error
59
- ? "Workspace usage failed to load."
60
- : props.loading
61
- ? "Loading Workspace usage."
62
- : "No usage in this period."}
63
- </EmptyTelemetry>
64
- </Card>
173
+ <EmptyTelemetry>
174
+ {props.error
175
+ ? "Workspace usage failed to load."
176
+ : props.loading
177
+ ? "Loading Workspace usage."
178
+ : "No usage in this period."}
179
+ </EmptyTelemetry>
65
180
  ) : (
66
181
  <div className="grid min-w-0 gap-3">
67
182
  {props.error ? (
@@ -71,16 +186,58 @@ export function WorkspaceDetailsContent(props: {
71
186
  ) : null}
72
187
  <WorkspaceUsageChart
73
188
  days={workspaceUsageDays({
74
- workspaceId: props.workspace.id,
75
- // Workspace switch counters are daily-only (`junior_stats.date`).
76
- range: props.range,
189
+ workspaceId: workspace.id,
190
+ range: USAGE_RANGE,
77
191
  stats: props.stats.stats,
78
192
  })}
79
- range={props.range}
80
- workspaceName={props.workspace.name}
193
+ range={USAGE_RANGE}
194
+ workspaceName={workspace.name}
81
195
  />
82
196
  </div>
83
197
  )}
84
198
  </div>
85
199
  );
86
200
  }
201
+
202
+ function WorkspaceSnapshot(props: {
203
+ state: "error" | "loading" | "ready";
204
+ workspace: WorkspaceReport;
205
+ }) {
206
+ if (props.state === "loading") {
207
+ return (
208
+ <span className="grid gap-1.5" role="status">
209
+ <span className="sr-only">Loading snapshot</span>
210
+ <Skeleton className="h-4 w-36" />
211
+ <Skeleton className="h-3.5 w-48" />
212
+ </span>
213
+ );
214
+ }
215
+ if (props.state === "error") {
216
+ return (
217
+ <span className="text-dashboard-text-muted">
218
+ Snapshot status could not be loaded.
219
+ </span>
220
+ );
221
+ }
222
+ const snapshot = props.workspace.snapshot;
223
+ if (!snapshot) {
224
+ return (
225
+ <span className="text-dashboard-text-muted">
226
+ Not built yet. The first successful prepare builds it.
227
+ </span>
228
+ );
229
+ }
230
+ return (
231
+ <>
232
+ <span className="block tabular-nums">
233
+ <span title={formatTime(snapshot.generatedAt)}>
234
+ {formatRelativeTime(snapshot.generatedAt)}
235
+ </span>{" "}
236
+ · built in {formatMs(snapshot.buildDurationMs)}
237
+ </span>
238
+ <span className="block break-all font-mono text-xs text-dashboard-text-muted">
239
+ {snapshot.id}
240
+ </span>
241
+ </>
242
+ );
243
+ }
@@ -1,12 +1,14 @@
1
1
  import { Plus, Trash2 } from "lucide-react";
2
2
  import type { FormEvent } from "react";
3
+ import { Link } from "react-router";
3
4
 
4
- import { Button } from "../../components/Button";
5
- import { Field } from "../../components/Field";
5
+ import { Button, buttonVariants } from "../../components/ui/button";
6
+ import { Input } from "../../components/ui/input";
7
+ import { Textarea } from "../../components/ui/textarea";
8
+ import { Field, FieldDescription, FieldLabel } from "../../components/ui/field";
6
9
  import { InlineError } from "../../components/InlineError";
7
- import { TextArea, TextInput } from "../../components/TextInput";
8
10
  import { Card } from "../../components/layout/Card";
9
- import { formatSandboxSize } from "./SandboxSizeSummary";
11
+ import { formatSandboxSize } from "./sandboxSize";
10
12
  import {
11
13
  createRepoDraft,
12
14
  type RepoDraft,
@@ -16,6 +18,7 @@ import {
16
18
  type WorkspaceEditorProps = {
17
19
  busy: boolean;
18
20
  canSave: boolean;
21
+ cancelPath: string;
19
22
  defaultVcpus: number | null;
20
23
  draft: WorkspaceDraft;
21
24
  error?: string;
@@ -51,24 +54,9 @@ export function WorkspaceEditor(props: WorkspaceEditorProps) {
51
54
  return (
52
55
  <Card className="mb-0" padding="md" variant="section">
53
56
  <form className="grid gap-6" onSubmit={submit}>
54
- {props.editing ? (
55
- <div className="grid gap-1">
56
- <h2 className="m-0 text-base font-semibold text-dashboard-text">
57
- Configuration
58
- </h2>
59
- <p className="m-0 text-sm leading-relaxed text-dashboard-text-muted">
60
- Change the recipe Junior uses the next time this Workspace is
61
- prepared.
62
- </p>
63
- </div>
64
- ) : null}
65
-
66
- <Field
67
- help="Lowercase name used when agents switch into this Workspace."
68
- htmlFor="workspace-name"
69
- label="Name"
70
- >
71
- <TextInput
57
+ <Field>
58
+ <FieldLabel htmlFor="workspace-name">Name</FieldLabel>
59
+ <Input
72
60
  autoComplete="off"
73
61
  id="workspace-name"
74
62
  maxLength={64}
@@ -78,6 +66,9 @@ export function WorkspaceEditor(props: WorkspaceEditorProps) {
78
66
  placeholder="sentry"
79
67
  value={props.draft.name}
80
68
  />
69
+ <FieldDescription id="workspace-name-help">
70
+ Lowercase name used when agents switch into this Workspace.
71
+ </FieldDescription>
81
72
  </Field>
82
73
 
83
74
  <section className="grid gap-3" aria-labelledby="workspace-repos-title">
@@ -120,12 +111,14 @@ export function WorkspaceEditor(props: WorkspaceEditorProps) {
120
111
  key={repo.key}
121
112
  >
122
113
  <div className="grid gap-3 sm:grid-cols-[minmax(0,8.5rem)_minmax(0,1fr)_auto] sm:items-end">
123
- <Field
124
- htmlFor={`workspace-repo-provider-${repo.key}`}
125
- label="Provider"
126
- size="compact"
127
- >
128
- <TextInput
114
+ <Field className="gap-1.5">
115
+ <FieldLabel
116
+ htmlFor={`workspace-repo-provider-${repo.key}`}
117
+ className="font-sans text-xs font-medium text-dashboard-text-muted"
118
+ >
119
+ Provider
120
+ </FieldLabel>
121
+ <Input
129
122
  aria-label={`Provider ${index + 1}`}
130
123
  id={`workspace-repo-provider-${repo.key}`}
131
124
  onChange={(event) =>
@@ -135,12 +128,14 @@ export function WorkspaceEditor(props: WorkspaceEditorProps) {
135
128
  value={repo.provider}
136
129
  />
137
130
  </Field>
138
- <Field
139
- htmlFor={`workspace-repo-name-${repo.key}`}
140
- label="Repository"
141
- size="compact"
142
- >
143
- <TextInput
131
+ <Field className="gap-1.5">
132
+ <FieldLabel
133
+ htmlFor={`workspace-repo-name-${repo.key}`}
134
+ className="font-sans text-xs font-medium text-dashboard-text-muted"
135
+ >
136
+ Repository
137
+ </FieldLabel>
138
+ <Input
144
139
  aria-label={`Repository ${index + 1}`}
145
140
  id={`workspace-repo-name-${repo.key}`}
146
141
  onChange={(event) =>
@@ -154,7 +149,7 @@ export function WorkspaceEditor(props: WorkspaceEditorProps) {
154
149
  aria-label={`Remove repository ${index + 1}`}
155
150
  className="w-full sm:w-auto"
156
151
  onClick={() => removeRepo(repo.key)}
157
- tone="danger"
152
+ variant="danger"
158
153
  type="button"
159
154
  >
160
155
  <Trash2 aria-hidden="true" size={14} />
@@ -166,8 +161,23 @@ export function WorkspaceEditor(props: WorkspaceEditorProps) {
166
161
  </div>
167
162
  </section>
168
163
 
169
- <Field
170
- help={
164
+ <Field>
165
+ <FieldLabel htmlFor="workspace-setup">Setup script</FieldLabel>
166
+ <Textarea
167
+ className="font-mono"
168
+ id="workspace-setup"
169
+ onChange={(event) =>
170
+ props.onChange({
171
+ ...props.draft,
172
+ setupScript: event.target.value,
173
+ })
174
+ }
175
+ placeholder={
176
+ 'pnpm install --dir "$JUNIOR_REPOS_ROOT/sentry"\n# cwd is $JUNIOR_WORKSPACE_ROOT'
177
+ }
178
+ value={props.draft.setupScript}
179
+ />
180
+ <FieldDescription id="workspace-setup-help">
171
181
  <>
172
182
  Runs once while Junior builds the reusable snapshot. Current
173
183
  working directory is{" "}
@@ -183,31 +193,12 @@ export function WorkspaceEditor(props: WorkspaceEditorProps) {
183
193
  </code>
184
194
  ). Use those variables instead of hard-coded absolute paths.
185
195
  </>
186
- }
187
- htmlFor="workspace-setup"
188
- label="Setup script"
189
- >
190
- <TextArea
191
- id="workspace-setup"
192
- onChange={(event) =>
193
- props.onChange({
194
- ...props.draft,
195
- setupScript: event.target.value,
196
- })
197
- }
198
- placeholder={
199
- 'pnpm install --dir "$JUNIOR_REPOS_ROOT/sentry"\n# cwd is $JUNIOR_WORKSPACE_ROOT'
200
- }
201
- value={props.draft.setupScript}
202
- />
196
+ </FieldDescription>
203
197
  </Field>
204
198
 
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
199
+ <Field>
200
+ <FieldLabel htmlFor="workspace-vcpus">vCPUs</FieldLabel>
201
+ <Input
211
202
  autoComplete="off"
212
203
  id="workspace-vcpus"
213
204
  inputMode="numeric"
@@ -217,18 +208,25 @@ export function WorkspaceEditor(props: WorkspaceEditorProps) {
217
208
  placeholder={formatSandboxSize(props.defaultVcpus)}
218
209
  value={props.draft.vcpus}
219
210
  />
211
+ <FieldDescription id="workspace-vcpus-help">
212
+ Sandbox vCPUs for this Workspace. Each vCPU adds 2 GB of memory.
213
+ Leave empty to use the app default.
214
+ </FieldDescription>
220
215
  </Field>
221
216
 
222
217
  {props.error ? <InlineError>{props.error}</InlineError> : null}
223
218
 
224
219
  <div className="flex flex-wrap items-center gap-3 border-t border-dashboard-border-subtle pt-5">
225
- <Button disabled={!props.canSave} type="submit">
220
+ <Button disabled={!props.canSave} type="submit" variant="primary">
226
221
  {props.busy
227
222
  ? "Saving…"
228
223
  : props.editing
229
224
  ? "Save changes"
230
225
  : "Create Workspace"}
231
226
  </Button>
227
+ <Link className={buttonVariants()} to={props.cancelPath}>
228
+ Cancel
229
+ </Link>
232
230
  <p className="m-0 text-xs text-dashboard-text-muted">
233
231
  {props.editing
234
232
  ? "Saved changes apply the next time this Workspace is prepared."
@@ -1,22 +1,26 @@
1
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
1
+ import { useMutation, 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,
6
5
  workspaceSchema,
7
6
  type WorkspaceListReport,
8
7
  } from "@sentry/junior/api/schema";
9
8
 
10
9
  import { EmptyTelemetry } from "../../components/EmptyTelemetry";
11
- import type { TimeRangeDays } from "../../components/controls/TimeRangeSelector";
12
10
  import { LoadingView } from "../../components/LoadingView";
13
11
  import { Card } from "../../components/layout/Card";
14
12
  import { PageHeader } from "../../components/layout/PageHeader";
15
- import { fetchDashboardJson, post, put } from "../../http";
13
+ import { workspacePath } from "../../format";
14
+ import { post, put } from "../../http";
16
15
  import { SystemPageLayout } from "./SystemPageLayout";
17
- import { WorkspaceDetails } from "./WorkspaceDetails";
18
16
  import { WorkspaceEditor } from "./WorkspaceEditor";
19
- import { readWorkspaceApiError, workspacesQueryKey } from "./WorkspacesPage";
17
+ import {
18
+ readWorkspaceApiError,
19
+ useWorkspaceData,
20
+ useWorkspacesData,
21
+ workspaceQueryKey,
22
+ workspacesQueryKey,
23
+ } from "./workspaceQueries";
20
24
  import {
21
25
  canSaveWorkspaceDraft,
22
26
  createWorkspaceDraft,
@@ -25,41 +29,21 @@ import {
25
29
  workspaceDraftBody,
26
30
  } from "./workspaceDraft";
27
31
 
28
- // TODO: support 24h once junior_stats can store hour keys (date column is day-only).
29
- /** Workspace switch stats are daily counters only. */
30
- const WORKSPACE_RANGE_OPTIONS: TimeRangeDays[] = [7, 30, 90];
31
-
32
- /** Create or edit one Workspace at a stable dashboard route. */
32
+ /** Create or edit one Workspace recipe on a full page. */
33
33
  export function WorkspaceFormPage() {
34
34
  const { workspaceId } = useParams();
35
35
  const editing = workspaceId !== undefined;
36
36
  const navigate = useNavigate();
37
37
  const queryClient = useQueryClient();
38
- const [range, setRange] = useState<TimeRangeDays>(30);
39
38
  const [newDraft, setNewDraft] = useState(createWorkspaceDraft);
40
39
  const [editedDraft, setEditedDraft] = useState<{
41
40
  draft: WorkspaceDraft;
42
41
  workspaceId: string;
43
42
  }>();
44
43
 
45
- const workspaceQuery = useQuery({
46
- enabled: editing,
47
- queryKey: ["dashboard", "workspace", workspaceId],
48
- queryFn: ({ signal }) =>
49
- fetchDashboardJson(
50
- workspaceSchema,
51
- `/api/workspaces/${encodeURIComponent(workspaceId!)}`,
52
- signal,
53
- ),
54
- retry: false,
55
- });
44
+ const workspaceQuery = useWorkspaceData(workspaceId);
56
45
  // 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
- });
46
+ const workspacesQuery = useWorkspacesData();
63
47
  const defaultVcpus = workspacesQuery.data?.defaultVcpus ?? null;
64
48
 
65
49
  // Seed the edit draft once per Workspace id. editWorkspaceDraft assigns fresh
@@ -103,7 +87,11 @@ export function WorkspaceFormPage() {
103
87
  ].sort((left, right) => left.name.localeCompare(right.name)),
104
88
  },
105
89
  );
106
- navigate("/system/workspaces");
90
+ // A recipe change can retire the current snapshot; read it again.
91
+ await queryClient.invalidateQueries({
92
+ queryKey: workspaceQueryKey(workspace.id),
93
+ });
94
+ navigate(workspacePath(workspace.id));
107
95
  },
108
96
  });
109
97
 
@@ -121,16 +109,9 @@ export function WorkspaceFormPage() {
121
109
  <PageHeader
122
110
  description={
123
111
  editing
124
- ? "Inspect the current snapshot and update how Junior prepares this Workspace."
112
+ ? "Change the repositories, setup script, or Sandbox size."
125
113
  : "Name the recipe, choose repositories, and optionally add a one-time setup script."
126
114
  }
127
- {...(editing
128
- ? {
129
- onRangeChange: setRange,
130
- range,
131
- rangeOptions: WORKSPACE_RANGE_OPTIONS,
132
- }
133
- : {})}
134
115
  title={
135
116
  editing
136
117
  ? (workspaceQuery.data?.name ?? "Workspace")
@@ -138,14 +119,6 @@ export function WorkspaceFormPage() {
138
119
  }
139
120
  />
140
121
 
141
- {editing && workspaceQuery.data ? (
142
- <WorkspaceDetails
143
- defaultVcpus={defaultVcpus}
144
- range={range}
145
- workspace={workspaceQuery.data}
146
- />
147
- ) : null}
148
-
149
122
  {workspaceQuery.error ? (
150
123
  <Card padding="sm">
151
124
  <EmptyTelemetry>
@@ -158,8 +131,11 @@ export function WorkspaceFormPage() {
158
131
  ) : draft ? (
159
132
  <WorkspaceEditor
160
133
  busy={saveMutation.isPending}
161
- defaultVcpus={defaultVcpus}
162
134
  canSave={canSaveWorkspaceDraft(draft, saveMutation.isPending)}
135
+ cancelPath={
136
+ workspaceId ? workspacePath(workspaceId) : "/system/workspaces"
137
+ }
138
+ defaultVcpus={defaultVcpus}
163
139
  draft={draft}
164
140
  editing={editing}
165
141
  error={