@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
@@ -0,0 +1,143 @@
1
+ import type { AutomationSummary } from "@sentry/junior/api/schema";
2
+ import { Link } from "react-router";
3
+
4
+ import { useAutomationExecutionsData } from "../../api";
5
+ import { Button } from "../../components/ui/button";
6
+ import { InlineError } from "../../components/InlineError";
7
+ import { Skeleton } from "../../components/ui/skeleton";
8
+ import { StatusChip } from "../../components/StatusChip";
9
+ import { conversationPath } from "../../conversations/conversationRoutes";
10
+ import {
11
+ formatCostSummary,
12
+ formatRelativeTime,
13
+ formatRuntime,
14
+ } from "../../format";
15
+ import { dashboardInlineLinkClass } from "../../styles";
16
+ import { formatAutomationRunTime } from "./automationFormat";
17
+
18
+ /** Preview the newest terminal runs without leaving the Automation list. */
19
+ export function AutomationRecentRuns({
20
+ automation,
21
+ }: {
22
+ automation: AutomationSummary;
23
+ }) {
24
+ const query = useAutomationExecutionsData(true, automation.id);
25
+ const runs = query.data?.executions.slice(0, 3) ?? [];
26
+ return (
27
+ <section aria-label="Recent runs" className="grid min-w-0 gap-2.5">
28
+ <div className="flex items-baseline justify-between gap-3">
29
+ <h3 className="m-0 text-xs font-medium text-dashboard-text-muted">
30
+ Recent runs
31
+ </h3>
32
+ {automation.totalRuns > 0 || runs.length > 0 ? (
33
+ <Link
34
+ className={`${dashboardInlineLinkClass} text-xs`}
35
+ to={`/automations/${encodeURIComponent(automation.id)}/executions`}
36
+ >
37
+ View all runs
38
+ </Link>
39
+ ) : null}
40
+ </div>
41
+ {query.isPending ? (
42
+ <div aria-busy="true" role="status" className="grid gap-2">
43
+ <span className="sr-only">Loading recent runs</span>
44
+ {Array.from({ length: 3 }, (_, index) => (
45
+ <Skeleton className="h-14 rounded-md" key={index} />
46
+ ))}
47
+ </div>
48
+ ) : null}
49
+ {query.error ? (
50
+ <InlineError>
51
+ Recent runs could not be loaded.{" "}
52
+ <Button onClick={() => void query.refetch()}>Try again</Button>
53
+ </InlineError>
54
+ ) : null}
55
+ {query.data && runs.length === 0 ? (
56
+ <p className="m-0 text-sm text-dashboard-text-muted">
57
+ {automation.totalRuns === 0
58
+ ? "No runs yet. Results will appear here after the first run."
59
+ : "No recent runs are available. Older run details may have expired."}
60
+ </p>
61
+ ) : null}
62
+ {runs.length > 0 ? (
63
+ <ol className="m-0 list-none divide-y divide-dashboard-border-subtle p-0">
64
+ {runs.map((run) => {
65
+ const metrics = [
66
+ run.durationMs !== undefined
67
+ ? formatRuntime(run.durationMs)
68
+ : undefined,
69
+ run.costUsd !== undefined
70
+ ? formatCostSummary({ total: run.costUsd })
71
+ : undefined,
72
+ ]
73
+ .filter(Boolean)
74
+ .join(" · ");
75
+ const title =
76
+ run.title !== automation.title ? run.title : undefined;
77
+ const content = (
78
+ <>
79
+ <div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-1">
80
+ <StatusChip
81
+ size="compact"
82
+ tone={
83
+ run.status === "completed"
84
+ ? "success"
85
+ : run.status === "failed"
86
+ ? "danger"
87
+ : "warning"
88
+ }
89
+ >
90
+ {run.status}
91
+ </StatusChip>
92
+ <time
93
+ className="text-xs text-dashboard-text-muted"
94
+ dateTime={run.executedAt}
95
+ title={formatAutomationRunTime(run.executedAt)}
96
+ >
97
+ {formatRelativeTime(run.executedAt)}
98
+ </time>
99
+ </div>
100
+ {title || metrics ? (
101
+ <div className="flex min-w-0 items-baseline justify-between gap-3 text-xs text-dashboard-text-muted">
102
+ {title ? (
103
+ <p
104
+ className="m-0 min-w-0 truncate text-dashboard-text"
105
+ title={title}
106
+ >
107
+ {title}
108
+ </p>
109
+ ) : null}
110
+ {metrics ? (
111
+ <p className="m-0 shrink-0 tabular-nums">{metrics}</p>
112
+ ) : null}
113
+ </div>
114
+ ) : null}
115
+ {!run.conversationId ? (
116
+ <p className="m-0 text-xs text-dashboard-text-muted">
117
+ No Conversation recorded
118
+ </p>
119
+ ) : null}
120
+ </>
121
+ );
122
+ const rowClass = "grid min-w-0 gap-1 rounded-md px-2 py-2.5";
123
+ return (
124
+ <li key={run.executionId}>
125
+ {run.conversationId ? (
126
+ <Link
127
+ aria-label={`Open ${run.status} run from ${formatAutomationRunTime(run.executedAt)}`}
128
+ className={`${rowClass} text-inherit no-underline transition-colors hover:bg-dashboard-fill-hover focus-visible:outline-2 focus-visible:outline-dashboard-focus`}
129
+ to={conversationPath(run.conversationId)}
130
+ >
131
+ {content}
132
+ </Link>
133
+ ) : (
134
+ <div className={rowClass}>{content}</div>
135
+ )}
136
+ </li>
137
+ );
138
+ })}
139
+ </ol>
140
+ ) : null}
141
+ </section>
142
+ );
143
+ }
@@ -1,38 +1,28 @@
1
1
  import type { AutomationSummary } from "@sentry/junior/api/schema";
