@sentry/junior-dashboard 0.264.0 → 0.266.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 (213) 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/ActorAvatar.tsx +10 -22
  14. package/src/client/components/BottomSheet.tsx +12 -2
  15. package/src/client/components/ConnectionBanner.tsx +4 -3
  16. package/src/client/components/CopyButton.tsx +82 -0
  17. package/src/client/components/DetailsPanel.tsx +133 -0
  18. package/src/client/components/Drawer.tsx +15 -24
  19. package/src/client/components/EmptyTelemetry.tsx +5 -3
  20. package/src/client/components/FilterBar.tsx +16 -31
  21. package/src/client/components/ImageAttachment.tsx +49 -63
  22. package/src/client/components/InlineError.tsx +8 -4
  23. package/src/client/components/Metric.tsx +33 -23
  24. package/src/client/components/Notice.tsx +19 -26
  25. package/src/client/components/PageContentSkeleton.tsx +1 -1
  26. package/src/client/components/Pagination.tsx +27 -27
  27. package/src/client/components/ParticipantAvatarStack.tsx +43 -39
  28. package/src/client/components/ProfileMenu.tsx +27 -93
  29. package/src/client/components/README.md +103 -78
  30. package/src/client/components/SearchInput.tsx +51 -41
  31. package/src/client/components/SegmentedNav.tsx +1 -1
  32. package/src/client/components/ShimmerText.tsx +2 -6
  33. package/src/client/components/ThemeSwitcher.tsx +19 -15
  34. package/src/client/components/VersionDriftBanner.tsx +9 -7
  35. package/src/client/components/charts/ActivityChart.tsx +173 -288
  36. package/src/client/components/charts/CacheWrites.tsx +59 -37
  37. package/src/client/components/charts/InputCacheChart.tsx +137 -131
  38. package/src/client/components/charts/README.md +4 -4
  39. package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
  40. package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
  41. package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
  42. package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
  43. package/src/client/components/layout/DashboardHeader.tsx +44 -90
  44. package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
  45. package/src/client/components/layout/VisualViewportShell.tsx +2 -2
  46. package/src/client/components/ui/accordion.tsx +77 -0
  47. package/src/client/components/ui/alert-dialog.tsx +89 -0
  48. package/src/client/components/ui/alert.tsx +64 -0
  49. package/src/client/components/ui/avatar.tsx +53 -0
  50. package/src/client/components/ui/button.tsx +86 -0
  51. package/src/client/components/ui/chart.tsx +74 -0
  52. package/src/client/components/ui/checkbox.tsx +26 -0
  53. package/src/client/components/ui/collapsible.tsx +22 -0
  54. package/src/client/components/ui/dialog.tsx +66 -0
  55. package/src/client/components/ui/empty.tsx +32 -0
  56. package/src/client/components/ui/field.tsx +49 -0
  57. package/src/client/components/ui/input-group.tsx +96 -0
  58. package/src/client/components/ui/input.tsx +35 -0
  59. package/src/client/components/ui/native-select.tsx +57 -0
  60. package/src/client/components/ui/pagination.tsx +60 -0
  61. package/src/client/components/ui/popover.tsx +67 -0
  62. package/src/client/components/ui/radio-group.tsx +32 -0
  63. package/src/client/components/ui/sheet.tsx +13 -3
  64. package/src/client/components/ui/skeleton.tsx +19 -0
  65. package/src/client/components/ui/spinner.tsx +21 -0
  66. package/src/client/components/ui/table.tsx +101 -0
  67. package/src/client/components/ui/tabs.tsx +75 -0
  68. package/src/client/components/ui/textarea.tsx +17 -0
  69. package/src/client/components/ui/toast.tsx +215 -0
  70. package/src/client/components/ui/toggle-group.tsx +36 -0
  71. package/src/client/components/ui/toggle.tsx +37 -0
  72. package/src/client/components/ui/tooltip.tsx +168 -0
  73. package/src/client/components/ui/trace.tsx +96 -0
  74. package/src/client/conversations/ChatLayout.tsx +7 -3
  75. package/src/client/conversations/ConversationBrief.tsx +17 -6
  76. package/src/client/conversations/ConversationChildAgents.tsx +5 -5
  77. package/src/client/conversations/ConversationComposer.tsx +26 -12
  78. package/src/client/conversations/ConversationDetails.tsx +147 -62
  79. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  80. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  81. package/src/client/conversations/ConversationHeader.tsx +13 -7
  82. package/src/client/conversations/ConversationHeaderActions.tsx +97 -93
  83. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  84. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  85. package/src/client/conversations/ConversationMeta.tsx +55 -88
  86. package/src/client/conversations/ConversationPage.tsx +220 -101
  87. package/src/client/conversations/ConversationSidebar.tsx +49 -29
  88. package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
  89. package/src/client/conversations/ConversationWatches.tsx +173 -0
  90. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  91. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  92. package/src/client/conversations/DetailsList.tsx +139 -0
  93. package/src/client/conversations/EventDetails.tsx +42 -8
  94. package/src/client/conversations/EventUsage.tsx +99 -74
  95. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  96. package/src/client/conversations/ObjectCard.tsx +20 -8
  97. package/src/client/conversations/PendingMailboxStack.tsx +52 -38
  98. package/src/client/conversations/README.md +89 -13
  99. package/src/client/conversations/ShareConversationButton.tsx +28 -17
  100. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  101. package/src/client/conversations/ToolFrame.tsx +36 -52
  102. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  103. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  104. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  105. package/src/client/conversations/TranscriptEventView.tsx +35 -16
  106. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  107. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  108. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  109. package/src/client/conversations/TranscriptMessageContextView.tsx +35 -18
  110. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  111. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  112. package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
  113. package/src/client/conversations/TranscriptSummary.tsx +34 -2
  114. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  115. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  116. package/src/client/conversations/TranscriptTurnContextView.tsx +144 -65
  117. package/src/client/conversations/TranscriptView.tsx +63 -27
  118. package/src/client/conversations/eventLog.ts +58 -0
  119. package/src/client/conversations/eventTime.ts +60 -0
  120. package/src/client/conversations/queries.ts +29 -0
  121. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  122. package/src/client/conversations/transcriptMentions.tsx +18 -5
  123. package/src/client/format.ts +5 -0
  124. package/src/client/pages/ConnectionsPage.tsx +2 -2
  125. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  126. package/src/client/pages/SettingsPage.tsx +9 -10
  127. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  128. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  129. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  130. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  131. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  132. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  133. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  134. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  135. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  136. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  137. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  138. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  139. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  140. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  141. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  142. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  143. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  144. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  145. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  146. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  147. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  148. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  149. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  150. package/src/client/pages/automations/README.md +57 -0
  151. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  152. package/src/client/pages/automations/automationFormat.ts +37 -23
  153. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  154. package/src/client/pages/code/CodePage.tsx +52 -55
  155. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  156. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  157. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  158. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  159. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  160. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  161. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  162. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  163. package/src/client/pages/memory/memoryRecord.ts +1 -1
  164. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  165. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  166. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  167. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  168. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  169. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  170. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  171. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  172. package/src/client/pages/system/README.md +23 -0
  173. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  174. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  175. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  176. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  177. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  178. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  179. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  180. package/src/client/pages/system/sandboxSize.ts +8 -0
  181. package/src/client/pages/system/workspaceQueries.ts +53 -0
  182. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  183. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  184. package/src/client/styles.ts +4 -0
  185. package/src/client.tsx +7 -1
  186. package/src/mock-reporting/automation-collection.ts +4 -2
  187. package/src/mock-reporting/fixtures.ts +23 -7
  188. package/src/mock-reporting/routes.ts +12 -0
  189. package/src/mock-reporting/watches.ts +94 -0
  190. package/src/shell.ts +4 -9
  191. package/src/tailwind.css +0 -10
  192. package/src/client/components/Button.tsx +0 -118
  193. package/src/client/components/Field.tsx +0 -53
  194. package/src/client/components/SegmentedTabs.tsx +0 -119
  195. package/src/client/components/Select.tsx +0 -52
  196. package/src/client/components/Skeleton.tsx +0 -17
  197. package/src/client/components/TextInput.tsx +0 -58
  198. package/src/client/components/Tooltip.tsx +0 -216
  199. package/src/client/components/charts/useChartLayout.ts +0 -27
  200. package/src/client/components/ui/sheet.LICENSE +0 -21
  201. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -119
  202. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  203. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  204. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  205. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  206. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  207. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  208. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  209. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  210. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  211. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  212. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  213. package/src/client/pages/system/SnapshotSummary.tsx +0 -78
