@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
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
+ import { matchPath, useLocation } from "react-router";
2
3
 
3
4
  import {
4
5
  PageContentSkeleton,
@@ -8,29 +9,47 @@ import { PageHeader } from "../../components/layout/PageHeader";
8
9
  import { PageLayout } from "../../components/layout/PageLayout";
9
10
  import { SecondaryNavigation } from "../../components/layout/SecondaryNavigation";
10
11
 
11
- const RESERVED_TASK_SEGMENTS = new Set(["list", "runs"]);
12
+ /** Identify the Automation list and its selected Automation routes. */
13
+ function isAutomationListPath(pathname: string): boolean {
14
+ const match = matchPath("/automations/:automationId", pathname);
15
+ return Boolean(match && match.params.automationId?.toLowerCase() !== "runs");
16
+ }
17
+
18
+ /** Identify Automation routes whose list scrolls on its own beside details. */
19
+ export function hasAutomationDetailsFrame(pathname: string): boolean {
20
+ return (
21
+ isAutomationListPath(pathname) ||
22
+ Boolean(matchPath("/automations/runs", pathname)) ||
23
+ Boolean(matchPath("/automations/:automationId/executions", pathname))
24
+ );
25
+ }
12
26
 
13
- function isTasksListPath(pathname: string): boolean {
14
- if (
15
- pathname === "/automations/list" ||
16
- /^\/automations\/(scheduled|event)\/[^/]+\/edit$/.test(pathname)
17
- )
18
- return true;
19
- const match = pathname.match(/^\/automations\/([^/]+)$/);
20
- return Boolean(match?.[1] && !RESERVED_TASK_SEGMENTS.has(match[1]));
27
+ function isAutomationListTabPath(pathname: string): boolean {
28
+ return (
29
+ isAutomationListPath(pathname) ||
30
+ Boolean(matchPath("/automations/:automationId/edit", pathname))
31
+ );
21
32
  }
22
33
 
23
34
  const taskNavigationItems = [
24
35
  { end: true, label: "Overview", to: "/automations" },
25
- { isActive: isTasksListPath, label: "Automations", to: "/automations/list" },
36
+ {
37
+ isActive: isAutomationListTabPath,
38
+ label: "Automations",
39
+ to: "/automations/list",
40
+ },
26
41
  { label: "Runs", to: "/automations/runs" },
27
42
  ];
28
43
 
29
44
  /** Keep Automation navigation consistent across lists, details, and editing. */
30
- export function AutomationsNavigation(props: { children: ReactNode }) {
45
+ export function AutomationsNavigation(props: {
46
+ children: ReactNode;
47
+ className?: string;
48
+ }) {
31
49
  return (
32
50
  <SecondaryNavigation
33
51
  ariaLabel="Automations navigation"
52
+ className={props.className}
34
53
  items={taskNavigationItems}
35
54
  >
36
55
  {props.children}
@@ -40,9 +59,20 @@ export function AutomationsNavigation(props: { children: ReactNode }) {
40
59
 
41
60
  /** Place the shared secondary navigation above one Automations page. */
42
61
  export function AutomationsPageLayout(props: { children: ReactNode }) {
62
+ const list = hasAutomationDetailsFrame(useLocation().pathname);
43
63
  return (
44
- <AutomationsNavigation>
45
- <PageLayout>{props.children}</PageLayout>
64
+ <AutomationsNavigation
65
+ className={
66
+ list
67
+ ? "grid h-full min-h-0 grid-rows-[auto_minmax(0,1fr)] md:grid-rows-[minmax(0,1fr)]"
68
+ : undefined
69
+ }
70
+ >
71
+ {list ? (
72
+ <div className="min-h-0">{props.children}</div>
73
+ ) : (
74
+ <PageLayout>{props.children}</PageLayout>
75
+ )}
46
76
  </AutomationsNavigation>
47
77
  );
48
78
  }
@@ -54,10 +84,12 @@ export function AutomationsRouteLoading(props: {
54
84
  title: string;
55
85
  variant: Extract<PageContentSkeletonVariant, "list" | "stats">;
56
86
  }) {
57
- return (
87
+ const list = hasAutomationDetailsFrame(useLocation().pathname);
88
+ const content = (
58
89
  <>
59
90
  <PageHeader description={props.description} title={props.title} />
60
91
  <PageContentSkeleton label={props.label} variant={props.variant} />
61
92
  </>
62
93
  );
94
+ return list ? <PageLayout>{content}</PageLayout> : content;
63
95
  }
@@ -28,3 +28,60 @@ shows its error when a public reader cannot make a version active.
28
28
 
29
29
  Browser behavior lives in `e2e/automation-editor.spec.ts`. API and persistence
30
30
  coverage lives in core's `tests/integration/api/automation-edits.test.ts`.
31
+
32
+ ## Automation details
33
+
34
+ The URL selects one Automation in the list. Route matching follows React
35
+ Router, including trailing slashes. `AutomationDetails` uses the shared
36
+ `DetailsPanel`: a 400px panel at 1280px and above, and a Base UI bottom sheet on
37
+ smaller screens. The list stays interactive beside the panel. Each pane scrolls
38
+ on its own. `VisualViewportShell` owns the viewport height and mobile body lock;
39
+ `AutomationsPageLayout` keeps navigation above the list and details.
40
+
41
+ Loading and access errors stay inside the details frame. Closing keeps the list
42
+ query string and returns focus to the selected row. If that row is not on the
43
+ current page, focus returns to the list. Selecting another Automation resets
44
+ its disclosures and lifecycle feedback. Deleting the selected Automation closes
45
+ its details. Edit and version history retain the list query string.
46
+
47
+ The existing Automation browser journeys cover selection, loading and retry,
48
+ focus, mobile dismissal, deletion, and lifecycle actions. Use mock reporting to
49
+ review scheduled and event Automations in both themes at desktop and mobile
50
+ widths. Keep layout and scrollbar checks in manual QA.
51
+
52
+ Details lead with lifecycle, the next trigger, and the last execution result.
53
+ Instructions follow, then compact trigger and delivery rows. The sheet header
54
+ uses the Automation name. `AutomationStatus`
55
+ keeps a failed execution distinct from whether future runs remain enabled. It
56
+ shows block reasons and links to the last Conversation when attention is needed.
57
+ Scheduled times use the shared shadcn Accordion to show the schedule timezone
58
+ and browser time. About this Automation uses the same component. Both start
59
+ collapsed and reset when the selection changes.
60
+ Paused, blocked, completed, and unavailable Automations explain why there is no
61
+ next run instead of showing "None".
62
+
63
+ `AutomationRecentRuns` uses the existing execution-history query and cache. It
64
+ shows up to three newest terminal runs, links to their Conversations when one
65
+ was recorded, and displays recorded runtime and estimated cost. It omits run
66
+ titles that repeat the Automation name. Its loading,
67
+ empty, and retry states do not replace Automation settings. It does not infer
68
+ queued or running work from terminal records.
69
+
70
+ `AutomationLifecycle` owns revision-checked actions in a fixed footer. Edit and
71
+ Delete use labeled icons there; list rows have no action menu. Public readers
72
+ can edit settings. Delete and lifecycle actions stay creator-only. The shared
73
+ shadcn Popover provides pause help beside those controls. The
74
+ scroll region above it owns the summary and settings. Creator, creation date,
75
+ connected accounts, run counts, and version history stay under About this
76
+ Automation. Selection changes remount the content and actions together.
77
+
78
+ Filters use a shadcn Popover at 768px and above and the shared BottomSheet below
79
+ that width. Both edit the same URL filters immediately. Active filter chips
80
+ remove one filter without clearing search, scope, or other filters. The mobile
81
+ sheet fits its controls instead of taking the full details height.
82
+
83
+ `AutomationDeleteDialog` names the Automation before deletion. It keeps errors
84
+ and retry inside the dialog and blocks dismissal while the request is pending.
85
+ Cancel returns focus to Delete. Successful deletion closes the selected details
86
+ and returns focus to the list. Success toasts confirm Save, Pause, Resume, and
87
+ Delete. Their state belongs to the mounted dashboard's Toaster provider.
@@ -0,0 +1,22 @@
1
+ /** List an Event trigger's filters. Every filter must match. */
2
+ export function TriggerFilters(props: { filters: readonly string[] }) {
3
+ if (props.filters.length === 0) {
4
+ return (
5
+ <p className="m-0 mt-1 text-xs text-dashboard-text-muted">
6
+ No filters. Runs on every selected event.
7
+ </p>
8
+ );
9
+ }
10
+ return (
11
+ <div className="mt-1 text-xs text-dashboard-text-muted">
12
+ <p className="m-0">Every filter must match:</p>
13
+ <ul className="m-0 list-disc pl-5">
14
+ {props.filters.map((filter) => (
15
+ <li className="break-words" key={filter}>
16
+ {filter}
17
+ </li>
18
+ ))}
19
+ </ul>
20
+ </div>
21
+ );
22
+ }
@@ -1,8 +1,11 @@
1
- import type { AutomationSummary } from "@sentry/junior/api/schema";
1
+ import type {
2
+ AutomationExecution,
3
+ AutomationSummary,
4
+ } from "@sentry/junior/api/schema";
5
+ import type { StatusChipTone } from "../../components/StatusChip";
2
6
  import { formatTime } from "../../format";
3
7
 
4
8
  type ScheduledAutomation = Extract<AutomationSummary, { kind: "scheduled" }>;
5
- type EventAutomation = Extract<AutomationSummary, { kind: "event" }>;
6
9
 
7
10
  /** Show the schedule with its timezone unless the schedule text already names it. */
8
11
  export function automationScheduleLabel(
@@ -13,31 +16,42 @@ export function automationScheduleLabel(
13
16
  : `${automation.schedule} (${automation.timezone})`;
14
17
  }
15
18
 
16
- /** Name event triggers in readable words. */
17
- export function automationEventNames(automation: EventAutomation): string {
18
- return automation.events
19
- .map((event) => event.replaceAll(/[._]/g, " "))
20
- .join(", ");
21
- }
22
-
23
- /** Summarize event match conditions as one readable line. */
24
- export function automationEventConditions(automation: EventAutomation): string {
25
- return Object.entries(automation.match ?? {})
26
- .map(
27
- ([field, value]) =>
28
- `${field}: ${Array.isArray(value) ? value.join(" or ") : String(value)}`,
29
- )
30
- .join(" · ");
31
- }
32
-
33
- /** Format a run time with date, time, and zone for details and tooltips. */
34
- export function formatAutomationRunTime(value: string): string {
35
- return formatTime(value, {
19
+ /** Format a run in an explicit timezone, or the dashboard timezone by default. */
20
+ export function formatAutomationRunTime(
21
+ value: string,
22
+ timeZone?: string,
23
+ ): string {
24
+ const options: Intl.DateTimeFormatOptions = {
36
25
  day: "numeric",
37
26
  hour: "numeric",
38
27
  minute: "2-digit",
39
28
  month: "short",
40
29
  timeZoneName: "short",
41
30
  year: "numeric",
42
- });
31
+ };
32
+ return timeZone
33
+ ? new Intl.DateTimeFormat(undefined, { ...options, timeZone }).format(
34
+ new Date(value),
35
+ )
36
+ : formatTime(value, options);
37
+ }
38
+
39
+ /** Map a run outcome to its status color. */
40
+ export function automationRunStatusTone(
41
+ status: AutomationExecution["status"],
42
+ ): StatusChipTone {
43
+ if (status === "failed") return "danger";
44
+ if (status === "blocked") return "warning";
45
+ return "success";
46
+ }
47
+
48
+ /** Name a run by its Conversation title, else by its Automation. */
49
+ export function automationRunTitle(
50
+ run: AutomationExecution,
51
+ automationTitle: string,
52
+ ): string {
53
+ return (
54
+ run.title?.trim() ||
55
+ (run.conversationId ? automationTitle : "No conversation")
56
+ );
43
57
  }
@@ -4,24 +4,32 @@ import {
4
4
  type TimeRangeDays,
5
5
  } from "../../components/controls/TimeRangeSelector";
6
6
  import type { CodeActivityDay } 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";
17
16
  import { ChartLegend } from "../../components/charts/ChartLegend";
18
17
 
19
18
  import { Card } from "../../components/layout/Card";
19
+ import { ChartContainer, ChartTooltip } from "../../components/ui/chart";
20
20
  import { formatActivityChartAverage } from "../../format";
21
21
 
22
22
  const CREATED_COLOR = "var(--color-cyan-300)";
23
23
  const MERGED_COLOR = "var(--color-emerald-300)";
24
24
  const CLOSED_COLOR = "var(--color-rose-400)";
25
+ const series = [
26
+ { color: CREATED_COLOR, key: "created", label: "Created" },
27
+ { color: MERGED_COLOR, key: "merged", label: "Merged" },
28
+ { color: CLOSED_COLOR, key: "closed", label: "Closed" },
29
+ ] as const;
30
+ const chartConfig = Object.fromEntries(
31
+ series.map((item) => [item.key, { color: item.color, label: item.label }]),
32
+ );
25
33
 
26
34
  /** Render created, merged, and closed code changes over a trailing window. */
27
35
  export function CodeActivityChart(props: {
@@ -33,12 +41,6 @@ export function CodeActivityChart(props: {
33
41
  const bucketUnit = props.bucketUnit ?? "day";
34
42
 
35
43
  const days = props.days;
36
- const layout = createActivityChartLayout(220, { left: 40 });
37
- const step =
38
- days.length > 0 ? layout.plotWidth / days.length : layout.plotWidth;
39
- const barWidth = Math.max(2, Math.min(10, step * 0.28));
40
- const gap = Math.max(1, barWidth * 0.2);
41
- const groupWidth = barWidth * 3 + gap * 2;
42
44
  const totals = days.map((day) => day.created + day.merged + day.closed);
43
45
  const maximum = Math.max(
44
46
  1,
@@ -58,99 +60,55 @@ export function CodeActivityChart(props: {
58
60
  Code changes created, merged, and closed each day.
59
61
  </p>
60
62
  </div>
61
- <ChartLegend
62
- ariaLabel="Code change legend"
63
- items={[
64
- { color: CREATED_COLOR, key: "created", label: "Created" },
65
- { color: MERGED_COLOR, key: "merged", label: "Merged" },
66
- { color: CLOSED_COLOR, key: "closed", label: "Closed" },
67
- ]}
68
- />
63
+ <ChartLegend ariaLabel="Code change legend" items={series} />
69
64
  </div>
70
65
 
71
66
  <div className="relative mt-5 overflow-hidden">
72
- <ChartSvg
73
- aria-label={`Code changes during the last ${props.range === 1 ? "24 hours" : `${props.range} days`}`}
74
- className="min-h-40"
75
- layout={layout}
67
+ <ChartContainer
68
+ className="aspect-auto h-55 w-full"
69
+ config={chartConfig}
76
70
  >
77
- <ActivityChartGrid layout={layout} maximum={maximum} />
78
- {days.map((day, dayIndex) => {
79
- const groupX =
80
- layout.left + dayIndex * step + (step - groupWidth) / 2;
81
- const series = [
82
- {
83
- color: CREATED_COLOR,
84
- key: "created",
85
- label: "created",
86
- value: day.created,
87
- },
88
- {
89
- color: MERGED_COLOR,
90
- key: "merged",
91
- label: "merged",
92
- value: day.merged,
93
- },
94
- {
95
- color: CLOSED_COLOR,
96
- key: "closed",
97
- label: "closed",
98
- value: day.closed,
99
- },
100
- ] as const;
101
- return (
102
- <ActivityChartTooltip
103
- key={day.date}
104
- content={
105
- <ActivityTooltipRows
106
- rows={series.map((entry) => [entry.label, entry.value])}
107
- />
108
- }
109
- date={day.date}
110
- summary={`${day.created} created, ${day.merged} merged, ${day.closed} closed`}
111
- >
112
- <g tabIndex={0}>
113
- {series.map((entry, seriesIndex) => {
114
- const height = (entry.value / maximum) * layout.plotHeight;
115
- const x = groupX + seriesIndex * (barWidth + gap);
116
- return (
117
- <rect
118
- fill={entry.color}
119
- height={height}
120
- key={entry.key}
121
- opacity={0.86}
122
- rx="1"
123
- width={barWidth}
124
- x={x}
125
- y={layout.top + layout.plotHeight - height}
126
- />
127
- );
128
- })}
129
- <rect
130
- fill="transparent"
131
- height={layout.plotHeight}
132
- width={Math.max(groupWidth, 8)}
133
- x={groupX - (Math.max(groupWidth, 8) - groupWidth) / 2}
134
- y={layout.top}
135
- />
136
- </g>
137
- </ActivityChartTooltip>
138
- );
139
- })}
140
- <ActivityChartAverageLine
141
- unit={timeRangeBucketAverageUnit(bucketUnit)}
142
- average={average}
143
- format={formatActivityChartAverage}
144
- layout={layout}
145
- maximum={maximum}
146
- stroke="var(--color-slate-200)"
147
- />
148
- <ActivityChartDateLabels
149
- dates={days.map((day) => day.date)}
150
- layout={layout}
151
- xPosition={(index) => layout.left + index * step + step / 2}
152
- />
153
- </ChartSvg>
71
+ <BarChart
72
+ aria-label={`Code changes during the last ${props.range === 1 ? "24 hours" : `${props.range} days`}`}
73
+ barGap={1}
74
+ data={days}
75
+ margin={activityChartMargin}
76
+ role="img"
77
+ >
78
+ <CartesianGrid strokeDasharray="3 5" vertical={false} />
79
+ <ActivityChartYAxis maximum={maximum} />
80
+ <ActivityChartXAxis keys={days.map((day) => day.date)} />
81
+ <ChartTooltip
82
+ content={
83
+ <ActivityChartTooltipContent
84
+ rows={(day: CodeActivityDay) =>
85
+ series.map((item) => [
86
+ item.label.toLowerCase(),
87
+ day[item.key],
88
+ ])
89
+ }
90
+ />
91
+ }
92
+ />
93
+ {series.map((item) => (
94
+ <Bar
95
+ dataKey={item.key}
96
+ fill={`var(--color-${item.key})`}
97
+ fillOpacity={0.86}
98
+ isAnimationActive={false}
99
+ key={item.key}
100
+ maxBarSize={10}
101
+ radius={1}
102
+ />
103
+ ))}
104
+ <ActivityChartAverageLine
105
+ unit={timeRangeBucketAverageUnit(bucketUnit)}
106
+ average={average}
107
+ format={formatActivityChartAverage}
108
+ stroke="var(--color-slate-200)"
109
+ />
110
+ </BarChart>
111
+ </ChartContainer>
154
112
  {!hasActivity ? (
155
113
  <div className="pointer-events-none absolute inset-0 grid place-items-center pt-12 font-sans text-xs text-dashboard-text-muted">
156
114
  No code changes in this period.
@@ -12,6 +12,14 @@ import {
12
12
  import { Card } from "../../components/layout/Card";
13
13
  import { PageHeader } from "../../components/layout/PageHeader";
14
14
  import { PageLayout } from "../../components/layout/PageLayout";
15
+ import {
16
+ Table,
17
+ TableBody,
18
+ TableCell,
19
+ TableHead,
20
+ TableHeader,
21
+ TableRow,
22
+ } from "../../components/ui/table";
15
23
  import { CodeActivityChart } from "./CodeActivityChart";
16
24
  import { CodeChangeRow } from "./CodeChangeRow";
17
25
  import {
@@ -88,61 +96,50 @@ function CodeOverview(props: {
88
96
  </EmptyTelemetry>
89
97
  </div>
90
98
  ) : (
91
- <div className="overflow-x-auto">
92
- <table className="w-full min-w-[36rem] table-fixed border-collapse text-left">
93
- <colgroup>
94
- <col />
95
- <col className="w-28" />
96
- <col className="w-28" />
97
- <col className="w-28" />
98
- </colgroup>
99
- <thead className="font-sans font-medium text-xs text-dashboard-text-muted">
100
- <tr className="border-b border-dashboard-border-subtle">
101
- <th className="px-4 py-2.5 font-medium">Repository</th>
102
- <th className="px-4 py-2.5 text-right font-medium whitespace-nowrap">
103
- Created
104
- </th>
105
- <th className="px-4 py-2.5 text-right font-medium whitespace-nowrap">
106
- Merge rate
107
- </th>
108
- <th className="px-4 py-2.5 text-right font-medium whitespace-nowrap">
109
- Median cost
110
- </th>
111
- </tr>
112
- </thead>
113
- <tbody>
114
- {data.repositories.map((repository) => (
115
- <tr
116
- className="border-b border-dashboard-border-subtle last:border-b-0"
117
- key={repository.id}
118
- >
119
- <td className="min-w-0 px-4 py-3">
120
- <div className="truncate font-display text-sm text-dashboard-text">
121
- <Link
122
- className="text-inherit no-underline hover:text-dashboard-accent-text"
123
- to={codeRepositoryPath(repository.id)}
124
- >
125
- {repository.name}
126
- </Link>
127
- </div>
128
- <div className="mt-1 truncate font-mono text-xs text-dashboard-text-muted">
129
- {repository.provider}
130
- </div>
131
- </td>
132
- <td className="px-4 py-3 text-right font-sans text-sm tabular-nums whitespace-nowrap text-dashboard-text">
133
- {repository.created}
134
- </td>
135
- <td className="px-4 py-3 text-right font-sans text-sm tabular-nums whitespace-nowrap text-dashboard-text">
136
- {formatMergeRate(repository.mergeRate)}
137
- </td>
138
- <td className="px-4 py-3 text-right font-sans text-sm tabular-nums whitespace-nowrap text-dashboard-text">
139
- {formatCodeCost(repository.medianCostUsd)}
140
- </td>
141
- </tr>
142
- ))}
143
- </tbody>
144
- </table>
145
- </div>
99
+ <Table className="min-w-[36rem] table-fixed">
100
+ <colgroup>
101
+ <col />
102
+ <col className="w-28" />
103
+ <col className="w-28" />
104
+ <col className="w-28" />
105
+ </colgroup>
106
+ <TableHeader>
107
+ <TableRow>
108
+ <TableHead className="px-4">Repository</TableHead>
109
+ <TableHead className="px-4 text-right">Created</TableHead>
110
+ <TableHead className="px-4 text-right">Merge rate</TableHead>
111
+ <TableHead className="px-4 text-right">Median cost</TableHead>
112
+ </TableRow>
113
+ </TableHeader>
114
+ <TableBody>
115
+ {data.repositories.map((repository) => (
116
+ <TableRow key={repository.id}>
117
+ <TableCell className="min-w-0 px-4 py-3">
118
+ <div className="truncate font-display text-sm text-dashboard-text">
119
+ <Link
120
+ className="text-inherit no-underline hover:text-dashboard-accent-text"
121
+ to={codeRepositoryPath(repository.id)}
122
+ >
123
+ {repository.name}
124
+ </Link>
125
+ </div>
126
+ <div className="mt-1 truncate font-mono text-xs text-dashboard-text-muted">
127
+ {repository.provider}
128
+ </div>
129
+ </TableCell>
130
+ <TableCell className="px-4 py-3 text-right font-sans tabular-nums">
131
+ {repository.created}
132
+ </TableCell>
133
+ <TableCell className="px-4 py-3 text-right font-sans tabular-nums">
134
+ {formatMergeRate(repository.mergeRate)}
135
+ </TableCell>
136
+ <TableCell className="px-4 py-3 text-right font-sans tabular-nums">
137
+ {formatCodeCost(repository.medianCostUsd)}
138
+ </TableCell>
139
+ </TableRow>
140
+ ))}
141
+ </TableBody>
142
+ </Table>
146
143
  )}
147
144
  </Card>
148
145
  {data.changes.length > 0 ? (
@@ -7,7 +7,7 @@ import {
7
7
  useCodeRepositoryConversationsData,
8
8
  useCodeRepositoryData,
9
9
  } from "../../api";
10
- import { ButtonLink } from "../../components/Button";
10
+ import { buttonVariants } from "../../components/ui/button";
11
11
  import { EmptyTelemetry } from "../../components/EmptyTelemetry";
12
12
  import { PageRouteLoading } from "../../components/PageRouteLoading";
13
13
  import {
@@ -18,7 +18,7 @@ import {
18
18
  import { PageHeader } from "../../components/layout/PageHeader";
19
19
  import { PageLayout } from "../../components/layout/PageLayout";
20
20
  import { SecondaryNavigation } from "../../components/layout/SecondaryNavigation";
21
- import { buildConversations } from "../../format";
21
+ import { buildConversations, workspacePath } from "../../format";
22
22
  import { CodeActivityChart } from "./CodeActivityChart";
23
23
  import { CodeChangeRow } from "./CodeChangeRow";
24
24
  import { CodeSummaryCards } from "./CodeSummaryCards";
@@ -98,7 +98,8 @@ export function RepositoryPage(props: { tab: CodeRepositoryTab }) {
98
98
  <div className="flex flex-wrap items-center gap-2">
99
99
  <TimeRangeSelector onChange={setRange} value={range} />
100
100
  {repository.url ? (
101
- <ButtonLink
101
+ <Link
102
+ className={buttonVariants()}
102
103
  rel="noopener noreferrer"
103
104
  target="_blank"
104
105
  to={repository.url}
@@ -108,7 +109,7 @@ export function RepositoryPage(props: { tab: CodeRepositoryTab }) {
108
109
  aria-hidden="true"
109
110
  className="size-3.5 shrink-0"
110
111
  />
111
- </ButtonLink>
112
+ </Link>
112
113
  ) : null}
113
114
  </div>
114
115
  }
@@ -156,10 +157,7 @@ function RepositoryDescription(props: { data: CodeRepositoryReport }) {
156
157
  {workspaces.map((workspace, index) => (
157
158
  <span key={workspace.id}>
158
159
  {index > 0 ? ", " : null}
159
- <Link
160
- className={inlineLinkClass}
161
- to={`/system/workspaces/${encodeURIComponent(workspace.id)}`}
162
- >
160
+ <Link className={inlineLinkClass} to={workspacePath(workspace.id)}>
163
161
  {workspace.name}
164
162
  </Link>
165
163
  </span>