2
- import {
3
- CalendarClock,
4
- LockKeyhole,
5
- MoreHorizontal,
6
- Trash2,
7
- Zap,
8
- } from "lucide-react";
9
- import { type ReactNode, useEffect, useId, useRef, useState } from "react";
10
- import { Link, useLocation } from "react-router";
2
+ import { CalendarClock, LockKeyhole, Zap } from "lucide-react";
3
+ import { type ReactNode } from "react";
4
+ import { Link } from "react-router";
11
5
  import { ActorAvatar } from "../../components/ActorAvatar";
12
6
  import { SelectableRow } from "../../components/SelectableRow";
13
7
  import { StatusChip } from "../../components/StatusChip";
14
8
  import { formatRelativeTime, peoplePath } from "../../format";
9
+ import { eventTriggerText } from "@sentry/junior-plugin-api";
15
10
  import {
16
- automationEventConditions,
17
- automationEventNames,
18
11
  automationScheduleLabel,
19
12
  formatAutomationRunTime,
20
13
  } from "./automationFormat";
21
14
 
22
15
  /**
23
16
  * Show one automation as a compact card: what it is, where it posts, who owns
24
- * it, and when it runs. Instructions, timezones, and event conditions stay in
25
- * the details drawer.
17
+ * it, and when it runs. Instructions, timezones, and event filters stay in
18
+ * the details panel.
26
19
  */
