@sentry/junior-dashboard 0.234.0 → 0.236.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 (53) hide show
  1. package/dist/app.js +100 -18
  2. package/dist/client.js +47 -46
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.js +100 -18
  5. package/dist/tailwind.css +1 -1
  6. package/package.json +3 -3
  7. package/src/app.ts +7 -1
  8. package/src/assets/junior-avatar.png +0 -0
  9. package/src/assets/junior-favicon.png +0 -0
  10. package/src/assets.ts +1 -0
  11. package/src/client/App.tsx +35 -0
  12. package/src/client/api.ts +63 -0
  13. package/src/client/components/README.md +8 -4
  14. package/src/client/components/SegmentedNav.tsx +167 -0
  15. package/src/client/components/SegmentedTabs.tsx +24 -5
  16. package/src/client/components/layout/SecondaryNavigation.tsx +40 -83
  17. package/src/client/conversations/ConversationEventLog.tsx +45 -5
  18. package/src/client/conversations/ConversationForkLinks.tsx +51 -0
  19. package/src/client/conversations/ConversationHeader.tsx +12 -1
  20. package/src/client/conversations/ConversationPage.tsx +72 -2
  21. package/src/client/conversations/ForkConversationButton.tsx +125 -0
  22. package/src/client/conversations/TranscriptMessageView.tsx +28 -7
  23. package/src/client/conversations/TranscriptView.tsx +4 -0
  24. package/src/client/conversations/conversationRoutes.ts +10 -0
  25. package/src/client/conversations/queries.ts +45 -0
  26. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +28 -27
  27. package/src/client/pages/automations/AutomationEditPage.tsx +23 -24
  28. package/src/client/pages/automations/AutomationEditor.tsx +66 -13
  29. package/src/client/pages/automations/AutomationFilters.tsx +22 -14
  30. package/src/client/pages/automations/AutomationLifecycle.tsx +6 -4
  31. package/src/client/pages/automations/AutomationOutcomeFields.tsx +18 -0
  32. package/src/client/pages/automations/AutomationRow.tsx +172 -187
  33. package/src/client/pages/automations/AutomationVersionsPage.tsx +356 -0
  34. package/src/client/pages/automations/AutomationsPage.tsx +40 -42
  35. package/src/client/pages/automations/AutomationsPageLayout.tsx +6 -5
  36. package/src/client/pages/automations/README.md +14 -3
  37. package/src/client/pages/automations/automationFormat.ts +43 -0
  38. package/src/client/pages/code/CodeChangeRow.tsx +41 -0
  39. package/src/client/pages/code/CodePage.tsx +18 -93
  40. package/src/client/pages/code/CodeSummaryCards.tsx +53 -0
  41. package/src/client/pages/code/RepositoryPage.tsx +227 -0
  42. package/src/client/pages/code/RepositorySections.tsx +108 -0
  43. package/src/client/pages/code/RepositoryTabs.tsx +146 -0
  44. package/src/client/pages/code/codeRepositoryRoutes.ts +18 -0
  45. package/src/client/pages/dev/ComponentsPage.tsx +5 -22
  46. package/src/client/pages/dev/SegmentedFixtures.tsx +58 -0
  47. package/src/client/pages/memory/MemoryPageLayout.tsx +12 -13
  48. package/src/client/pages/system/SystemNavigation.tsx +6 -2
  49. package/src/client/pages/system/SystemPageLayout.tsx +4 -6
  50. package/src/mock-reporting/code-repository.ts +77 -0
  51. package/src/mock-reporting/fixtures.ts +8 -2
  52. package/src/mock-reporting/routes.ts +25 -8
  53. package/src/shell.ts +21 -5
@@ -6,38 +6,24 @@ import {
6
6
  Trash2,
7
7
  Zap,
8
8
  } from "lucide-react";
9
- import { useEffect, useId, useRef, useState } from "react";
9
+ import { type ReactNode, useEffect, useId, useRef, useState } from "react";
10
10
  import { Link, useLocation } from "react-router";
