@sentry/junior-dashboard 0.264.0 → 0.266.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 (213) 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/ActorAvatar.tsx +10 -22
  14. package/src/client/components/BottomSheet.tsx +12 -2
  15. package/src/client/components/ConnectionBanner.tsx +4 -3
  16. package/src/client/components/CopyButton.tsx +82 -0
  17. package/src/client/components/DetailsPanel.tsx +133 -0
  18. package/src/client/components/Drawer.tsx +15 -24
  19. package/src/client/components/EmptyTelemetry.tsx +5 -3
  20. package/src/client/components/FilterBar.tsx +16 -31
  21. package/src/client/components/ImageAttachment.tsx +49 -63
  22. package/src/client/components/InlineError.tsx +8 -4
  23. package/src/client/components/Metric.tsx +33 -23
  24. package/src/client/components/Notice.tsx +19 -26
  25. package/src/client/components/PageContentSkeleton.tsx +1 -1
  26. package/src/client/components/Pagination.tsx +27 -27
  27. package/src/client/components/ParticipantAvatarStack.tsx +43 -39
  28. package/src/client/components/ProfileMenu.tsx +27 -93
  29. package/src/client/components/README.md +103 -78
  30. package/src/client/components/SearchInput.tsx +51 -41
  31. package/src/client/components/SegmentedNav.tsx +1 -1
  32. package/src/client/components/ShimmerText.tsx +2 -6
  33. package/src/client/components/ThemeSwitcher.tsx +19 -15
  34. package/src/client/components/VersionDriftBanner.tsx +9 -7
  35. package/src/client/components/charts/ActivityChart.tsx +173 -288
  36. package/src/client/components/charts/CacheWrites.tsx +59 -37
  37. package/src/client/components/charts/InputCacheChart.tsx +137 -131
  38. package/src/client/components/charts/README.md +4 -4
  39. package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
  40. package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
  41. package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
  42. package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
  43. package/src/client/components/layout/DashboardHeader.tsx +44 -90
  44. package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
  45. package/src/client/components/layout/VisualViewportShell.tsx +2 -2
  46. package/src/client/components/ui/accordion.tsx +77 -0
  47. package/src/client/components/ui/alert-dialog.tsx +89 -0
  48. package/src/client/components/ui/alert.tsx +64 -0
  49. package/src/client/components/ui/avatar.tsx +53 -0
  50. package/src/client/components/ui/button.tsx +86 -0
  51. package/src/client/components/ui/chart.tsx +74 -0
  52. package/src/client/components/ui/checkbox.tsx +26 -0
  53. package/src/client/components/ui/collapsible.tsx +22 -0
  54. package/src/client/components/ui/dialog.tsx +66 -0
  55. package/src/client/components/ui/empty.tsx +32 -0
  56. package/src/client/components/ui/field.tsx +49 -0
  57. package/src/client/components/ui/input-group.tsx +96 -0
  58. package/src/client/components/ui/input.tsx +35 -0
  59. package/src/client/components/ui/native-select.tsx +57 -0
  60. package/src/client/components/ui/pagination.tsx +60 -0
  61. package/src/client/components/ui/popover.tsx +67 -0
  62. package/src/client/components/ui/radio-group.tsx +32 -0
  63. package/src/client/components/ui/sheet.tsx +13 -3
  64. package/src/client/components/ui/skeleton.tsx +19 -0
  65. package/src/client/components/ui/spinner.tsx +21 -0
  66. package/src/client/components/ui/table.tsx +101 -0
  67. package/src/client/components/ui/tabs.tsx +75 -0
  68. package/src/client/components/ui/textarea.tsx +17 -0
  69. package/src/client/components/ui/toast.tsx +215 -0
  70. package/src/client/components/ui/toggle-group.tsx +36 -0
  71. package/src/client/components/ui/toggle.tsx +37 -0
  72. package/src/client/components/ui/tooltip.tsx +168 -0
  73. package/src/client/components/ui/trace.tsx +96 -0
  74. package/src/client/conversations/ChatLayout.tsx +7 -3
  75. package/src/client/conversations/ConversationBrief.tsx +17 -6
  76. package/src/client/conversations/ConversationChildAgents.tsx +5 -5
  77. package/src/client/conversations/ConversationComposer.tsx +26 -12
  78. package/src/client/conversations/ConversationDetails.tsx +147 -62
  79. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  80. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  81. package/src/client/conversations/ConversationHeader.tsx +13 -7
  82. package/src/client/conversations/ConversationHeaderActions.tsx +97 -93
  83. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  84. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  85. package/src/client/conversations/ConversationMeta.tsx +55 -88
  86. package/src/client/conversations/ConversationPage.tsx +220 -101
  87. package/src/client/conversations/ConversationSidebar.tsx +49 -29
  88. package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
  89. package/src/client/conversations/ConversationWatches.tsx +173 -0
  90. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  91. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  92. package/src/client/conversations/DetailsList.tsx +139 -0
  93. package/src/client/conversations/EventDetails.tsx +42 -8
  94. package/src/client/conversations/EventUsage.tsx +99 -74
  95. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  96. package/src/client/conversations/ObjectCard.tsx +20 -8
  97. package/src/client/conversations/PendingMailboxStack.tsx +52 -38
  98. package/src/client/conversations/README.md +89 -13
  99. package/src/client/conversations/ShareConversationButton.tsx +28 -17
  100. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  101. package/src/client/conversations/ToolFrame.tsx +36 -52
  102. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  103. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  104. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  105. package/src/client/conversations/TranscriptEventView.tsx +35 -16
  106. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  107. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  108. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  109. package/src/client/conversations/TranscriptMessageContextView.tsx +35 -18
  110. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  111. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  112. package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
  113. package/src/client/conversations/TranscriptSummary.tsx +34 -2
  114. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  115. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  116. package/src/client/conversations/TranscriptTurnContextView.tsx +144 -65
  117. package/src/client/conversations/TranscriptView.tsx +63 -27
  118. package/src/client/conversations/eventLog.ts +58 -0
  119. package/src/client/conversations/eventTime.ts +60 -0
  120. package/src/client/conversations/queries.ts +29 -0
  121. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  122. package/src/client/conversations/transcriptMentions.tsx +18 -5
  123. package/src/client/format.ts +5 -0
  124. package/src/client/pages/ConnectionsPage.tsx +2 -2
  125. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  126. package/src/client/pages/SettingsPage.tsx +9 -10
  127. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  128. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  129. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  130. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  131. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  132. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  133. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  134. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  135. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  136. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  137. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  138. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  139. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  140. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  141. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  142. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  143. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  144. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  145. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  146. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  147. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  148. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  149. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  150. package/src/client/pages/automations/README.md +57 -0
  151. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  152. package/src/client/pages/automations/automationFormat.ts +37 -23
  153. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  154. package/src/client/pages/code/CodePage.tsx +52 -55
  155. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  156. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  157. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  158. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  159. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  160. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  161. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  162. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  163. package/src/client/pages/memory/memoryRecord.ts +1 -1
  164. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  165. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  166. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  167. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  168. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  169. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  170. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  171. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  172. package/src/client/pages/system/README.md +23 -0
  173. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  174. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  175. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  176. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  177. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  178. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  179. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  180. package/src/client/pages/system/sandboxSize.ts +8 -0
  181. package/src/client/pages/system/workspaceQueries.ts +53 -0
  182. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  183. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  184. package/src/client/styles.ts +4 -0
  185. package/src/client.tsx +7 -1
  186. package/src/mock-reporting/automation-collection.ts +4 -2
  187. package/src/mock-reporting/fixtures.ts +23 -7
  188. package/src/mock-reporting/routes.ts +12 -0
  189. package/src/mock-reporting/watches.ts +94 -0
  190. package/src/shell.ts +4 -9
  191. package/src/tailwind.css +0 -10
  192. package/src/client/components/Button.tsx +0 -118
  193. package/src/client/components/Field.tsx +0 -53
  194. package/src/client/components/SegmentedTabs.tsx +0 -119
  195. package/src/client/components/Select.tsx +0 -52
  196. package/src/client/components/Skeleton.tsx +0 -17
  197. package/src/client/components/TextInput.tsx +0 -58
  198. package/src/client/components/Tooltip.tsx +0 -216
  199. package/src/client/components/charts/useChartLayout.ts +0 -27
  200. package/src/client/components/ui/sheet.LICENSE +0 -21
  201. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -119
  202. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  203. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  204. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  205. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  206. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  207. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  208. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  209. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  210. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  211. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  212. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  213. package/src/client/pages/system/SnapshotSummary.tsx +0 -78
