@sentry/junior-dashboard 0.264.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 (191) 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/BottomSheet.tsx +12 -2
  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 +15 -24
  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 +86 -77
  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 +13 -3
  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/conversations/ChatLayout.tsx +7 -3
  60. package/src/client/conversations/ConversationBrief.tsx +17 -6
  61. package/src/client/conversations/ConversationComposer.tsx +26 -12
  62. package/src/client/conversations/ConversationDetails.tsx +147 -62
  63. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  64. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  65. package/src/client/conversations/ConversationHeader.tsx +8 -3
  66. package/src/client/conversations/ConversationHeaderActions.tsx +90 -82
  67. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  68. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  69. package/src/client/conversations/ConversationMeta.tsx +55 -88
  70. package/src/client/conversations/ConversationPage.tsx +220 -101
  71. package/src/client/conversations/ConversationSidebar.tsx +48 -29
  72. package/src/client/conversations/ConversationWatches.tsx +165 -0
  73. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  74. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  75. package/src/client/conversations/DetailsList.tsx +124 -0
  76. package/src/client/conversations/EventDetails.tsx +42 -8
  77. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  78. package/src/client/conversations/ObjectCard.tsx +20 -8
  79. package/src/client/conversations/PendingMailboxStack.tsx +52 -33
  80. package/src/client/conversations/README.md +86 -13
  81. package/src/client/conversations/ShareConversationButton.tsx +25 -13
  82. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  83. package/src/client/conversations/ToolFrame.tsx +36 -52
  84. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  85. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  86. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  87. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  88. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  89. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  90. package/src/client/conversations/TranscriptMessageContextView.tsx +8 -2
  91. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  92. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  93. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  94. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  95. package/src/client/conversations/TranscriptTurnContextView.tsx +99 -27
  96. package/src/client/conversations/TranscriptView.tsx +63 -27
  97. package/src/client/conversations/eventLog.ts +57 -0
  98. package/src/client/conversations/eventTime.ts +60 -0
  99. package/src/client/conversations/queries.ts +29 -0
  100. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  101. package/src/client/format.ts +5 -0
  102. package/src/client/pages/ConnectionsPage.tsx +2 -2
  103. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  104. package/src/client/pages/SettingsPage.tsx +9 -10
  105. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  106. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  107. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  108. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  109. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  110. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  111. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  112. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  113. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  114. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  115. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  116. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  117. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  118. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  119. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  120. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  121. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  122. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  123. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  124. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  125. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  126. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  127. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  128. package/src/client/pages/automations/README.md +57 -0
  129. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  130. package/src/client/pages/automations/automationFormat.ts +37 -23
  131. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  132. package/src/client/pages/code/CodePage.tsx +52 -55
  133. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  134. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  135. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  136. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  137. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  138. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  139. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  140. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  141. package/src/client/pages/memory/memoryRecord.ts +1 -1
  142. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  143. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  144. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  145. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  146. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  147. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  148. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  149. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  150. package/src/client/pages/system/README.md +23 -0
  151. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  152. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  153. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  154. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  155. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  156. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  157. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  158. package/src/client/pages/system/sandboxSize.ts +8 -0
  159. package/src/client/pages/system/workspaceQueries.ts +53 -0
  160. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  161. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  162. package/src/client/styles.ts +4 -0
  163. package/src/client.tsx +7 -1
  164. package/src/mock-reporting/automation-collection.ts +4 -2
  165. package/src/mock-reporting/fixtures.ts +23 -7
  166. package/src/mock-reporting/routes.ts +12 -0
  167. package/src/mock-reporting/watches.ts +94 -0
  168. package/src/shell.ts +4 -9
  169. package/src/tailwind.css +0 -10
  170. package/src/client/components/Button.tsx +0 -118
  171. package/src/client/components/Field.tsx +0 -53
  172. package/src/client/components/SegmentedTabs.tsx +0 -119
  173. package/src/client/components/Select.tsx +0 -52
  174. package/src/client/components/Skeleton.tsx +0 -17
  175. package/src/client/components/TextInput.tsx +0 -58
  176. package/src/client/components/Tooltip.tsx +0 -216
  177. package/src/client/components/charts/useChartLayout.ts +0 -27
  178. package/src/client/components/ui/sheet.LICENSE +0 -21
  179. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -119
  180. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  181. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  182. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  183. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  184. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  185. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  186. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  187. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  188. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  189. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  190. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  191. package/src/client/pages/system/SnapshotSummary.tsx +0 -78
