@sentry/junior-dashboard 0.265.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 (104) 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/ActorAvatar.tsx +10 -22
  10. package/src/client/components/ConnectionBanner.tsx +4 -3
  11. package/src/client/components/CopyButton.tsx +3 -4
  12. package/src/client/components/DetailsPanel.tsx +12 -13
  13. package/src/client/components/Drawer.tsx +46 -23
  14. package/src/client/components/EmptyTelemetry.tsx +5 -3
  15. package/src/client/components/ImageAttachment.tsx +49 -63
  16. package/src/client/components/InlineError.tsx +8 -4
  17. package/src/client/components/Notice.tsx +19 -26
  18. package/src/client/components/Pagination.tsx +26 -26
  19. package/src/client/components/ParticipantAvatarStack.tsx +13 -10
  20. package/src/client/components/README.md +28 -3
  21. package/src/client/components/SearchInput.tsx +51 -41
  22. package/src/client/components/VersionDriftBanner.tsx +9 -7
  23. package/src/client/components/charts/CacheWrites.tsx +52 -43
  24. package/src/client/components/ui/alert.tsx +64 -0
  25. package/src/client/components/ui/avatar.tsx +53 -0
  26. package/src/client/components/ui/button-group.tsx +59 -0
  27. package/src/client/components/ui/button.tsx +2 -1
  28. package/src/client/components/ui/dialog.tsx +60 -0
  29. package/src/client/components/ui/drawer.tsx +140 -0
  30. package/src/client/components/ui/empty.tsx +61 -0
  31. package/src/client/components/ui/input-group.tsx +96 -0
  32. package/src/client/components/ui/pagination.tsx +60 -0
  33. package/src/client/components/ui/separator.tsx +22 -0
  34. package/src/client/components/ui/spinner.tsx +21 -0
  35. package/src/client/components/ui/table.tsx +22 -1
  36. package/src/client/components/ui/tooltip.tsx +1 -1
  37. package/src/client/components/ui/useSheet.ts +5 -1
  38. package/src/client/conversations/ComposerImages.tsx +66 -14
  39. package/src/client/conversations/ConversationChildAgents.tsx +5 -5
  40. package/src/client/conversations/ConversationComposer.tsx +84 -12
  41. package/src/client/conversations/ConversationDetails.tsx +3 -27
  42. package/src/client/conversations/ConversationEventDetails.tsx +26 -10
  43. package/src/client/conversations/ConversationHeader.tsx +5 -4
  44. package/src/client/conversations/ConversationHeaderActions.tsx +7 -11
  45. package/src/client/conversations/ConversationPage.tsx +156 -2
  46. package/src/client/conversations/ConversationSidebar.tsx +5 -4
  47. package/src/client/conversations/ConversationTranscript.tsx +48 -16
  48. package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
  49. package/src/client/conversations/ConversationWatches.tsx +67 -59
  50. package/src/client/conversations/DetailsList.tsx +77 -29
  51. package/src/client/conversations/EventDetails.tsx +46 -19
  52. package/src/client/conversations/EventUsage.tsx +99 -74
  53. package/src/client/conversations/ForkConversationButton.tsx +79 -73
  54. package/src/client/conversations/PendingMailboxStack.tsx +312 -119
  55. package/src/client/conversations/README.md +30 -10
  56. package/src/client/conversations/ShareConversationButton.tsx +3 -4
  57. package/src/client/conversations/SubagentTranscriptDrawer.tsx +19 -22
  58. package/src/client/conversations/TranscriptActivityGroup.tsx +22 -19
  59. package/src/client/conversations/TranscriptAttachment.tsx +39 -4
  60. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +14 -19
  61. package/src/client/conversations/TranscriptEventView.tsx +35 -16
  62. package/src/client/conversations/TranscriptFailureView.tsx +10 -13
  63. package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
  64. package/src/client/conversations/TranscriptMessageContextView.tsx +31 -20
  65. package/src/client/conversations/TranscriptMessageView.tsx +7 -1
  66. package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
  67. package/src/client/conversations/TranscriptSummary.tsx +13 -7
  68. package/src/client/conversations/TranscriptTurnContextView.tsx +78 -56
  69. package/src/client/conversations/TranscriptView.tsx +7 -3
  70. package/src/client/conversations/UserInputRequestCard.tsx +244 -0
  71. package/src/client/conversations/conversationOutbox.ts +46 -0
  72. package/src/client/conversations/eventLog.ts +1 -0
  73. package/src/client/conversations/eventTime.ts +13 -1
  74. package/src/client/conversations/eventTranscript.ts +12 -5
  75. package/src/client/conversations/queries.ts +127 -26
  76. package/src/client/conversations/transcriptMarkdownLinks.ts +5 -367
  77. package/src/client/conversations/transcriptMentions.tsx +18 -5
  78. package/src/client/conversations/userInputRequest.ts +56 -0
  79. package/src/client/format.ts +10 -3
  80. package/src/client/pages/automations/{AutomationLifecycle.tsx → AutomationActions.tsx} +60 -69
  81. package/src/client/pages/automations/AutomationDraftChanges.tsx +171 -0
  82. package/src/client/pages/automations/AutomationEditor.tsx +182 -156
  83. package/src/client/pages/automations/AutomationEventFields.tsx +119 -45
  84. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +1 -1
  85. package/src/client/pages/automations/AutomationFormSection.tsx +20 -2
  86. package/src/client/pages/automations/AutomationOutcomeFields.tsx +1 -1
  87. package/src/client/pages/automations/AutomationOverview.tsx +238 -0
  88. package/src/client/pages/automations/AutomationPage.tsx +343 -0
  89. package/src/client/pages/automations/AutomationRecentRuns.tsx +3 -2
  90. package/src/client/pages/automations/AutomationRow.tsx +11 -13
  91. package/src/client/pages/automations/AutomationRunDetails.tsx +4 -2
  92. package/src/client/pages/automations/{AutomationExecutionsPage.tsx → AutomationRunsTab.tsx} +94 -103
  93. package/src/client/pages/automations/AutomationStatus.tsx +164 -108
  94. package/src/client/pages/automations/AutomationVersionsTab.tsx +518 -0
  95. package/src/client/pages/automations/AutomationsPage.tsx +10 -98
  96. package/src/client/pages/automations/AutomationsPageLayout.tsx +5 -23
  97. package/src/client/pages/automations/README.md +56 -46
  98. package/src/client/types.ts +3 -0
  99. package/src/mock-reporting/fixtures.ts +44 -2
  100. package/src/mock-reporting/watches.ts +1 -1
  101. package/src/client/conversations/transcriptStyles.ts +0 -10
  102. package/src/client/pages/automations/AutomationDetails.tsx +0 -193
  103. package/src/client/pages/automations/AutomationEditPage.tsx +0 -176
  104. package/src/client/pages/automations/AutomationVersionsPage.tsx +0 -390