@@ -1,17 +1,43 @@
1
- import { useId, useState } from "react";
2
- import { SlidersHorizontal } from "lucide-react";
3
- import { cn } from "../../styles";
1
+ import { useState, useSyncExternalStore } from "react";
2
+ import { SlidersHorizontal, X } from "lucide-react";
4
3
  import type {
5
4
  AutomationList,
6
5
  AutomationListQuery,
7
6
  } from "@sentry/junior/api/schema";
8
- import { Button, ToggleButton } from "../../components/Button";
7
+ import { BottomSheet } from "../../components/BottomSheet";
8
+ import {
9
+ Popover,
10
+ PopoverContent,
11
+ PopoverTitle,
12
+ PopoverTrigger,
13
+ } from "../../components/ui/popover";
14
+ import { Button } from "../../components/ui/button";
9
15
  import { DirectorySortSelect } from "../../components/controls/DirectorySortSelect";
10
16
  import { FilterGroup } from "../../components/FilterBar";
11
17
  import { SearchInput } from "../../components/SearchInput";
12
- import { Select } from "../../components/Select";
18
+ import { NativeSelect } from "../../components/ui/native-select";
19
+ import { ToggleGroup, ToggleGroupItem } from "../../components/ui/toggle-group";
13
20
 
14
- /** Keep collection controls visible while a filtered page loads. */
21
+ const filterMediaQuery = "(min-width: 768px)";
22
+ const stateOptions = [
23
+ { value: "active", label: "Active" },
24
+ { value: "blocked", label: "Blocked" },
25
+ { value: "paused", label: "Paused" },
26
+ { value: "unavailable", label: "Trigger unavailable" },
27
+ { value: "completed", label: "Completed" },
28
+ ];
29
+
30
+ function subscribeToFilterLayout(onChange: () => void) {
31
+ const media = window.matchMedia(filterMediaQuery);
32
+ media.addEventListener("change", onChange);
33
+ return () => media.removeEventListener("change", onChange);
34
+ }
35
+
36
+ function filtersFitPopover() {
37
+ return window.matchMedia(filterMediaQuery).matches;
38
+ }
39
+
40
+ /** Keep URL filters visible while editing them in a popover or mobile sheet. */
15
41
  export function AutomationFilters(props: {
16
42
  filters: AutomationListQuery;
17
43
  data: AutomationList | undefined;
@@ -21,17 +47,68 @@ export function AutomationFilters(props: {
21
47
  }) {
22
48
  const { filters, data, onChange } = props;
23
49
  const [expanded, setExpanded] = useState(false);
24
- const controlsId = useId();
25
- const activeCount = [
26
- filters.type !== "all",
27
- Boolean(filters.creator),
28
- Boolean(filters.destination),
29
- filters.state !== "all",
30
- ].filter(Boolean).length;
50
+ const desktop = useSyncExternalStore(
51
+ subscribeToFilterLayout,
52
+ filtersFitPopover,
53
+ () => false,
54
+ );
55
+ const activeFilters = [
56
+ {
57
+ key: "type",
58
+ label: "Type",
59
+ value: filters.type === "all" ? "" : filters.type,
60
+ text: filters.type === "scheduled" ? "Scheduled" : "Event",
61
+ },
62
+ {
63
+ key: "creator",
64
+ label: "Creator",
65
+ value: filters.creator,
66
+ text:
67
+ data?.creators.find((option) => option.value === filters.creator)
68
+ ?.label ?? filters.creator,
69
+ },
70
+ {
71
+ key: "destination",
72
+ label: "Destination",
73
+ value: filters.destination,
74
+ text:
75
+ data?.destinations.find(
76
+ (option) => option.value === filters.destination,
77
+ )?.label ?? filters.destination,
78
+ },
79
+ {
80
+ key: "state",
81
+ label: "State",
82
+ value: filters.state === "all" ? "" : filters.state,
83
+ text: stateOptions.find((option) => option.value === filters.state)
84
+ ?.label,
85
+ },
86
+ ].filter((filter) => filter.value);
87
+ const trigger = (
88
+ <Button
89
+ className="h-11 shrink-0"
90
+ aria-expanded={expanded}
91
+ aria-haspopup="dialog"
92
+ onClick={desktop ? undefined : () => setExpanded(true)}
93
+ >
94
+ <SlidersHorizontal aria-hidden="true" size={14} /> Filters
95
+ {activeFilters.length ? ` (${activeFilters.length})` : ""}
96
+ </Button>
97
+ );
98
+ const controls = (
99
+ <AutomationFilterFields filters={filters} data={data} onChange={onChange} />
100
+ );
31
101
  return (
32
102
  <div className="grid gap-3">
33
103
  <div className="grid gap-2 lg:grid-cols-[auto_minmax(16rem,1fr)] lg:items-center lg:gap-4">
34
- <div aria-label="Scope" role="group" className="flex flex-wrap gap-2">
104
+ <ToggleGroup
105
+ aria-label="Scope"
106
+ className="flex-wrap gap-2"
107
+ onValueChange={([next]) => {
108
+ if (next) onChange("scope", next);
109
+ }}
110
+ value={[filters.scope]}
111
+ >
35
112
  {(
36
113
  [
37
114
  ["all", "All"],
@@ -40,20 +117,19 @@ export function AutomationFilters(props: {
40
117
  ["attention", "Needs attention"],
41
118
  ] as const
42
119
  ).map(([value, label]) => (
43
- <ToggleButton
44
- className="min-h-11 font-sans! normal-case! lg:min-h-9"
120
+ <ToggleGroupItem
121
+ className="min-h-11 lg:min-h-9"
45
122
  key={value}
46
- onClick={() => onChange("scope", value)}
47
- pressed={filters.scope === value}
123
+ value={value}
48
124
  variant="pill"
49
125
  >
50
126
  {label}
51
127
  {data && value !== "attention" ? (
52
128
  <span className="ml-1.5 opacity-65">{data.counts[value]}</span>
53
129
  ) : null}
54
- </ToggleButton>
130
+ </ToggleGroupItem>
55
131
  ))}
56
- </div>
132
+ </ToggleGroup>
57
133
  <div className="flex items-center gap-2">
58
134
  <SearchInput
59
135
  className="flex-1"
@@ -63,68 +139,107 @@ export function AutomationFilters(props: {
63
139
  value={props.searchText}
64
140
  onChange={props.onSearch}
65
141
  />
66
- <Button
67
- className="h-11 shrink-0"
68
- aria-expanded={expanded}
69
- aria-controls={controlsId}
70
- onClick={() => setExpanded(!expanded)}
71
- >
72
- <SlidersHorizontal aria-hidden="true" size={14} /> Filters
73
- {activeCount ? ` (${activeCount})` : ""}
74
- </Button>
142
+ {desktop ? (
143
+ <Popover open={expanded} onOpenChange={setExpanded}>
144
+ <PopoverTrigger render={trigger} />
145
+ <PopoverContent align="end" className="w-80">
146
+ <PopoverTitle>Filters</PopoverTitle>
147
+ <div className="mt-4">{controls}</div>
148
+ </PopoverContent>
149
+ </Popover>
150
+ ) : (
151
+ trigger
152
+ )}
75
153
  </div>
76
154
  </div>
77
- <div
78
- id={controlsId}
79
- className={cn(
80
- "grid-cols-2 items-end gap-3 lg:grid-cols-3 xl:grid-cols-[auto_repeat(3,minmax(0,1fr))]",
81
- expanded ? "grid" : "hidden",
82
- )}
83
- >
84
- <FilterGroup
85
- className="col-span-2 lg:col-span-3 xl:col-span-1"
86
- label="Type"
155
+ {activeFilters.length > 0 ? (
156
+ <div
157
+ aria-label="Active filters"
158
+ role="group"
159
+ className="flex min-w-0 flex-wrap gap-2"
160
+ >
161
+ {activeFilters.map((filter) => (
162
+ <Button
163
+ key={filter.key}
164
+ aria-label={`Remove ${filter.key} filter`}
165
+ className="min-h-9 max-w-full text-xs"
166
+ title={`${filter.label}: ${filter.text}`}
167
+ onClick={() => onChange(filter.key, "")}
168
+ >
169
+ <span className="truncate">
170
+ {filter.label}: {filter.text}
171
+ </span>
172
+ <X aria-hidden="true" className="shrink-0" size={12} />
173
+ </Button>
174
+ ))}
175
+ </div>
176
+ ) : null}
177
+ {!desktop && expanded ? (
178
+ <BottomSheet
179
+ label="Filters"
180
+ size="content"
181
+ onClose={() => setExpanded(false)}
182
+ >
183
+ <div className="p-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
184
+ {controls}
185
+ </div>
186
+ </BottomSheet>
187
+ ) : null}
188
+ </div>
189
+ );
190
+ }
191
+
192
+ function AutomationFilterFields({
193
+ filters,
194
+ data,
195
+ onChange,
196
+ }: Pick<
197
+ Parameters<typeof AutomationFilters>[0],
198
+ "filters" | "data" | "onChange"
199
+ >) {
200
+ return (
201
+ <div className="grid gap-4">
202
+ <FilterGroup label="Type">
203
+ <ToggleGroup
204
+ className="flex-wrap gap-2"
205
+ onValueChange={([next]) => {
206
+ if (next) onChange("type", next);
207
+ }}
208
+ value={[filters.type]}
87
209
  >
88
210
  {(["all", "scheduled", "event"] as const).map((value) => (
89
- <ToggleButton
211
+ <ToggleGroupItem
90
212
  className="min-h-9"
91
213
  key={value}
92
- onClick={() => onChange("type", value)}
93
- pressed={filters.type === value}
214
+ value={value}
94
215
  variant="pill"
95
216
  >
96
217
  {value}
97
- </ToggleButton>
218
+ </ToggleGroupItem>
98
219
  ))}
99
- </FilterGroup>
100
- <AutomationFilterSelect
101
- label="Creator"
102
- emptyLabel="All"
103
- value={filters.creator ?? ""}
104
- options={data?.creators ?? []}
105
- onChange={(value) => onChange("creator", value)}
106
- />
107
- <AutomationFilterSelect
108
- label="Destination"
109
- emptyLabel="All"
110
- value={filters.destination ?? ""}
111
- options={data?.destinations ?? []}
112
- onChange={(value) => onChange("destination", value)}
113
- />
114
- <AutomationFilterSelect
115
- label="State"
116
- emptyLabel="All"
117
- value={filters.state === "all" ? "" : filters.state}
118
- options={[
119
- { value: "active", label: "Active" },
120
- { value: "blocked", label: "Blocked" },
121
- { value: "paused", label: "Paused" },
122
- { value: "unavailable", label: "Trigger unavailable" },
123
- { value: "completed", label: "Completed" },
124
- ]}
125
- onChange={(value) => onChange("state", value)}
126
- />
127
- </div>
220
+ </ToggleGroup>
221
+ </FilterGroup>
222
+ <AutomationFilterSelect
223
+ label="Creator"
224
+ emptyLabel="All"
225
+ value={filters.creator ?? ""}
226
+ options={data?.creators ?? []}
227
+ onChange={(value) => onChange("creator", value)}
228
+ />
229
+ <AutomationFilterSelect
230
+ label="Destination"
231
+ emptyLabel="All"
232
+ value={filters.destination ?? ""}
233
+ options={data?.destinations ?? []}
234
+ onChange={(value) => onChange("destination", value)}
235
+ />
236
+ <AutomationFilterSelect
237
+ label="State"
238
+ emptyLabel="All"
239
+ value={filters.state === "all" ? "" : filters.state}
240
+ options={stateOptions}
241
+ onChange={(value) => onChange("state", value)}
242
+ />
128
243
  </div>
129
244
  );
130
245
  }
@@ -158,7 +273,7 @@ function AutomationFilterSelect(props: {
158
273
  }) {
159
274
  return (
160
275
  <FilterGroup label={props.label}>
161
- <Select
276
+ <NativeSelect
162
277
  aria-label={props.ariaLabel ?? `Filter by ${props.label.toLowerCase()}`}
163
278
  variant="filter"
164
279
  value={props.value}
@@ -174,7 +289,7 @@ function AutomationFilterSelect(props: {
174
289
  {option.label}
175
290
  </option>
176
291
  ))}
177
- </Select>
292
+ </NativeSelect>
178
293
  </FilterGroup>
179
294
  );
180
295
  }
@@ -1,32 +1,44 @@
1
1
  import { useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import {
3
+ Popover,
4
+ PopoverTrigger,
5
+ PopoverContent,
6
+ PopoverDescription,
7
+ } from "../../components/ui/popover";
8
+ import { useToastManager } from "../../components/ui/toast";
9
+ import {
10
+ Tooltip,
11
+ TooltipContent,
12
+ TooltipTrigger,
13
+ } from "../../components/ui/tooltip";
14
+ import { Link } from "react-router";
15
+ import { CircleHelp, Pencil, Settings2, Trash2 } from "lucide-react";
2
16
  import {
3
17
  automationEditSchema,
4
18
  type AutomationSummary,
5
19
  } from "@sentry/junior/api/schema";
6
- import { Link } from "react-router";
7
- import { Button, ButtonLink } from "../../components/Button";
8
- import { StatusChip } from "../../components/StatusChip";
20
+ import { Button } from "../../components/ui/button";
9
21
  import { FormNotice } from "../../components/FormNotice";
10
22
  import { DashboardApiError, fetchDashboardJson, post } from "../../http";
11
23
 
12
- /** Explain lifecycle separately from the last execution. Pause and resume are creator-only. */
24
+ /** Keep revision-checked lifecycle actions available in the fixed details footer. */
13
25
  export function AutomationLifecycle({
14
26
  automation,
15
27
  editPath,
28
+ onDelete,
16
29
  }: {
17
30
  automation: AutomationSummary;
18
31
  editPath: string;
32
+ onDelete(source: HTMLButtonElement): void;
19
33
  }) {
34
+ const toast = useToastManager();
20
35
  const queryClient = useQueryClient();
21
36
  const completed = automation.status === "completed";
22
37
  const paused = automation.status === "paused";
23
38
  const blocked = automation.status === "blocked";
24
- const unavailable =
25
- automation.kind === "event" && !automation.triggerAvailable;
26
39
  // Public readers can edit settings, but only the creator controls lifecycle.
27
40
  const canEdit = !completed;
28
41
  const canPause = automation.ownedByViewer && !completed;
29
- const blockReason = automation.statusReason;
30
42
  const action = paused || blocked ? "resume" : "pause";
31
43
  const mutation = useMutation({
32
44
  mutationFn: async () => {
@@ -46,40 +58,77 @@ export function AutomationLifecycle({
46
58
  revision: current.revision,
47
59
  });
48
60
  },
49
- onSuccess: async () => {
61
+ onSuccess: async (updated) => {
50
62
  await queryClient.invalidateQueries({
51
63
  queryKey: ["dashboard", "automations"],
52
64
  });
65
+ toast.add({
66
+ title:
67
+ updated.status === "blocked"
68
+ ? "Automation is still blocked"
69
+ : action === "pause"
70
+ ? "Automation paused"
71
+ : "Automation resumed",
72
+ description:
73
+ updated.status === "blocked"
74
+ ? "Fix the reported requirement before resuming again."
75
+ : action === "pause"
76
+ ? "Work already queued or running may finish."
77
+ : "Future triggers are enabled.",
78
+ type: updated.status === "blocked" ? "warning" : "success",
79
+ });
53
80
  },
54
81
  });
55
- const status =
56
- automation.status === "active" && unavailable
57
- ? "Trigger unavailable"
58
- : automation.status;
59
- const failed =
60
- automation.lastRunStatus === "failed" ||
61
- automation.lastRunStatus === "blocked";
62
82
  return (
63
- <div className="grid gap-3 text-sm">
64
- <div className="flex flex-wrap items-center justify-between gap-3">
65
- <div role="status" aria-live="polite">
66
- <StatusChip
67
- tone={
68
- paused || completed
69
- ? "neutral"
70
- : blocked || unavailable
71
- ? "warning"
72
- : "success"
73
- }
74
- >
75
- {status}
76
- </StatusChip>
83
+ <footer
84
+ aria-label="Automation actions"
85
+ className="grid gap-3 border-t border-dashboard-border bg-dashboard-bg-elevated p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]"
86
+ >
87
+ <div className="flex items-center gap-2">
88
+ <div className="mr-auto flex items-center gap-1">
89
+ <Tooltip>
90
+ <TooltipTrigger
91
+ render={
92
+ <Link
93
+ aria-label={canEdit ? "Edit automation" : "View settings"}
94
+ className="grid size-11 place-items-center rounded-md text-dashboard-text-muted no-underline hover:bg-dashboard-fill-strong hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-dashboard-focus"
95
+ to={editPath}
96
+ >
97
+ {canEdit ? (
98
+ <Pencil aria-hidden="true" size={16} />
99
+ ) : (
100
+ <Settings2 aria-hidden="true" size={16} />
101
+ )}
102
+ </Link>
103
+ }
104
+ />
105
+ <TooltipContent size="compact">
106
+ {canEdit ? "Edit automation" : "View settings"}
107
+ </TooltipContent>
108
+ </Tooltip>
109
+ {automation.ownedByViewer ? (
110
+ <Tooltip>
111
+ <TooltipTrigger
112
+ render={
113
+ <Button
114
+ focusableWhenDisabled
115
+ aria-label="Delete automation"
116
+ className="min-h-11 min-w-11"
117
+ size="icon"
118
+ variant="danger"
119
+ onClick={(event) => onDelete(event.currentTarget)}
120
+ disabled={mutation.isPending}
121
+ >
122
+ <Trash2 aria-hidden="true" size={16} />
123
+ </Button>
124
+ }
125
+ />
126
+ <TooltipContent size="compact">Delete automation</TooltipContent>
127
+ </Tooltip>
128
+ ) : null}
77
129
  </div>
78
- <div className="flex items-center gap-2">
79
- <ButtonLink className="min-h-11" to={editPath}>
80
- {canEdit ? "Edit automation" : "View settings"}
81
- </ButtonLink>
82
- {canPause ? (
130
+ {canPause ? (
131
+ <>
83
132
  <Button
84
133
  className="min-h-11"
85
134
  disabled={mutation.isPending}
@@ -91,50 +140,32 @@ export function AutomationLifecycle({
91
140
  ? "Pause"
92
141
  : "Resume"}
93
142
  </Button>
94
- ) : null}
95
- </div>
143
+ <Popover>
144
+ <PopoverTrigger
145
+ render={
146
+ <Button
147
+ aria-label="About pausing"
148
+ className="min-h-11 min-w-11"
149
+ size="icon"
150
+ />
151
+ }
152
+ >
153
+ <CircleHelp aria-hidden="true" size={16} />
154
+ </PopoverTrigger>
155
+ <PopoverContent side="top" align="end" aria-label="About pausing">
156
+ <PopoverDescription>
157
+ Pausing stops new runs. Work already queued or running may
158
+ finish. Resuming skips missed runs and waits for the next
159
+ scheduled time or new event.
160
+ {paused && automation.statusReason
161
+ ? " Removing the pause keeps the block until you fix it and resume again."
162
+ : ""}
163
+ </PopoverDescription>
164
+ </PopoverContent>
165
+ </Popover>
166
+ </>
167
+ ) : null}
96
168
  </div>
97
- {blocked || blockReason ? (
98
- <p className="m-0 break-words text-dashboard-text-muted">
99
- {blockReason ?? "Check the last run to see what needs fixing."}{" "}
100
- {paused ? "Fix this before resuming." : "Fix this, then resume."}
101
- </p>
102
- ) : null}
103
- {unavailable ? (
104
- <p className="m-0 text-dashboard-text-muted">
105
- This event source is unavailable. Enable its plugin or choose another
106
- event.
107
- </p>
108
- ) : null}
109
- {failed ? (
110
- <p className="m-0 text-dashboard-text-muted">
111
- Last run {automation.lastRunStatus}.
112
- {automation.status === "active" && !unavailable
113
- ? " Future runs are still on."
114
- : ""}{" "}
115
- <Link
116
- className="text-dashboard-text underline underline-offset-2"
117
- to={`/automations/${encodeURIComponent(automation.id)}/executions`}
118
- >
119
- View run history
120
- </Link>
121
- </p>
122
- ) : null}
123
- {canPause ? (
124
- <details className="text-dashboard-text-muted">
125
- <summary className="w-fit cursor-pointer py-2 text-xs hover:text-dashboard-text focus-visible:outline focus-visible:outline-dashboard-focus">
126
- About pausing
127
- </summary>
128
- <p className="mt-1 mb-0 leading-relaxed">
129
- Pausing stops new runs. Work already queued or running may finish.
130
- Resuming skips missed runs and waits for the next scheduled time or
131
- new event.
132
- {paused && blockReason
133
- ? " Removing the pause keeps the block until you fix it and resume again."
134
- : ""}
135
- </p>
136
- </details>
137
- ) : null}
138
169
  {mutation.error ? (
139
170
  <FormNotice tone="error" title="Could not update this automation.">
140
171
  {mutation.error instanceof DashboardApiError
@@ -142,6 +173,6 @@ export function AutomationLifecycle({
142
173
  : mutation.error.message}
143
174
  </FormNotice>
144
175
  ) : null}
145
- </div>
176
+ </footer>
146
177
  );
147
178
  }
@@ -3,8 +3,9 @@ import type {
3
3
  AutomationEdit,
4
4
  AutomationSummary,
5
5
  } from "@sentry/junior/api/schema";
6
- import { Button } from "../../components/Button";
7
- import { Select } from "../../components/Select";
6
+ import { Button } from "../../components/ui/button";
7
+ import { NativeSelect } from "../../components/ui/native-select";
8
+ import { RadioGroup, RadioGroupItem } from "../../components/ui/radio-group";
8
9
  import { cn } from "../../styles";
9
10
  import type { AutomationDraft } from "./automationDraft";
10
11
  import { automationOutcomeLabel } from "./automationOutcomes";
@@ -32,6 +33,8 @@ export function AutomationOutcomeFields(props: {
32
33
  value: AutomationDraft["outcomes"];
33
34
  original: AutomationEdit["outcomes"];
34
35
  destination: AutomationSummary["destination"];
36
+ /** Base UI radios do not inherit a native fieldset's disabled state. */
37
+ disabled?: boolean;
35
38
  onChange(value: AutomationDraft["outcomes"]): void;
36
39
  }) {
37
40
  const defaultOutcomes: AutomationDraft["outcomes"] = props.original.length
@@ -65,26 +68,25 @@ export function AutomationOutcomeFields(props: {
65
68
  ];
66
69
  return (
67
70
  <>
68
- <fieldset>
69
- <legend className="sr-only">Success messages</legend>
70
- <div className="flex flex-wrap gap-x-6 gap-y-3">
71
- {[true, false].map((send) => (
72
- <label
73
- key={String(send)}
74
- className="inline-flex min-h-8 cursor-pointer items-center gap-2 text-sm"
75
- >
76
- <input
77
- type="radio"
78
- name="messages"
79
- className="size-4 accent-dashboard-focus"
80
- checked={Boolean(props.value.length) === send}
81
- onChange={() => props.onChange(send ? defaultOutcomes : [])}
82
- />
83
- {send ? "Send a message" : "No success message"}
84
- </label>
85
- ))}
86
- </div>
87
- </fieldset>
71
+ <RadioGroup
72
+ aria-label="Success messages"
73
+ disabled={props.disabled}
74
+ className="flex flex-wrap gap-x-6 gap-y-3"
75
+ onValueChange={(send: boolean) =>
76
+ props.onChange(send ? defaultOutcomes : [])
77
+ }
78
+ value={props.value.length > 0}
79
+ >
80
+ {[true, false].map((send) => (
81
+ <label
82
+ key={String(send)}
83
+ className="inline-flex min-h-8 cursor-pointer items-center gap-2 text-sm"
84
+ >
85
+ <RadioGroupItem value={send} />
86
+ {send ? "Send a message" : "No success message"}
87
+ </label>
88
+ ))}
89
+ </RadioGroup>
88
90
  {props.value.length ? (
89
91
  <div className="grid gap-4 sm:gap-2">
90
92
  {props.value.map((outcome, index) => {
@@ -106,7 +108,7 @@ export function AutomationOutcomeFields(props: {
106
108
  "order-last basis-full sm:order-none sm:basis-auto",
107
109
  )}
108
110
  >
109
- <Select
111
+ <NativeSelect
110
112
  aria-label={`Message ${index + 1} destination`}
111
113
  value={
112
114
  typeof outcome.destination === "string"
@@ -130,7 +132,7 @@ export function AutomationOutcomeFields(props: {
130
132
  {option.label}
131
133
  </option>
132
134
  ))}
133
- </Select>
135
+ </NativeSelect>
134
136
  </div>
135
137
  {props.value.length > 1 ? (
136
138
  <>