27
20
  export function AutomationRow(props: {
28
- deleting: boolean;
29
- onDelete(): void;
30
21
  onSelect(): void;
31
22
  selected: boolean;
32
23
  automation: AutomationSummary;
33
24
  }) {
34
25
  const { automation } = props;
35
- const location = useLocation();
36
26
  const status = automationStatus(automation);
37
27
  const creator = (
38
28
  <>
@@ -47,6 +37,7 @@ export function AutomationRow(props: {
47
37
  return (
48
38
  <article
49
39
  className="min-w-0 rounded-lg border border-dashboard-border-subtle bg-dashboard-fill-faint"
40
+ data-automation-id={automation.id}
50
41
  role="listitem"
51
42
  >
52
43
  <SelectableRow
@@ -105,16 +96,6 @@ export function AutomationRow(props: {
105
96
  <AutomationTiming automation={automation} />
106
97
  </div>
107
98
  </div>
108
- {automation.ownedByViewer ? (
109
- <div className="-mr-2 -mt-1.5 shrink-0">
110
- <AutomationActions
111
- editPath={`/automations/${encodeURIComponent(automation.id)}/edit${location.search}`}
112
- deleting={props.deleting}
113
- onDelete={props.onDelete}
114
- title={automation.title}
115
- />
116
- </div>
117
- ) : null}
118
99
  </SelectableRow>
119
100
  </article>
120
101
  );
@@ -126,7 +107,9 @@ function automationStatus(automation: AutomationSummary): string | undefined {
126
107
  return automation.status === "active" ? undefined : automation.status;
127
108
  }
128
109
  if (automation.status !== "active") return automation.status;
129
- return automation.triggerAvailable ? undefined : "Trigger unavailable";
110
+ return automation.trigger.problems.length === 0
111
+ ? undefined
112
+ : "Trigger unavailable";
130
113
  }
131
114
 
132
115
  /** Combine the trigger and Destination into one muted line under the title. */
@@ -140,15 +123,11 @@ function AutomationSummaryLine({
140
123
  const trigger =
141
124
  automation.kind === "scheduled"
142
125
  ? automation.schedule
143
- : `${automation.resource} · ${automationEventNames(automation)}`;
144
- const conditions =
145
- automation.kind === "event" ? automationEventConditions(automation) : "";
126
+ : eventTriggerText(automation.resource, automation.trigger);
146
127
  const triggerDetail =
147
128
  automation.kind === "scheduled"
148
129
  ? automationScheduleLabel(automation)
149
- : conditions
150
- ? `${trigger} · ${conditions}`
151
- : trigger;
130
+ : trigger;
152
131
  return (
153
132
  <span className="mt-1 flex min-w-0 items-center gap-1.5 text-xs text-dashboard-text-muted">
154
133
  <Icon
@@ -236,108 +215,3 @@ function MetaItem(props: { children: ReactNode; title?: string }) {
236
215
  </span>
237
216
  );
238
217
  }
239
-
240
- function AutomationActions(props: {
241
- editPath: string;
242
- deleting: boolean;
243
- onDelete(): void;
244
- title: string;
245
- }) {
246
- const [open, setOpen] = useState(false);
247
- const root = useRef<HTMLDivElement>(null);
248
- const trigger = useRef<HTMLButtonElement>(null);
249
- const item = useRef<HTMLButtonElement>(null);
250
- const menuId = useId();
251
-
252
- useEffect(() => {
253
- if (!open) return;
254
- item.current?.focus();
255
- function closeOutside(event: PointerEvent) {
256
- if (!root.current?.contains(event.target as Node)) setOpen(false);
257
- }
258
- document.addEventListener("pointerdown", closeOutside);
259
- return () => document.removeEventListener("pointerdown", closeOutside);
260
- }, [open]);
261
-
262
- function close() {
263
- setOpen(false);
264
- trigger.current?.focus();
265
- }
266
-
267
- return (
268
- <div
269
- className="relative"
270
- onBlur={(event) => {
271
- if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
272
- }}
273
- onKeyDown={(event) => {
274
- if (event.key === "Escape") {
275
- event.stopPropagation();
276
- close();
277
- } else if (
278
- ["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)
279
- ) {
280
- event.preventDefault();
281
- setOpen(true);
282
- const items = Array.from(
283
- root.current?.querySelectorAll<HTMLElement>('[role="menuitem"]') ??
284
- [],
285
- );
286
- const index = items.indexOf(document.activeElement as HTMLElement);
287
- const next =
288
- event.key === "Home"
289
- ? 0
290
- : event.key === "End"
291
- ? items.length - 1
292
- : (index + (event.key === "ArrowUp" ? -1 : 1) + items.length) %
293
- items.length;
294
- items[next]?.focus();
295
- }
296
- }}
297
- ref={root}
298
- >
299
- <button
300
- aria-controls={open ? menuId : undefined}
301
- aria-expanded={open}
302
- aria-haspopup="menu"
303
- aria-label={`Actions: ${props.title}`}
304
- className="grid size-11 cursor-pointer place-items-center rounded-md border-0 bg-transparent text-dashboard-text-muted hover:bg-dashboard-fill-hover hover:text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus disabled:cursor-not-allowed disabled:opacity-50 xl:size-9"
305
- disabled={props.deleting}
306
- onClick={() => setOpen(!open)}
307
- ref={trigger}
308
- type="button"
309
- >
310
- <MoreHorizontal aria-hidden="true" size={18} />
311
- </button>
312
- {open ? (
313
- <div
314
- aria-label={`Actions: ${props.title}`}
315
- className="absolute right-0 top-full z-20 min-w-36 rounded-md border border-dashboard-border bg-dashboard-surface-raised p-1 shadow-lg"
316
- id={menuId}
317
- onClick={(event) => event.stopPropagation()}
318
- role="menu"
319
- >
320
- <Link
321
- role="menuitem"
322
- to={props.editPath}
323
- className="flex min-h-11 items-center rounded-md px-3 text-sm text-dashboard-text no-underline hover:bg-dashboard-fill-hover focus-visible:outline focus-visible:outline-dashboard-focus"
324
- >
325
- Edit automation
326
- </Link>
327
- <button
328
- className="flex min-h-11 w-full cursor-pointer items-center gap-2 rounded-md border-0 bg-transparent px-3 text-sm text-rose-300 hover:bg-dashboard-fill-hover focus-visible:bg-dashboard-fill-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
329
- onClick={() => {
330
- close();
331
- props.onDelete();
332
- }}
333
- ref={item}
334
- role="menuitem"
335
- type="button"
336
- >
337
- <Trash2 aria-hidden="true" size={15} /> Delete
338
- </button>
339
- </div>
340
- ) : null}
341
- </div>
342
- );
343
- }
@@ -0,0 +1,307 @@
1
+ import type { AutomationRun } from "@sentry/junior/api/schema";
2
+ import { MessageSquareText } from "lucide-react";
3
+ import {
4
+ useRef,
5
+ type KeyboardEvent,
6
+ type ReactNode,
7
+ type RefObject,
8
+ } from "react";
9
+ import { Link, useSearchParams } from "react-router";
10
+
11
+ import { DetailsPanel } from "../../components/DetailsPanel";
12
+ import { PageContentSkeleton } from "../../components/PageContentSkeleton";
13
+ import { StatusChip } from "../../components/StatusChip";
14
+ import { PageLayout } from "../../components/layout/PageLayout";
15
+ import { buttonVariants } from "../../components/ui/button";
16
+ import {
17
+ Tooltip,
18
+ TooltipContent,
19
+ TooltipTrigger,
20
+ } from "../../components/ui/tooltip";
21
+ import { conversationPath } from "../../conversations/conversationRoutes";
22
+ import {
23
+ automationPath,
24
+ formatCompactNumber,
25
+ formatCostSummary,
26
+ formatRelativeTime,
27
+ formatRuntime,
28
+ } from "../../format";
29
+ import { cn, dashboardInlineLinkClass } from "../../styles";
30
+ import {
31
+ automationRunStatusTone,
32
+ automationRunTitle,
33
+ formatAutomationRunTime,
34
+ } from "./automationFormat";
35
+
36
+ /** Search param that owns the selected run, so a run link can be shared. */
37
+ export const AUTOMATION_RUN_PARAM = "run";
38
+
39
+ /**
40
+ * Read the run that the URL selects, and change it. Closing returns focus to
41
+ * the run row. Up and Down move the selection while details are open.
42
+ */
43
+ export function useAutomationRunSelection(runIds: readonly string[]) {
44
+ const [searchParams, setSearchParams] = useSearchParams();
45
+ const listRef = useRef<HTMLDivElement>(null);
46
+ const runId = searchParams.get(AUTOMATION_RUN_PARAM) ?? undefined;
47
+ const select = (id: string | undefined) =>
48
+ setSearchParams(
49
+ (current) => {
50
+ const next = new URLSearchParams(current);
51
+ if (id) next.set(AUTOMATION_RUN_PARAM, id);
52
+ else next.delete(AUTOMATION_RUN_PARAM);
53
+ return next;
54
+ },
55
+ { replace: true },
56
+ );
57
+ const rowButton = (id: string) =>
58
+ listRef.current?.querySelector<HTMLButtonElement>(
59
+ `[data-run-id="${CSS.escape(id)}"] button`,
60
+ );
61
+ const close = () => {
62
+ const trigger = runId ? rowButton(runId) : undefined;
63
+ select(undefined);
64
+ (trigger ?? listRef.current)?.focus({ preventScroll: true });
65
+ };
66
+ const toggle = (id: string) => select(id === runId ? undefined : id);
67
+ const onRowsKeyDown = (event: KeyboardEvent<HTMLElement>) => {
68
+ if (!runId || (event.key !== "ArrowDown" && event.key !== "ArrowUp")) {
69
+ return;
70
+ }
71
+ // Read the focused row. The URL can lag behind fast key repeats.
72
+ const current = (event.target as Element)
73
+ .closest("[data-run-id]")
74
+ ?.getAttribute("data-run-id");
75
+ const index = current ? runIds.indexOf(current) : -1;
76
+ const next = runIds[index + (event.key === "ArrowDown" ? 1 : -1)];
77
+ if (index === -1 || !next) return;
78
+ event.preventDefault();
79
+ select(next);
80
+ rowButton(next)?.focus();
81
+ };
82
+ return { close, listRef, onRowsKeyDown, runId, toggle };
83
+ }
84
+
85
+ /** Scroll a run list on its own, beside the selected run's details. */
86
+ export function AutomationRunsFrame(props: {
87
+ children: ReactNode;
88
+ details: ReactNode;
89
+ label: string;
90
+ listRef: RefObject<HTMLDivElement | null>;
91
+ }) {
92
+ return (
93
+ <div
94
+ className={cn(
95
+ "grid h-full min-h-0 min-w-0",
96
+ props.details ? "xl:grid-cols-[minmax(0,1fr)_25rem]" : undefined,
97
+ )}
98
+ >
99
+ <div
100
+ aria-label={props.label}
101
+ 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"
102
+ ref={props.listRef}
103
+ role="region"
104
+ tabIndex={0}
105
+ >
106
+ <PageLayout>{props.children}</PageLayout>
107
+ </div>
108
+ {props.details}
109
+ </div>
110
+ );
111
+ }
112
+
113
+ /** Open a run's Conversation from its row without opening run details. */
114
+ export function RunConversationLink(props: { conversationId?: string }) {
115
+ if (!props.conversationId) return <span aria-hidden="true" />;
116
+ return (
117
+ <Tooltip>
118
+ <TooltipTrigger
119
+ render={
120
+ <Link
121
+ aria-label="Open conversation"
122
+ className={cn(buttonVariants({ size: "icon" }), "size-8")}
123
+ to={conversationPath(props.conversationId)}
124
+ >
125
+ <MessageSquareText aria-hidden="true" size={15} />
126
+ </Link>
127
+ }
128
+ />
129
+ <TooltipContent size="compact">Open conversation</TooltipContent>
130
+ </Tooltip>
131
+ );
132
+ }
133
+
134
+ /** Show what one run did, what it cost, and why it failed. */
135
+ export function AutomationRunDetails(props: {
136
+ loading: boolean;
137
+ onClose(): void;
138
+ /** Undefined while runs load, or when the selected run is not loaded. */
139
+ run: AutomationRun | undefined;
140
+ /** The executions page already names its Automation. */
141
+ showAutomation: boolean;
142
+ }) {
143
+ const { run } = props;
144
+ const title = run ? automationRunTitle(run, run.automationTitle) : "Run";
145
+ return (
146
+ <DetailsPanel
147
+ actions={
148
+ run?.conversationId ? (
149
+ <Link
150
+ className={cn(buttonVariants(), "shrink-0 whitespace-nowrap")}
151
+ to={conversationPath(run.conversationId)}
152
+ >
153
+ Open conversation
154
+ </Link>
155
+ ) : undefined
156
+ }
157
+ label="Run details"
158
+ onClose={props.onClose}
159
+ sheetTitle={title}
160
+ title={
161
+ <h2 className="m-0 line-clamp-2 break-words font-display text-base font-medium text-dashboard-text">
162
+ {title}
163
+ </h2>
164
+ }
165
+ >
166
+ <div className="p-4 [overflow-wrap:anywhere]">
167
+ {run ? (
168
+ <AutomationRunContent
169
+ run={run}
170
+ showAutomation={props.showAutomation}
171
+ />
172
+ ) : props.loading ? (
173
+ <PageContentSkeleton label="Loading run details" variant="panel" />
174
+ ) : (
175
+ <p className="m-0 text-sm text-dashboard-text-muted">
176
+ This run is not in the loaded runs.
177
+ </p>
178
+ )}
179
+ </div>
180
+ </DetailsPanel>
181
+ );
182
+ }
183
+
184
+ function AutomationRunContent(props: {
185
+ run: AutomationRun;
186
+ showAutomation: boolean;
187
+ }) {
188
+ const { run } = props;
189
+ const versionsPath = `${automationPath(run.automationId)}/versions`;
190
+ return (
191
+ <div className="grid min-w-0 gap-3">
192
+ <div className="flex flex-wrap items-center gap-x-2 gap-y-1 px-1">
193
+ <StatusChip tone={automationRunStatusTone(run.status)}>
194
+ {run.status}
195
+ </StatusChip>
196
+ <time
197
+ className="text-xs text-dashboard-text-muted"
198
+ dateTime={run.executedAt}
199
+ title={formatAutomationRunTime(run.executedAt)}
200
+ >
201
+ {formatRelativeTime(run.executedAt)}
202
+ </time>
203
+ </div>
204
+ {run.error ? (
205
+ <RunSection title="Error">
206
+ <p className="m-0 whitespace-pre-wrap font-mono text-xs leading-relaxed text-rose-300">
207
+ {run.error}
208
+ </p>
209
+ </RunSection>
210
+ ) : null}
211
+ <RunSection title="Run">
212
+ <dl className={runFactsClass}>
213
+ <RunFact label="Trigger">
214
+ {run.kind === "scheduled" ? "Schedule" : "Event"}
215
+ </RunFact>
216
+ {run.scheduledFor ? (
217
+ <RunFact label="Scheduled for">
218
+ {formatAutomationRunTime(run.scheduledFor)}
219
+ </RunFact>
220
+ ) : null}
221
+ {run.startedAt ? (
222
+ <RunFact label="Started">
223
+ {formatAutomationRunTime(run.startedAt)}
224
+ </RunFact>
225
+ ) : null}
226
+ <RunFact label="Finished">
227
+ {formatAutomationRunTime(run.executedAt)}
228
+ </RunFact>
229
+ <RunFact label="Duration">
230
+ {formatRuntime(run.durationMs) || "—"}
231
+ </RunFact>
232
+ <RunFact label="Tokens">
233
+ {run.totalTokens === undefined
234
+ ? "—"
235
+ : formatCompactNumber(run.totalTokens)}
236
+ </RunFact>
237
+ <RunFact label="Cost">
238
+ {run.costUsd === undefined
239
+ ? "—"
240
+ : formatCostSummary({ total: run.costUsd })}
241
+ </RunFact>
242
+ </dl>
243
+ </RunSection>
244
+ <RunSection title="Setup">
245
+ <dl className={runFactsClass}>
246
+ {props.showAutomation ? (
247
+ <RunFact label="Automation">
248
+ <Link
249
+ className={dashboardInlineLinkClass}
250
+ to={automationPath(run.automationId)}
251
+ >
252
+ {run.automationTitle}
253
+ </Link>
254
+ </RunFact>
255
+ ) : null}
256
+ {run.automationVersion ? (
257
+ <RunFact label="Version">
258
+ <Link className={dashboardInlineLinkClass} to={versionsPath}>
259
+ Version {run.automationVersion}
260
+ </Link>
261
+ </RunFact>
262
+ ) : null}
263
+ {run.credentialMode ? (
264
+ <RunFact label="Accounts">
265
+ {run.credentialMode === "creator"
266
+ ? "Creator’s connected accounts"
267
+ : "Junior’s accounts only"}
268
+ </RunFact>
269
+ ) : null}
270
+ <RunFact label="Run ID">
271
+ <span className="font-mono">{run.executionId}</span>
272
+ </RunFact>
273
+ </dl>
274
+ </RunSection>
275
+ {run.conversationId ? null : (
276
+ <p className="m-0 px-1 text-xs text-dashboard-text-muted">
277
+ No Conversation was recorded for this run.
278
+ </p>
279
+ )}
280
+ </div>
281
+ );
282
+ }
283
+
284
+ // Match the Summary tab cards in conversation details.
285
+ const runFactsClass = "m-0 grid min-w-0 gap-2 text-xs leading-snug";
286
+
287
+ function RunSection(props: { children: ReactNode; title: string }) {
288
+ return (
289
+ <section className="grid min-w-0 gap-2.5 rounded-lg border border-dashboard-border-subtle bg-dashboard-surface-panel p-3">
290
+ <h3 className="m-0 font-sans text-xs font-medium text-dashboard-text-muted">
291
+ {props.title}
292
+ </h3>
293
+ {props.children}
294
+ </section>
295
+ );
296
+ }
297
+
298
+ function RunFact(props: { label: string; children: ReactNode }) {
299
+ return (
300
+ <div className="flex min-w-0 items-baseline justify-between gap-4">
301
+ <dt className="shrink-0 text-dashboard-text-muted">{props.label}</dt>
302
+ <dd className="m-0 min-w-0 break-words text-right tabular-nums text-dashboard-text">
303
+ {props.children}
304
+ </dd>
305
+ </div>
306
+ );
307
+ }