@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,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
+ }