@sentry/junior-dashboard 0.264.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 (191) 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 +145 -136
  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 +5 -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 +12 -2
  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 +15 -24
  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 +86 -77
  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 +13 -3
  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/conversations/ChatLayout.tsx +7 -3
  60. package/src/client/conversations/ConversationBrief.tsx +17 -6
  61. package/src/client/conversations/ConversationComposer.tsx +26 -12
  62. package/src/client/conversations/ConversationDetails.tsx +147 -62
  63. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  64. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  65. package/src/client/conversations/ConversationHeader.tsx +8 -3
  66. package/src/client/conversations/ConversationHeaderActions.tsx +90 -82
  67. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  68. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  69. package/src/client/conversations/ConversationMeta.tsx +55 -88
  70. package/src/client/conversations/ConversationPage.tsx +220 -101
  71. package/src/client/conversations/ConversationSidebar.tsx +48 -29
  72. package/src/client/conversations/ConversationWatches.tsx +165 -0
  73. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  74. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  75. package/src/client/conversations/DetailsList.tsx +124 -0
  76. package/src/client/conversations/EventDetails.tsx +42 -8
  77. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  78. package/src/client/conversations/ObjectCard.tsx +20 -8
  79. package/src/client/conversations/PendingMailboxStack.tsx +52 -33
  80. package/src/client/conversations/README.md +86 -13
  81. package/src/client/conversations/ShareConversationButton.tsx +25 -13
  82. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  83. package/src/client/conversations/ToolFrame.tsx +36 -52
  84. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  85. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  86. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  87. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  88. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  89. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  90. package/src/client/conversations/TranscriptMessageContextView.tsx +8 -2
  91. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  92. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  93. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  94. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  95. package/src/client/conversations/TranscriptTurnContextView.tsx +99 -27
  96. package/src/client/conversations/TranscriptView.tsx +63 -27
  97. package/src/client/conversations/eventLog.ts +57 -0
  98. package/src/client/conversations/eventTime.ts +60 -0
  99. package/src/client/conversations/queries.ts +29 -0
  100. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  101. package/src/client/format.ts +5 -0
  102. package/src/client/pages/ConnectionsPage.tsx +2 -2
  103. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  104. package/src/client/pages/SettingsPage.tsx +9 -10
  105. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  106. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  107. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  108. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  109. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  110. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  111. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  112. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  113. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  114. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  115. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  116. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  117. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  118. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  119. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  120. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  121. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  122. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  123. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  124. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  125. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  126. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  127. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  128. package/src/client/pages/automations/README.md +57 -0
  129. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  130. package/src/client/pages/automations/automationFormat.ts +37 -23
  131. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  132. package/src/client/pages/code/CodePage.tsx +52 -55
  133. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  134. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  135. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  136. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  137. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  138. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  139. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  140. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  141. package/src/client/pages/memory/memoryRecord.ts +1 -1
  142. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  143. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  144. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  145. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  146. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  147. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  148. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  149. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  150. package/src/client/pages/system/README.md +23 -0
  151. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  152. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  153. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  154. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  155. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  156. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  157. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  158. package/src/client/pages/system/sandboxSize.ts +8 -0
  159. package/src/client/pages/system/workspaceQueries.ts +53 -0
  160. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  161. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  162. package/src/client/styles.ts +4 -0
  163. package/src/client.tsx +7 -1
  164. package/src/mock-reporting/automation-collection.ts +4 -2
  165. package/src/mock-reporting/fixtures.ts +23 -7
  166. package/src/mock-reporting/routes.ts +12 -0
  167. package/src/mock-reporting/watches.ts +94 -0
  168. package/src/shell.ts +4 -9
  169. package/src/tailwind.css +0 -10
  170. package/src/client/components/Button.tsx +0 -118
  171. package/src/client/components/Field.tsx +0 -53
  172. package/src/client/components/SegmentedTabs.tsx +0 -119
  173. package/src/client/components/Select.tsx +0 -52
  174. package/src/client/components/Skeleton.tsx +0 -17
  175. package/src/client/components/TextInput.tsx +0 -58
  176. package/src/client/components/Tooltip.tsx +0 -216
  177. package/src/client/components/charts/useChartLayout.ts +0 -27
  178. package/src/client/components/ui/sheet.LICENSE +0 -21
  179. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -119
  180. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  181. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  182. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  183. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  184. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  185. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  186. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  187. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  188. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  189. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  190. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  191. package/src/client/pages/system/SnapshotSummary.tsx +0 -78
@@ -1,16 +1,18 @@
1
1
  import type { PluginOperationalReport } from "@sentry/junior/api/schema";
