@sentry/junior-dashboard 0.266.0 → 0.267.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 (73) hide show
  1. package/dist/app.js +49 -12
  2. package/dist/client.js +94 -63
  3. package/dist/index.js +49 -12
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +11 -5
  6. package/src/app.ts +0 -2
  7. package/src/auth.ts +4 -7
  8. package/src/client/App.tsx +35 -53
  9. package/src/client/components/DetailsPanel.tsx +12 -13
  10. package/src/client/components/Drawer.tsx +46 -23
  11. package/src/client/components/ImageAttachment.tsx +1 -1
  12. package/src/client/components/ParticipantAvatarStack.tsx +4 -1
  13. package/src/client/components/README.md +11 -2
  14. package/src/client/components/ui/alert.tsx +1 -1
  15. package/src/client/components/ui/button-group.tsx +59 -0
  16. package/src/client/components/ui/dialog.tsx +29 -35
  17. package/src/client/components/ui/drawer.tsx +140 -0
  18. package/src/client/components/ui/empty.tsx +30 -1
  19. package/src/client/components/ui/separator.tsx +22 -0
  20. package/src/client/components/ui/useSheet.ts +5 -1
  21. package/src/client/conversations/ComposerImages.tsx +66 -14
  22. package/src/client/conversations/ConversationComposer.tsx +84 -12
  23. package/src/client/conversations/ConversationDetails.tsx +3 -27
  24. package/src/client/conversations/ConversationEventDetails.tsx +26 -10
  25. package/src/client/conversations/ConversationPage.tsx +156 -2
  26. package/src/client/conversations/ConversationTranscript.tsx +48 -16
  27. package/src/client/conversations/DetailsList.tsx +33 -0
  28. package/src/client/conversations/EventDetails.tsx +46 -19
  29. package/src/client/conversations/ForkConversationButton.tsx +79 -73
  30. package/src/client/conversations/PendingMailboxStack.tsx +310 -112
  31. package/src/client/conversations/README.md +30 -13
  32. package/src/client/conversations/SubagentTranscriptDrawer.tsx +19 -22
  33. package/src/client/conversations/TranscriptActivityGroup.tsx +22 -19
  34. package/src/client/conversations/TranscriptAttachment.tsx +39 -4
  35. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +14 -19
  36. package/src/client/conversations/TranscriptFailureView.tsx +10 -13
  37. package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
  38. package/src/client/conversations/TranscriptMessageView.tsx +7 -1
  39. package/src/client/conversations/TranscriptSummary.tsx +5 -31
  40. package/src/client/conversations/TranscriptTurnContextView.tsx +33 -18
  41. package/src/client/conversations/TranscriptView.tsx +7 -3
  42. package/src/client/conversations/UserInputRequestCard.tsx +244 -0
  43. package/src/client/conversations/conversationOutbox.ts +46 -0
  44. package/src/client/conversations/eventTime.ts +13 -1
  45. package/src/client/conversations/eventTranscript.ts +12 -5
  46. package/src/client/conversations/queries.ts +127 -26
  47. package/src/client/conversations/transcriptMarkdownLinks.ts +5 -367
  48. package/src/client/conversations/userInputRequest.ts +56 -0
  49. package/src/client/format.ts +10 -3
  50. package/src/client/pages/automations/{AutomationLifecycle.tsx → AutomationActions.tsx} +60 -69
  51. package/src/client/pages/automations/AutomationDraftChanges.tsx +171 -0
  52. package/src/client/pages/automations/AutomationEditor.tsx +182 -156
  53. package/src/client/pages/automations/AutomationEventFields.tsx +119 -45
  54. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +1 -1
  55. package/src/client/pages/automations/AutomationFormSection.tsx +20 -2
  56. package/src/client/pages/automations/AutomationOutcomeFields.tsx +1 -1
  57. package/src/client/pages/automations/AutomationOverview.tsx +238 -0
  58. package/src/client/pages/automations/AutomationPage.tsx +343 -0
  59. package/src/client/pages/automations/AutomationRecentRuns.tsx +3 -2
  60. package/src/client/pages/automations/AutomationRow.tsx +11 -13
  61. package/src/client/pages/automations/AutomationRunDetails.tsx +4 -2
  62. package/src/client/pages/automations/{AutomationExecutionsPage.tsx → AutomationRunsTab.tsx} +94 -103
  63. package/src/client/pages/automations/AutomationStatus.tsx +164 -108
  64. package/src/client/pages/automations/AutomationVersionsTab.tsx +518 -0
  65. package/src/client/pages/automations/AutomationsPage.tsx +10 -98
  66. package/src/client/pages/automations/AutomationsPageLayout.tsx +5 -23
  67. package/src/client/pages/automations/README.md +56 -46
  68. package/src/client/types.ts +3 -0
  69. package/src/mock-reporting/fixtures.ts +44 -2
  70. package/src/client/conversations/transcriptStyles.ts +0 -10
  71. package/src/client/pages/automations/AutomationDetails.tsx +0 -193
  72. package/src/client/pages/automations/AutomationEditPage.tsx +0 -176
  73. package/src/client/pages/automations/AutomationVersionsPage.tsx +0 -390
