@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
@@ -0,0 +1,201 @@
1
+ import type { AutomationSummary } from "@sentry/junior/api/schema";
2
+ import { Link } from "react-router";
3
+
4
+ import {
5
+ Accordion,
6
+ AccordionContent,
7
+ AccordionItem,
8
+ AccordionTrigger,
9
+ } from "../../components/ui/accordion";
10
+ import { FormNotice } from "../../components/FormNotice";
11
+ import { StatusChip } from "../../components/StatusChip";
12
+ import { conversationPath } from "../../conversations/conversationRoutes";
13
+ import { formatRelativeTime } from "../../format";
14
+ import { dashboardInlineLinkClass } from "../../styles";
15
+ import { formatAutomationRunTime } from "./automationFormat";
16
+
17
+ /** Separate the Automation's lifecycle, next trigger, and last execution result. */
18
+ export function AutomationStatus({
19
+ automation,
20
+ }: {
21
+ automation: AutomationSummary;
22
+ }) {
23
+ const paused = automation.status === "paused";
24
+ const blocked = automation.status === "blocked";
25
+ const completed = automation.status === "completed";
26
+ const triggerProblems =
27
+ automation.kind === "event" ? automation.trigger.problems : [];
28
+ const unavailable = triggerProblems.length > 0;
29
+ const failed =
30
+ automation.lastRunStatus === "failed" ||
31
+ automation.lastRunStatus === "blocked";
32
+ const needsFix = blocked || (paused && Boolean(automation.statusReason));
33
+ const showAttention = needsFix || unavailable || failed;
34
+ const lastResult = automation.lastRunStatus
35
+ ? `Last run ${automation.lastRunStatus}`
36
+ : "Last run";
37
+ return (
38
+ <div className="grid gap-3">
39
+ <div className="grid gap-1.5">
40
+ <div aria-label="Automation state" aria-live="polite" role="status">
41
+ <StatusChip
42
+ tone={
43
+ paused || completed
44
+ ? "neutral"
45
+ : blocked || unavailable
46
+ ? "warning"
47
+ : "success"
48
+ }
49
+ >
50
+ {automation.status === "active" && unavailable
51
+ ? "Trigger unavailable"
52
+ : automation.status}
53
+ </StatusChip>
54
+ </div>
55
+ <AutomationNextTrigger automation={automation} />
56
+ <p className="m-0 text-xs text-dashboard-text-muted">
57
+ {automation.lastRunAt ? (
58
+ <>
59
+ {lastResult} ·{" "}
60
+ <time
61
+ dateTime={automation.lastRunAt}
62
+ title={formatAutomationRunTime(automation.lastRunAt)}
63
+ >
64
+ {formatRelativeTime(automation.lastRunAt)}
65
+ </time>
66
+ </>
67
+ ) : automation.lastRunStatus ? (
68
+ `${lastResult} · time not recorded`
69
+ ) : automation.totalRuns === 0 ? (
70
+ "No runs yet"
71
+ ) : (
72
+ "Last run time not recorded"
73
+ )}
74
+ </p>
75
+ </div>
76
+ {showAttention ? (
77
+ <FormNotice
78
+ title={
79
+ needsFix
80
+ ? "Fix this before resuming"
81
+ : unavailable
82
+ ? "Trigger unavailable"
83
+ : "Check the last run"
84
+ }
85
+ >
86
+ {needsFix ? (
87
+ <p className="m-0 break-words">
88
+ {automation.statusReason ??
89
+ "Check the last run to see what needs fixing."}{" "}
90
+ {!automation.ownedByViewer
91
+ ? `Ask ${automation.createdBy} to resume after this is fixed.`
92
+ : null}
93
+ </p>
94
+ ) : null}
95
+ {unavailable ? (
96
+ <>
97
+ <ul className="my-1 list-disc pl-5">
98
+ {triggerProblems.map((problem) => (
99
+ <li className="break-words" key={problem}>
100
+ {problem}
101
+ </li>
102
+ ))}
103
+ </ul>
104
+ <p className="m-0">
105
+ Edit the trigger, or enable its plugin again.
106
+ </p>
107
+ </>
108
+ ) : null}
109
+ {failed && !needsFix && !unavailable ? (
110
+ <p className="m-0">
111
+ {automation.status === "active"
112
+ ? "Future runs are still on."
113
+ : paused
114
+ ? "Review the last result before resuming."
115
+ : "Review the last result for details."}
116
+ </p>
117
+ ) : null}
118
+ {automation.lastConversationId ? (
119
+ <Link
120
+ className={`${dashboardInlineLinkClass} mt-2 inline-block`}
121
+ to={conversationPath(automation.lastConversationId)}
122
+ >
123
+ Open last run
124
+ </Link>
125
+ ) : automation.totalRuns > 0 ? (
126
+ <Link
127
+ className={`${dashboardInlineLinkClass} mt-2 inline-block`}
128
+ to={`/automations/${encodeURIComponent(automation.id)}/executions`}
129
+ >
130
+ View run history
131
+ </Link>
132
+ ) : null}
133
+ </FormNotice>
134
+ ) : null}
135
+ </div>
136
+ );
137
+ }
138
+
139
+ function AutomationNextTrigger({
140
+ automation,
141
+ }: {
142
+ automation: AutomationSummary;
143
+ }) {
144
+ let description: string;
145
+ if (automation.status === "completed") description = "No future runs";
146
+ else if (automation.status === "paused")
147
+ description = "No new runs while paused";
148
+ else if (automation.status === "blocked")
149
+ description = "New runs are blocked until this is fixed";
150
+ else if (automation.kind === "event")
151
+ description =
152
+ automation.trigger.problems.length === 0
153
+ ? "Waiting for a matching event"
154
+ : "Waiting for a trigger fix";
155
+ else if (automation.nextRunAt) {
156
+ const browserZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
157
+ const scheduleTime = formatAutomationRunTime(
158
+ automation.nextRunAt,
159
+ automation.timezone,
160
+ );
161
+ const localTime = formatAutomationRunTime(
162
+ automation.nextRunAt,
163
+ browserZone,
164
+ );
165
+ const timingLabel =
166
+ Date.parse(automation.nextRunAt) <= Date.now()
167
+ ? "Run was due"
168
+ : "Next run";
169
+ return (
170
+ <Accordion>
171
+ <AccordionItem value="next-run">
172
+ <AccordionTrigger className="py-1 text-sm font-normal">
173
+ <span>
174
+ {timingLabel}{" "}
175
+ <time dateTime={automation.nextRunAt}>
176
+ {formatRelativeTime(automation.nextRunAt)}
177
+ </time>
178
+ </span>
179
+ </AccordionTrigger>
180
+ <AccordionContent>
181
+ <dl className="m-0 mt-1 grid gap-1 border-l border-dashboard-border pl-3 text-xs leading-relaxed">
182
+ <dt className="text-dashboard-text-muted">Schedule time</dt>
183
+ <dd className="m-0">
184
+ <time dateTime={automation.nextRunAt}>{scheduleTime}</time>
185
+ </dd>
186
+ {localTime !== scheduleTime ? (
187
+ <>
188
+ <dt className="mt-1 text-dashboard-text-muted">Your time</dt>
189
+ <dd className="m-0">
190
+ <time dateTime={automation.nextRunAt}>{localTime}</time>
191
+ </dd>
192
+ </>
193
+ ) : null}
194
+ </dl>
195
+ </AccordionContent>
196
+ </AccordionItem>
197
+ </Accordion>
198
+ );
199
+ } else description = "Next run not scheduled";
200
+ return <p className="m-0 text-sm text-dashboard-text">{description}</p>;
201
+ }
@@ -1,16 +1,31 @@
1
1
  /** Version history for one Automation. Making a version active saves it again. */