@@ -663,6 +663,35 @@ export function useConversationData(conversationId: string | undefined) {
663
663
  },
664
664
  );
665
665
  }, [conversationId, detailData, mailbox, outbox.data, queryClient]);
666
+ // The title is generated after the first message, and only the open
667
+ // Conversation polls. Copy a newer title into loaded feeds, such as the
668
+ // sidebar and the home list, so they do not keep the old one.
669
+ const displayTitle = detail.data?.displayTitle;
670
+ const detailUpdatedAt = detail.dataUpdatedAt;
671
+ useEffect(() => {
672
+ if (!conversationId || !displayTitle) return;
673
+ const feeds = queryClient
674
+ .getQueryCache()
675
+ .findAll({ queryKey: ["dashboard", "conversations"] });
676
+ for (const query of feeds) {
677
+ const feed = query.state.data as ConversationFeed | undefined;
678
+ if (!feed || query.state.dataUpdatedAt > detailUpdatedAt) continue;
679
+ const stale = feed.conversations.some(
680
+ (conversation) =>
681
+ conversation.conversationId === conversationId &&
682
+ conversation.displayTitle !== displayTitle,
683
+ );
684
+ if (!stale) continue;
685
+ queryClient.setQueryData<ConversationFeed>(query.queryKey, {
686
+ ...feed,
687
+ conversations: feed.conversations.map((conversation) =>
688
+ conversation.conversationId === conversationId
689
+ ? { ...conversation, displayTitle }
690
+ : conversation,
691
+ ),
692
+ });
693
+ }
694
+ }, [conversationId, detailUpdatedAt, displayTitle, queryClient]);
666
695
  const historyStatus = detailData?.eventHistory.status;
