@sentry/junior-dashboard 0.263.0 → 0.265.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +2 -5
  3. package/dist/app.js +119 -35
  4. package/dist/client.js +148 -135
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.js +119 -35
  7. package/dist/tailwind.css +1 -1
  8. package/package.json +9 -4
  9. package/src/api/schema.ts +0 -1
  10. package/src/app.ts +5 -20
  11. package/src/client/App.tsx +29 -31
  12. package/src/client/api.ts +7 -0
  13. package/src/client/components/BottomSheet.tsx +26 -18
  14. package/src/client/components/CopyButton.tsx +83 -0
  15. package/src/client/components/DetailsPanel.tsx +133 -0
  16. package/src/client/components/Drawer.tsx +34 -33
  17. package/src/client/components/FilterBar.tsx +16 -31
  18. package/src/client/components/Metric.tsx +33 -23
  19. package/src/client/components/PageContentSkeleton.tsx +1 -1
  20. package/src/client/components/Pagination.tsx +1 -1
  21. package/src/client/components/ParticipantAvatarStack.tsx +40 -36
  22. package/src/client/components/ProfileMenu.tsx +27 -93
  23. package/src/client/components/README.md +101 -64
  24. package/src/client/components/SegmentedNav.tsx +1 -1
  25. package/src/client/components/ShimmerText.tsx +2 -6
  26. package/src/client/components/ThemeSwitcher.tsx +19 -15
  27. package/src/client/components/charts/ActivityChart.tsx +173 -288
  28. package/src/client/components/charts/CacheWrites.tsx +63 -50
  29. package/src/client/components/charts/InputCacheChart.tsx +137 -131
  30. package/src/client/components/charts/README.md +4 -4
  31. package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
  32. package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
  33. package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
  34. package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
  35. package/src/client/components/layout/DashboardHeader.tsx +44 -90
  36. package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
  37. package/src/client/components/layout/VisualViewportShell.tsx +2 -2
  38. package/src/client/components/ui/accordion.tsx +77 -0
  39. package/src/client/components/ui/alert-dialog.tsx +89 -0
  40. package/src/client/components/ui/button.tsx +85 -0
  41. package/src/client/components/ui/chart.tsx +74 -0
  42. package/src/client/components/ui/checkbox.tsx +26 -0
  43. package/src/client/components/ui/collapsible.tsx +22 -0
  44. package/src/client/components/ui/field.tsx +49 -0
  45. package/src/client/components/ui/input.tsx +35 -0
  46. package/src/client/components/ui/native-select.tsx +57 -0
  47. package/src/client/components/ui/popover.tsx +67 -0
  48. package/src/client/components/ui/radio-group.tsx +32 -0
  49. package/src/client/components/ui/sheet.tsx +151 -0
  50. package/src/client/components/ui/skeleton.tsx +19 -0
  51. package/src/client/components/ui/table.tsx +80 -0
  52. package/src/client/components/ui/tabs.tsx +75 -0
  53. package/src/client/components/ui/textarea.tsx +17 -0
  54. package/src/client/components/ui/toast.tsx +215 -0
  55. package/src/client/components/ui/toggle-group.tsx +36 -0
  56. package/src/client/components/ui/toggle.tsx +37 -0
  57. package/src/client/components/ui/tooltip.tsx +168 -0
  58. package/src/client/components/ui/trace.tsx +96 -0
  59. package/src/client/components/ui/useSheet.ts +74 -0
  60. package/src/client/conversations/ChatLayout.tsx +7 -3
  61. package/src/client/conversations/ConversationBrief.tsx +17 -6
  62. package/src/client/conversations/ConversationChildAgents.tsx +87 -58
  63. package/src/client/conversations/ConversationComposer.tsx +26 -12
  64. package/src/client/conversations/ConversationDetails.tsx +198 -95
  65. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  66. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  67. package/src/client/conversations/ConversationHeader.tsx +8 -3
  68. package/src/client/conversations/ConversationHeaderActions.tsx +90 -82
  69. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  70. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  71. package/src/client/conversations/ConversationMeta.tsx +111 -92
  72. package/src/client/conversations/ConversationPage.tsx +241 -110
  73. package/src/client/conversations/ConversationSidebar.tsx +48 -29
  74. package/src/client/conversations/ConversationUsageBreakdown.tsx +16 -3
  75. package/src/client/conversations/ConversationWatches.tsx +165 -0
  76. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  77. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  78. package/src/client/conversations/DetailsList.tsx +124 -0
  79. package/src/client/conversations/EventDetails.tsx +42 -8
  80. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  81. package/src/client/conversations/ObjectCard.tsx +20 -8
  82. package/src/client/conversations/PendingMailboxStack.tsx +52 -33
  83. package/src/client/conversations/README.md +93 -19
  84. package/src/client/conversations/ShareConversationButton.tsx +28 -3
  85. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  86. package/src/client/conversations/ToolFrame.tsx +36 -52
  87. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  88. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  89. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  90. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  91. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  92. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  93. package/src/client/conversations/TranscriptMessageContextView.tsx +8 -2
  94. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  95. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  96. package/src/client/conversations/TranscriptSubagentView.tsx +3 -3
  97. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  98. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  99. package/src/client/conversations/TranscriptTurnContextView.tsx +99 -27
  100. package/src/client/conversations/TranscriptView.tsx +63 -27
  101. package/src/client/conversations/eventLog.ts +57 -0
  102. package/src/client/conversations/eventTime.ts +60 -0
  103. package/src/client/conversations/queries.ts +29 -0
  104. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  105. package/src/client/format.ts +5 -0
  106. package/src/client/lib/utils.ts +1 -0
  107. package/src/client/pages/ConnectionsPage.tsx +2 -2
  108. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  109. package/src/client/pages/SettingsPage.tsx +9 -10
  110. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  111. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  112. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  113. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  114. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  115. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  116. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  117. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  118. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  119. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  120. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  121. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  122. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  123. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  124. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  125. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  126. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  127. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  128. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  129. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  130. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  131. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  132. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  133. package/src/client/pages/automations/README.md +57 -0
  134. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  135. package/src/client/pages/automations/automationFormat.ts +37 -23
  136. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  137. package/src/client/pages/code/CodePage.tsx +52 -55
  138. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  139. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  140. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  141. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  142. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  143. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  144. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  145. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  146. package/src/client/pages/memory/memoryRecord.ts +1 -1
  147. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  148. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  149. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  150. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  151. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  152. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  153. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  154. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  155. package/src/client/pages/system/README.md +23 -0
  156. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  157. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  158. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  159. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  160. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  161. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  162. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  163. package/src/client/pages/system/sandboxSize.ts +8 -0
  164. package/src/client/pages/system/workspaceQueries.ts +53 -0
  165. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  166. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  167. package/src/client/styles.ts +4 -0
  168. package/src/client.tsx +7 -1
  169. package/src/mock-reporting/automation-collection.ts +4 -2
  170. package/src/mock-reporting/fixtures.ts +23 -7
  171. package/src/mock-reporting/routes.ts +12 -0
  172. package/src/mock-reporting/watches.ts +94 -0
  173. package/src/shell.ts +4 -9
  174. package/src/tailwind.css +77 -10
  175. package/src/client/components/Button.tsx +0 -118
  176. package/src/client/components/Field.tsx +0 -53
  177. package/src/client/components/SegmentedTabs.tsx +0 -119
  178. package/src/client/components/Select.tsx +0 -52
  179. package/src/client/components/Sheet.LICENSE +0 -21
  180. package/src/client/components/Sheet.tsx +0 -141
  181. package/src/client/components/Skeleton.tsx +0 -17
  182. package/src/client/components/TextInput.tsx +0 -58
  183. package/src/client/components/Tooltip.tsx +0 -216
  184. package/src/client/components/charts/useChartLayout.ts +0 -27
  185. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -91
  186. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  187. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  188. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  189. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  190. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  191. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  192. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  193. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  194. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  195. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  196. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  197. package/src/client/pages/system/SnapshotSummary.tsx +0 -78
