@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
@@ -14,15 +14,15 @@ import { AutomationsNavigation } from "./AutomationsPageLayout";
14
14
  import { AutomationEditor } from "./AutomationEditor";
15
15
  import { AutomationEditorLoading } from "./AutomationEditorLoading";
16
16
  import { AutomationFormSection } from "./AutomationFormSection";
17
- import { automationOutcomeLabel } from "./automationOutcomes";
17
+ import { AutomationOutcomeList } from "./AutomationOutcomeFields";
18
18
 
19
- /** Load a linkable editor, with readable public values for non-creators. */
19
+ /** Load a linkable editor. Owners and public readers can edit until work completes. */
20
20
  export function AutomationEditPage(props: { enabled: boolean }) {
21
21
  const { kind, automationId } = useParams();
22
22
  const location = useLocation();
23
23
  const summary = useAutomationData(props.enabled, automationId);
24
24
  const canEdit =
25
- summary.data?.ownedByViewer &&
25
+ summary.data &&
26
26
  !(summary.data.kind === "scheduled" && summary.data.status === "completed");
27
27
  const edit = useQuery({
28
28
  queryKey: ["dashboard", "automations", "edit", kind, automationId],
@@ -52,9 +52,8 @@ export function AutomationEditPage(props: { enabled: boolean }) {
52
52
  ? "warning"
53
53
  : "neutral";
54
54
  return (
55
- <>
56
- <AutomationsNavigation />
57
- <div className="mx-auto w-full min-w-0 max-w-5xl px-5 pt-7 sm:px-8 sm:pt-9">
55
+ <AutomationsNavigation>
56
+ <div className="mx-auto w-full min-w-0 max-w-5xl px-4 pt-7 sm:px-8 sm:pt-9">
58
57
  <Link
59
58
  to={returnPath}
60
59
  className="inline-flex min-h-8 items-center gap-2 text-sm text-dashboard-text-muted no-underline hover:text-dashboard-text"
@@ -75,9 +74,17 @@ export function AutomationEditPage(props: { enabled: boolean }) {
75
74
  ) : null}
76
75
  </div>
77
76
  {value ? (
78
- <StatusChip tone={statusTone}>
79
- {status === "unavailable" ? "Trigger unavailable" : status}
80
- </StatusChip>
77
+ <div className="flex items-center gap-4">
78
+ <Link
79
+ to={`/automations/${value.kind}/${encodeURIComponent(value.id)}/versions${location.search}`}
80
+ className="text-sm text-dashboard-text-muted underline underline-offset-2 hover:text-dashboard-text"
81
+ >
82
+ Version history
83
+ </Link>
84
+ <StatusChip tone={statusTone}>
85
+ {status === "unavailable" ? "Trigger unavailable" : status}
86
+ </StatusChip>
87
+ </div>
81
88
  ) : null}
82
89
  </div>
83
90
  {(!value && summary.error) ||
@@ -107,7 +114,7 @@ export function AutomationEditPage(props: { enabled: boolean }) {
107
114
  <AutomationReadOnly automation={value} />
108
115
  )}
109
116
  </div>
110
- </>
117
+ </AutomationsNavigation>
111
118
  );
112
119
  }
113
120
 
@@ -115,9 +122,8 @@ function AutomationReadOnly({ automation }: { automation: AutomationSummary }) {
115
122
  return (
116
123
  <>
117
124
  <p className="mb-6 text-sm text-dashboard-text-muted">
118
- {automation.ownedByViewer
119
- ? "This automation has completed and cannot be edited. It will not run again."
120
- : `Only ${automation.createdBy} can edit this automation.`}
125
+ This automation has completed and cannot be edited. It will not run
126
+ again.
121
127
  </p>
122
128
  <AutomationFormSection title="What to do" detail="The saved instruction.">
123
129
  <p className="m-0 whitespace-pre-wrap break-words text-sm leading-relaxed">
@@ -145,17 +151,10 @@ function AutomationReadOnly({ automation }: { automation: AutomationSummary }) {
145
151
  title="Where results go"
146
152
  detail="Messages after successful work."
147
153
  >
148
- {automation.outcomes.length ? (
149
- <ol className="m-0 grid gap-2 pl-5 text-sm">
150
- {automation.outcomes.map((outcome, index) => (
151
- <li key={index}>
152
- {automationOutcomeLabel(outcome, automation.destination)}
153
- </li>
154
- ))}
155
- </ol>
156
- ) : (
157
- <p className="m-0 text-sm">No success message</p>
158
- )}
154
+ <AutomationOutcomeList
155
+ outcomes={automation.outcomes}
156
+ destination={automation.destination}
157
+ />
159
158
  </AutomationFormSection>
160
159
  <AutomationFormSection
161
160
  title="Credentials"
@@ -16,7 +16,10 @@ import { DashboardApiError, fetchDashboardJson, patch } from "../../http";
16
16
  import { AutomationFormSection } from "./AutomationFormSection";
17
17
  import { AutomationScheduleFields } from "./AutomationScheduleFields";
18
18
  import { AutomationEventFields } from "./AutomationEventFields";
19
- import { AutomationOutcomeFields } from "./AutomationOutcomeFields";
19
+ import {
20
+ AutomationOutcomeFields,
21
+ AutomationOutcomeList,
22
+ } from "./AutomationOutcomeFields";
20
23
  import {
21
24
  automationDraftChanges,
22
25
  createAutomationDraft,
@@ -110,6 +113,14 @@ export function AutomationEditor(props: {
110
113
  .flatMap(([, messages]) => messages)
111
114
  .join(" ") || undefined;
112
115
  const triggerField = original.kind === "scheduled" ? "schedule" : "trigger";
116
+ const owned = original.ownedByViewer;
117
+ const creator = props.summary.createdBy;
118
+ // Core switches a non-creator's execution change to system credentials.
119
+ const dropsCreatorCredentials =
120
+ !owned &&
121
+ original.credentialMode === "creator" &&
122
+ (changes.instruction !== undefined ||
123
+ (original.kind === "event" && changes.trigger !== undefined));
113
124
  const triggerError = fieldError(triggerField);
114
125
  const inputState = (key: string) => ({
115
126
  "aria-invalid": Boolean(fieldError(key)) || undefined,
@@ -223,6 +234,13 @@ export function AutomationEditor(props: {
223
234
  </FormNotice>
224
235
  </div>
225
236
  ) : null}
237
+ {owned ? null : (
238
+ <p className="mt-0 mb-6 max-w-2xl text-sm leading-relaxed text-dashboard-text-muted">
239
+ {creator} created this automation. You can edit it because it is in a
240
+ public channel. Only {creator} can change where results go or turn on
241
+ their connected accounts. Version history keeps every saved change.
242
+ </p>
243
+ )}
226
244
  <fieldset disabled={save.isPending} className="min-w-0">
227
245
  <AutomationFormSection
228
246
  title="What to do"
@@ -300,12 +318,24 @@ export function AutomationEditor(props: {
300
318
  {fieldError("outcomes")}
301
319
  </p>
302
320
  ) : null}
303
- <AutomationOutcomeFields
304
- value={draft.outcomes}
305
- original={original.outcomes}
306
- destination={props.summary.destination}
307
- onChange={(outcomes) => change({ outcomes })}
308
- />
321
+ {owned ? (
322
+ <AutomationOutcomeFields
323
+ value={draft.outcomes}
324
+ original={original.outcomes}
325
+ destination={props.summary.destination}
326
+ onChange={(outcomes) => change({ outcomes })}
327
+ />
328
+ ) : (
329
+ <>
330
+ <AutomationOutcomeList
331
+ outcomes={original.outcomes}
332
+ destination={props.summary.destination}
333
+ />
334
+ <p className="m-0 text-xs text-dashboard-text-muted">
335
+ Only {creator} can change where results go.
336
+ </p>
337
+ </>
338
+ )}
309
339
  </AutomationFormSection>
310
340
  <AutomationFormSection
311
341
  title="Credentials"
@@ -317,25 +347,41 @@ export function AutomationEditor(props: {
317
347
  [
318
348
  [
319
349
  "creator",
320
- "Your connected accounts",
321
- "Uses accounts connected by you, the creator.",
350
+ owned
351
+ ? "Your connected accounts"
352
+ : `${creator}’s connected accounts`,
353
+ owned
354
+ ? "Uses accounts connected by you, the creator."
355
+ : `Uses accounts connected by ${creator}. Only ${creator} can turn this on.`,
322
356
  ],
323
357
  [
324
358
  "system",
325
359
  "System credentials only",
326
- "Does not use your connected accounts. Some work may not be possible.",
360
+ owned
361
+ ? "Does not use your connected accounts. Some work may not be possible."
362
+ : "Does not use anyone’s connected accounts. Some work may not be possible.",
327
363
  ],
328
364
  ] as const
329
365
  ).map(([value, label, detail]) => (
330
366
  <label
331
367
  key={value}
332
- className="flex cursor-pointer items-start gap-3 px-4 py-4 has-checked:bg-dashboard-fill-soft"
368
+ className="flex cursor-pointer items-start gap-3 px-4 py-4 has-checked:bg-dashboard-fill-soft has-disabled:cursor-not-allowed has-disabled:opacity-60"
333
369
  >
334
370
  <input
335
371
  type="radio"
336
372
  name="credentials"
337
373
  className="mt-0.5 size-4 shrink-0 accent-dashboard-focus"
338
- checked={draft.credentialMode === value}
374
+ checked={
375
+ dropsCreatorCredentials
376
+ ? value === "system"
377
+ : draft.credentialMode === value
378
+ }
379
+ disabled={
380
+ value === "creator" &&
381
+ !owned &&
382
+ (original.credentialMode !== "creator" ||
383
+ dropsCreatorCredentials)
384
+ }
339
385
  onChange={() => change({ credentialMode: value })}
340
386
  />
341
387
  <span>
@@ -347,6 +393,13 @@ export function AutomationEditor(props: {
347
393
  </label>
348
394
  ))}
349
395
  </fieldset>
396
+ {dropsCreatorCredentials ? (
397
+ <FormNotice title="Saving switches this automation to system credentials.">
398
+ Only {creator} can let a changed instruction
399
+ {original.kind === "event" ? " or trigger" : ""} use their
400
+ connected accounts.
401
+ </FormNotice>
402
+ ) : null}
350
403
  {fieldError("credentialMode") ? (
351
404
  <p role="alert">{fieldError("credentialMode")}</p>
352
405
  ) : null}
@@ -355,7 +408,7 @@ export function AutomationEditor(props: {
355
408
  <p className="mb-5 text-xs leading-relaxed text-dashboard-text-muted sm:hidden">
356
409
  Changes apply to future work. Saving does not run this automation.
357
410
  </p>
358
- <div className="sticky bottom-0 z-20 -mx-5 border-t border-dashboard-border-emphasis bg-dashboard-bg px-5 py-4 sm:-mx-8 sm:px-8">
411
+ <div className="sticky bottom-0 z-20 -mx-4 border-t border-dashboard-border-emphasis bg-dashboard-bg px-4 py-4 sm:-mx-8 sm:px-8">
359
412
  <div className="flex items-center justify-between gap-3">
360
413
  <div className="min-w-0 flex-1" role="status">
361
414
  <p className="m-0 text-xs font-medium sm:text-sm">
@@ -6,6 +6,7 @@ import type {
6
6
  AutomationListQuery,
7
7
  } from "@sentry/junior/api/schema";
8
8
  import { Button, ToggleButton } from "../../components/Button";
9
+ import { DirectorySortSelect } from "../../components/controls/DirectorySortSelect";
9
10
  import { FilterGroup } from "../../components/FilterBar";
10
11
  import { SearchInput } from "../../components/SearchInput";
11
12
 
@@ -25,7 +26,6 @@ export function AutomationFilters(props: {
25
26
  Boolean(filters.creator),
26
27
  Boolean(filters.destination),
27
28
  filters.state !== "all",
28
- filters.sort !== "newest",
29
29
  ].filter(Boolean).length;
30
30
  return (
31
31
  <div className="grid gap-3">
@@ -76,12 +76,12 @@ export function AutomationFilters(props: {
76
76
  <div
77
77
  id={controlsId}
78
78
  className={cn(
79
- "grid-cols-2 items-end gap-3 lg:grid-cols-4 xl:grid-cols-[auto_repeat(4,minmax(0,1fr))]",
79
+ "grid-cols-2 items-end gap-3 lg:grid-cols-3 xl:grid-cols-[auto_repeat(3,minmax(0,1fr))]",
80
80
  expanded ? "grid" : "hidden",
81
81
  )}
82
82
  >
83
83
  <FilterGroup
84
- className="col-span-2 lg:col-span-4 xl:col-span-1"
84
+ className="col-span-2 lg:col-span-3 xl:col-span-1"
85
85
  label="Type"
86
86
  >
87
87
  {(["all", "scheduled", "event"] as const).map((value) => (
@@ -123,22 +123,30 @@ export function AutomationFilters(props: {
123
123
  ]}
124
124
  onChange={(value) => onChange("state", value)}
125
125
  />
126
- <AutomationFilterSelect
127
- label="Sort"
128
- ariaLabel="Sort automations"
129
- value={filters.sort}
130
- onChange={(value) => onChange("sort", value)}
131
- options={[
132
- { value: "newest", label: "Newest first" },
133
- { value: "oldest", label: "Oldest first" },
134
- { value: "title", label: "Title" },
135
- ]}
136
- />
137
126
  </div>
138
127
  </div>
139
128
  );
140
129
  }
141
130
 
131
+ /** Keep list order visible beside the result count instead of inside Filters. */
132
+ export function AutomationSort(props: {
133
+ value: AutomationListQuery["sort"];
134
+ onChange(value: string): void;
135
+ }) {
136
+ return (
137
+ <DirectorySortSelect
138
+ ariaLabel="Sort automations"
139
+ onChange={props.onChange}
140
+ options={[
141
+ { value: "newest", label: "Recently created" },
142
+ { value: "oldest", label: "Oldest created" },
143
+ { value: "title", label: "Title A–Z" },
144
+ ]}
145
+ value={props.value}
146
+ />
147
+ );
148
+ }
149
+
142
150
  function AutomationFilterSelect(props: {
143
151
  label: string;
144
152
  ariaLabel?: string;
@@ -9,7 +9,7 @@ import { StatusChip } from "../../components/StatusChip";
9
9
  import { FormNotice } from "../../components/FormNotice";
10
10
  import { DashboardApiError, fetchDashboardJson, post } from "../../http";
11
11
 
12
- /** Explain lifecycle separately from the last execution and offer creator-only actions. */
12
+ /** Explain lifecycle separately from the last execution. Pause and resume are creator-only. */
13
13
  export function AutomationLifecycle({
14
14
  automation,
15
15
  editPath,
@@ -23,7 +23,9 @@ export function AutomationLifecycle({
23
23
  const blocked = automation.status === "blocked";
24
24
  const unavailable =
25
25
  automation.kind === "event" && !automation.triggerAvailable;
26
- const canEdit = automation.ownedByViewer && !completed;
26
+ // Public readers can edit settings, but only the creator controls lifecycle.
27
+ const canEdit = !completed;
28
+ const canPause = automation.ownedByViewer && !completed;
27
29
  const blockReason =
28
30
  automation.kind === "scheduled" ? automation.statusReason : undefined;
29
31
  const action = paused || blocked ? "resume" : "pause";
@@ -78,7 +80,7 @@ export function AutomationLifecycle({
78
80
  <ButtonLink className="min-h-11" to={editPath}>
79
81
  {canEdit ? "Edit automation" : "View settings"}
80
82
  </ButtonLink>
81
- {canEdit ? (
83
+ {canPause ? (
82
84
  <Button
83
85
  className="min-h-11"
84
86
  disabled={mutation.isPending}
@@ -119,7 +121,7 @@ export function AutomationLifecycle({
119
121
  </Link>
120
122
  </p>
121
123
  ) : null}
122
- {canEdit ? (
124
+ {canPause ? (
123
125
  <details className="text-dashboard-text-muted">
124
126
  <summary className="w-fit cursor-pointer py-2 text-xs hover:text-dashboard-text focus-visible:outline focus-visible:outline-dashboard-focus">
125
127
  About pausing
@@ -9,6 +9,24 @@ import { cn } from "../../styles";
9
9
  import type { AutomationDraft } from "./automationDraft";
10
10
  import { automationOutcomeLabel } from "./automationOutcomes";
11
11
 
12
+ /** Show saved outcomes in order for a reader who cannot change them. */
13
+ export function AutomationOutcomeList(props: {
14
+ outcomes: AutomationEdit["outcomes"];
15
+ destination: AutomationSummary["destination"];
16
+ }) {
17
+ if (!props.outcomes.length)
18
+ return <p className="m-0 text-sm">No success message</p>;
19
+ return (
20
+ <ol className="m-0 grid gap-2 pl-5 text-sm">
21
+ {props.outcomes.map((outcome, index) => (
22
+ <li key={index}>
23
+ {automationOutcomeLabel(outcome, props.destination)}
24
+ </li>
25
+ ))}
26
+ </ol>
27
+ );
28
+ }
29
+
12
30
  /** Keep outcome order and offer only supported message Destinations. */
13
31
  export function AutomationOutcomeFields(props: {
14
32
  value: AutomationDraft["outcomes"];