@@ -5,7 +5,7 @@ import { Link } from "react-router";
5
5
  import { ActorAvatar } from "../../components/ActorAvatar";
6
6
  import { SelectableRow } from "../../components/SelectableRow";
7
7
  import { StatusChip } from "../../components/StatusChip";
8
- import { formatRelativeTime, peoplePath } from "../../format";
8
+ import { automationPath, formatRelativeTime, peoplePath } from "../../format";
9
9
  import { eventTriggerText } from "@sentry/junior-plugin-api";
10
10
  import {
11
11
  automationScheduleLabel,
@@ -14,13 +14,13 @@ import {
14
14
 
15
15
  /**
16
16
  * Show one automation as a compact card: what it is, where it posts, who owns
17
- * it, and when it runs. Instructions, timezones, and event filters stay in
18
- * the details panel.
17
+ * it, and when it runs. Instructions, timezones, and event filters stay on
18
+ * the Automation page. The title links to that page.
19
19
  */
20
20
  export function AutomationRow(props: {
21
21
  onSelect(): void;
22
- selected: boolean;
23
22
  automation: AutomationSummary;
23
+ to: string;
24
24
  }) {
25
25
  const { automation } = props;
26
26
  const status = automationStatus(automation);
@@ -43,15 +43,13 @@ export function AutomationRow(props: {
43
43
  <SelectableRow
44
44
  className="flex items-start gap-3 rounded-lg px-4 py-3.5"
45
45
  onSelect={props.onSelect}
46
- selected={props.selected}
46
+ selected={false}
47
47
  >
48
48
  <div className="min-w-0 flex-1">
49
- <button
50
- aria-expanded={props.selected}
51
- aria-label={`View automation details: ${automation.title}`}
52
- className="block w-full min-w-0 cursor-pointer rounded-md border-0 bg-transparent p-0 text-left focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
53
- onClick={props.onSelect}
54
- type="button"
49
+ <Link
50
+ aria-label={`Open automation: ${automation.title}`}
51
+ className="block w-full min-w-0 rounded-md text-left no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
52
+ to={props.to}
55
53
  >
56
54
  <span className="flex min-w-0 items-center gap-2">
57
55
  <span
@@ -75,7 +73,7 @@ export function AutomationRow(props: {
75
73
  ) : null}
76
74
  </span>
77
75
  <AutomationSummaryLine automation={automation} />
78
- </button>
76
+ </Link>
79
77
  <div className="mt-2.5 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-dashboard-text-muted">
80
78
  {automation.createdByEmail ? (
81
79
  <Link
@@ -187,7 +185,7 @@ function AutomationTiming({ automation }: { automation: AutomationSummary }) {
187
185
  {lastRunFailed ? (
188
186
  <Link
189
187
  className="text-amber-300 underline"
190
- to={`/automations/${encodeURIComponent(automation.id)}/executions`}
188
+ to={automationPath(automation.id, "runs")}
191
189
  >
192
190
  Last run {automation.lastRunStatus}
193
191
  {automation.lastRunAt
@@ -186,7 +186,6 @@ function AutomationRunContent(props: {
186
186
  showAutomation: boolean;
187
187
  }) {
188
188
  const { run } = props;
189
- const versionsPath = `${automationPath(run.automationId)}/versions`;
190
189
  return (
191
190
  <div className="grid min-w-0 gap-3">
192
191
  <div className="flex flex-wrap items-center gap-x-2 gap-y-1 px-1">
@@ -255,7 +254,10 @@ function AutomationRunContent(props: {
255
254
  ) : null}
256
255
  {run.automationVersion ? (
257
256
  <RunFact label="Version">
258
- <Link className={dashboardInlineLinkClass} to={versionsPath}>
257
+ <Link
258
+ className={dashboardInlineLinkClass}
259
+ to={`${automationPath(run.automationId, "versions")}?version=${run.automationVersion}`}
260
+ >
259
261
  Version {run.automationVersion}
260
262
  </Link>
261
263
  </RunFact>
@@ -1,13 +1,14 @@
1
- import { useMemo, useState, type KeyboardEvent } from "react";
2
- import { ArrowLeft } from "lucide-react";
3
- import { Link, Navigate, useParams, useSearchParams } from "react-router";
1
+ import { useMemo, type KeyboardEvent, type ReactNode } from "react";
4
2
  import type {
5
3
  AutomationExecution,
6
4
  AutomationExecutionList,
7
5
  AutomationRun,
6
+ AutomationSummary,
8
7
  } from "@sentry/junior/api/schema";
9
8
 
10
9
  import { useAutomationExecutionsData } from "../../api";
10
+ import { Button } from "../../components/ui/button";
11
+ import { FilterTabList } from "../../components/FilterBar";
11
12
  import { InlineError } from "../../components/InlineError";
12
13
  import { PageContentSkeleton } from "../../components/PageContentSkeleton";
13
14
  import { SelectableRow } from "../../components/SelectableRow";
@@ -18,19 +19,15 @@ import {
18
19
  type TimeRangeDays,
19
20
  } from "../../components/controls/TimeRangeSelector";
20
21
  import { Card } from "../../components/layout/Card";
21
- import { PageHeader } from "../../components/layout/PageHeader";
22
22
  import {
23
23
  formatCompactNumber,
24
24
  formatCostSummary,
25
25
  formatRuntime,
26
- automationPath,
27
26
  } from "../../format";
28
- import { DashboardApiError } from "../../http";
29
- import { pathWithSearch } from "../../searchParams";
27
+ import { useSearchParamEnum } from "../../searchParams";
30
28
  import { cn } from "../../styles";
31
29
  import { AutomationExecutionStatusChart } from "./AutomationExecutionStatusChart";
32
30
  import {
33
- AUTOMATION_RUN_PARAM,
34
31
  AutomationRunDetails,
35
32
  AutomationRunsFrame,
36
33
  RunConversationLink,
@@ -42,139 +39,109 @@ import {
42
39
  formatAutomationRunTime,
43
40
  } from "./automationFormat";
44
41
 
45
- /** Leading conversation column flexes; metric columns stay equal fixed widths. */
42
+ /** Search param that filters the Runs tab by result. */
43
+ export const AUTOMATION_RESULT_PARAM = "result";
44
+
45
+ const RESULT_OPTIONS = ["", "completed", "failed", "blocked"] as const;
46
+
47
+ /** Leading run column flexes; metric columns stay equal fixed widths. */
46
48
  const EXECUTION_GRID =
47
49
  "grid-cols-[minmax(0,1fr)_5.5rem_5.5rem_5.5rem_auto_2rem]";
48
50
 
49
- /** Render one automation's terminal executions beside the selected run. */
50
- export function AutomationExecutionsPage(props: { enabled: boolean }) {
51
- const { automationId } = useParams();
52
- const [searchParams] = useSearchParams();
53
- const query = useAutomationExecutionsData(props.enabled, automationId);
54
- const backSearch = new URLSearchParams(searchParams);
55
- backSearch.delete(AUTOMATION_RUN_PARAM);
56
- const backTo = pathWithSearch(
57
- automationId ? automationPath(automationId) : "/automations/list",
58
- backSearch,
51
+ /** Render one Automation's terminal runs beside the selected run. */
52
+ export function AutomationRunsTab(props: {
53
+ automation: AutomationSummary;
54
+ header: ReactNode;
55
+ range: TimeRangeDays;
56
+ }) {
57
+ const { automation } = props;
58
+ const query = useAutomationExecutionsData(true, automation.id);
59
+ const [result, setResult] = useSearchParamEnum(
60
+ AUTOMATION_RESULT_PARAM,
61
+ "",
62
+ RESULT_OPTIONS,
63
+ );
64
+ const executions = useMemo(
65
+ () =>
66
+ (query.data?.executions ?? []).filter(
67
+ (execution) => !result || execution.status === result,
68
+ ),
69
+ [query.data, result],
59
70
  );
71
+ // Details look up every loaded run, so a filter never hides the open run.
60
72
  const runs = useMemo(
61
73
  () =>
62
- query.data
63
- ? query.data.executions.map(
64
- (execution): AutomationRun => ({
65
- ...execution,
66
- automationId: query.data.automation.id,
67
- automationTitle: query.data.automation.title,
68
- kind: query.data.automation.kind,
69
- }),
70
- )
71
- : [],
72
- [query.data],
74
+ (query.data?.executions ?? []).map(
75
+ (execution): AutomationRun => ({
76
+ ...execution,
77
+ automationId: automation.id,
78
+ automationTitle: automation.title,
79
+ kind: automation.kind,
80
+ }),
81
+ ),
82
+ [automation, query.data],
73
83
  );
74
84
  const selection = useAutomationRunSelection(
75
- runs.map((run) => run.executionId),
85
+ executions.map((execution) => execution.executionId),
76
86
  );
77
-
78
- if (!automationId) {
79
- return <Navigate replace to="/automations" />;
80
- }
81
- const loading = !query.data && !query.error;
82
87
  return (
83
88
  <AutomationRunsFrame
84
89
  details={
85
90
  selection.runId ? (
86
91
  <AutomationRunDetails
87
92
  key={selection.runId}
88
- loading={loading}
93
+ loading={!query.data && !query.error}
89
94
  onClose={selection.close}
90
95
  run={runs.find((run) => run.executionId === selection.runId)}
91
96
  showAutomation={false}
92
97
  />
93
98
  ) : null
94
99
  }
95
- label="Executions list"
100
+ label="Runs list"
96
101
  listRef={selection.listRef}
97
102
  >
103
+ {props.header}
98
104
  {query.data ? (
99
- <AutomationExecutionsView
100
- backTo={backTo}
105
+ <AutomationRunsView
101
106
  data={query.data}
107
+ executions={executions}
108
+ onResultChange={setResult}
102
109
  onRowsKeyDown={selection.onRowsKeyDown}
103
110
  onSelect={selection.toggle}
111
+ range={props.range}
112
+ result={result}
104
113
  selectedRunId={selection.runId}
105
114
  />
115
+ ) : query.error ? (
116
+ <InlineError>
117
+ Runs could not be loaded.{" "}
118
+ <Button onClick={() => void query.refetch()}>Try again</Button>
119
+ </InlineError>
106
120
  ) : (
107
- <>
108
- <PageHeader
109
- description="Terminal runs for one scheduled or event automation."
110
- title="Automation executions"
111
- />
112
- {loading ? (
113
- <PageContentSkeleton
114
- label="Loading automation executions"
115
- variant="list"
116
- />
117
- ) : (
118
- <Card padding="md">
119
- <InlineError>
120
- {query.error instanceof DashboardApiError &&
121
- query.error.status === 404
122
- ? "This automation was not found or is not visible to you."
123
- : "Automation executions could not be loaded. Try again."}
124
- </InlineError>
125
- <Link
126
- className="mt-3 inline-flex items-center gap-2 font-sans text-xs text-dashboard-text-muted no-underline hover:text-dashboard-text"
127
- to={automationPath(automationId)}
128
- >
129
- <ArrowLeft aria-hidden="true" size={14} />
130
- Back to automations
131
- </Link>
132
- </Card>
133
- )}
134
- </>
121
+ <PageContentSkeleton label="Loading runs" variant="list" />
135
122
  )}
136
123
  </AutomationRunsFrame>
137
124
  );
138
125
  }
139
126
 
140
- function AutomationExecutionsView(props: {
141
- backTo: string;
127
+ function AutomationRunsView(props: {
142
128
  data: AutomationExecutionList;
129
+ executions: AutomationExecution[];
130
+ onResultChange(value: (typeof RESULT_OPTIONS)[number]): void;
143
131
  onRowsKeyDown(event: KeyboardEvent<HTMLElement>): void;
144
132
  onSelect(executionId: string): void;
133
+ range: TimeRangeDays;
134
+ result: (typeof RESULT_OPTIONS)[number];
145
135
  selectedRunId: string | undefined;
146
136
  }) {
147
- const { data } = props;
148
- const [range, setRange] = useState<TimeRangeDays>(30);
137
+ const { data, range } = props;
149
138
  const counts = useMemo(
150
139
  () => countByStatus(data.executions),
151
140
  [data.executions],
152
141
  );
153
- const statusCounts = `${counts.completed} completed · ${counts.failed} failed · ${counts.blocked} blocked`;
154
- const statusSummary = data.truncated
155
- ? `${data.automation.totalRuns} total · loaded runs: ${statusCounts}`
156
- : `${data.automation.totalRuns} total · ${statusCounts}`;
157
142
 
158
143
  return (
159
144
  <>
160
- <div>
161
- <Link
162
- className="mb-3 inline-flex items-center gap-2 font-sans text-xs text-dashboard-text-muted no-underline hover:text-dashboard-text"
163
- to={props.backTo}
164
- >
165
- <ArrowLeft aria-hidden="true" size={14} />
166
- Back to automation
167
- </Link>
168
- <PageHeader
169
- description={`${data.automation.kind} automation · ${data.automation.destination.label} · ${statusSummary}`}
170
- {...(data.executionDays.length > 0
171
- ? { onRangeChange: setRange, range }
172
- : {})}
173
- documentTitle={`${data.automation.title} · Executions`}
174
- title={data.automation.title}
175
- />
176
- </div>
177
-
178
145
  {data.executionDays.length > 0 ? (
179
146
  <AutomationExecutionStatusChart
180
147
  bucketUnit={timeRangeBucketUnit(range)}
@@ -194,18 +161,31 @@ function AutomationExecutionsView(props: {
194
161
  />
195
162
  ) : null}
196
163
 
197
- <div className="flex flex-wrap items-baseline justify-between gap-x-5 gap-y-1 border-b border-dashboard-border-subtle pb-3">
198
- <p className="m-0 text-sm text-dashboard-text-muted">
199
- {data.executions.length}{" "}
200
- {data.executions.length === 1 ? "run" : "runs"}
201
- <span className="text-dashboard-text-muted/70"> · newest first</span>
164
+ <div className="flex flex-wrap items-end justify-between gap-x-5 gap-y-3">
165
+ <FilterTabList
166
+ ariaLabel="Run result"
167
+ items={[
168
+ { count: data.executions.length, label: "All", value: "" },
169
+ { count: counts.completed, label: "Completed", value: "completed" },
170
+ { count: counts.failed, label: "Failed", value: "failed" },
171
+ { count: counts.blocked, label: "Blocked", value: "blocked" },
172
+ ]}
173
+ onChange={(value) =>
174
+ props.onResultChange(value as (typeof RESULT_OPTIONS)[number])
175
+ }
176
+ value={props.result}
177
+ />
178
+ <p className="m-0 text-xs text-dashboard-text-muted">
179
+ {data.automation.totalRuns} total · newest first
202
180
  </p>
203
181
  </div>
204
182
 
205
- {data.executions.length === 0 ? (
183
+ {props.executions.length === 0 ? (
206
184
  <Card padding="md">
207
185
  <p className="m-0 text-sm text-dashboard-text-muted">
208
- This automation has not produced any terminal executions yet.
186
+ {data.executions.length === 0
187
+ ? "This automation has not run yet."
188
+ : `No ${props.result} runs in the loaded history.`}
209
189
  </p>
210
190
  </Card>
211
191
  ) : (
@@ -229,7 +209,7 @@ function AutomationExecutionsView(props: {
229
209
  onKeyDown={props.onRowsKeyDown}
230
210
  role="list"
231
211
  >
232
- {data.executions.map((execution) => (
212
+ {props.executions.map((execution) => (
233
213
  <ExecutionRow
234
214
  execution={execution}
235
215
  fallbackTitle={data.automation.title}
@@ -246,7 +226,7 @@ function AutomationExecutionsView(props: {
246
226
 
247
227
  {data.truncated ? (
248
228
  <p className="m-0 text-center text-xs text-dashboard-text-muted">
249
- Showing the 100 most recent executions.
229
+ Showing the 100 most recent runs.
250
230
  </p>
251
231
  ) : null}
252
232
  </>
@@ -305,7 +285,18 @@ function ExecutionRow(props: {
305
285
  </span>
306
286
  <span className="mt-1 block truncate text-xs leading-relaxed text-dashboard-text-muted">
307
287
  {formatAutomationRunTime(execution.executedAt)}
288
+ {execution.automationVersion
289
+ ? ` · Version ${execution.automationVersion}`
290
+ : null}
308
291
  </span>
292
+ {execution.error ? (
293
+ <span
294
+ className="mt-1 block truncate text-xs leading-relaxed text-rose-300"
295
+ title={execution.error}
296
+ >
297
+ {execution.error}
298
+ </span>
299
+ ) : null}
309
300
  </button>
310
301
  <MetricCell label="Duration" value={durationLabel} />
311
302
  <MetricCell label="Tokens" value={tokensLabel} />