2
2
  import { useState } from "react";
3
- import { ArrowLeft } from "lucide-react";
3
+ import { ArrowLeft, ChevronRight } from "lucide-react";
4
4
  import { Link, Navigate, useParams, useSearchParams } from "react-router";
5
5
  import { useMutation, useQueryClient } from "@tanstack/react-query";
6
6
  import {
7
7
  automationEditSchema,
8
8
  type AutomationSummary,
9
- type AutomationVersion,
10
9
  type AutomationVersionList,
10
+ type ListedAutomationVersion,
11
11
  } from "@sentry/junior/api/schema";
12
12
  import { useAutomationData, useAutomationVersionsData } from "../../api";
13
- import { Button } from "../../components/Button";
13
+ import {
14
+ AlertDialog,
15
+ AlertDialogAction,
16
+ AlertDialogCancel,
17
+ AlertDialogContent,
18
+ AlertDialogDescription,
19
+ AlertDialogFooter,
20
+ AlertDialogTitle,
21
+ } from "../../components/ui/alert-dialog";
22
+ import { Button } from "../../components/ui/button";
23
+ import {
24
+ Collapsible,
25
+ CollapsibleContent,
26
+ CollapsibleTrigger,
27
+ } from "../../components/ui/collapsible";
28
+ import { dashboardDisclosureChevronClass } from "../../styles";
14
29
  import { Detail, DetailList } from "../../components/DetailList";