2
2
 
3
+ import { Bar, BarChart, CartesianGrid } from "recharts";
3
4
  import {
4
- ChartAxisLabel,
5
- ChartCategoryLabels,
6
- ChartSvg,
7
- createActivityChartLayout,
5
+ activityChartMargin,
6
+ ActivityChartTooltipContent,
7
+ ActivityChartXAxis,
8
+ ActivityChartYAxis,
9
+ formatActivityCategoryLabel,
8
10
  formatActivityCategoryTooltipLabel,
9
11
  isActivityHourBucket,
10
12
  } from "../../components/charts/ActivityChart";
11
13
  import { ChartLegend } from "../../components/charts/ChartLegend";
12
14
  import type { TimeRangeDays } from "../../components/controls/TimeRangeSelector";
13
- import { Tooltip } from "../../components/Tooltip";
15
+ import { ChartContainer, ChartTooltip } from "../../components/ui/chart";
14
16
  import { formatCostSummary } from "../../format";
15
17
 
16
18
  type Widget = NonNullable<PluginOperationalReport["widgets"]>[number];
@@ -35,34 +37,54 @@ function seriesColor(series: Widget["series"][number], index: number): string {
35
37
  : (colors[index % colors.length] ?? colors[0]);
36
38
  }
37
39
 