@@ -8,10 +8,13 @@ import {
8
8
  type AutomationEdit,
9
9
  type AutomationSummary,
10
10
  } from "@sentry/junior/api/schema";
11
- import { Field } from "../../components/Field";
12
- import { TextArea, TextInput } from "../../components/TextInput";
13
- import { Button } from "../../components/Button";
11
+ import { Button } from "../../components/ui/button";
12
+ import { Input } from "../../components/ui/input";
13
+ import { Textarea } from "../../components/ui/textarea";
14
+ import { Field, FieldError, FieldLabel } from "../../components/ui/field";
15
+ import { useToastManager } from "../../components/ui/toast";
14
16
  import { FormNotice } from "../../components/FormNotice";
17
+ import { RadioGroup, RadioGroupItem } from "../../components/ui/radio-group";
15
18
  import { DashboardApiError, fetchDashboardJson, patch } from "../../http";
16
19
  import { AutomationFormSection } from "./AutomationFormSection";
17
20
  import { AutomationScheduleFields } from "./AutomationScheduleFields";
@@ -33,6 +36,7 @@ export function AutomationEditor(props: {
33
36
  summary: AutomationSummary;
34
37
  returnPath: string;
35
38
  }) {
39
+ const toast = useToastManager();
36
40
  const [original, setOriginal] = useState(props.automation);
37
41
  const [draft, setDraft] = useState(() =>
38
42
  createAutomationDraft(props.automation),
@@ -80,7 +84,12 @@ export function AutomationEditor(props: {
80
84
  await queryClient.invalidateQueries({
81
85
  queryKey: ["dashboard", "automations"],
82
86
  });
83
- navigate(props.returnPath, { state: { automationSaved: true } });
87
+ toast.add({
88
+ title: "Automation saved",
89
+ description: "Changes apply to future work.",
90
+ type: "success",
91
+ });
92
+ navigate(props.returnPath);
84
93
  },
85
94
  onError: (error) => {
86
95
  setFields(error instanceof DashboardApiError ? (error.fields ?? {}) : {});
@@ -246,8 +255,9 @@ export function AutomationEditor(props: {
246
255
  title="What to do"
247
256
  detail="Give Junior a clear instruction and a name you can find later."
248
257
  >
249
- <Field label="Title" htmlFor="title" error={fieldError("title")}>
250
- <TextInput
258
+ <Field>
259
+ <FieldLabel htmlFor="title">Title</FieldLabel>
260
+ <Input
251
261
  size="comfortable"
252
262
  id="title"
253
263
  value={draft.title}
@@ -255,24 +265,24 @@ export function AutomationEditor(props: {
255
265
  onChange={(e) => change({ title: e.target.value })}
256
266
  {...inputState("title")}
257
267
  />
268
+ <FieldError id="title-error">{fieldError("title")}</FieldError>
258
269
  </Field>
259
- <Field
260
- label="Instruction"
261
- htmlFor="instruction"
262
- error={fieldError("instruction")}
263
- >
264
- <TextArea
265
- prose
270
+ <Field>
271
+ <FieldLabel htmlFor="instruction">Instruction</FieldLabel>
272
+ <Textarea
266
273
  id="instruction"
267
274
  rows={8}
268
275
  value={draft.instruction}
269
276
  onChange={(e) => change({ instruction: e.target.value })}
270
277
  {...inputState("instruction")}
271
- className="resize-y text-base sm:text-sm"
278
+ className="resize-y text-base leading-relaxed sm:text-sm"
272
279
  />
273
280
  <p className="m-0 text-right text-xs text-dashboard-text-muted">
274
281
  {draft.instruction.length.toLocaleString()} / 4,000
275
282
  </p>
283
+ <FieldError id="instruction-error">
284
+ {fieldError("instruction")}
285
+ </FieldError>
276
286
  </Field>
277
287
  </AutomationFormSection>
278
288
  <AutomationFormSection
@@ -304,6 +314,7 @@ export function AutomationEditor(props: {
304
314
  <AutomationEventFields
305
315
  value={draft.trigger ?? original.trigger}
306
316
  original={original.trigger}
317
+ disabled={save.isPending}
307
318
  onChange={(trigger) => change({ trigger })}
308
319
  />
309
320
  )}
@@ -323,6 +334,7 @@ export function AutomationEditor(props: {
323
334
  value={draft.outcomes}
324
335
  original={original.outcomes}
325
336
  destination={props.summary.destination}
337
+ disabled={save.isPending}
326
338
  onChange={(outcomes) => change({ outcomes })}
327
339
  />
328
340
  ) : (
@@ -341,8 +353,15 @@ export function AutomationEditor(props: {
341
353
  title="Credentials"
342
354
  detail="Choose which connected accounts Junior can use."
343
355
  >
344
- <fieldset className="divide-y divide-dashboard-border-subtle rounded-md border border-dashboard-border-emphasis">
345
- <legend className="sr-only">Credential mode</legend>
356
+ <RadioGroup
357
+ aria-label="Credential mode"
358
+ disabled={save.isPending}
359
+ className="divide-y divide-dashboard-border-subtle rounded-md border border-dashboard-border-emphasis"
360
+ onValueChange={(credentialMode: "creator" | "system") =>
361
+ change({ credentialMode })
362
+ }
363
+ value={dropsCreatorCredentials ? "system" : draft.credentialMode}
364
+ >
346
365
  {(
347
366
  [
348
367
  [
@@ -365,24 +384,17 @@ export function AutomationEditor(props: {
365
384
  ).map(([value, label, detail]) => (
366
385
  <label
367
386
  key={value}
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"
387
+ className="flex cursor-pointer items-start gap-3 px-4 py-4 has-data-checked:bg-dashboard-fill-soft has-data-disabled:cursor-not-allowed has-data-disabled:opacity-60"
369
388
  >
370
- <input
371
- type="radio"
372
- name="credentials"
373
- className="mt-0.5 size-4 shrink-0 accent-dashboard-focus"
374
- checked={
375
- dropsCreatorCredentials
376
- ? value === "system"
377
- : draft.credentialMode === value
378
- }
389
+ <RadioGroupItem
390
+ className="mt-0.5"
391
+ value={value}
379
392
  disabled={
380
393
  value === "creator" &&
381
394
  !owned &&
382
395
  (original.credentialMode !== "creator" ||
383
396
  dropsCreatorCredentials)
384
397
  }
385
- onChange={() => change({ credentialMode: value })}
386
398
  />
387
399
  <span>
388
400
  <span className="block text-sm font-medium">{label}</span>
@@ -392,7 +404,7 @@ export function AutomationEditor(props: {
392
404
  </span>
393
405
  </label>
394
406
  ))}
395
- </fieldset>
407
+ </RadioGroup>
396
408
  {dropsCreatorCredentials ? (
397
409
  <FormNotice title="Saving switches this automation to system credentials.">
398
410
  Only {creator} can let a changed instruction
@@ -431,7 +443,7 @@ export function AutomationEditor(props: {
431
443
  </Link>
432
444
  <Button
433
445
  type="submit"
434
- tone="primary"
446
+ variant="primary"
435
447
  className="min-h-11 sm:min-h-9"
436
448
  disabled={!dirty || save.isPending || conflict}
437
449
  >
@@ -1,4 +1,4 @@
1
- import { Skeleton } from "../../components/Skeleton";
1
+ import { Skeleton } from "../../components/ui/skeleton";
2
2
  import { AutomationFormSection } from "./AutomationFormSection";
3
3
 
4
4
  /** Keep the editor's field columns stable while exact values load. */
@@ -1,13 +1,37 @@
1
+ import { useState } from "react";
1
2
  import { useQuery } from "@tanstack/react-query";
3
+ import {
4
+ describeEventTrigger,
5
+ eventMatchFilterText,
6
+ eventTriggerProblems,
7
+ eventTypeText,
8
+ eventsWithoutMatchField,
9
+ } from "@sentry/junior-plugin-api";
2
10
  import {
3
11
  automationEventCatalogSchema,
4
12
  type AutomationEdit,
5
13
  } from "@sentry/junior/api/schema";
6
- import { Field } from "../../components/Field";
7
- import { Select } from "../../components/Select";
8
- import { TextInput } from "../../components/TextInput";
9
- import { Button } from "../../components/Button";
14
+ import {
15
+ AlertDialog,
16
+ AlertDialogAction,
17
+ AlertDialogCancel,
18
+ AlertDialogContent,
19
+ AlertDialogDescription,
20
+ AlertDialogFooter,
21
+ AlertDialogTitle,
22
+ } from "../../components/ui/alert-dialog";
23
+ import { Button } from "../../components/ui/button";
24
+ import { Input } from "../../components/ui/input";
25
+ import {
26
+ Field,
27
+ FieldDescription,
28
+ FieldError,
29
+ FieldLabel,
30
+ } from "../../components/ui/field";
31
+ import { NativeSelect } from "../../components/ui/native-select";
10
32
  import { FormNotice } from "../../components/FormNotice";
33
+ import { TriggerFilters } from "./TriggerFilters";
34
+ import { Checkbox } from "../../components/ui/checkbox";
11
35
  import { fetchDashboardJson } from "../../http";
12
36
 
13
37
  type Trigger = Extract<AutomationEdit, { kind: "event" }>["trigger"];
@@ -16,8 +40,11 @@ type Trigger = Extract<AutomationEdit, { kind: "event" }>["trigger"];
16
40
  export function AutomationEventFields(props: {
17
41
  value: Trigger;
18
42
  original: Trigger;
43
+ /** Base UI checkboxes do not inherit a native fieldset's disabled state. */
44
+ disabled?: boolean;
19
45
  onChange(value: Trigger): void;
20
46
  }) {
47
+ const [replacement, setReplacement] = useState<Trigger>();
21
48
  const catalog = useQuery({
22
49
  queryKey: ["dashboard", "automation-event-catalog"],
23
50
  queryFn: ({ signal }) =>
@@ -28,25 +55,25 @@ export function AutomationEventFields(props: {
28
55
  ),
29
56
  retry: false,
30
57
  });
31
- const resource = catalog.data?.find(
32
- (item) =>
33
- item.namespace === props.value.namespace &&
34
- item.type === props.value.resourceType,
58
+ const sourceResourceTypes =
59
+ catalog.data?.filter((item) => item.namespace === props.value.namespace) ??
60
+ [];
61
+ const resource = sourceResourceTypes.find(
62
+ (item) => item.type === props.value.resourceType,
35
63
  );
64
+ const problems = catalog.data
65
+ ? eventTriggerProblems(sourceResourceTypes, props.value)
66
+ : [];
67
+ // A filter that a selected event does not set is fixable here. Other
68
+ // problems mean the catalog changed after the trigger was saved.
36
69
  const unsupported =
37
- !resource ||
38
- props.value.events.some(
39
- (event) => !resource.supportedEvents.includes(event),
40
- ) ||
41
- Object.entries(props.value.match ?? {}).some(([key, value]) => {
42
- const field = resource.matchFields?.[key];
43
- if (!field) return true;
44
- return (Array.isArray(value) ? value : [value]).some(
45
- (item) =>
46
- typeof item !== field.kind ||
47
- (field.enum && !field.enum.includes(String(item))),
48
- );
49
- });
70
+ !resource || problems.some((problem) => problem.reason !== "field_not_set");
71
+ const matchFields = Object.entries(resource?.matchFields ?? {});
72
+ const shownMatchFields = matchFields.filter(
73
+ ([key, field]) =>
74
+ props.value.match?.[key] !== undefined ||
75
+ eventsWithoutMatchField(field, props.value.events).length === 0,
76
+ );
50
77
  const setMatch = (
51
78
  key: string,
52
79
  value: NonNullable<Trigger["match"]>[string] | undefined,
@@ -70,16 +97,17 @@ export function AutomationEventFields(props: {
70
97
  <FormNotice title="Saved trigger · Kept unchanged">
71
98
  Some saved values are not in the current catalog. You can edit other
72
99
  fields, or select a new resource type to replace the trigger.
73
- <pre className="whitespace-pre-wrap break-all text-xs">
74
- {JSON.stringify(props.value, null, 2)}
75
- </pre>
100
+ <SavedTrigger
101
+ resourceTypes={sourceResourceTypes}
102
+ trigger={props.value}
103
+ />
76
104
  </FormNotice>
77
105
  ) : null}
78
- <Field
79
- label={unsupported ? "Replace trigger" : "Resource type"}
80
- htmlFor="trigger-resource"
81
- >
82
- <Select
106
+ <Field>
107
+ <FieldLabel htmlFor="trigger-resource">
108
+ {unsupported ? "Replace trigger" : "Resource type"}
109
+ </FieldLabel>
110
+ <NativeSelect
83
111
  id="trigger-resource"
84
112
  value={
85
113
  unsupported
@@ -91,13 +119,7 @@ export function AutomationEventFields(props: {
91
119
  (item) => `${item.namespace}:${item.type}` === e.target.value,
92
120
  );
93
121
  if (!next) return;
94
- if (
95
- !window.confirm(
96
- "Replace the saved trigger? Its resource, events, and conditions will be cleared.",
97
- )
98
- )
99
- return;
100
- props.onChange({
122
+ setReplacement({
101
123
  namespace: next.namespace,
102
124
  resourceType: next.type,
103
125
  identifier: "",
@@ -117,12 +139,38 @@ export function AutomationEventFields(props: {
117
139
  {item.namespace} · {item.type.replaceAll("_", " ")}
118
140
  </option>
119
141
  ))}
120
- </Select>
142
+ </NativeSelect>
121
143
  </Field>
144
+ <AlertDialog
145
+ onOpenChange={(open) => {
146
+ if (!open) setReplacement(undefined);
147
+ }}
148
+ open={replacement !== undefined}
149
+ >
150
+ <AlertDialogContent>
151
+ <AlertDialogTitle>Replace the saved trigger?</AlertDialogTitle>
152
+ <AlertDialogDescription>
153
+ Its resource, events, and filters will be cleared.
154
+ </AlertDialogDescription>
155
+ <AlertDialogFooter>
156
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
157
+ <AlertDialogAction
158
+ onClick={() => {
159
+ if (replacement) props.onChange(replacement);
160
+ setReplacement(undefined);
161
+ }}
162
+ variant="primary"
163
+ >
164
+ Replace trigger
165
+ </AlertDialogAction>
166
+ </AlertDialogFooter>
167
+ </AlertDialogContent>
168
+ </AlertDialog>
122
169
  {!unsupported && resource ? (
123
170
  <>
124
- <Field label="Resource name" htmlFor="trigger-label">
125
- <TextInput
171
+ <Field>
172
+ <FieldLabel htmlFor="trigger-label">Resource name</FieldLabel>
173
+ <Input
126
174
  size="comfortable"
127
175
  id="trigger-label"
128
176
  value={props.value.label}
@@ -131,12 +179,11 @@ export function AutomationEventFields(props: {
131
179
  }
132
180
  />
133
181
  </Field>
134
- <Field
135
- label="Resource identifier"
136
- htmlFor="trigger-identifier"
137
- help="Use the exact provider resource identifier, not its display name."
138
- >
139
- <TextInput
182
+ <Field>
183
+ <FieldLabel htmlFor="trigger-identifier">
184
+ Resource identifier
185
+ </FieldLabel>
186
+ <Input
140
187
  size="comfortable"
141
188
  id="trigger-identifier"
142
189
  value={props.value.identifier}
@@ -144,6 +191,9 @@ export function AutomationEventFields(props: {
144
191
  props.onChange({ ...props.value, identifier: e.target.value })
145
192
  }
146
193
  />
194
+ <FieldDescription id="trigger-identifier-help">
195
+ Use the exact provider resource identifier, not its display name.
196
+ </FieldDescription>
147
197
  </Field>
148
198
  <fieldset>
149
199
  <legend className="mb-2 text-sm font-semibold">
@@ -155,11 +205,11 @@ export function AutomationEventFields(props: {
155
205
  key={event}
156
206
  className="flex min-h-11 cursor-pointer items-start gap-3 px-3 py-3 text-sm"
157
207
  >
158
- <input
159
- type="checkbox"
160
- className="mt-0.5 size-4 shrink-0 accent-dashboard-focus"
208
+ <Checkbox
209
+ className="mt-0.5"
161
210
  checked={props.value.events.includes(event)}
162
- onChange={() =>
211
+ disabled={props.disabled}
212
+ onCheckedChange={() =>
163
213
  props.onChange({
164
214
  ...props.value,
165
215
  events: props.value.events.includes(event)
@@ -169,84 +219,89 @@ export function AutomationEventFields(props: {
169
219
  }
170
220
  />
171
221
  <span className="min-w-0 break-words">
172
- {event.replaceAll("_", " ").replaceAll(".", " · ")}
173
- {resource.guidance?.[event] ? (
174
- <span className="mt-1 block text-xs leading-relaxed text-dashboard-text-muted">
175
- {resource.guidance[event]}
176
- </span>
177
- ) : null}
222
+ {eventTypeText(event)}
178
223
  </span>
179
224
  </label>
180
225
  ))}
181
226
  </div>
182
227
  </fieldset>
183
- {Object.keys(resource.matchFields ?? {}).length ? (
228
+ {matchFields.length ? (
184
229
  <fieldset className="grid gap-4">
185
- <legend className="mb-3 text-sm font-semibold">Only when</legend>
230
+ <legend className="mb-3 text-sm font-semibold">Filters</legend>
186
231
  <p className="m-0 text-xs leading-relaxed text-dashboard-text-muted">
187
- All conditions must match. Leave a field blank to match any
188
- value. Existing lists match any one listed value.
232
+ Every filter must match. Leave a filter blank to match any
233
+ value.
234
+ {shownMatchFields.length < matchFields.length
235
+ ? " Filters that a selected event does not set are hidden."
236
+ : null}
189
237
  </p>
190
- {Object.entries(resource.matchFields ?? {}).map(
191
- ([key, field]) => {
192
- const value = props.value.match?.[key];
193
- return (
194
- <Field
195
- key={key}
196
- label={key}
197
- htmlFor={`match-${key}`}
198
- help={field.description}
199
- >
200
- {Array.isArray(value) ? (
201
- <div className="text-sm">
202
- <span className="break-all">{value.join(", ")}</span>{" "}
203
- <Button onClick={() => setMatch(key, undefined)}>
204
- Remove condition
205
- </Button>
206
- </div>
207
- ) : field.kind === "boolean" || field.enum ? (
208
- <Select
209
- id={`match-${key}`}
210
- value={value === undefined ? "" : String(value)}
211
- onChange={(e) =>
212
- setMatch(
213
- key,
214
- e.target.value === ""
215
- ? undefined
216
- : field.kind === "boolean"
217
- ? e.target.value === "true"
218
- : e.target.value,
219
- )
220
- }
221
- >
222
- <option value="">Any value</option>
223
- {(field.enum ?? ["true", "false"]).map((v) => (
224
- <option key={v}>{v}</option>
225
- ))}
226
- </Select>
227
- ) : (
228
- <TextInput
229
- size="comfortable"
230
- id={`match-${key}`}
231
- type={field.kind === "number" ? "number" : "text"}
232
- step="any"
233
- value={value === undefined ? "" : String(value)}
234
- onChange={(e) =>
235
- setMatch(
236
- key,
237
- e.target.value === ""
238
- ? undefined
239
- : field.kind === "number"
240
- ? e.target.valueAsNumber
241
- : e.target.value,
242
- )
243
- }
244
- />
245
- )}
246
- </Field>
247
- );
248
- },
249
- )}
238
+ {shownMatchFields.map(([key, field]) => {
239
+ const value = props.value.match?.[key];
240
+ const problem = problems.find(
241
+ (candidate) => candidate.field === key,
242
+ );
243
+ return (
244
+ <Field key={key}>
245
+ <FieldLabel htmlFor={`match-${key}`}>{key}</FieldLabel>
246
+ {Array.isArray(value) ? (
247
+ <div className="text-sm">
248
+ <span className="break-all">
249
+ {eventMatchFilterText(key, field, value)}
250
+ </span>{" "}
251
+ <Button onClick={() => setMatch(key, undefined)}>
252
+ Remove filter
253
+ </Button>
254
+ </div>
255
+ ) : field.kind === "boolean" || field.enum ? (
256
+ <NativeSelect
257
+ id={`match-${key}`}
258
+ value={value === undefined ? "" : String(value)}
259
+ onChange={(e) =>
260
+ setMatch(
261
+ key,
262
+ e.target.value === ""
263
+ ? undefined
264
+ : field.kind === "boolean"
265
+ ? e.target.value === "true"
266
+ : e.target.value,
267
+ )
268
+ }
269
+ >
270
+ <option value="">Any value</option>
271
+ {(field.enum ?? ["true", "false"]).map((v) => (
272
+ <option key={v}>{v}</option>
273
+ ))}
274
+ </NativeSelect>
275
+ ) : (
276
+ <Input
277
+ size="comfortable"
278
+ id={`match-${key}`}
279
+ type={field.kind === "number" ? "number" : "text"}
280
+ step="any"
281
+ value={value === undefined ? "" : String(value)}
282
+ onChange={(e) =>
283
+ setMatch(
284
+ key,
285
+ e.target.value === ""
286
+ ? undefined
287
+ : field.kind === "number"
288
+ ? e.target.valueAsNumber
289
+ : e.target.value,
290
+ )
291
+ }
292
+ />
293
+ )}
294
+ {field.description ? (
295
+ <FieldDescription id={`match-${key}-help`}>
296
+ {field.description}
297
+ </FieldDescription>
298
+ ) : null}
299
+ {problem ? (
300
+ <FieldError>{problem.message}</FieldError>
301
+ ) : null}
302
+ </Field>
303
+ );
304
+ })}
250
305
  </fieldset>
251
306
  ) : null}
252
307
  </>
@@ -262,3 +317,31 @@ export function AutomationEventFields(props: {
262
317
  </>
263
318
  );
264
319
  }
320
+
321
+ /** Show the saved trigger and its problems in the same words as the details. */
322
+ function SavedTrigger(props: {
323
+ resourceTypes: Parameters<typeof describeEventTrigger>[0];
324
+ trigger: Trigger;
325
+ }) {
326
+ const description = describeEventTrigger(props.resourceTypes, props.trigger);
327
+ return (
328
+ <>
329
+ <ul className="my-2 list-disc pl-5">
330
+ {description.problems.map((problem) => (
331
+ <li key={problem}>{problem}</li>
332
+ ))}
333
+ </ul>
334
+ <div className="text-xs">
335
+ <p className="m-0 break-words">
336
+ {props.trigger.namespace} ·{" "}
337
+ {props.trigger.resourceType.replaceAll("_", " ")} ·{" "}
338
+ {props.trigger.identifier}
339
+ </p>
340
+ <p className="m-0 mt-1 break-words text-dashboard-text-muted">
341
+ {description.events.join(", ")}
342
+ </p>
343
+ <TriggerFilters filters={description.filters} />
344
+ </div>
345
+ </>
346
+ );
347
+ }