11
11
  import { ActorAvatar } from "../../components/ActorAvatar";
12
12
  import { SelectableRow } from "../../components/SelectableRow";
13
13
  import { StatusChip } from "../../components/StatusChip";
14
- import { formatRelativeTime, formatTime, peoplePath } from "../../format";
15
- import { cn } from "../../styles";
16
-
17
- const desktopColumns =
18
- "xl:grid-cols-[minmax(0,1fr)_9rem_16rem_10rem_7rem_2.75rem]";
19
-
20
- /** Keep the management headings aligned with the row fields. */
21
- export function AutomationListHeader() {
22
- return (
23
- <div
24
- aria-hidden="true"
25
- className={cn(
26
- "hidden items-center gap-4 border-b border-dashboard-border-subtle px-4 py-2.5 text-xs text-dashboard-text-muted xl:grid",
27
- desktopColumns,
28
- )}
29
- >
30
- <span>Automation</span>
31
- <span>Creator</span>
32
- <span>Trigger</span>
33
- <span>Destination</span>
34
- <span>Last run</span>
35
- <span className="sr-only">Actions</span>
36
- </div>
37
- );
38
- }
14
+ import { formatRelativeTime, peoplePath } from "../../format";
15
+ import {
16
+ automationEventConditions,
17
+ automationEventNames,
18
+ automationScheduleLabel,
19
+ formatAutomationRunTime,
20
+ } from "./automationFormat";
39
21
 