667
696
  const historyQueryKey = useMemo(
668
697
  () => ["conversation", conversationId, "history", historyStatus] as const,
@@ -15,6 +15,7 @@ import type { ConversationTranscript } from "../types";
15
15
  import type { TranscriptViewMode } from "./transcriptRenderModel";
16
16
 
17
17
  const BOTTOM_PROXIMITY_PX = 96;
18
+ const PREVIOUS_PAGE_PROXIMITY_PX = 600;
18
19
  const USER_SCROLL_DELTA_PX = 2;
19
20
 
20
21
  type ScrollRoot = HTMLElement | Window;
@@ -650,6 +651,39 @@ export function usePinnedTranscriptBottom(input: {
650
651
  );
651
652
  }
652
653
 
654
+ /** Load the previous history page when the reader scrolls near the top. */
655
+ export function useAutoLoadPreviousPage(input: {
656
+ enabled: boolean;
657
+ historyVersion: string;
658
+ onLoad: () => void;
659
+ }): RefCallback<HTMLElement> {
660
+ const [sentinel, setSentinel] = useState<HTMLElement | null>(null);
661
+ const onLoadRef = useRef(input.onLoad);
662
+ onLoadRef.current = input.onLoad;
663
+
664
+ useEffect(() => {
665
+ if (!input.enabled || !sentinel) return;
666
+ if (typeof IntersectionObserver === "undefined") return;
667
+
668
+ const root = scrollRootFor(sentinel);
669
+ const observer = new IntersectionObserver(
670
+ (entries) => {
671
+ if (!entries.some((entry) => entry.isIntersecting)) return;
672
+ observer.disconnect();
673
+ onLoadRef.current();
674
+ },
675
+ {
676
+ root: root && !isWindowRoot(root) ? root : null,
677
+ rootMargin: `${PREVIOUS_PAGE_PROXIMITY_PX}px 0px 0px 0px`,
678
+ },
679
+ );
680
+ observer.observe(sentinel);
681
+ return () => observer.disconnect();
682
+ }, [input.enabled, input.historyVersion, sentinel]);
683
+
684
+ return setSentinel;
685
+ }
686
+
653
687
  /** Keep the previously visible content at the same viewport offset after a prepend. */
654
688
  export function scrollTopAfterPrepend(
655
689
  previous: Pick<ScrollSnapshot, "scrollHeight" | "scrollTop">,
@@ -791,6 +791,11 @@ export function locationPath(locationId: string): string {
791
791
  return `/system/locations/${encodeURIComponent(locationId)}`;
792
792
  }
793
793
 
794
+ /** Build the Workspace list route that opens one Workspace's details. */
795
+ export function workspacePath(workspaceId: string): string {
796
+ return `/system/workspaces/${encodeURIComponent(workspaceId)}`;
797
+ }
798
+
794
799
  /** Build the canonical automation detail route for an automation id. */
795
800
  export function automationPath(automationId: string): string {
796
801
  return `/automations/${encodeURIComponent(automationId)}`;
@@ -11,7 +11,7 @@ import { del, fetchDashboardJson, post } from "../http";
11
11
  import { dashboardContainerClass } from "../styles";
12
12
  import { getDashboardAgentName } from "../agentName";
13
13
  import { useDocumentTitle } from "../documentTitle";
14
- import { Button } from "../components/Button";
14
+ import { Button } from "../components/ui/button";
15
15
  import { InlineError } from "../components/InlineError";
16
16
  import { Card } from "../components/layout/Card";
17
17
 
@@ -168,7 +168,7 @@ export function ConnectionsPage() {
168
168
  connectMutation.reset();
169
169
  disconnectMutation.mutate(connection);
170
170
  }}
171
- tone="danger"
171
+ variant="danger"
172
172
  >
173
173
  {pendingProvider === connection.provider
174
174
  ? "Disconnecting…"
@@ -10,10 +10,10 @@ import { deleteDashboardResource, fetchDashboardJson, post } from "../http";
10
10
  import { dashboardContainerClass } from "../styles";
11
11
  import { getDashboardAgentName } from "../agentName";
12
12
  import { useDocumentTitle } from "../documentTitle";
13
- import { Button } from "../components/Button";
13
+ import { Button } from "../components/ui/button";
14
+ import { Input } from "../components/ui/input";
14
15
  import { InlineError } from "../components/InlineError";
15
16
  import { Card } from "../components/layout/Card";
16
- import { TextInput } from "../components/TextInput";
17
17
 
18
18
  const personalTokensQueryKey = ["dashboard", "personal-tokens"] as const;
19
19
 
@@ -129,7 +129,7 @@ export function PersonalTokensPage() {
129
129
  </div>
130
130
  ) : (
131
131
  <div className="mt-5 flex gap-2">
132
- <TextInput
132
+ <Input
133
133
  aria-label="Token name"
134
134
  className="min-w-0 flex-1"
135
135
  maxLength={80}
@@ -189,7 +189,7 @@ export function PersonalTokensPage() {
189
189
  revokeTokenMutation.mutate(token);
190
190
  }}
191
191
  size="icon"
192
- tone="danger"
192
+ variant="danger"
193
193
  type="button"
194
194
  >
195
195
  <Trash2 size={16} />
@@ -5,11 +5,11 @@ import {
5
5
  dashboardIdentitySchema,
6
6
  type DashboardIdentity,
7
7
  } from "../../api/schema";
8
- import { Button } from "../components/Button";
9
- import { Field } from "../components/Field";
8
+ import { Button } from "../components/ui/button";
9
+ import { Input } from "../components/ui/input";
10
+ import { Field, FieldDescription, FieldLabel } from "../components/ui/field";
10
11
  import { InlineError } from "../components/InlineError";
11
12
  import { Card } from "../components/layout/Card";
12
- import { TextInput } from "../components/TextInput";
13
13
  import { useDocumentTitle } from "../documentTitle";
14
14
  import { patch } from "../http";
15
15
  import { dashboardContainerClass } from "../styles";
@@ -64,13 +64,9 @@ export function SettingsPage({ identity }: SettingsPageProps) {
64
64
  <form className="mt-6" onSubmit={submit}>
65
65
  <Card className="mb-0" padding="md" variant="raised">
66
66
  <h2 className="m-0 text-lg font-bold">Profile</h2>
67
- <Field
68
- className="mt-5"
69
- help="Your display name is shown with your conversations and activity."
70
- htmlFor="display-name"
71
- label="Display name"
72
- >
73
- <TextInput
67
+ <Field className="mt-5">
68
+ <FieldLabel htmlFor="display-name">Display name</FieldLabel>
69
+ <Input
74
70
  autoComplete="name"
75
71
  id="display-name"
76
72
  maxLength={80}
@@ -82,6 +78,9 @@ export function SettingsPage({ identity }: SettingsPageProps) {
82
78
  }}
83
79
  value={displayName}
84
80
  />
81
+ <FieldDescription id="display-name-help">
82
+ Your display name is shown with your conversations and activity.
83
+ </FieldDescription>
85
84
  </Field>
86
85
  <div className="mt-5 flex items-center gap-3">
87
86
  <Button disabled={!canSave} type="submit">
@@ -13,7 +13,7 @@ import {
13
13
  type serviceAccountSchema,
14
14
  } from "@sentry/junior/api/schema";
15
15
  import { getDashboardAgentName } from "../../agentName";
16
- import { Button } from "../../components/Button";
16
+ import { Button } from "../../components/ui/button";
17
17
  import { InlineError } from "../../components/InlineError";
18
18
  import { StatusChip } from "../../components/StatusChip";
19
19
  import { Card } from "../../components/layout/Card";
@@ -192,7 +192,7 @@ export function ServiceAccountsSection(props: {
192
192
  connectMutation.reset();
193
193
  disconnectMutation.mutate(account);
194
194
  }}
195
- tone="danger"
195
+ variant="danger"
196
196
  >
197
197
  Disconnect
198
198
  </Button>
@@ -4,21 +4,27 @@ import {
4
4
  type TimeRangeDays,
5
5
  } from "../../components/controls/TimeRangeSelector";
6
6
  import type { AutomationExecutionDay } from "@sentry/junior/api/schema";
7
+ import { Bar, BarChart, CartesianGrid } from "recharts";
7
8
  import {
8
9
  ActivityChartAverageLine,
9
- ActivityChartDateLabels,
10
- ActivityChartGrid,
11
10
  activityChartAverage,
12
- ActivityChartTooltip,
13
- ActivityTooltipRows,
14
- ChartSvg,
15
- createActivityChartLayout,
11
+ activityChartMargin,
12
+ ActivityChartTooltipContent,
13
+ ActivityChartXAxis,
14
+ ActivityChartYAxis,
16
15
  } from "../../components/charts/ActivityChart";
16
+ import { ChartContainer, ChartTooltip } from "../../components/ui/chart";
17
17
 
18
18
  import { Card } from "../../components/layout/Card";
19
19
  import { formatCostSummary } from "../../format";
20
20
 
21
- const COST_COLOR = "var(--color-cyan-300)";
21
+ const chartConfig = {
22
+ costUsd: { color: "var(--color-cyan-300)", label: "Spend" },
23
+ };
24
+
25
+ function formatSpend(value: number): string {
26
+ return formatCostSummary({ total: value });
27
+ }
22
28
 
23
29
  /** Render linked conversation spend for completed automation executions. */
24
30
  export function AutomationCostChart(props: {
@@ -28,11 +34,6 @@ export function AutomationCostChart(props: {
28
34
  }) {
29
35
  const bucketUnit = props.bucketUnit ?? "day";
30
36
  const days = props.days;
31
- // Cost charts need a wider left gutter for currency tick labels.
32
- const layout = createActivityChartLayout(200, { left: 112 });
33
- const step =
34
- days.length > 0 ? layout.plotWidth / days.length : layout.plotWidth;
35
- const barWidth = Math.max(2, Math.min(13, step * 0.68));
36
37
  const totals = days.map((day) => day.costUsd);
37
38
  const total = totals.reduce((sum, value) => sum + value, 0);
38
39
  const maximum = Math.max(0.01, ...totals);
@@ -51,66 +52,44 @@ export function AutomationCostChart(props: {
51
52
  </div>
52
53
 
53
54
  <div className="relative mt-5 overflow-hidden">
54
- <ChartSvg
55
- aria-label={`Automation execution spend during the last ${props.range === 1 ? "24 hours" : `${props.range} days`}`}
56
- className="min-h-40"
57
- layout={layout}
55
+ <ChartContainer
56
+ className="aspect-auto h-50 w-full"
57
+ config={chartConfig}
58
58
  >
59
- <ActivityChartGrid
60
- format={(value) => formatCostSummary({ total: value })}
61
- layout={layout}
62
- maximum={maximum}
63
- />
64
- {days.map((day, dayIndex) => {
65
- const x = layout.left + dayIndex * step + (step - barWidth) / 2;
66
- const value = totals[dayIndex] ?? 0;
67
- const height = (value / maximum) * layout.plotHeight;
68
- return (
69
- <ActivityChartTooltip
70
- key={day.date}
71
- content={
72
- <ActivityTooltipRows
73
- rows={[["spend", formatCostSummary({ total: value })]]}
74
- />
75
- }
76
- date={day.date}
77
- summary={`${formatCostSummary({ total: value })}`}
78
- >
79
- <g tabIndex={0}>
80
- <rect
81
- fill={COST_COLOR}
82
- height={height}
83
- opacity={value > 0 ? 0.82 : 0.1}
84
- rx="1"
85
- width={barWidth}
86
- x={x}
87
- y={layout.top + layout.plotHeight - height}
88
- />
89
- <rect
90
- fill="transparent"
91
- height={layout.plotHeight}
92
- width={Math.max(barWidth, 8)}
93
- x={x - (Math.max(barWidth, 8) - barWidth) / 2}
94
- y={layout.top}
95
- />
96
- </g>
97
- </ActivityChartTooltip>
98
- );
99
- })}
100
- <ActivityChartAverageLine
101
- unit={timeRangeBucketAverageUnit(bucketUnit)}
102
- average={average}
103
- format={(value) => formatCostSummary({ total: value })}
104
- layout={layout}
105
- maximum={maximum}
106
- stroke="var(--color-slate-200)"
107
- />
108
- <ActivityChartDateLabels
109
- dates={days.map((day) => day.date)}
110
- layout={layout}
111
- xPosition={(index) => layout.left + index * step + step / 2}
112
- />
113
- </ChartSvg>
59
+ <BarChart
60
+ aria-label={`Automation execution spend during the last ${props.range === 1 ? "24 hours" : `${props.range} days`}`}
61
+ data={days}
62
+ margin={activityChartMargin}
63
+ role="img"
64
+ >
65
+ <CartesianGrid strokeDasharray="3 5" vertical={false} />
66
+ <ActivityChartYAxis format={formatSpend} maximum={maximum} />
67
+ <ActivityChartXAxis keys={days.map((day) => day.date)} />
68
+ <ChartTooltip
69
+ content={
70
+ <ActivityChartTooltipContent
71
+ rows={(day: AutomationExecutionDay) => [
72
+ ["spend", formatSpend(day.costUsd)],
73
+ ]}
74
+ />
75
+ }
76
+ />
77
+ <Bar
78
+ dataKey="costUsd"
79
+ fill="var(--color-costUsd)"
80
+ fillOpacity={0.82}
81
+ isAnimationActive={false}
82
+ maxBarSize={13}
83
+ radius={1}
84
+ />
85
+ <ActivityChartAverageLine
86
+ unit={timeRangeBucketAverageUnit(bucketUnit)}
87
+ average={average}
88
+ format={formatSpend}
89
+ stroke="var(--color-slate-200)"
90
+ />
91
+ </BarChart>
92
+ </ChartContainer>
114
93
  {!hasCost ? (
115
94
  <div className="pointer-events-none absolute inset-0 grid place-items-center pt-12 font-sans text-xs text-dashboard-text-muted">
116
95
  No linked spend in this period.
@@ -0,0 +1,87 @@
1
+ import { useState } from "react";
2
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
3
+ import type { AutomationSummary } from "@sentry/junior/api/schema";
4
+ import {
5
+ AlertDialog,
6
+ AlertDialogContent,
7
+ AlertDialogTitle,
8
+ AlertDialogDescription,
9
+ AlertDialogFooter,
10
+ AlertDialogCancel,
11
+ AlertDialogAction,
12
+ } from "../../components/ui/alert-dialog";
13
+ import { FormNotice } from "../../components/FormNotice";
14
+ import { useToastManager } from "../../components/ui/toast";
15
+ import { deleteDashboardResource } from "../../http";
16
+
17
+ /** Confirm deletion by name and keep a failed request available for retry. */
18
+ export function AutomationDeleteDialog(props: {
19
+ automation: AutomationSummary;
20
+ onClose(): void;
21
+ onDeleted(): void;
22
+ returnFocus(): HTMLElement | false;
23
+ }) {
24
+ const [open, setOpen] = useState(true);
25
+ const queryClient = useQueryClient();
26
+ const toast = useToastManager();
27
+ const deletion = useMutation({
28
+ mutationFn: () =>
29
+ deleteDashboardResource(
30
+ `/api/automations/${encodeURIComponent(props.automation.id)}`,
31
+ ),
32
+ onSuccess: async () => {
33
+ await queryClient.invalidateQueries({
34
+ queryKey: ["dashboard", "automations"],
35
+ });
36
+ props.onDeleted();
37
+ toast.add({
38
+ title: "Automation deleted",
39
+ description: props.automation.title,
40
+ type: "success",
41
+ });
42
+ setOpen(false);
43
+ },
44
+ });
45
+ return (
46
+ <AlertDialog
47
+ open={open}
48
+ onOpenChange={(next, details) => {
49
+ if (deletion.isPending) details.cancel();
50
+ else setOpen(next);
51
+ }}
52
+ onOpenChangeComplete={(next) => {
53
+ if (!next) props.onClose();
54
+ }}
55
+ >
56
+ <AlertDialogContent finalFocus={props.returnFocus}>
57
+ <AlertDialogTitle>Delete automation?</AlertDialogTitle>
58
+ <AlertDialogDescription>
59
+ Delete{" "}
60
+ <strong className="font-medium text-dashboard-text">
61
+ {props.automation.title}
62
+ </strong>
63
+ ? This removes the Automation and stops future triggers. Work already
64
+ queued or running may finish.
65
+ </AlertDialogDescription>
66
+ {deletion.error ? (
67
+ <FormNotice
68
+ tone="error"
69
+ title="The automation could not be deleted. Try again."
70
+ />
71
+ ) : null}
72
+ <AlertDialogFooter>
73
+ <AlertDialogCancel disabled={deletion.isPending}>
74
+ Cancel
75
+ </AlertDialogCancel>
76
+ <AlertDialogAction
77
+ variant="danger"
78
+ disabled={deletion.isPending}
79
+ onClick={() => deletion.mutate()}
80
+ >
81
+ {deletion.isPending ? "Deleting…" : "Delete automation"}
82
+ </AlertDialogAction>
83
+ </AlertDialogFooter>
84
+ </AlertDialogContent>
85
+ </AlertDialog>
86
+ );
87
+ }
@@ -0,0 +1,193 @@
1
+ import type { AutomationSummary } from "@sentry/junior/api/schema";
2
+ import type { ReactNode } from "react";
3
+ import { Link, useLocation } from "react-router";
4
+
5
+ import {
6
+ Accordion,
7
+ AccordionContent,
8
+ AccordionItem,
9
+ AccordionTrigger,
10
+ } from "../../components/ui/accordion";
11
+ import { Button } from "../../components/ui/button";
12
+ import { DetailsPanel } from "../../components/DetailsPanel";
13
+ import { InlineError } from "../../components/InlineError";
14
+ import { PageContentSkeleton } from "../../components/PageContentSkeleton";
15
+ import {
16
+ timeRangeLabel,
17
+ type TimeRangeDays,
18
+ } from "../../components/controls/TimeRangeSelector";
19
+ import { TranscriptText } from "../../conversations/TranscriptText";
20
+ import { peoplePath } from "../../format";
21
+ import { dashboardInlineLinkClass } from "../../styles";
22
+ import { TriggerFilters } from "./TriggerFilters";
23
+ import { AutomationLifecycle } from "./AutomationLifecycle";
24
+ import { AutomationRecentRuns } from "./AutomationRecentRuns";
25
+ import { AutomationStatus } from "./AutomationStatus";
26
+ import {
27
+ automationScheduleLabel,
28
+ formatAutomationRunTime,
29
+ } from "./automationFormat";
30
+ import { automationOutcomeLabel } from "./automationOutcomes";
31
+
32
+ /** Keep Automation context scrollable above persistent, permission-aware actions. */
33
+ export function AutomationDetails(props: {
34
+ onClose(): void;
35
+ onDelete(source: HTMLButtonElement): void;
36
+ onRetry(): void;
37
+ error: boolean;
38
+ range: TimeRangeDays;
39
+ automation: AutomationSummary | undefined;
40
+ }) {
41
+ const location = useLocation();
42
+ const { automation } = props;
43
+ return (
44
+ <DetailsPanel
45
+ label="Automation details"
46
+ sheetTitle={automation?.title}
47
+ onClose={props.onClose}
48
+ title={
49
+ <h2 className="m-0 break-words font-display text-base font-medium text-dashboard-text">
50
+ {automation?.title ?? "Automation details"}
51
+ </h2>
52
+ }
53
+ >
54
+ <div className="grid h-full min-h-0 grid-rows-[minmax(0,1fr)_auto]">
55
+ <div
56
+ aria-label="Automation content"
57
+ role="region"
58
+ tabIndex={0}
59
+ className="min-h-0 overflow-y-auto overscroll-contain p-4 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus [overflow-wrap:anywhere]"
60
+ >
61
+ {automation ? (
62
+ <AutomationDetailsContent
63
+ automation={automation}
64
+ range={props.range}
65
+ />
66
+ ) : null}
67
+ {props.error ? (
68
+ <InlineError>
69
+ Automation details could not be loaded. It may be unavailable or
70
+ you may not have access.{" "}
71
+ <Button onClick={props.onRetry}>Try again</Button>
72
+ </InlineError>
73
+ ) : !automation ? (
74
+ <PageContentSkeleton
75
+ label="Loading automation details"
76
+ variant="panel"
77
+ />
78
+ ) : null}
79
+ </div>
80
+ {automation ? (
81
+ <AutomationLifecycle
82
+ automation={automation}
83
+ onDelete={props.onDelete}
84
+ editPath={`/automations/${encodeURIComponent(automation.id)}/edit${location.search}`}
85
+ />
86
+ ) : null}
87
+ </div>
88
+ </DetailsPanel>
89
+ );
90
+ }
91
+
92
+ function AutomationDetailsContent({
93
+ automation,
94
+ range,
95
+ }: {
96
+ automation: AutomationSummary;
97
+ range: TimeRangeDays;
98
+ }) {
99
+ const location = useLocation();
100
+ return (
101
+ <div className="grid min-w-0 gap-5">
102
+ <AutomationStatus automation={automation} />
103
+ <section aria-label="Instructions" className="text-sm leading-relaxed">
104
+ <TranscriptText
105
+ mentions={automation.mentions ?? []}
106
+ text={automation.instruction}
107
+ />
108
+ </section>
109
+ <dl className="m-0 grid gap-3 text-xs leading-relaxed">
110
+ <AutomationFact label="Trigger">
111
+ {automation.kind === "scheduled" ? (
112
+ <p className="m-0">{automationScheduleLabel(automation)}</p>
113
+ ) : (
114
+ <>
115
+ <p className="m-0 break-words">{automation.resource}</p>
116
+ <p className="m-0 mt-1 text-dashboard-text-muted">
117
+ {automation.trigger.events.join(", ")}
118
+ </p>
119
+ <TriggerFilters filters={automation.trigger.filters} />
120
+ </>
121
+ )}
122
+ </AutomationFact>
123
+ <AutomationFact label="On success">
124
+ {automation.outcomes.length === 0 ? (
125
+ <span className="text-dashboard-text-muted">No message</span>
126
+ ) : (
127
+ <ol className="m-0 grid list-none gap-2 p-0">
128
+ {automation.outcomes.map((outcome, index) => (
129
+ <li key={index}>
130
+ Message{" "}
131
+ {automationOutcomeLabel(outcome, automation.destination)}
132
+ </li>
133
+ ))}
134
+ </ol>
135
+ )}
136
+ </AutomationFact>
137
+ </dl>
138
+ <AutomationRecentRuns automation={automation} />
139
+ <Accordion className="border-t border-dashboard-border pt-1">
140
+ <AccordionItem value="about">
141
+ <AccordionTrigger className="min-h-11 text-xs text-dashboard-text-muted">
142
+ About this Automation
143
+ </AccordionTrigger>
144
+ <AccordionContent>
145
+ <dl className="m-0 grid gap-3 text-xs leading-relaxed">
146
+ <AutomationFact label="Created by">
147
+ {automation.createdByEmail ? (
148
+ <Link
149
+ className={dashboardInlineLinkClass}
150
+ to={peoplePath(automation.createdByEmail)}
151
+ >
152
+ {automation.ownedByViewer ? "you" : automation.createdBy}
153
+ </Link>
154
+ ) : automation.ownedByViewer ? (
155
+ "you"
156
+ ) : (
157
+ automation.createdBy
158
+ )}
159
+ </AutomationFact>
160
+ <AutomationFact label="Created">
161
+ {formatAutomationRunTime(automation.createdAt)}
162
+ </AutomationFact>
163
+ <AutomationFact label="Accounts">
164
+ {automation.credentialMode === "creator"
165
+ ? `Uses accounts connected by ${automation.ownedByViewer ? "you" : automation.createdBy}.`
166
+ : "Junior’s accounts only"}
167
+ </AutomationFact>
168
+ <AutomationFact label="Runs">
169
+ {automation.runs[range]} runs / {timeRangeLabel(range)} ·{" "}
170
+ {automation.totalRuns} total
171
+ </AutomationFact>
172
+ </dl>
173
+ <Link
174
+ className={`${dashboardInlineLinkClass} mt-3 inline-block py-2 text-xs`}
175
+ to={`/automations/${encodeURIComponent(automation.id)}/versions${location.search}`}
176
+ >
177
+ Version history
178
+ </Link>
179
+ </AccordionContent>
180
+ </AccordionItem>
181
+ </Accordion>
182
+ </div>
183
+ );
184
+ }
185
+
186
+ function AutomationFact(props: { label: string; children: ReactNode }) {
187
+ return (
188
+ <div className="grid min-w-0 grid-cols-[5.25rem_minmax(0,1fr)] gap-3">
189
+ <dt className="text-dashboard-text-muted">{props.label}</dt>
190
+ <dd className="m-0 min-w-0">{props.children}</dd>
191
+ </div>
192
+ );
193
+ }