15
30
  import { FormNotice } from "../../components/FormNotice";
16
31
  import { InlineError } from "../../components/InlineError";
@@ -23,6 +38,7 @@ import { DashboardApiError, fetchDashboardJson, post } from "../../http";
23
38
  import { pathWithSearch } from "../../searchParams";
24
39
  import { formatAutomationRunTime } from "./automationFormat";
25
40
  import { automationOutcomeLabel } from "./automationOutcomes";
41
+ import { TriggerFilters } from "./TriggerFilters";
26
42
 
27
43
  const DESCRIPTION = "Saved settings for this automation, newest first.";
28
44
 
@@ -90,6 +106,9 @@ function AutomationVersionsView(props: {
90
106
  const { automation, data } = props;
91
107
  const queryClient = useQueryClient();
92
108
  const [activated, setActivated] = useState<number>();
109
+ // Keep the version after close so the closing dialog keeps its text.
110
+ const [confirming, setConfirming] = useState<number>();
111
+ const [confirmOpen, setConfirmOpen] = useState(false);
93
112
  const canActivate = !(
94
113
  automation.kind === "scheduled" && automation.status === "completed"
95
114
  );
@@ -155,15 +174,8 @@ function AutomationVersionsView(props: {
155
174
  activate.variables === version.version ? activate.error : null
156
175
  }
157
176
  onActivate={() => {
158
- const creatorOnly = automation.ownedByViewer
159
- ? ""
160
- : ` Only ${automation.createdBy} can turn on their connected accounts or change where results go.`;
161
- if (
162
- window.confirm(
163
- `Make version ${version.version} active? Junior saves these settings as a new version. Future runs use them.${creatorOnly}`,
164
- )
165
- )
166
- activate.mutate(version.version);
177
+ setConfirming(version.version);
178
+ setConfirmOpen(true);
167
179
  }}
168
180
  pending={
169
181
  activate.isPending && activate.variables === version.version
@@ -176,6 +188,29 @@ function AutomationVersionsView(props: {
176
188
  </ol>
177
189
  </Card>
178
190
  )}
191
+ <AlertDialog onOpenChange={setConfirmOpen} open={confirmOpen}>
192
+ <AlertDialogContent>
193
+ <AlertDialogTitle>Make version {confirming} active?</AlertDialogTitle>
194
+ <AlertDialogDescription>
195
+ Junior saves these settings as a new version. Future runs use them.
196
+ {automation.ownedByViewer
197
+ ? null
198
+ : ` Only ${automation.createdBy} can turn on their connected accounts or change where results go.`}
199
+ </AlertDialogDescription>
200
+ <AlertDialogFooter>
201
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
202
+ <AlertDialogAction
203
+ onClick={() => {
204
+ if (confirming !== undefined) activate.mutate(confirming);
205
+ setConfirmOpen(false);
206
+ }}
207
+ variant="primary"
208
+ >
209
+ Make active
210
+ </AlertDialogAction>
211
+ </AlertDialogFooter>
212
+ </AlertDialogContent>
213
+ </AlertDialog>
179
214
  {data.truncated ? (
180
215
  <p className="m-0 text-center text-xs text-dashboard-text-muted">
181
216
  Showing the 100 most recent versions.
@@ -192,9 +227,9 @@ function VersionRow(props: {
192
227
  error: Error | null;
193
228
  onActivate(): void;
194
229
  pending: boolean;
195
- previous: AutomationVersion | undefined;
230
+ previous: ListedAutomationVersion | undefined;
196
231
  truncated: boolean;
197
- version: AutomationVersion;
232
+ version: ListedAutomationVersion;
198
233
  }) {
199
234
  const { automation, version } = props;
200
235
  const editor = version.editedBy
@@ -255,19 +290,25 @@ function VersionRow(props: {
255
290
  : "Try again."}
256
291
  </FormNotice>
257
292
  ) : null}
258
- <details>
259
- <summary className="w-fit cursor-pointer py-1 text-xs text-dashboard-text-muted hover:text-dashboard-text focus-visible:outline focus-visible:outline-dashboard-focus">
293
+ <Collapsible>
294
+ <CollapsibleTrigger className="group flex cursor-pointer items-center gap-1 py-1 text-xs text-dashboard-text-muted hover:text-dashboard-text focus-visible:outline focus-visible:outline-dashboard-focus">
295
+ <ChevronRight
296
+ aria-hidden="true"
297
+ className={dashboardDisclosureChevronClass}
298
+ />
260
299
  Show settings
261
- </summary>
262
- <VersionSettings automation={automation} version={version} />
263
- </details>
300
+ </CollapsibleTrigger>
301
+ <CollapsibleContent hiddenUntilFound>
302
+ <VersionSettings automation={automation} version={version} />
303
+ </CollapsibleContent>
304
+ </Collapsible>
264
305
  </li>
265
306
  );
266
307
  }
267
308
 
268
309
  function VersionSettings(props: {
269
310
  automation: AutomationSummary;
270
- version: AutomationVersion;
311
+ version: ListedAutomationVersion;
271
312
  }) {
272
313
  const { automation, version } = props;
273
314
  const { definition } = version;
@@ -298,12 +339,8 @@ function VersionSettings(props: {
298
339
  {version.definition.trigger.label} ·{" "}
299
340
  {version.definition.trigger.identifier}
300
341
  <br />
301
- {version.definition.trigger.events.join(", ")}
302
- {Object.keys(version.definition.trigger.match ?? {}).length ? (
303
- <pre className="mt-2 mb-0 whitespace-pre-wrap break-words text-xs">
304
- {JSON.stringify(version.definition.trigger.match, null, 2)}
305
- </pre>
306
- ) : null}
342
+ {version.trigger.events.join(", ")}
343
+ <TriggerFilters filters={version.trigger.filters} />
307
344
  </>
308
345
  )}
309
346
  </Detail>
@@ -332,8 +369,8 @@ function VersionSettings(props: {
332
369
 
333
370
  /** Name the settings that differ from the previous version. */
334
371
  function changedFields(
335
- previous: AutomationVersion,
336
- version: AutomationVersion,
372
+ previous: ListedAutomationVersion,
373
+ version: ListedAutomationVersion,
337
374
  ): string[] {
338
375
  const before: Record<string, unknown> = previous.definition;
339
376
  const after: Record<string, unknown> = version.definition;
@@ -1,18 +1,17 @@
1
- import { useEffect } from "react";
1
+ import { useEffect, useRef, useState } from "react";
2
2
  import {
3
3
  useLocation,
4
4
  useNavigate,
5
5
  useParams,
6
6
  useSearchParams,
7
7
  } from "react-router";
8
- import { useMutation, useQueryClient } from "@tanstack/react-query";
9
8
  import {
10
9
  automationListQuerySchema,
11
10
  type AutomationSummary,
12
11
  } from "@sentry/junior/api/schema";
13
12
  import { Globe2, ListChecks, LockKeyhole, UserRound } from "lucide-react";
14
13
  import { useAutomationsData, useAutomationData } from "../../api";
15
- import { Button } from "../../components/Button";
14
+ import { Button } from "../../components/ui/button";
16
15
  import { AutomationFilters, AutomationSort } from "./AutomationFilters";
17
16
  import { InlineError } from "../../components/InlineError";
18
17
  import { PageContentSkeleton } from "../../components/PageContentSkeleton";
@@ -25,7 +24,6 @@ import {
25
24
  import { Card } from "../../components/layout/Card";
26
25
  import { PageHeader } from "../../components/layout/PageHeader";
27
26
  import { StatCard } from "../../components/metrics/StatCard";
28
- import { deleteDashboardResource } from "../../http";
29
27
  import { automationPath } from "../../format";
30
28
  import { useDocumentTitle } from "../../documentTitle";
31
29
  import {
@@ -33,9 +31,12 @@ import {
33
31
  useDebouncedSearchParam,
34
32
  useSearchParamEnum,
35
33
  } from "../../searchParams";
34
+ import { AutomationDeleteDialog } from "./AutomationDeleteDialog";
36
35
  import { AutomationRow } from "./AutomationRow";
37
36
  import { AutomationCostChart } from "./AutomationCostChart";
38
- import { AutomationDetailsDrawer } from "./AutomationDetailsDrawer";
37
+ import { AutomationDetails } from "./AutomationDetails";
38
+ import { PageLayout } from "../../components/layout/PageLayout";
39
+ import { cn } from "../../styles";
39
40
  import { AutomationExecutionChart } from "./AutomationExecutionChart";
40
41
 
41
42
  const AUTOMATION_RANGE_OPTIONS = ["1", "7", "30", "90"] as const;
@@ -54,7 +55,9 @@ export function AutomationsPage(props: {
54
55
  enabled: boolean;
55
56
  view: "list" | "overview";
56
57
  }) {
57
- const queryClient = useQueryClient();
58
+ const [deleteTarget, setDeleteTarget] = useState<AutomationSummary>();
59
+ const deleteSource = useRef<HTMLButtonElement | null>(null);
60
+ const listRef = useRef<HTMLDivElement>(null);
58
61
  const navigate = useNavigate();
59
62
  const location = useLocation();
60
63
  const { automationId } = useParams();
@@ -93,17 +96,14 @@ export function AutomationsPage(props: {
93
96
  props.view === "list" ? request.toString() : "",
94
97
  );
95
98
  const detail = useAutomationData(props.enabled, automationId);
96
- const setFilter = (key: string, value: string) =>
97
- setSearchParams(
98
- (current) => {
99
- const next = new URLSearchParams(current);
100
- next.delete("page");
101
- if (value) next.set(key, value);
102
- else next.delete(key);
103
- return next;
104
- },
105
- { replace: true },
106
- );
99
+ const setFilter = (key: string, value: string) => {
100
+ // Consecutive control events can precede the router's next render.
101
+ const next = new URLSearchParams(window.location.search);
102
+ next.delete("page");
103
+ if (value) next.set(key, value);
104
+ else next.delete(key);
105
+ setSearchParams(next, { replace: true });
106
+ };
107
107
  const setPage = (page: number) =>
108
108
  setSearchParams((current) => {
109
109
  const next = new URLSearchParams(current);
@@ -123,6 +123,15 @@ export function AutomationsPage(props: {
123
123
  const totalPages = pageCount(visibleTaskCount, filters.pageSize);
124
124
  const page = query.data?.page ?? filters.page;
125
125
  const selectedTask = detail.data;
126
+ const closeDetails = () => {
127
+ const trigger = automationId
128
+ ? listRef.current?.querySelector<HTMLButtonElement>(
129
+ `[data-automation-id="${CSS.escape(automationId)}"] button`,
130
+ )
131
+ : undefined;
132
+ navigate(tasksPath(listPath));
133
+ (trigger ?? listRef.current)?.focus();
134
+ };
126
135
  // Overview names the tab through PageHeader; the list has a plain heading.
127
136
  useDocumentTitle(
128
137
  props.view === "list"
@@ -155,18 +164,6 @@ export function AutomationsPage(props: {
155
164
  query.isPlaceholderData,
156
165
  setSearchParams,
157
166
  ]);
158
- const deletion = useMutation({
159
- mutationFn: async (automation: AutomationSummary) => {
160
- await deleteDashboardResource(
161
- `/api/automations/${encodeURIComponent(automation.id)}`,
162
- );
163
- },
164
- onSuccess: async () => {
165
- await queryClient.invalidateQueries({
166
- queryKey: ["dashboard", "automations"],
167
- });
168
- },
169
- });
170
167
 
171
168
  const loading = !query.data && !query.error;
172
169
  const executionSeries = query.data?.executionDays?.length
@@ -185,13 +182,8 @@ export function AutomationsPage(props: {
185
182
  : [];
186
183
  const showExecutionCharts = executionSeries.length > 0;
187
184
 
188
- return (
185
+ const content = (
189
186
  <>
190
- {location.state?.automationSaved ? (
191
- <p role="status" className="m-0 text-sm text-emerald-300">
192
- Automation saved. Changes apply to future work.
193
- </p>
194
- ) : null}
195
187
  {props.view === "overview" ? (
196
188
  <PageHeader
197
189
  description="Scheduled and event-driven work created by users."
@@ -225,16 +217,6 @@ export function AutomationsPage(props: {
225
217
  <Button onClick={() => void query.refetch()}>Try again</Button>
226
218
  </InlineError>
227
219
  ) : null}
228
- {automationId && detail.isPending ? (
229
- <p role="status">Loading automation details…</p>
230
- ) : null}
231
- {automationId && detail.error ? (
232
- <InlineError>
233
- Automation details could not be loaded. It may be unavailable or you
234
- may not have access.{" "}
235
- <Button onClick={() => void detail.refetch()}>Try again</Button>
236
- </InlineError>
237
- ) : null}
238
220
  {query.data && props.view === "overview" ? (
239
221
  <>
240
222
  <div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
@@ -308,20 +290,7 @@ export function AutomationsPage(props: {
308
290
  const key = `${automation.kind}:${automation.id}`;
309
291
  return (
310
292
  <AutomationRow
311
- deleting={
312
- deletion.isPending &&
313
- deletion.variables?.id === automation.id
314
- }
315
293
  key={key}
316
- onDelete={() => {
317
- if (
318
- window.confirm(
319
- `Delete this ${automation.kind} automation?`,
320
- )
321
- ) {
322
- deletion.mutate(automation);
323
- }
324
- }}
325
294
  onSelect={() =>
326
295
  navigate(
327
296
  automationId === automation.id
@@ -343,18 +312,61 @@ export function AutomationsPage(props: {
343
312
  pageSize={filters.pageSize}
344
313
  total={visibleTaskCount}
345
314
  />
346
- {deletion.error ? (
347
- <InlineError className="text-center">
348
- The automation could not be deleted. Try again.
349
- </InlineError>
350
- ) : null}
351
315
  </>
352
316
  ) : null}
353
- <AutomationDetailsDrawer
354
- onClose={() => navigate(tasksPath(listPath))}
355
- range={range}
356
- automation={selectedTask}
357
- />
358
317
  </>
359
318
  );
319
+ if (props.view === "overview") return content;
320
+
321
+ return (
322
+ <div
323
+ className={cn(
324
+ "grid h-full min-h-0 min-w-0",
325
+ automationId && "xl:grid-cols-[minmax(0,1fr)_25rem]",
326
+ )}
327
+ >
328
+ <div
329
+ aria-label="Automations list"
330
+ className="min-h-0 overflow-y-auto overscroll-contain outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
331
+ ref={listRef}
332
+ role="region"
333
+ tabIndex={0}
334
+ >
335
+ <PageLayout>{content}</PageLayout>
336
+ </div>
337
+ {automationId ? (
338
+ <AutomationDetails
339
+ key={automationId}
340
+ onClose={closeDetails}
341
+ onDelete={(source) => {
342
+ if (!selectedTask?.ownedByViewer) return;
343
+ deleteSource.current = source;
344
+ setDeleteTarget(selectedTask);
345
+ }}
346
+ onRetry={() => void detail.refetch()}
347
+ error={Boolean(detail.error)}
348
+ range={range}
349
+ automation={selectedTask}
350
+ />
351
+ ) : null}
352
+ {deleteTarget ? (
353
+ <AutomationDeleteDialog
354
+ automation={deleteTarget}
355
+ onClose={() => setDeleteTarget(undefined)}
356
+ onDeleted={() => {
357
+ if (automationId !== deleteTarget.id) return;
358
+ // The details unmount after the route change, which can finish
359
+ // after the dialog closes. Do not return focus to their button.
360
+ deleteSource.current = null;
361
+ navigate(tasksPath(listPath));
362
+ }}
363
+ returnFocus={() =>
364
+ deleteSource.current?.isConnected
365
+ ? deleteSource.current
366
+ : (listRef.current ?? false)
367
+ }
368
+ />
369
+ ) : null}
370
+ </div>
371
+ );
360
372
  }