38
- /** Render a validated plugin-owned categorical bar chart. */
39
- export function PluginBarChart(props: {
40
- range?: TimeRangeDays;
41
- widget: Widget;
42
- }) {
43
- const { widget } = props;
40
+ /**
41
+ * Pick the categories a plugin chart shows for the page range and the value
42
+ * scale they need. The scale always includes zero and is never empty.
43
+ */
44
+ export function pluginChartScale(widget: Widget, range?: TimeRangeDays) {
44
45
  const categories = widget.timeRangeDays
45
- ? widget.categories.slice(-visibleCategoryCount(widget, props.range))
46
+ ? widget.categories.slice(-visibleCategoryCount(widget, range))
46
47
  : widget.categories;
47
- const seriesFormat = commonSeriesFormat(widget);
48
- const layout = createActivityChartLayout(250, {
49
- bottom: 36,
50
- left: seriesFormat === "usd" ? 104 : 56,
51
- right: 12,
52
- top: 16,
53
- width: 520,
54
- });
55
- const step = layout.plotWidth / Math.max(1, categories.length);
56
- const groupWidth = Math.min(72, step * 0.72);
57
- const barWidth = groupWidth / Math.max(1, widget.series.length);
58
48
  const values = categories.flatMap((category) =>
59
49
  widget.series.map((series) => category.values[series.key] ?? 0),
60
50
  );
61
51
  const minimum = Math.min(0, ...values);
62
52
  const dataMaximum = Math.max(0, ...values);
63
53
  const maximum = dataMaximum === minimum ? minimum + 1 : dataMaximum;
64
- const span = maximum - minimum;
65
- const zeroY = layout.top + (maximum / span) * layout.plotHeight;
54
+ return { categories, maximum, minimum };
55
+ }
56
+
57
+ /** Render a validated plugin-owned categorical bar chart. */
58
+ export function PluginBarChart(props: {
59
+ range?: TimeRangeDays;
60
+ widget: Widget;
61
+ }) {
62
+ const { widget } = props;
63
+ const { categories, maximum, minimum } = pluginChartScale(
64
+ widget,
65
+ props.range,
66
+ );
67
+ const seriesFormat = commonSeriesFormat(widget);
68
+ // Series keys come from plugins, so chart keys use the series index.
69
+ const data = categories.map((category) => ({
70
+ category,
71
+ id: category.id,
72
+ ...Object.fromEntries(
73
+ widget.series.map((series, index) => [
74
+ `series-${index}`,
75
+ category.values[series.key] ?? 0,
76
+ ]),
77
+ ),
78
+ }));
79
+ const labels = new Map(
80
+ categories.map((category) => [category.id, category.label]),
81
+ );
82
+ const chartConfig = Object.fromEntries(
83
+ widget.series.map((series, index) => [
84
+ `series-${index}`,
85
+ { color: seriesColor(series, index), label: series.label },
86
+ ]),
87
+ );
66
88
 
67
89
  return (
68
90
  <div className="overflow-hidden rounded-md border border-dashboard-border bg-dashboard-surface-panel">
@@ -93,69 +115,62 @@ export function PluginBarChart(props: {
93
115
  {widget.emptyText ?? "No chart data."}
94
116
  </p>
95
117
  ) : (
96
- <ChartSvg
97
- aria-label={widget.title}
98
- className="min-h-48"
99
- layout={layout}
118
+ <ChartContainer
119
+ className="aspect-auto h-60 w-full"
120
+ config={chartConfig}
100
121
  >
101
- {[maximum, maximum - span / 2, minimum].map((tickValue, index) => {
102
- const y = layout.top + index * (layout.plotHeight / 2);
103
- return (
104
- <g key={index}>
105
- <line
106
- stroke="var(--color-dashboard-border-subtle)"
107
- strokeDasharray="3 5"
108
- x1={layout.left}
109
- x2={layout.width - layout.right}
110
- y1={y}
111
- y2={y}
122
+ <BarChart
123
+ aria-label={widget.title}
124
+ data={data}
125
+ margin={activityChartMargin}
126
+ role="img"
127
+ >
128
+ <CartesianGrid strokeDasharray="3 5" vertical={false} />
129
+ <ActivityChartYAxis
130
+ format={(value) => formatChartValue(value, seriesFormat)}
131
+ maximum={maximum}
132
+ minimum={minimum}
133
+ />
134
+ <ActivityChartXAxis
135
+ centered
136
+ dataKey="id"
137
+ formatLabel={(id) =>
138
+ formatActivityCategoryLabel(labels.get(id) ?? id)
139
+ }
140
+ keys={categories.map((category) => category.id)}
141
+ />
142
+ <ChartTooltip
143
+ content={
144
+ <ActivityChartTooltipContent
145
+ formatLabel={(id) =>
146
+ formatActivityCategoryTooltipLabel(labels.get(id) ?? id)
147
+ }
148
+ rows={({ category }: (typeof data)[number]) =>
149
+ widget.series.map((series) => [
150
+ series.label,
151
+ formatChartValue(
152
+ category.values[series.key] ?? 0,
153
+ series.format,
154
+ ),
155
+ ])
156
+ }
112
157
  />
113
- <ChartAxisLabel textAnchor="end" x={layout.left - 6} y={y + 3}>
114
- {formatChartValue(tickValue, seriesFormat)}
115
- </ChartAxisLabel>
116
- </g>
117
- );
118
- })}
119
- {categories.flatMap((category, categoryIndex) =>
120
- widget.series.map((series, seriesIndex) => {
121
- const value = category.values[series.key] ?? 0;
122
- const renderedHeight = Math.max(
123
- value ? 2 : 0,
124
- (Math.abs(value) / span) * layout.plotHeight,
125
- );
126
- const formatted = formatChartValue(value, series.format);
127
- return (
128
- <Tooltip
129
- content={`${series.label}: ${formatted}`}
130
- key={`${category.id}:${series.key}`}
131
- label={formatActivityCategoryTooltipLabel(category.label)}
132
- >
133
- <rect
134
- aria-label={`${formatActivityCategoryTooltipLabel(category.label)}, ${series.label}: ${formatted}`}
135
- fill={seriesColor(series, seriesIndex)}
136
- height={renderedHeight}
137
- opacity={value ? 0.82 : 0.1}
138
- rx="1.5"
139
- tabIndex={value ? 0 : -1}
140
- width={barWidth * 0.75}
141
- x={
142
- layout.left +
143
- categoryIndex * step +
144
- (step - groupWidth) / 2 +
145
- seriesIndex * barWidth
146
- }
147
- y={value >= 0 ? zeroY - renderedHeight : zeroY}
148
- />
149
- </Tooltip>
150
- );
151
- }),
152
- )}
153
- <ChartCategoryLabels
154
- categories={categories}
155
- layout={layout}
156
- xPosition={(index) => layout.left + index * step + step / 2}
157
- />
158
- </ChartSvg>
158
+ }
159
+ />
160
+ {widget.series.map((series, index) => (
161
+ <Bar
162
+ dataKey={`series-${index}`}
163
+ fill={`var(--color-series-${index})`}
164
+ fillOpacity={0.82}
165
+ isAnimationActive={false}
166
+ key={series.key}
167
+ maxBarSize={54 / widget.series.length}
168
+ minPointSize={(value) => (value ? 2 : 0)}
169
+ radius={1.5}
170
+ />
171
+ ))}
172
+ </BarChart>
173
+ </ChartContainer>
159
174
  )}
160
175
  </div>
161
176
  );
@@ -199,7 +214,11 @@ function commonSeriesFormat(widget: Widget): "usd" | undefined {
199
214
  return formats.size === 1 ? widget.series[0]?.format : undefined;
200
215
  }
201
216
 
202
- function formatChartValue(value: number, format: "usd" | undefined): string {
217
+ /** Format a plugin chart value as USD or as a number without float noise. */
218
+ export function formatChartValue(
219
+ value: number,
220
+ format: "usd" | undefined,
221
+ ): string {
203
222
  return format === "usd"
204
223
  ? formatCostSummary({ total: value })
205
224
  : formatChartNumber(value);
@@ -1,5 +1,13 @@
1
1
  import type { PluginOperationalReport } from "@sentry/junior/api/schema";
2
2
 
3
+ import {
4
+ Table,
5
+ TableBody,
6
+ TableCell,
7
+ TableHead,
8
+ TableHeader,
9
+ TableRow,
10
+ } from "../../components/ui/table";
3
11
  import { cn } from "../../styles";
4
12
 
5
13
  export type PluginReportRecordSetType = NonNullable<
@@ -63,42 +71,35 @@ export function PluginReportRecordSet(props: {
63
71
  </div>
64
72
  ))}
65
73
  </div>
66
- <div className="hidden overflow-x-auto border-t border-dashboard-border-subtle sm:block">
67
- <table className="w-full min-w-[36rem] border-collapse text-left">
68
- <thead className="bg-dashboard-overlay-soft font-sans font-medium text-xs text-dashboard-text-muted">
69
- <tr>
74
+ <div className="hidden border-t border-dashboard-border-subtle sm:block">
75
+ <Table className="min-w-[36rem]">
76
+ <TableHeader className="bg-dashboard-overlay-soft">
77
+ <TableRow>
70
78
  {fields.map((field) => (
71
- <th
72
- className="border-b border-dashboard-border-subtle px-5 py-2.5 font-medium"
73
- key={field.key}
74
- scope="col"
75
- >
79
+ <TableHead className="px-5" key={field.key} scope="col">
76
80
  {field.label}
77
- </th>
81
+ </TableHead>
78
82
  ))}
79
- </tr>
80
- </thead>
81
- <tbody>
83
+ </TableRow>
84
+ </TableHeader>
85
+ <TableBody>
82
86
  {records.map((record) => (
83
- <tr
84
- className={cn(
85
- "transition-colors hover:bg-dashboard-fill-soft",
86
- rowToneClass(record.tone),
87
- )}
87
+ <TableRow
88
+ className={rowToneClass(record.tone)}
88
89
  key={record.id}
89
90
  >
90
91
  {fields.map((field) => (
91
- <td
92
- className="max-w-72 truncate border-b border-dashboard-border-subtle px-5 py-3 font-mono text-xs text-dashboard-text-muted"
92
+ <TableCell
93
+ className="max-w-72 truncate px-5 py-3 font-mono text-xs text-dashboard-text-muted"
93
94
  key={field.key}
94
95
  >
95
96
  {record.values[field.key] ?? ""}
96
- </td>
97
+ </TableCell>
97
98
  ))}
98
- </tr>
99
+ </TableRow>
99
100
  ))}
100
- </tbody>
101
- </table>
101
+ </TableBody>
102
+ </Table>
102
103
  </div>
103
104
  </>
104
105
  )}
@@ -0,0 +1,23 @@
1
+ # System pages
2
+
3
+ ## Workspaces
4
+
5
+ `/system/workspaces` lists the Workspace recipes. The URL selects one recipe:
6
+ `/system/workspaces/:id` keeps the list and opens `WorkspaceDetails` in the
7
+ shared `DetailsPanel`. The panel is 400px wide at 1280px and above. Smaller
8
+ screens use the bottom sheet. Repository pages link to these details.
9
+
10
+ The panel shows the list item at once. The detail read adds the current
11
+ snapshot, because the list report does not include snapshots. Snapshot loading
12
+ and errors stay in the Snapshot row. Usage shows switches for the last 30 days.
13
+ Workspace switch counts are daily only.
14
+
15
+ Edit and Delete are in the panel header. List rows have no actions. Edit opens
16
+ the full-page editor at `/system/workspaces/:id/edit`. Save and Cancel return to
17
+ the details. A new Workspace opens its details after create.
18
+
19
+ Close and Escape return focus to the selected row. `WorkspaceDeleteDialog`
20
+ names the recipe, keeps errors inside the dialog, and blocks dismissal while the
21
+ request runs. A successful delete closes the details and shows a toast.
22
+
23
+ Browser coverage lives in `e2e/system.spec.ts`.
@@ -12,10 +12,14 @@ const systemNavigationItems = [
12
12
  ];
13
13
 
14
14
  /** Render the stable secondary navigation shared by System pages. */
15
- export function SystemNavigation(props: { children: ReactNode }) {
15
+ export function SystemNavigation(props: {
16
+ children: ReactNode;
17
+ className?: string;
18
+ }) {
16
19
  return (
17
20
  <SecondaryNavigation
18
21
  ariaLabel="System navigation"
22
+ className={props.className}
19
23
  items={systemNavigationItems}
20
24
  >
21
25
  {props.children}
@@ -0,0 +1,111 @@
1
+ import { useState } from "react";
2
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
3
+ import type {
4
+ WorkspaceListReport,
5
+ WorkspaceReport,
6
+ } from "@sentry/junior/api/schema";
7
+
8
+ import {
9
+ AlertDialog,
10
+ AlertDialogAction,
11
+ AlertDialogCancel,
12
+ AlertDialogContent,
13
+ AlertDialogDescription,
14
+ AlertDialogFooter,
15
+ AlertDialogTitle,
16
+ } from "../../components/ui/alert-dialog";
17
+ import { useToastManager } from "../../components/ui/toast";
18
+ import { FormNotice } from "../../components/FormNotice";
19
+ import { deleteDashboardResource } from "../../http";
20
+ import {
21
+ readWorkspaceApiError,
22
+ workspaceQueryKey,
23
+ workspacesQueryKey,
24
+ } from "./workspaceQueries";
25
+
26
+ /** Confirm Workspace deletion by name and keep a failed request open for retry. */
27
+ export function WorkspaceDeleteDialog(props: {
28
+ onClose(): void;
29
+ onDeleted(): void;
30
+ returnFocus(): HTMLElement | false;
31
+ workspace: WorkspaceReport;
32
+ }) {
33
+ const [open, setOpen] = useState(true);
34
+ const queryClient = useQueryClient();
35
+ const toast = useToastManager();
36
+ const deletion = useMutation({
37
+ mutationFn: () =>
38
+ deleteDashboardResource(
39
+ `/api/workspaces/${encodeURIComponent(props.workspace.id)}`,
40
+ ),
41
+ onSuccess: async () => {
42
+ await queryClient.cancelQueries({ queryKey: workspacesQueryKey });
43
+ queryClient.setQueryData<WorkspaceListReport>(
44
+ workspacesQueryKey,
45
+ (current) =>
46
+ current && {
47
+ ...current,
48
+ workspaces: current.workspaces.filter(
49
+ (item) => item.id !== props.workspace.id,
50
+ ),
51
+ },
52
+ );
53
+ props.onDeleted();
54
+ // Drop the detail read too, so a later visit shows the not-found state.
55
+ queryClient.removeQueries({
56
+ queryKey: workspaceQueryKey(props.workspace.id),
57
+ });
58
+ toast.add({
59
+ title: "Workspace deleted",
60
+ description: props.workspace.name,
61
+ type: "success",
62
+ });
63
+ setOpen(false);
64
+ },
65
+ });
66
+ return (
67
+ <AlertDialog
68
+ open={open}
69
+ onOpenChange={(next, details) => {
70
+ if (deletion.isPending) details.cancel();
71
+ else setOpen(next);
72
+ }}
73
+ onOpenChangeComplete={(next) => {
74
+ if (!next) props.onClose();
75
+ }}
76
+ >
77
+ <AlertDialogContent finalFocus={props.returnFocus}>
78
+ <AlertDialogTitle>Delete Workspace?</AlertDialogTitle>
79
+ <AlertDialogDescription>
80
+ Delete{" "}
81
+ <strong className="font-medium text-dashboard-text">
82
+ {props.workspace.name}
83
+ </strong>
84
+ ? Active conversations keep their current Sandbox until the next
85
+ switch.
86
+ </AlertDialogDescription>
87
+ {deletion.error ? (
88
+ <FormNotice
89
+ tone="error"
90
+ title={readWorkspaceApiError(
91
+ deletion.error,
92
+ "Could not delete the Workspace. Try again.",
93
+ )}
94
+ />
95
+ ) : null}
96
+ <AlertDialogFooter>
97
+ <AlertDialogCancel disabled={deletion.isPending}>
98
+ Cancel
99
+ </AlertDialogCancel>
100
+ <AlertDialogAction
101
+ disabled={deletion.isPending}
102
+ onClick={() => deletion.mutate()}
103
+ variant="danger"
104
+ >
105
+ {deletion.isPending ? "Deleting…" : "Delete Workspace"}
106
+ </AlertDialogAction>
107
+ </AlertDialogFooter>
108
+ </AlertDialogContent>
109
+ </AlertDialog>
110
+ );
111
+ }