40
- /** Show purpose, ownership, and trigger state without opening the details. */
22
+ /**
23
+ * Show one automation as a compact card: what it is, where it posts, who owns
24
+ * it, and when it runs. Instructions, timezones, and event conditions stay in
25
+ * the details drawer.
26
+ */
41
27
  export function AutomationRow(props: {
42
28
  deleting: boolean;
43
29
  onDelete(): void;
@@ -47,6 +33,7 @@ export function AutomationRow(props: {
47
33
  }) {
48
34
  const { automation } = props;
49
35
  const location = useLocation();
36
+ const status = automationStatus(automation);
50
37
  const creator = (
51
38
  <>
52
39
  <ActorAvatar
@@ -58,197 +45,195 @@ export function AutomationRow(props: {
58
45
  </>
59
46
  );
60
47
  return (
61
- <article role="listitem">
48
+ <article
49
+ className="min-w-0 rounded-lg border border-dashboard-border-subtle bg-dashboard-fill-faint"
50
+ role="listitem"
51
+ >
62
52
  <SelectableRow
63
- className={cn(
64
- "relative grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] items-center gap-x-3 gap-y-2 px-4 py-3 xl:gap-x-4",
65
- desktopColumns,
66
- )}
53
+ className="flex items-start gap-3 rounded-lg px-4 py-3.5"
67
54
  onSelect={props.onSelect}
68
55
  selected={props.selected}
69
56
  >
70
- <button
71
- aria-expanded={props.selected}
72
- aria-label={`View automation details: ${automation.title}`}
73
- className="col-span-2 pr-11 xl:pr-0 min-w-0 cursor-pointer rounded border-0 bg-transparent p-0 text-left focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus xl:col-span-1"
74
- onClick={props.onSelect}
75
- type="button"
76
- >
77
- <span
78
- className="block truncate font-display text-base font-medium text-dashboard-text"
79
- title={automation.title}
80
- >
81
- {automation.title}
82
- </span>
83
- <span
84
- className="mt-1 block truncate text-xs text-dashboard-text-muted"
85
- title={automation.instruction}
57
+ <div className="min-w-0 flex-1">
58
+ <button
59
+ aria-expanded={props.selected}
60
+ aria-label={`View automation details: ${automation.title}`}
61
+ className="block w-full min-w-0 cursor-pointer rounded border-0 bg-transparent p-0 text-left focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
62
+ onClick={props.onSelect}
63
+ type="button"
86
64
  >
87
- {automation.instruction}
88
- </span>
89
- </button>
90
- <div className="row-start-2 min-w-0 xl:row-auto">
91
- {automation.createdByEmail ? (
92
- <Link
93
- className="inline-flex max-w-full items-center gap-2 rounded text-sm text-dashboard-text no-underline hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
94
- title={automation.createdBy}
95
- to={peoplePath(automation.createdByEmail)}
96
- >
97
- {creator}
98
- </Link>
99
- ) : (
100
- <span
101
- className="inline-flex max-w-full items-center gap-2 text-sm text-dashboard-text"
102
- title={automation.createdBy}
103
- >
104
- {creator}
65
+ <span className="flex min-w-0 items-center gap-2">
66
+ <span
67
+ className="truncate font-display text-base font-medium text-dashboard-text"
68
+ title={automation.title}
69
+ >
70
+ {automation.title}
71
+ </span>
72
+ {status ? (
73
+ <StatusChip
74
+ className="shrink-0"
75
+ size="compact"
76
+ tone={
77
+ status === "completed" || status === "paused"
78
+ ? "neutral"
79
+ : "warning"
80
+ }
81
+ >
82
+ {status}
83
+ </StatusChip>
84
+ ) : null}
105
85
  </span>
106
- )}
107
- </div>
108
- <div className="col-span-2 row-start-3 min-w-0 xl:col-span-1 xl:row-auto">
109
- <AutomationTrigger automation={automation} />
110
- </div>
111
- <div className="col-start-2 row-start-2 min-w-0 text-right xl:col-auto xl:row-auto xl:text-left">
112
- <span className="inline-flex max-w-full items-center gap-1 text-xs text-dashboard-text-muted xl:text-sm xl:text-dashboard-text">
113
- {automation.destination.visibility === "private" ? (
114
- <LockKeyhole
115
- aria-label="Private destination"
116
- role="img"
117
- size={12}
118
- className="shrink-0"
119
- />
120
- ) : null}
121
- <span className="truncate">{automation.destination.label}</span>
122
- </span>
123
- </div>
124
- <div className="col-span-2 min-w-0 xl:col-span-1">
125
- <span className="text-xs text-dashboard-text-muted xl:hidden">
126
- Last run{" "}
127
- </span>
128
- <span
129
- className="text-xs text-dashboard-text-muted xl:text-sm xl:text-dashboard-text"
130
- title={
131
- automation.lastRunAt
132
- ? formatTime(automation.lastRunAt, {
133
- dateStyle: "medium",
134
- timeStyle: "short",
135
- })
136
- : undefined
137
- }
138
- >
139
- {automation.lastRunAt
140
- ? formatRelativeTime(automation.lastRunAt)
141
- : "Never"}
142
- </span>
143
- {automation.lastRunStatus === "failed" ||
144
- automation.lastRunStatus === "blocked" ? (
145
- <Link
146
- className="block text-xs text-amber-300 underline"
147
- to={`/automations/${automation.kind}/${encodeURIComponent(automation.id)}/executions`}
148
- >
149
- Last run {automation.lastRunStatus}
150
- </Link>
151
- ) : null}
86
+ <AutomationSummaryLine automation={automation} />
87
+ </button>
88
+ <div className="mt-2.5 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-dashboard-text-muted">
89
+ {automation.createdByEmail ? (
90
+ <Link
91
+ className="inline-flex min-w-0 max-w-full items-center gap-2 rounded text-dashboard-text no-underline hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
92
+ title={automation.createdBy}
93
+ to={peoplePath(automation.createdByEmail)}
94
+ >
95
+ {creator}
96
+ </Link>
97
+ ) : (
98
+ <span
99
+ className="inline-flex min-w-0 max-w-full items-center gap-2 text-dashboard-text"
100
+ title={automation.createdBy}
101
+ >
102
+ {creator}
103
+ </span>
104
+ )}
105
+ <AutomationTiming automation={automation} />
106
+ </div>
152
107
  </div>
153
- <div className="absolute right-3 top-3 xl:static">
154
- {automation.ownedByViewer ? (
108
+ {automation.ownedByViewer ? (
109
+ <div className="-mr-2 -mt-1.5 shrink-0">
155
110
  <AutomationActions
156
111
  editPath={`/automations/${automation.kind}/${encodeURIComponent(automation.id)}/edit${location.search}`}
157
112
  deleting={props.deleting}
158
113
  onDelete={props.onDelete}
159
114
  title={automation.title}
160
115
  />
161
- ) : null}
162
- </div>
116
+ </div>
117
+ ) : null}
163
118
  </SelectableRow>
164
119
  </article>
165
120
  );
166
121
  }
167
122
 
168
- function AutomationTrigger({ automation }: { automation: AutomationSummary }) {
123
+ /** Return the one state worth flagging in the list, if the automation is not simply active. */
124
+ function automationStatus(automation: AutomationSummary): string | undefined {
125
+ if (automation.kind === "scheduled") {
126
+ return automation.status === "active" ? undefined : automation.status;
127
+ }
128
+ if (automation.status === "paused") return "paused";
129
+ return automation.triggerAvailable ? undefined : "Trigger unavailable";
130
+ }
131
+
132
+ /** Combine the trigger and Destination into one muted line under the title. */
133
+ function AutomationSummaryLine({
134
+ automation,
135
+ }: {
136
+ automation: AutomationSummary;
137
+ }) {
169
138
  const scheduled = automation.kind === "scheduled";
170
139
  const Icon = scheduled ? CalendarClock : Zap;
171
- let summary: string;
172
- let detail: string;
173
- let status: string | undefined;
174
- let conditions: string | undefined;
175
-
176
- if (automation.kind === "scheduled") {
177
- summary = automation.schedule.includes(automation.timezone)
140
+ const trigger =
141
+ automation.kind === "scheduled"
178
142
  ? automation.schedule
179
- : `${automation.schedule} (${automation.timezone})`;
180
- status = automation.status === "active" ? undefined : automation.status;
181
- detail = "No next run";
182
- if (automation.status === "blocked") {
183
- detail = "Future runs are blocked";
184
- } else if (automation.status === "paused") {
185
- detail = "Future triggers will not start work";
186
- } else if (automation.status === "active" && automation.nextRunAt) {
187
- const nextRun = new Date(automation.nextRunAt).toLocaleString(undefined, {
188
- month: "short",
189
- day: "numeric",
190
- hour: "numeric",
191
- minute: "2-digit",
192
- timeZone: automation.timezone,
193
- timeZoneName: "short",
194
- });
195
- detail = `Next ${nextRun}`;
196
- }
197
- } else {
198
- summary = automation.resource;
199
- detail = automation.events
200
- .map((event) => event.replaceAll(/[._]/g, " "))
201
- .join(", ");
202
- status =
203
- automation.status === "paused"
204
- ? "paused"
205
- : automation.triggerAvailable
206
- ? undefined
207
- : "Trigger unavailable";
208
- conditions = Object.entries(automation.match ?? {})
209
- .map(
210
- ([field, value]) =>
211
- `${field}: ${Array.isArray(value) ? value.join(" or ") : String(value)}`,
212
- )
213
- .join(" · ");
214
- }
143
+ : `${automation.resource} · ${automationEventNames(automation)}`;
144
+ const conditions =
145
+ automation.kind === "event" ? automationEventConditions(automation) : "";
146
+ const triggerDetail =
147
+ automation.kind === "scheduled"
148
+ ? automationScheduleLabel(automation)
149
+ : conditions
150
+ ? `${trigger} · ${conditions}`
151
+ : trigger;
215
152
  return (
216
- <div className="flex items-start gap-2">
153
+ <span className="mt-1 flex min-w-0 items-center gap-1.5 text-xs text-dashboard-text-muted">
217
154
  <Icon
218
155
  aria-label={
219
156
  scheduled
220
157
  ? "Scheduled automation"
221
158
  : `${automation.source} event automation`
222
159
  }
223
- className="mt-0.5 shrink-0 text-dashboard-text-muted"
160
+ className="shrink-0"
224
161
  role="img"
225
- size={16}
162
+ size={13}
226
163
  />
227
- <div className="min-w-0">
228
- <div className="break-words text-sm text-dashboard-text">{summary}</div>
229
- <div className="mt-1 break-words text-xs text-dashboard-text-muted">
230
- {detail}
231
- </div>
232
- {conditions ? (
233
- <div className="mt-1 break-words text-xs text-dashboard-text-muted">
234
- {conditions}
235
- </div>
236
- ) : null}
237
- {status ? (
238
- <StatusChip
239
- className="mt-1 xl:mt-2"
240
- size="compact"
241
- tone={
242
- status === "completed" || status === "paused"
243
- ? "neutral"
244
- : "warning"
245
- }
164
+ <span className="min-w-0 truncate" title={triggerDetail}>
165
+ {trigger}
166
+ </span>
167
+ <span aria-hidden="true" className="shrink-0 opacity-45">
168
+ ·
169
+ </span>
170
+ {automation.destination.visibility === "private" ? (
171
+ <LockKeyhole
172
+ aria-label="Private destination"
173
+ className="shrink-0"
174
+ role="img"
175
+ size={12}
176
+ />
177
+ ) : null}
178
+ <span
179
+ className="min-w-0 max-w-[45%] truncate"
180
+ title={automation.destination.label}
181
+ >
182
+ {automation.destination.label}
183
+ </span>
184
+ </span>
185
+ );
186
+ }
187
+
188
+ /** Show next and last run as short relative times after the creator. */
189
+ function AutomationTiming({ automation }: { automation: AutomationSummary }) {
190
+ const nextRunAt =
191
+ automation.kind === "scheduled" && automation.status === "active"
192
+ ? automation.nextRunAt
193
+ : undefined;
194
+ const lastRunTitle = automation.lastRunAt
195
+ ? formatAutomationRunTime(automation.lastRunAt)
196
+ : undefined;
197
+ const lastRunFailed =
198
+ automation.lastRunStatus === "failed" ||
199
+ automation.lastRunStatus === "blocked";
200
+ return (
201
+ <>
202
+ {nextRunAt ? (
203
+ <MetaItem title={formatAutomationRunTime(nextRunAt)}>
204
+ Next run {formatRelativeTime(nextRunAt)}
205
+ </MetaItem>
206
+ ) : null}
207
+ <MetaItem title={lastRunTitle}>
208
+ {lastRunFailed ? (
209
+ <Link
210
+ className="text-amber-300 underline"
211
+ to={`/automations/${automation.kind}/${encodeURIComponent(automation.id)}/executions`}
246
212
  >
247
- {status}
248
- </StatusChip>
249
- ) : null}
250
- </div>
251
- </div>
213
+ Last run {automation.lastRunStatus}
214
+ {automation.lastRunAt
215
+ ? ` ${formatRelativeTime(automation.lastRunAt)}`
216
+ : null}
217
+ </Link>
218
+ ) : automation.lastRunAt ? (
219
+ `Last run ${formatRelativeTime(automation.lastRunAt)}`
220
+ ) : (
221
+ "Never run"
222
+ )}
223
+ </MetaItem>
224
+ </>
225
+ );
226
+ }
227
+
228
+ /** Keep each separator with the item after it so wrapped lines never end in a dot. */
229
+ function MetaItem(props: { children: ReactNode; title?: string }) {
230
+ return (
231
+ <span className="whitespace-nowrap" title={props.title}>
232
+ <span aria-hidden="true" className="mr-2 opacity-45">
233
+ ·
234
+ </span>
235
+ {props.children}
236
+ </span>
252
237
  );
253
238
  }
254
239