@@ -1,15 +1,33 @@
1
1
  import type { ReactNode } from "react";
2
2
 
3
- /** Keep section headings and fields aligned without nested form cards. */
3
+ import { cn } from "../../styles";
4
+
5
+ /**
6
+ * Keep section headings and fields aligned without nested form cards. The
7
+ * read view and the editor use the same sections, so editing stays in place.
8
+ */
4
9
  export function AutomationFormSection(props: {
5
10
  title: string;
6
11
  detail?: string;
12
+ /** Mark a section with unsaved edits. */
13
+ changed?: boolean;
7
14
  children: ReactNode;
8
15
  }) {
9
16
  return (
10
- <section className="grid min-w-0 gap-5 border-t border-dashboard-border-emphasis py-8 md:grid-cols-[12rem_minmax(0,1fr)] md:gap-10">
17
+ <section
18
+ className={cn(
19
+ "grid min-w-0 gap-5 border-t border-dashboard-border-emphasis py-8 md:grid-cols-[12rem_minmax(0,1fr)] md:gap-10",
20
+ props.changed &&
21
+ "-ml-4 pl-4 shadow-[inset_2px_0_0_var(--color-dashboard-accent)] sm:-ml-5 sm:pl-5",
22
+ )}
23
+ >
11
24
  <div>
12
25
  <h2 className="m-0 font-display text-lg font-medium">{props.title}</h2>
26
+ {props.changed ? (
27
+ <p className="mt-1 mb-0 text-xs text-dashboard-accent-text">
28
+ Unsaved changes
29
+ </p>
30
+ ) : null}
13
31
  {props.detail ? (
14
32
  <p className="mt-2 mb-0 text-sm leading-relaxed text-dashboard-text-muted">
15
33
  {props.detail}
@@ -18,7 +18,7 @@ export function AutomationOutcomeList(props: {
18
18
  if (!props.outcomes.length)
19
19
  return <p className="m-0 text-sm">No success message</p>;
20
20
  return (
21
- <ol className="m-0 grid gap-2 pl-5 text-sm">
21
+ <ol className="m-0 grid list-decimal gap-2 pl-5 text-sm">
22
22
  {props.outcomes.map((outcome, index) => (
23
23
  <li key={index}>
24
24
  {automationOutcomeLabel(outcome, props.destination)}
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Read view of one Automation. The sections match the editor, so edit mode
3
+ * changes the same rows in place. The side column holds status and activity.
4
+ */
5
+ import type { ReactNode } from "react";
6
+ import { Link, useLocation } from "react-router";
7
+ import type { AutomationSummary } from "@sentry/junior/api/schema";
8
+
9
+ import {
10
+ useAutomationExecutionsData,
11
+ useAutomationVersionsData,
12
+ } from "../../api";
13
+ import {
14
+ selectTimeSeries,
15
+ type TimeRangeDays,
16
+ } from "../../components/controls/TimeRangeSelector";
17
+ import { Card } from "../../components/layout/Card";
18
+ import { TranscriptText } from "../../conversations/TranscriptText";
19
+ import { automationPath, formatCostSummary, peoplePath } from "../../format";
20
+ import { pathWithSearch } from "../../searchParams";
21
+ import { dashboardInlineLinkClass } from "../../styles";
22
+ import { AutomationFormSection } from "./AutomationFormSection";
23
+ import { AutomationOutcomeList } from "./AutomationOutcomeFields";
24
+ import { AutomationRecentRuns } from "./AutomationRecentRuns";
25
+ import { AutomationAttention, AutomationStatus } from "./AutomationStatus";
26
+ import { TriggerFilters } from "./TriggerFilters";
27
+ import {
28
+ automationScheduleLabel,
29
+ formatAutomationRunTime,
30
+ } from "./automationFormat";
31
+
32
+ /** Settings on the left; status, activity, and ownership on the right. */
33
+ export function AutomationOverview(props: {
34
+ automation: AutomationSummary;
35
+ editPath: string;
36
+ range: TimeRangeDays;
37
+ }) {
38
+ const { automation } = props;
39
+ return (
40
+ <div className="grid min-w-0 gap-6 lg:grid-cols-[minmax(0,1fr)_18rem] lg:gap-10">
41
+ <div className="grid min-w-0 content-start gap-6">
42
+ <AutomationAttention
43
+ automation={automation}
44
+ editPath={props.editPath}
45
+ />
46
+ <AutomationSettings automation={automation} />
47
+ <Card padding="sm">
48
+ <AutomationRecentRuns automation={automation} />
49
+ </Card>
50
+ {/* On small screens, status leads and About follows the settings. */}
51
+ <div className="lg:hidden">
52
+ <AutomationAbout automation={automation} />
53
+ </div>
54
+ </div>
55
+ <aside
56
+ aria-label="Automation status"
57
+ className="grid min-w-0 content-start gap-3 max-lg:row-start-1"
58
+ >
59
+ <RailCard title="Status">
60
+ <AutomationStatus automation={automation} />
61
+ </RailCard>
62
+ <AutomationActivity automation={automation} range={props.range} />
63
+ <div className="max-lg:hidden">
64
+ <AutomationAbout automation={automation} />
65
+ </div>
66
+ </aside>
67
+ </div>
68
+ );
69
+ }
70
+
71
+ /** Saved settings, in the same sections and order as the editor. */
72
+ function AutomationSettings(props: { automation: AutomationSummary }) {
73
+ const { automation } = props;
74
+ const creator = automation.ownedByViewer ? "you" : automation.createdBy;
75
+ return (
76
+ <div className="min-w-0 [&>section:first-child]:border-t-0 [&>section:first-child]:pt-0">
77
+ <AutomationFormSection title="What to do">
78
+ <div className="text-base leading-relaxed [overflow-wrap:anywhere]">
79
+ <TranscriptText
80
+ mentions={automation.mentions ?? []}
81
+ text={automation.instruction}
82
+ />
83
+ </div>
84
+ </AutomationFormSection>
85
+ <AutomationFormSection title="When to run">
86
+ {automation.kind === "scheduled" ? (
87
+ <p className="m-0 text-sm">{automationScheduleLabel(automation)}</p>
88
+ ) : (
89
+ <div className="grid gap-1 text-sm">
90
+ <p className="m-0 break-words">{automation.resource}</p>
91
+ <p className="m-0 break-words text-dashboard-text-muted">
92
+ {automation.trigger.events.join(", ")}
93
+ </p>
94
+ <TriggerFilters filters={automation.trigger.filters} />
95
+ </div>
96
+ )}
97
+ </AutomationFormSection>
98
+ <AutomationFormSection title="Where results go">
99
+ <AutomationOutcomeList
100
+ outcomes={automation.outcomes}
101
+ destination={automation.destination}
102
+ />
103
+ </AutomationFormSection>
104
+ <AutomationFormSection title="Credentials">
105
+ <p className="m-0 text-sm">
106
+ {automation.credentialMode === "creator"
107
+ ? `Uses accounts connected by ${creator}.`
108
+ : "System credentials only. Junior does not use anyone’s connected accounts."}
109
+ </p>
110
+ </AutomationFormSection>
111
+ </div>
112
+ );
113
+ }
114
+
115
+ /** Runs, problems, and spend in the selected range. */
116
+ function AutomationActivity(props: {
117
+ automation: AutomationSummary;
118
+ range: TimeRangeDays;
119
+ }) {
120
+ const { automation, range } = props;
121
+ // AutomationRecentRuns reads the same query, so this adds no request.
122
+ const executions = useAutomationExecutionsData(true, automation.id);
123
+ const problems = executions.data
124
+ ? selectTimeSeries({
125
+ days: executions.data.executionDays,
126
+ hours: executions.data.executionHours,
127
+ sixHours: executions.data.executionSixHours,
128
+ range,
129
+ }).reduce(
130
+ (total, bucket) => ({
131
+ blocked: total.blocked + bucket.blocked,
132
+ failed: total.failed + bucket.failed,
133
+ }),
134
+ { blocked: 0, failed: 0 },
135
+ )
136
+ : undefined;
137
+ return (
138
+ <RailCard title={range === 1 ? "Last 24 hours" : `Last ${range} days`}>
139
+ <dl className="m-0 grid grid-cols-2 gap-x-4 gap-y-3">
140
+ <RailMetric label="Runs" value={automation.runs[range]} />
141
+ <RailMetric
142
+ label="Spend"
143
+ value={formatCostSummary({ total: automation.costUsd[range] })}
144
+ />
145
+ <RailMetric label="Failed" value={problems?.failed ?? "—"} />
146
+ <RailMetric label="Blocked" value={problems?.blocked ?? "—"} />
147
+ </dl>
148
+ </RailCard>
149
+ );
150
+ }
151
+
152
+ /** Who owns the Automation, where it lives, and which version is active. */
153
+ function AutomationAbout(props: { automation: AutomationSummary }) {
154
+ const { automation } = props;
155
+ const location = useLocation();
156
+ const versions = useAutomationVersionsData(true, automation.id);
157
+ const activeVersion = versions.data?.activeVersion;
158
+ return (
159
+ <RailCard title="About">
160
+ <dl className="m-0 grid gap-3 text-sm">
161
+ <RailFact label="Created by">
162
+ {automation.createdByEmail ? (
163
+ <Link
164
+ className={dashboardInlineLinkClass}
165
+ to={peoplePath(automation.createdByEmail)}
166
+ >
167
+ {automation.ownedByViewer ? "You" : automation.createdBy}
168
+ </Link>
169
+ ) : automation.ownedByViewer ? (
170
+ "You"
171
+ ) : (
172
+ automation.createdBy
173
+ )}
174
+ </RailFact>
175
+ <RailFact label="Created">
176
+ {formatAutomationRunTime(automation.createdAt)}
177
+ </RailFact>
178
+ <RailFact label="Destination">
179
+ {automation.destination.label} ·{" "}
180
+ {automation.destination.visibility === "private"
181
+ ? "private"
182
+ : "public"}
183
+ </RailFact>
184
+ <RailFact label="Version">
185
+ <Link
186
+ className={dashboardInlineLinkClass}
187
+ to={pathWithSearch(
188
+ automationPath(automation.id, "versions"),
189
+ location.search,
190
+ )}
191
+ >
192
+ {activeVersion ? `Version ${activeVersion}` : "Version history"}
193
+ </Link>
194
+ </RailFact>
195
+ </dl>
196
+ {automation.ownedByViewer ? null : (
197
+ <p className="mt-3 mb-0 text-xs leading-relaxed text-dashboard-text-muted">
198
+ {automation.destination.visibility === "private"
199
+ ? null
200
+ : "You can edit its settings because it is in a public channel. "}
201
+ Only {automation.createdBy} can pause, resume, or delete it, change
202
+ where results go, or turn on their connected accounts.
203
+ </p>
204
+ )}
205
+ </RailCard>
206
+ );
207
+ }
208
+
209
+ function RailCard(props: { children: ReactNode; title: string }) {
210
+ return (
211
+ <Card as="section" padding="sm">
212
+ <h2 className="m-0 mb-3 font-sans text-xs font-medium text-dashboard-text-muted">
213
+ {props.title}
214
+ </h2>
215
+ {props.children}
216
+ </Card>
217
+ );
218
+ }
219
+
220
+ function RailMetric(props: { label: string; value: ReactNode }) {
221
+ return (
222
+ <div className="min-w-0">
223
+ <dt className="text-xs text-dashboard-text-muted">{props.label}</dt>
224
+ <dd className="m-0 mt-0.5 font-display text-xl font-light tabular-nums text-dashboard-text">
225
+ {props.value}
226
+ </dd>
227
+ </div>
228
+ );
229
+ }
230
+
231
+ function RailFact(props: { children: ReactNode; label: string }) {
232
+ return (
233
+ <div className="grid min-w-0 gap-0.5">
234
+ <dt className="text-xs text-dashboard-text-muted">{props.label}</dt>
235
+ <dd className="m-0 min-w-0 break-words">{props.children}</dd>
236
+ </div>
237
+ );
238
+ }
@@ -0,0 +1,343 @@
1
+ /**
2
+ * One Automation page. Overview, Runs, and Versions are tabs under one header.
3
+ * Edit mode replaces the Overview read view in place.
4
+ */
5
+ import { useRef, useState, type ReactNode } from "react";
6
+ import { ArrowLeft } from "lucide-react";
7
+ import {
8
+ Link,
9
+ matchPath,
10
+ Navigate,
11
+ useLocation,
12
+ useNavigate,
13
+ useParams,
14
+ } from "react-router";
15
+ import { useQuery } from "@tanstack/react-query";
16
+ import {
17
+ automationEditSchema,
18
+ type AutomationSummary,
19
+ } from "@sentry/junior/api/schema";
20
+ import { eventTriggerText } from "@sentry/junior-plugin-api";
21
+
22
+ import { useAutomationData } from "../../api";
23
+ import { Button } from "../../components/ui/button";
24
+ import { FormNotice } from "../../components/FormNotice";
25
+ import { PageContentSkeleton } from "../../components/PageContentSkeleton";
26
+ import {
27
+ TimeRangeSelector,
28
+ type TimeRangeDays,
29
+ } from "../../components/controls/TimeRangeSelector";
30
+ import { PageHeader } from "../../components/layout/PageHeader";
31
+ import { PageLayout } from "../../components/layout/PageLayout";
32
+ import { SecondaryNavigation } from "../../components/layout/SecondaryNavigation";
33
+ import { automationPath, type AutomationTab } from "../../format";
34
+ import { DashboardApiError, fetchDashboardJson } from "../../http";
35
+ import { pathWithSearch, useSearchParamEnum } from "../../searchParams";
36
+ import { cn } from "../../styles";
37
+ import { AutomationActions } from "./AutomationActions";
38
+ import { AutomationDeleteDialog } from "./AutomationDeleteDialog";
39
+ import { AutomationEditor } from "./AutomationEditor";
40
+ import { AutomationEditorLoading } from "./AutomationEditorLoading";
41
+ import { AutomationOverview } from "./AutomationOverview";
42
+ import { AUTOMATION_RUN_PARAM } from "./AutomationRunDetails";
43
+ import {
44
+ AutomationRunsTab,
45
+ AUTOMATION_RESULT_PARAM,
46
+ } from "./AutomationRunsTab";
47
+ import {
48
+ AutomationStateChip,
49
+ automationNeedsAttention,
50
+ } from "./AutomationStatus";
51
+ import {
52
+ AUTOMATION_VERSION_PARAM,
53
+ AutomationVersionsTab,
54
+ } from "./AutomationVersionsTab";
55
+
56
+ const RANGE_OPTIONS = ["1", "7", "30", "90"] as const;
57
+
58
+ /** Search params that belong to one tab. The list link drops them. */
59
+ const PAGE_PARAMS = [
60
+ AUTOMATION_RESULT_PARAM,
61
+ AUTOMATION_RUN_PARAM,
62
+ AUTOMATION_VERSION_PARAM,
63
+ ];
64
+
65
+ const TAB_LABELS: Record<AutomationTab, string> = {
66
+ edit: "Edit",
67
+ overview: "Overview",
68
+ runs: "Runs",
69
+ versions: "Versions",
70
+ };
71
+
72
+ /** Render one Automation tab, or edit mode, under the shared header and tabs. */
73
+ export function AutomationPage(props: {
74
+ enabled: boolean;
75
+ tab: AutomationTab;
76
+ }) {
77
+ const { automationId } = useParams();
78
+ const location = useLocation();
79
+ const summary = useAutomationData(props.enabled, automationId);
80
+ const [rangeParam, setRangeParam] = useSearchParamEnum(
81
+ "range",
82
+ "30",
83
+ RANGE_OPTIONS,
84
+ );
85
+ const range = Number(rangeParam) as TimeRangeDays;
86
+ if (!automationId) return <Navigate replace to="/automations/list" />;
87
+ const listSearch = new URLSearchParams(location.search);
88
+ for (const param of PAGE_PARAMS) listSearch.delete(param);
89
+ const listPath = pathWithSearch("/automations/list", listSearch);
90
+ const automation = summary.data;
91
+ const tabs = (["overview", "runs", "versions"] as const).map((tab) => ({
92
+ label: TAB_LABELS[tab],
93
+ to: automationPath(automationId, tab),
94
+ // Edit mode is part of Overview.
95
+ isActive: (pathname: string) =>
96
+ tab === "overview"
97
+ ? Boolean(
98
+ matchPath("/automations/:automationId", pathname) ||
99
+ matchPath("/automations/:automationId/edit", pathname),
100
+ )
101
+ : Boolean(matchPath(`/automations/:automationId/${tab}`, pathname)),
102
+ }));
103
+ const runsFrame = props.tab === "runs" && Boolean(automation);
104
+ return (
105
+ <SecondaryNavigation
106
+ ariaLabel="Automation navigation"
107
+ className={
108
+ runsFrame
109
+ ? "grid h-full min-h-0 grid-rows-[auto_minmax(0,1fr)] md:grid-rows-[minmax(0,1fr)]"
110
+ : undefined
111
+ }
112
+ items={tabs}
113
+ >
114
+ {automation ? (
115
+ <AutomationPageContent
116
+ automation={automation}
117
+ listPath={listPath}
118
+ onRangeChange={(value) =>
119
+ setRangeParam(String(value) as (typeof RANGE_OPTIONS)[number])
120
+ }
121
+ range={range}
122
+ tab={props.tab}
123
+ />
124
+ ) : (
125
+ <PageLayout>
126
+ <BackLink to={listPath} />
127
+ <PageHeader
128
+ description="Settings, runs, and versions for one automation."
129
+ title="Automation"
130
+ />
131
+ {summary.error ? (
132
+ <FormNotice tone="error" title="Automation could not be loaded.">
133
+ {summary.error instanceof DashboardApiError &&
134
+ summary.error.status === 404
135
+ ? "This automation was not found or is not visible to you."
136
+ : "Try again."}{" "}
137
+ <Button onClick={() => void summary.refetch()}>Try again</Button>
138
+ </FormNotice>
139
+ ) : (
140
+ <PageContentSkeleton label="Loading automation" variant="list" />
141
+ )}
142
+ </PageLayout>
143
+ )}
144
+ </SecondaryNavigation>
145
+ );
146
+ }
147
+
148
+ function AutomationPageContent(props: {
149
+ automation: AutomationSummary;
150
+ listPath: string;
151
+ onRangeChange(value: TimeRangeDays): void;
152
+ range: TimeRangeDays;
153
+ tab: AutomationTab;
154
+ }) {
155
+ const { automation, tab } = props;
156
+ const location = useLocation();
157
+ const navigate = useNavigate();
158
+ const [deleting, setDeleting] = useState(false);
159
+ const deleteSource = useRef<HTMLButtonElement | null>(null);
160
+ const completed = automation.status === "completed";
161
+ // Completed Automations cannot change, so edit mode shows the read view.
162
+ if (tab === "edit" && completed) {
163
+ return (
164
+ <Navigate
165
+ replace
166
+ to={pathWithSearch(automationPath(automation.id), location.search)}
167
+ />
168
+ );
169
+ }
170
+ const editPath = pathWithSearch(
171
+ automationPath(automation.id, "edit"),
172
+ location.search,
173
+ );
174
+ const header = (
175
+ <>
176
+ <BackLink to={props.listPath} />
177
+ <PageHeader
178
+ actions={
179
+ tab === "edit" ? undefined : (
180
+ <div className="flex flex-wrap items-start gap-2 md:justify-end">
181
+ {tab === "overview" || tab === "runs" ? (
182
+ <TimeRangeSelector
183
+ onChange={props.onRangeChange}
184
+ value={props.range}
185
+ />
186
+ ) : null}
187
+ <AutomationActions
188
+ automation={automation}
189
+ editPath={editPath}
190
+ editVariant={
191
+ tab === "overview" && automationNeedsAttention(automation)
192
+ ? "default"
193
+ : "primary"
194
+ }
195
+ onDelete={(source) => {
196
+ deleteSource.current = source;
197
+ setDeleting(true);
198
+ }}
199
+ />
200
+ </div>
201
+ )
202
+ }
203
+ description={<AutomationHeaderLine automation={automation} />}
204
+ documentTitle={
205
+ tab === "overview"
206
+ ? automation.title
207
+ : `${automation.title} · ${TAB_LABELS[tab]}`
208
+ }
209
+ title={automation.title}
210
+ />
211
+ </>
212
+ );
213
+ const dialog = deleting ? (
214
+ <AutomationDeleteDialog
215
+ automation={automation}
216
+ onClose={() => setDeleting(false)}
217
+ onDeleted={() => {
218
+ deleteSource.current = null;
219
+ navigate(props.listPath);
220
+ }}
221
+ returnFocus={() =>
222
+ deleteSource.current?.isConnected ? deleteSource.current : false
223
+ }
224
+ />
225
+ ) : null;
226
+ if (tab === "runs") {
227
+ return (
228
+ <>
229
+ <AutomationRunsTab
230
+ automation={automation}
231
+ header={header}
232
+ range={props.range}
233
+ />
234
+ {dialog}
235
+ </>
236
+ );
237
+ }
238
+ return (
239
+ <PageLayout>
240
+ {header}
241
+ {tab === "overview" ? (
242
+ <AutomationOverview
243
+ automation={automation}
244
+ editPath={editPath}
245
+ range={props.range}
246
+ />
247
+ ) : null}
248
+ {tab === "edit" ? <AutomationEditMode automation={automation} /> : null}
249
+ {tab === "versions" ? (
250
+ <AutomationVersionsTab automation={automation} />
251
+ ) : null}
252
+ {dialog}
253
+ </PageLayout>
254
+ );
255
+ }
256
+
257
+ /** Load exact editable values, then edit the Overview sections in place. */
258
+ function AutomationEditMode(props: { automation: AutomationSummary }) {
259
+ const { automation } = props;
260
+ const location = useLocation();
261
+ const edit = useQuery({
262
+ queryKey: ["dashboard", "automations", "edit", automation.id],
263
+ retry: false,
264
+ queryFn: ({ signal }) =>
265
+ fetchDashboardJson(
266
+ automationEditSchema,
267
+ `/api/automations/${encodeURIComponent(automation.id)}/edit`,
268
+ signal,
269
+ ),
270
+ });
271
+ if (edit.error && !edit.data) {
272
+ return (
273
+ <FormNotice tone="error" title="Automation could not be loaded.">
274
+ It may no longer be available, or you may not have access.{" "}
275
+ <Button onClick={() => void edit.refetch()}>Try again</Button>
276
+ </FormNotice>
277
+ );
278
+ }
279
+ if (!edit.data) return <AutomationEditorLoading />;
280
+ return (
281
+ <AutomationEditor
282
+ key={automation.id}
283
+ automation={edit.data}
284
+ summary={automation}
285
+ returnPath={pathWithSearch(
286
+ automationPath(automation.id),
287
+ location.search,
288
+ )}
289
+ />
290
+ );
291
+ }
292
+
293
+ /** State, trigger, Destination, and creator in one line under the title. */
294
+ function AutomationHeaderLine(props: { automation: AutomationSummary }) {
295
+ const { automation } = props;
296
+ const trigger =
297
+ automation.kind === "scheduled"
298
+ ? automation.schedule
299
+ : eventTriggerText(automation.resource, automation.trigger);
300
+ const parts: ReactNode[] = [
301
+ trigger,
302
+ automation.destination.label,
303
+ `Created by ${automation.ownedByViewer ? "you" : automation.createdBy}`,
304
+ ];
305
+ // Each part starts with its separator. The negative margin and the clip
306
+ // hide the separator of any part that starts a line.
307
+ return (
308
+ <span className="block min-w-0 overflow-hidden">
309
+ <span className="-ml-4 flex min-w-0 flex-wrap items-center gap-y-1">
310
+ <span className="flex items-center">
311
+ <span aria-hidden="true" className="inline-block w-4" />
312
+ <AutomationStateChip automation={automation} />
313
+ </span>
314
+ {parts.map((part, index) => (
315
+ <span className="min-w-0 break-words" key={index}>
316
+ <span
317
+ aria-hidden="true"
318
+ className={cn(
319
+ "inline-block w-4 text-center opacity-45",
320
+ index === 0 && "invisible",
321
+ )}
322
+ >
323
+ ·
324
+ </span>
325
+ {part}
326
+ </span>
327
+ ))}
328
+ </span>
329
+ </span>
330
+ );
331
+ }
332
+
333
+ function BackLink(props: { to: string }) {
334
+ return (
335
+ <Link
336
+ className="flex w-fit items-center gap-2 font-display text-sm font-medium text-dashboard-text-muted no-underline transition-colors hover:text-dashboard-text"
337
+ to={props.to}
338
+ >
339
+ <ArrowLeft aria-hidden="true" size={15} strokeWidth={1.8} />
340
+ All automations
341
+ </Link>
342
+ );
343
+ }
@@ -8,6 +8,7 @@ import { Skeleton } from "../../components/ui/skeleton";
8
8
  import { StatusChip } from "../../components/StatusChip";
9
9
  import { conversationPath } from "../../conversations/conversationRoutes";
10
10
  import {
11
+ automationPath,
11
12
  formatCostSummary,
12
13
  formatRelativeTime,
13
14
  formatRuntime,
@@ -15,7 +16,7 @@ import {
15
16
  import { dashboardInlineLinkClass } from "../../styles";
16
17
  import { formatAutomationRunTime } from "./automationFormat";
17
18
 
18
- /** Preview the newest terminal runs without leaving the Automation list. */
19
+ /** Preview the newest terminal runs on the Automation overview. */
19
20
  export function AutomationRecentRuns({
20
21
  automation,
21
22
  }: {
@@ -32,7 +33,7 @@ export function AutomationRecentRuns({
32
33
  {automation.totalRuns > 0 || runs.length > 0 ? (
33
34
  <Link
34
35
  className={`${dashboardInlineLinkClass} text-xs`}
35
- to={`/automations/${encodeURIComponent(automation.id)}/executions`}
36
+ to={automationPath(automation.id, "runs")}
36
37
  >
37
38
  View all runs
38
39
  </Link>