@@ -0,0 +1,165 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import {
3
+ Bot,
4
+ CircleDot,
5
+ Eye,
6
+ FolderGit2,
7
+ GitPullRequest,
8
+ Hash,
9
+ LockKeyhole,
10
+ type LucideIcon,
11
+ Package,
12
+ Rocket,
13
+ Tag,
14
+ Timer,
15
+ Users,
16
+ } from "lucide-react";
17
+ import type { ReactNode } from "react";
18
+ import {
19
+ conversationWatchesReportSchema,
20
+ type ConversationWatch,
21
+ } from "@sentry/junior/api/schema";
22
+
23
+ import { formatTime } from "../format";
24
+ import { fetchDashboardJson } from "../http";
25
+ import { DetailsList, DetailsRow, DetailsRowLine } from "./DetailsList";
26
+
27
+ /** Load the active Watches that one Conversation owns. */
28
+ export function useConversationWatches(conversationId: string) {
29
+ return useQuery({
30
+ queryFn: ({ signal }) =>
31
+ fetchDashboardJson(
32
+ conversationWatchesReportSchema,
33
+ `/api/conversations/${encodeURIComponent(conversationId)}/watches`,
34
+ signal,
35
+ ),
36
+ queryKey: ["conversation", conversationId, "watches"],
37
+ retry: false,
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Show what can wake this Conversation: each active Watch with the resource,
43
+ * the reason, and the event types that deliver to it.
44
+ */
45
+ export function ConversationWatches(props: { watches: ConversationWatch[] }) {
46
+ return (
47
+ <DetailsList
48
+ itemKey={(watch) => watch.id}
49
+ items={props.watches}
50
+ renderItem={(watch) => <WatchRow watch={watch} />}
51
+ />
52
+ );
53
+ }
54
+
55
+ const resourceIcons: Partial<Record<string, LucideIcon>> = {
56
+ channel: Hash,
57
+ deployment: Rocket,
58
+ deployment_source: Rocket,
59
+ issue: CircleDot,
60
+ pull_request: GitPullRequest,
61
+ release_source: Tag,
62
+ repository: FolderGit2,
63
+ team: Users,
64
+ timer: Timer,
65
+ workspace_snapshot: Package,
66
+ };
67
+
68
+ /**
69
+ * Show one Watch in up to three short lines: the resource and time left, the
70
+ * reason it exists, then the event types that wake the Conversation.
71
+ */
72
+ function WatchRow(props: { watch: ConversationWatch }) {
73
+ const { watch } = props;
74
+ const Icon = watch.redacted
75
+ ? LockKeyhole
76
+ : (resourceIcons[watch.resourceType] ?? Eye);
77
+ const isTimer = watch.firesAt !== undefined;
78
+ const until = watch.firesAt ?? watch.expiresAt;
79
+ const resource = `${watch.namespace} ${watch.resourceType.replaceAll("_", " ")}`;
80
+ // A timer has one event, and the time already says when it fires.
81
+ const events = isTimer ? [] : watch.events;
82
+ const filters = Object.entries(watch.match ?? {});
83
+
84
+ return (
85
+ <DetailsRow
86
+ icon={<Icon aria-hidden="true" className="size-3.5" />}
87
+ meta={
88
+ <time dateTime={until}>
89
+ {isTimer
90
+ ? `in ${formatRemaining(until)}`
91
+ : `${formatRemaining(until)} left`}
92
+ </time>
93
+ }
94
+ title={watch.label ?? `Private ${resource}`}
95
+ tooltip={`${isTimer ? "Fires" : "Expires"} ${formatTime(until, {
96
+ dateStyle: "medium",
97
+ timeStyle: "short",
98
+ })}`}
99
+ >
100
+ {watch.intent ? (
101
+ <DetailsRowLine className="line-clamp-2 break-words">
102
+ {watch.intent}
103
+ </DetailsRowLine>
104
+ ) : null}
105
+ {events.length > 0 || filters.length > 0 || watch.childAgent ? (
106
+ <DetailsRowLine className="flex flex-wrap items-center gap-1 pt-0.5">
107
+ {events.map((event) => (
108
+ <WatchChip key={event}>
109
+ {watchEventLabel(event, watch.resourceType)}
110
+ </WatchChip>
111
+ ))}
112
+ {filters.map(([key, value]) => (
113
+ <WatchChip filter key={`match:${key}`}>
114
+ {key.replaceAll("_", " ")}: {[value].flat().join(", ")}
115
+ </WatchChip>
116
+ ))}
117
+ {watch.childAgent ? (
118
+ <span className="inline-flex min-w-0 items-center gap-1 px-0.5">
119
+ <Bot aria-hidden="true" className="size-3 shrink-0" />
120
+ <span className="truncate">
121
+ via {watch.childAgent.name ?? "subagent"}
122
+ </span>
123
+ </span>
124
+ ) : null}
125
+ </DetailsRowLine>
126
+ ) : null}
127
+ </DetailsRow>
128
+ );
129
+ }
130
+
131
+ /** Label one event type, or a dashed match filter that narrows the events. */
132
+ function WatchChip(props: { children: ReactNode; filter?: boolean }) {
133
+ return (
134
+ <span
135
+ className={
136
+ props.filter
137
+ ? "min-w-0 truncate rounded-md border border-dashed border-dashboard-border px-1.5 py-px text-xs leading-4 text-dashboard-text-muted"
138
+ : "min-w-0 truncate rounded-md bg-dashboard-fill-soft px-1.5 py-px text-xs leading-4 text-dashboard-text-subtle"
139
+ }
140
+ >
141
+ {props.children}
142
+ </span>
143
+ );
144
+ }
145
+
146
+ /**
147
+ * Turn an event type into a short trigger label. The row already names the
148
+ * resource, so `pull_request.checks.failed` reads as "checks failed".
149
+ */
150
+ function watchEventLabel(event: string, resourceType: string): string {
151
+ const parts = event.split(".");
152
+ const rest =
153
+ parts[0] === resourceType && parts.length > 1 ? parts.slice(1) : parts;
154
+ return rest.join(" ").replaceAll("_", " ");
155
+ }
156
+
157
+ /** Format the time until a Watch fires or expires as one compact unit. */
158
+ function formatRemaining(value: string): string {
159
+ const ms = Math.max(0, Date.parse(value) - Date.now());
160
+ const minutes = Math.round(ms / 60_000);
161
+ if (minutes < 60) return `${Math.max(1, minutes)}m`;
162
+ const hours = Math.round(minutes / 60);
163
+ if (hours < 48) return `${hours}h`;
164
+ return `${Math.round(hours / 24)}d`;
165
+ }
@@ -5,8 +5,8 @@ import { useNavigate, useParams } from "react-router";
5
5
 
6
6
  import { useConversationsData } from "../api";
7
7
  import { ConversationSidebar } from "./ConversationSidebar";
8
- import { ToggleButton } from "../components/Button";
9
8
  import { SearchInput } from "../components/SearchInput";
9
+ import { ToggleGroup, ToggleGroupItem } from "../components/ui/toggle-group";
10
10
  import { pageCount, pageItems, PagePagination } from "../components/Pagination";
11
11
  import { ConversationHomeList } from "./ConversationHomeList";
12
12
  import { ConversationComposer } from "./ConversationComposer";
@@ -171,7 +171,6 @@ function NewConversationView(props: {
171
171
  ): Promise<void>;
172
172
  }) {
173
173
  const [visibility, setVisibility] = useState<"private" | "public">("public");
174
- const isPublic = visibility === "public";
175
174
 
176
175
  return (
177
176
  <section
@@ -185,32 +184,23 @@ function NewConversationView(props: {
185
184
  draftId="new"
186
185
  error={props.error}
187
186
  footerStart={
188
- <div
187
+ <ToggleGroup
189
188
  aria-label="Conversation visibility"
190
- className="inline-flex shrink-0 items-center gap-0.5 rounded-lg border border-dashboard-border bg-dashboard-surface-panel p-0.5"
191
- role="group"
189
+ className="inline-flex shrink-0 gap-0.5 rounded-lg border border-dashboard-border bg-dashboard-surface-panel p-0.5"
190
+ onValueChange={([next]) => {
191
+ if (next) setVisibility(next);
192
+ }}
193
+ value={[visibility]}
192
194
  >
193
- <ToggleButton
194
- className="min-w-20 gap-1.5"
195
- onClick={() => setVisibility("public")}
196
- pressed={isPublic}
197
- type="button"
198
- variant="segment"
199
- >
195
+ <ToggleGroupItem className="min-w-20 gap-1.5" value="public">
200
196
  <Globe2 aria-hidden="true" className="size-3" />
201
197
  Public
202
- </ToggleButton>
203
- <ToggleButton
204
- className="min-w-20 gap-1.5"
205
- onClick={() => setVisibility("private")}
206
- pressed={!isPublic}
207
- type="button"
208
- variant="segment"
209
- >
198
+ </ToggleGroupItem>
199
+ <ToggleGroupItem className="min-w-20 gap-1.5" value="private">
210
200
  <LockKeyhole aria-hidden="true" className="size-3" />
211
201
  Private
212
- </ToggleButton>
213
- </div>
202
+ </ToggleGroupItem>
203
+ </ToggleGroup>
214
204
  }
215
205
  label="Start a conversation"
216
206
  restoreDraftOnError
@@ -1,4 +1,4 @@
1
- import { Skeleton } from "../components/Skeleton";
1
+ import { Skeleton } from "../components/ui/skeleton";
2
2
  import { dashboardContainerClass, cn } from "../styles";
3
3
  import { ChatLayout } from "./ChatLayout";
4
4
  import { ConversationHomeListLoading } from "./ConversationHomeList";
@@ -0,0 +1,124 @@
1
+ import { ChevronRight } from "lucide-react";
2
+ import { useState, type ReactNode } from "react";
3
+
4
+ import { cn, dashboardDisclosureChevronClass } from "../styles";
5
+
6
+ /** Rows a details list shows before the reader asks for the rest. */
7
+ const COLLAPSED_ROW_COUNT = 3;
8
+
9
+ /**
10
+ * Show rows in a details card. Rows are split by thin rules. A list with more
11
+ * than three rows shows the first three and a row that shows the rest.
12
+ */
13
+ export function DetailsList<T>(props: {
14
+ items: readonly T[];
15
+ itemKey(item: T): string;
16
+ renderItem(item: T): ReactNode;
17
+ }) {
18
+ const [expanded, setExpanded] = useState(false);
19
+ const hidden = props.items.length - COLLAPSED_ROW_COUNT;
20
+ const visible =
21
+ expanded || hidden <= 0
22
+ ? props.items
23
+ : props.items.slice(0, COLLAPSED_ROW_COUNT);
24
+
25
+ return (
26
+ // Rows own their padding, so hover fills reach the rules. The negative
27
+ // margin keeps the first and last text on the card padding.
28
+ <ul className="-my-2.5 grid list-none p-0">
29
+ {visible.map((item) => (
30
+ <li
31
+ className="border-t border-dashboard-border-subtle first:border-t-0"
32
+ key={props.itemKey(item)}
33
+ >
34
+ {props.renderItem(item)}
35
+ </li>
36
+ ))}
37
+ {hidden > 0 ? (
38
+ <li className="border-t border-dashboard-border-subtle">
39
+ <button
40
+ aria-expanded={expanded}
41
+ className="group -mx-2 flex min-h-8 items-center gap-2.5 rounded-md px-2 py-2 text-left text-xs font-medium leading-4 text-dashboard-text-muted transition-colors hover:bg-dashboard-surface-hover hover:text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
42
+ onClick={() => setExpanded((value) => !value)}
43
+ type="button"
44
+ >
45
+ <span className="grid size-4 shrink-0 place-items-center">
46
+ <ChevronRight
47
+ aria-hidden="true"
48
+ className={dashboardDisclosureChevronClass}
49
+ />
50
+ </span>
51
+ {expanded ? "Show fewer" : `Show ${hidden} more`}
52
+ </button>
53
+ </li>
54
+ ) : null}
55
+ </ul>
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Lay out one details list row: an icon, a title, and short trailing text on
61
+ * the first line, then optional lines under the title. A row with `href`
62
+ * opens that link in a new tab.
63
+ */
64
+ export function DetailsRow(props: {
65
+ children?: ReactNode;
66
+ href?: string;
67
+ icon: ReactNode;
68
+ meta?: ReactNode;
69
+ title: ReactNode;
70
+ /** Native hover text with detail that the row does not show. */
71
+ tooltip?: string;
72
+ }) {
73
+ const className = cn(
74
+ "-mx-2 grid min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-x-2.5 gap-y-1 rounded-md px-2 py-2.5 text-inherit no-underline",
75
+ props.href &&
76
+ "group transition-colors hover:bg-dashboard-surface-hover focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus",
77
+ );
78
+ const content = (
79
+ <>
80
+ <span className="grid size-5 shrink-0 place-items-center text-dashboard-text-muted">
81
+ {props.icon}
82
+ </span>
83
+ <span className="min-w-0 truncate text-sm font-medium leading-5 text-dashboard-text group-hover:underline">
84
+ {props.title}
85
+ </span>
86
+ <span className="shrink-0 text-xs leading-5 tabular-nums text-dashboard-text-muted">
87
+ {props.meta}
88
+ </span>
89
+ {props.children}
90
+ </>
91
+ );
92
+ return props.href ? (
93
+ <a
94
+ className={className}
95
+ href={props.href}
96
+ rel="noreferrer"
97
+ target="_blank"
98
+ title={props.tooltip}
99
+ >
100
+ {content}
101
+ </a>
102
+ ) : (
103
+ <div className={className} title={props.tooltip}>
104
+ {content}
105
+ </div>
106
+ );
107
+ }
108
+
109
+ /** Place one extra line under the title of a `DetailsRow`. */
110
+ export function DetailsRowLine(props: {
111
+ children: ReactNode;
112
+ className?: string;
113
+ }) {
114
+ return (
115
+ <div
116
+ className={cn(
117
+ "col-start-2 col-end-4 min-w-0 text-xs leading-4 text-dashboard-text-muted",
118
+ props.className,
119
+ )}
120
+ >
121
+ {props.children}
122
+ </div>
123
+ );
124
+ }
@@ -1,5 +1,15 @@
1
1
  import type { ConversationReportEvent } from "@sentry/junior/api/schema";
2
- import type { ReactNode } from "react";
2
+ import { ChevronRight } from "lucide-react";
3
+ import { useState, type ReactNode } from "react";
4
+
5
+ import { HighlightedCode } from "../code";
6
+ import { CopyButton } from "../components/CopyButton";
7
+ import {
8
+ Collapsible,
9
+ CollapsibleContent,
10
+ CollapsibleTrigger,
11
+ } from "../components/ui/collapsible";
12
+ import { dashboardDisclosureChevronClass } from "../styles";
3
13
 
4
14
  import { RedactedMarker } from "./TranscriptRedacted";
5
15
  import { TranscriptText } from "./TranscriptText";
@@ -178,14 +188,38 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
178
188
  </DetailSection>
179
189
  ) : null}
180
190
  {content}
181
- <details className="border-t border-dashboard-border pt-4">
182
- <summary className="cursor-pointer text-xs text-dashboard-text-muted hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-dashboard-focus">
191
+ <RawEventJson event={event} />
192
+ </div>
193
+ );
194
+ }
195
+
196
+ function RawEventJson({ event }: { event: ConversationReportEvent }) {
197
+ const [open, setOpen] = useState(false);
198
+ const json = open ? JSON.stringify(event, null, 2) : "";
199
+ return (
200
+ <div className="relative min-w-0 border-t border-dashboard-border pt-4">
201
+ <Collapsible onOpenChange={setOpen} open={open}>
202
+ <CollapsibleTrigger className="group flex min-h-9 cursor-pointer items-center gap-1 pr-12 text-xs text-dashboard-text-muted hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-dashboard-focus">
203
+ <ChevronRight
204
+ aria-hidden="true"
205
+ className={dashboardDisclosureChevronClass}
206
+ />
183
207
  Raw JSON
184
- </summary>
185
- <pre className="mb-0 mt-3 whitespace-pre-wrap break-words font-mono text-xs leading-relaxed text-dashboard-text">
186
- {JSON.stringify(event, null, 2)}
187
- </pre>
188
- </details>
208
+ </CollapsibleTrigger>
209
+ <CollapsibleContent>
210
+ <div className="absolute right-0 top-4">
211
+ <CopyButton getText={() => json} label="Copy JSON" />
212
+ </div>
213
+ <div
214
+ aria-label="Raw event JSON"
215
+ className="mt-3 max-h-80 min-w-0 overflow-auto overscroll-contain rounded-lg border border-dashboard-border bg-dashboard-surface-panel p-3 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
216
+ role="region"
217
+ tabIndex={0}
218
+ >
219
+ <HighlightedCode code={json} language="json" />
220
+ </div>
221
+ </CollapsibleContent>
222
+ </Collapsible>
189
223
  </div>
190
224
  );
191
225
  }
@@ -2,10 +2,15 @@ import { GitFork } from "lucide-react";
2
2
  import { useId, useState } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
  import { useNavigate } from "react-router";
5
- import { Button } from "../components/Button";
5
+ import { Button } from "../components/ui/button";
6
+ import { Textarea } from "../components/ui/textarea";
7
+ import { Field, FieldLabel } from "../components/ui/field";
8
+ import {
9
+ Tooltip,
10
+ TooltipContent,
11
+ TooltipTrigger,
12
+ } from "../components/ui/tooltip";
6
13
  import { Drawer } from "../components/Drawer";
7
- import { Field } from "../components/Field";
8
- import { TextArea } from "../components/TextInput";
9
14
  import { DashboardApiError } from "../http";
10
15
  import { conversationPath } from "./conversationRoutes";
11
16
  import { useForkConversation } from "./queries";
@@ -17,14 +22,20 @@ export function ForkConversationButton(props: ForkTarget) {
17
22
  const [open, setOpen] = useState(false);
18
23
  return (
19
24
  <>
20
- <Button
21
- size="icon"
22
- aria-label="Fork after this message"
23
- title="Fork after this message"
24
- onClick={() => setOpen(true)}
25
- >
26
- <GitFork size={14} aria-hidden="true" />
27
- </Button>
25
+ <Tooltip>
26
+ <TooltipTrigger
27
+ render={
28
+ <Button
29
+ size="icon"
30
+ aria-label="Fork after this message"
31
+ onClick={() => setOpen(true)}
32
+ >
33
+ <GitFork size={14} aria-hidden="true" />
34
+ </Button>
35
+ }
36
+ />
37
+ <TooltipContent size="compact">Fork after this message</TooltipContent>
38
+ </Tooltip>
28
39
  {open
29
40
  ? createPortal(
30
41
  <ForkConversationDialog
@@ -82,12 +93,12 @@ function ForkConversationDialog(props: ForkTarget & { onClose(): void }) {
82
93
  <p className="m-0">
83
94
  Creates a fork of the conversation from this point.
84
95
  </p>
85
- <Field htmlFor={inputId} label="Message">
86
- <TextArea
96
+ <Field>
97
+ <FieldLabel htmlFor={inputId}>Message</FieldLabel>
98
+ <Textarea
87
99
  id={inputId}
88
- prose
89
100
  autoFocus
90
- className="min-h-28"
101
+ className="min-h-28 leading-relaxed"
91
102
  placeholder="Message Junior…"
92
103
  value={message}
93
104
  disabled={fork.isPending}
@@ -112,7 +123,7 @@ function ForkConversationDialog(props: ForkTarget & { onClose(): void }) {
112
123
  </p>
113
124
  ) : null}
114
125
  <Button
115
- tone="primary"
126
+ variant="primary"
116
127
  type="submit"
117
128
  disabled={!message.trim() || fork.isPending}
118
129
  >
@@ -3,10 +3,16 @@ import {
3
3
  objectFactFields,
4
4
  objectPresentation,
5
5
  } from "@sentry/junior-plugin-api";
6
- import { ArrowUpRight } from "lucide-react";
6
+ import { ArrowUpRight, ChevronRight } from "lucide-react";
7
7
  import { ObjectIcon } from "../components/ObjectIcon";
8
8
  import { AutomationCard } from "../components/AutomationCard";
9
9
  import { StatusChip } from "../components/StatusChip";
10
+ import {
11
+ Collapsible,
12
+ CollapsibleContent,
13
+ CollapsibleTrigger,
14
+ } from "../components/ui/collapsible";
15
+ import { dashboardDisclosureChevronClass } from "../styles";
10
16
 
11
17
  /** Show the latest saved annotation. Opening a source is always explicit. */
12
18
  export function ObjectCard({ card }: { card: OwnedObjectAnnotation }) {
@@ -78,14 +84,20 @@ export function ObjectCard({ card }: { card: OwnedObjectAnnotation }) {
78
84
  )}
79
85
  {fields.length > 0 && facts(fields)}
80
86
  {card.description && (
81
- <details className="min-w-0 border-t border-dashboard-border pt-3">
82
- <summary className="cursor-pointer text-xs font-medium text-dashboard-text-muted focus-visible:outline-dashboard-focus">
87
+ <Collapsible className="min-w-0 border-t border-dashboard-border pt-3">
88
+ <CollapsibleTrigger className="group flex cursor-pointer items-center gap-1 text-xs font-medium text-dashboard-text-muted focus-visible:outline-dashboard-focus">
89
+ <ChevronRight
90
+ aria-hidden="true"
91
+ className={dashboardDisclosureChevronClass}
92
+ />
83
93
  More details
84
- </summary>
85
- <p className="mb-0 mt-3 whitespace-pre-wrap break-words text-sm text-dashboard-text-muted">
86
- {card.description}
87
- </p>
88
- </details>
94
+ </CollapsibleTrigger>
95
+ <CollapsibleContent hiddenUntilFound>
96
+ <p className="mb-0 mt-3 whitespace-pre-wrap break-words text-sm text-dashboard-text-muted">
97
+ {card.description}
98
+ </p>
99
+ </CollapsibleContent>
100
+ </Collapsible>
89
101
  )}
90
102
  {card.warning && (
91
103
  <p className="m-0 break-words text-xs text-amber-500">
@@ -15,7 +15,11 @@ import {
15
15
  type LucideIcon,
16
16
  } from "lucide-react";
17
17
 
18
- import { Tooltip } from "../components/Tooltip";
18
+ import {
19
+ Tooltip,
20
+ TooltipContent,
21
+ TooltipTrigger,
22
+ } from "../components/ui/tooltip";
19
23
  import { actorLabel } from "../format";
20
24
  import type { ConversationMailboxMessage } from "./conversationOutbox";
21
25
  import {
@@ -44,13 +48,18 @@ function pendingDeliveryMeta(
44
48
 
45
49
  function PendingMetaIcon(props: { children: ReactElement; label: string }) {
46
50
  return (
47
- <Tooltip content={props.label} placement="above">
48
- <span
49
- aria-label={props.label}
50
- className="inline-flex text-dashboard-text-muted"
51
- >
52
- {props.children}
53
- </span>
51
+ <Tooltip>
52
+ <TooltipTrigger
53
+ render={
54
+ <span
55
+ aria-label={props.label}
56
+ className="inline-flex text-dashboard-text-muted"
57
+ >
58
+ {props.children}
59
+ </span>
60
+ }
61
+ />
62
+ <TooltipContent>{props.label}</TooltipContent>
54
63
  </Tooltip>
55
64
  );
56
65
  }
@@ -69,10 +78,15 @@ function PendingMetaIcons(props: {
69
78
  return (
70
79
  <TranscriptHeadingMeta className="flex min-w-0 items-center justify-end gap-1.5 text-xs leading-none text-dashboard-text-muted">
71
80
  {props.showSlack ? (
72
- <Tooltip content="Slack" placement="above">
73
- <span className="inline-flex text-dashboard-text-muted">
74
- <SlackMark className="size-3.5" />
75
- </span>
81
+ <Tooltip>
82
+ <TooltipTrigger
83
+ render={
84
+ <span className="inline-flex text-dashboard-text-muted">
85
+ <SlackMark className="size-3.5" />
86
+ </span>
87
+ }
88
+ />
89
+ <TooltipContent>Slack</TooltipContent>
76
90
  </Tooltip>
77
91
  ) : null}
78
92
  <PendingMetaIcon label={delivery.label}>
@@ -84,28 +98,33 @@ function PendingMetaIcons(props: {
84
98
  />
85
99
  </PendingMetaIcon>
86
100
  {props.onCancel ? (
87
- <Tooltip content="Remove queued message" placement="above">
88
- <button
89
- aria-label={
90
- props.cancelError
91
- ? "Could not remove. Try again."
92
- : "Remove queued message"
101
+ <Tooltip>
102
+ <TooltipTrigger
103
+ render={
104
+ <button
105
+ aria-label={
106
+ props.cancelError
107
+ ? "Could not remove. Try again."
108
+ : "Remove queued message"
109
+ }
110
+ className="inline-flex size-5 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent p-0 text-dashboard-text-muted transition-colors hover:bg-dashboard-fill-hover hover:text-amber-50 focus-visible:outline focus-visible:outline-1 focus-visible:outline-amber-200/55 disabled:cursor-default disabled:opacity-50"
111
+ disabled={props.cancelDisabled}
112
+ onClick={props.onCancel}
113
+ type="button"
114
+ >
115
+ {props.cancelPending ? (
116
+ <LoaderCircle
117
+ aria-hidden="true"
118
+ className="animate-spin"
119
+ size={13}
120
+ />
121
+ ) : (
122
+ <X aria-hidden="true" size={14} strokeWidth={2.2} />
123
+ )}
124
+ </button>
93
125
  }
94
- className="inline-flex size-5 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent p-0 text-dashboard-text-muted transition-colors hover:bg-dashboard-fill-hover hover:text-amber-50 focus-visible:outline focus-visible:outline-1 focus-visible:outline-amber-200/55 disabled:cursor-default disabled:opacity-50"
95
- disabled={props.cancelDisabled}
96
- onClick={props.onCancel}
97
- type="button"
98
- >
99
- {props.cancelPending ? (
100
- <LoaderCircle
101
- aria-hidden="true"
102
- className="animate-spin"
103
- size={13}
104
- />
105
- ) : (
106
- <X aria-hidden="true" size={14} strokeWidth={2.2} />
107
- )}
108
- </button>
126
+ />
127
+ <TooltipContent>Remove queued message</TooltipContent>
109
128
  </Tooltip>
110
129
  ) : null}
111
130
  </TranscriptHeadingMeta>