@sentry/junior-dashboard 0.265.0 → 0.267.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 (104) hide show
  1. package/dist/app.js +49 -12
  2. package/dist/client.js +94 -63
  3. package/dist/index.js +49 -12
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +11 -5
  6. package/src/app.ts +0 -2
  7. package/src/auth.ts +4 -7
  8. package/src/client/App.tsx +35 -53
  9. package/src/client/components/ActorAvatar.tsx +10 -22
  10. package/src/client/components/ConnectionBanner.tsx +4 -3
  11. package/src/client/components/CopyButton.tsx +3 -4
  12. package/src/client/components/DetailsPanel.tsx +12 -13
  13. package/src/client/components/Drawer.tsx +46 -23
  14. package/src/client/components/EmptyTelemetry.tsx +5 -3
  15. package/src/client/components/ImageAttachment.tsx +49 -63
  16. package/src/client/components/InlineError.tsx +8 -4
  17. package/src/client/components/Notice.tsx +19 -26
  18. package/src/client/components/Pagination.tsx +26 -26
  19. package/src/client/components/ParticipantAvatarStack.tsx +13 -10
  20. package/src/client/components/README.md +28 -3
  21. package/src/client/components/SearchInput.tsx +51 -41
  22. package/src/client/components/VersionDriftBanner.tsx +9 -7
  23. package/src/client/components/charts/CacheWrites.tsx +52 -43
  24. package/src/client/components/ui/alert.tsx +64 -0
  25. package/src/client/components/ui/avatar.tsx +53 -0
  26. package/src/client/components/ui/button-group.tsx +59 -0
  27. package/src/client/components/ui/button.tsx +2 -1
  28. package/src/client/components/ui/dialog.tsx +60 -0
  29. package/src/client/components/ui/drawer.tsx +140 -0
  30. package/src/client/components/ui/empty.tsx +61 -0
  31. package/src/client/components/ui/input-group.tsx +96 -0
  32. package/src/client/components/ui/pagination.tsx +60 -0
  33. package/src/client/components/ui/separator.tsx +22 -0
  34. package/src/client/components/ui/spinner.tsx +21 -0
  35. package/src/client/components/ui/table.tsx +22 -1
  36. package/src/client/components/ui/tooltip.tsx +1 -1
  37. package/src/client/components/ui/useSheet.ts +5 -1
  38. package/src/client/conversations/ComposerImages.tsx +66 -14
  39. package/src/client/conversations/ConversationChildAgents.tsx +5 -5
  40. package/src/client/conversations/ConversationComposer.tsx +84 -12
  41. package/src/client/conversations/ConversationDetails.tsx +3 -27
  42. package/src/client/conversations/ConversationEventDetails.tsx +26 -10
  43. package/src/client/conversations/ConversationHeader.tsx +5 -4
  44. package/src/client/conversations/ConversationHeaderActions.tsx +7 -11
  45. package/src/client/conversations/ConversationPage.tsx +156 -2
  46. package/src/client/conversations/ConversationSidebar.tsx +5 -4
  47. package/src/client/conversations/ConversationTranscript.tsx +48 -16
  48. package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
  49. package/src/client/conversations/ConversationWatches.tsx +67 -59
  50. package/src/client/conversations/DetailsList.tsx +77 -29
  51. package/src/client/conversations/EventDetails.tsx +46 -19
  52. package/src/client/conversations/EventUsage.tsx +99 -74
  53. package/src/client/conversations/ForkConversationButton.tsx +79 -73
  54. package/src/client/conversations/PendingMailboxStack.tsx +312 -119
  55. package/src/client/conversations/README.md +30 -10
  56. package/src/client/conversations/ShareConversationButton.tsx +3 -4
  57. package/src/client/conversations/SubagentTranscriptDrawer.tsx +19 -22
  58. package/src/client/conversations/TranscriptActivityGroup.tsx +22 -19
  59. package/src/client/conversations/TranscriptAttachment.tsx +39 -4
  60. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +14 -19
  61. package/src/client/conversations/TranscriptEventView.tsx +35 -16
  62. package/src/client/conversations/TranscriptFailureView.tsx +10 -13
  63. package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
  64. package/src/client/conversations/TranscriptMessageContextView.tsx +31 -20
  65. package/src/client/conversations/TranscriptMessageView.tsx +7 -1
  66. package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
  67. package/src/client/conversations/TranscriptSummary.tsx +13 -7
  68. package/src/client/conversations/TranscriptTurnContextView.tsx +78 -56
  69. package/src/client/conversations/TranscriptView.tsx +7 -3
  70. package/src/client/conversations/UserInputRequestCard.tsx +244 -0
  71. package/src/client/conversations/conversationOutbox.ts +46 -0
  72. package/src/client/conversations/eventLog.ts +1 -0
  73. package/src/client/conversations/eventTime.ts +13 -1
  74. package/src/client/conversations/eventTranscript.ts +12 -5
  75. package/src/client/conversations/queries.ts +127 -26
  76. package/src/client/conversations/transcriptMarkdownLinks.ts +5 -367
  77. package/src/client/conversations/transcriptMentions.tsx +18 -5
  78. package/src/client/conversations/userInputRequest.ts +56 -0
  79. package/src/client/format.ts +10 -3
  80. package/src/client/pages/automations/{AutomationLifecycle.tsx → AutomationActions.tsx} +60 -69
  81. package/src/client/pages/automations/AutomationDraftChanges.tsx +171 -0
  82. package/src/client/pages/automations/AutomationEditor.tsx +182 -156
  83. package/src/client/pages/automations/AutomationEventFields.tsx +119 -45
  84. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +1 -1
  85. package/src/client/pages/automations/AutomationFormSection.tsx +20 -2
  86. package/src/client/pages/automations/AutomationOutcomeFields.tsx +1 -1
  87. package/src/client/pages/automations/AutomationOverview.tsx +238 -0
  88. package/src/client/pages/automations/AutomationPage.tsx +343 -0
  89. package/src/client/pages/automations/AutomationRecentRuns.tsx +3 -2
  90. package/src/client/pages/automations/AutomationRow.tsx +11 -13
  91. package/src/client/pages/automations/AutomationRunDetails.tsx +4 -2
  92. package/src/client/pages/automations/{AutomationExecutionsPage.tsx → AutomationRunsTab.tsx} +94 -103
  93. package/src/client/pages/automations/AutomationStatus.tsx +164 -108
  94. package/src/client/pages/automations/AutomationVersionsTab.tsx +518 -0
  95. package/src/client/pages/automations/AutomationsPage.tsx +10 -98
  96. package/src/client/pages/automations/AutomationsPageLayout.tsx +5 -23
  97. package/src/client/pages/automations/README.md +56 -46
  98. package/src/client/types.ts +3 -0
  99. package/src/mock-reporting/fixtures.ts +44 -2
  100. package/src/mock-reporting/watches.ts +1 -1
  101. package/src/client/conversations/transcriptStyles.ts +0 -10
  102. package/src/client/pages/automations/AutomationDetails.tsx +0 -193
  103. package/src/client/pages/automations/AutomationEditPage.tsx +0 -176
  104. package/src/client/pages/automations/AutomationVersionsPage.tsx +0 -390
@@ -16,6 +16,7 @@ import { useToastManager } from "../../components/ui/toast";
16
16
  import { FormNotice } from "../../components/FormNotice";
17
17
  import { RadioGroup, RadioGroupItem } from "../../components/ui/radio-group";
18
18
  import { DashboardApiError, fetchDashboardJson, patch } from "../../http";
19
+ import { AutomationDraftChanges } from "./AutomationDraftChanges";
19
20
  import { AutomationFormSection } from "./AutomationFormSection";
20
21
  import { AutomationScheduleFields } from "./AutomationScheduleFields";
21
22
  import { AutomationEventFields } from "./AutomationEventFields";
@@ -43,6 +44,8 @@ export function AutomationEditor(props: {
43
44
  );
44
45
  const [fields, setFields] = useState<Record<string, string[]>>({});
45
46
  const [latest, setLatest] = useState<AutomationEdit>();
47
+ // Undo remounts the fields, so fields with local state show the saved value.
48
+ const [undoCount, setUndoCount] = useState(0);
46
49
  const form = useRef<HTMLFormElement>(null);
47
50
  const allowLeave = useRef(false);
48
51
  const navigate = useNavigate();
@@ -116,6 +119,10 @@ export function AutomationEditor(props: {
116
119
  save.error instanceof DashboardApiError && save.error.code === "conflict";
117
120
  const change = (value: Partial<AutomationDraft>) =>
118
121
  setDraft((current) => ({ ...current, ...value }));
122
+ const undo = (key: keyof AutomationDraft) => {
123
+ change({ [key]: createAutomationDraft(original)[key] });
124
+ setUndoCount((count) => count + 1);
125
+ };
119
126
  const fieldError = (key: string) =>
120
127
  Object.entries(fields)
121
128
  .filter(([name]) => name === key || name.startsWith(`${key}.`))
@@ -250,173 +257,192 @@ export function AutomationEditor(props: {
250
257
  their connected accounts. Version history keeps every saved change.
251
258
  </p>
252
259
  )}
253
- <fieldset disabled={save.isPending} className="min-w-0">
254
- <AutomationFormSection
255
- title="What to do"
256
- detail="Give Junior a clear instruction and a name you can find later."
257
- >
258
- <Field>
259
- <FieldLabel htmlFor="title">Title</FieldLabel>
260
- <Input
261
- size="comfortable"
262
- id="title"
263
- value={draft.title}
264
- placeholder={props.summary.title}
265
- onChange={(e) => change({ title: e.target.value })}
266
- {...inputState("title")}
267
- />
268
- <FieldError id="title-error">{fieldError("title")}</FieldError>
269
- </Field>
270
- <Field>
271
- <FieldLabel htmlFor="instruction">Instruction</FieldLabel>
272
- <Textarea
273
- id="instruction"
274
- rows={8}
275
- value={draft.instruction}
276
- onChange={(e) => change({ instruction: e.target.value })}
277
- {...inputState("instruction")}
278
- className="resize-y text-base leading-relaxed sm:text-sm"
279
- />
280
- <p className="m-0 text-right text-xs text-dashboard-text-muted">
281
- {draft.instruction.length.toLocaleString()} / 4,000
282
- </p>
283
- <FieldError id="instruction-error">
284
- {fieldError("instruction")}
285
- </FieldError>
286
- </Field>
287
- </AutomationFormSection>
288
- <AutomationFormSection
289
- title="When to run"
290
- detail={
291
- original.kind === "scheduled"
292
- ? "Choose a schedule in a specific timezone."
293
- : "Run when a matching event occurs."
294
- }
260
+ <div className="grid min-w-0 gap-6 lg:grid-cols-[minmax(0,1fr)_18rem] lg:gap-10">
261
+ <fieldset
262
+ key={undoCount}
263
+ disabled={save.isPending}
264
+ className="min-w-0 [&>section:first-of-type]:border-t-0 [&>section:first-of-type]:pt-0"
295
265
  >
296
- <div
297
- id={`${triggerField}-error`}
298
- tabIndex={-1}
299
- aria-invalid={Boolean(triggerError) || undefined}
300
- className="grid min-w-0 gap-5 outline-none"
266
+ <AutomationFormSection
267
+ changed={
268
+ changes.title !== undefined || changes.instruction !== undefined
269
+ }
270
+ title="What to do"
271
+ detail="Give Junior a clear instruction and a name you can find later."
272
+ >
273
+ <Field>
274
+ <FieldLabel htmlFor="title">Title</FieldLabel>
275
+ <Input
276
+ size="comfortable"
277
+ id="title"
278
+ value={draft.title}
279
+ placeholder={props.summary.title}
280
+ onChange={(e) => change({ title: e.target.value })}
281
+ {...inputState("title")}
282
+ />
283
+ <FieldError id="title-error">{fieldError("title")}</FieldError>
284
+ </Field>
285
+ <Field>
286
+ <FieldLabel htmlFor="instruction">Instruction</FieldLabel>
287
+ <Textarea
288
+ id="instruction"
289
+ rows={8}
290
+ value={draft.instruction}
291
+ onChange={(e) => change({ instruction: e.target.value })}
292
+ {...inputState("instruction")}
293
+ className="resize-y text-base leading-relaxed sm:text-sm"
294
+ />
295
+ <p className="m-0 text-right text-xs text-dashboard-text-muted">
296
+ {draft.instruction.length.toLocaleString()} / 4,000
297
+ </p>
298
+ <FieldError id="instruction-error">
299
+ {fieldError("instruction")}
300
+ </FieldError>
301
+ </Field>
302
+ </AutomationFormSection>
303
+ <AutomationFormSection
304
+ changed={changes[triggerField] !== undefined}
305
+ title="When to run"
306
+ detail={
307
+ original.kind === "scheduled"
308
+ ? "Choose a schedule in a specific timezone."
309
+ : "Run when a matching event occurs."
310
+ }
301
311
  >
302
- {triggerError ? (
312
+ <div
313
+ id={`${triggerField}-error`}
314
+ tabIndex={-1}
315
+ aria-invalid={Boolean(triggerError) || undefined}
316
+ className="grid min-w-0 gap-5 outline-none"
317
+ >
318
+ {triggerError ? (
319
+ <p role="alert" className="m-0 text-sm text-rose-300">
320
+ {triggerError}
321
+ </p>
322
+ ) : null}
323
+ {original.kind === "scheduled" ? (
324
+ <AutomationScheduleFields
325
+ automation={original}
326
+ value={draft.schedule}
327
+ onChange={(schedule) => change({ schedule })}
328
+ />
329
+ ) : (
330
+ <AutomationEventFields
331
+ value={draft.trigger ?? original.trigger}
332
+ original={original.trigger}
333
+ disabled={save.isPending}
334
+ onChange={(trigger) => change({ trigger })}
335
+ />
336
+ )}
337
+ </div>
338
+ </AutomationFormSection>
339
+ <AutomationFormSection
340
+ changed={changes.outcomes !== undefined}
341
+ title="Where results go"
342
+ detail="Choose who gets a message after successful work."
343
+ >
344
+ {fieldError("outcomes") ? (
303
345
  <p role="alert" className="m-0 text-sm text-rose-300">
304
- {triggerError}
346
+ {fieldError("outcomes")}
305
347
  </p>
306
348
  ) : null}
307
- {original.kind === "scheduled" ? (
308
- <AutomationScheduleFields
309
- automation={original}
310
- value={draft.schedule}
311
- onChange={(schedule) => change({ schedule })}
312
- />
313
- ) : (
314
- <AutomationEventFields
315
- value={draft.trigger ?? original.trigger}
316
- original={original.trigger}
349
+ {owned ? (
350
+ <AutomationOutcomeFields
351
+ value={draft.outcomes}
352
+ original={original.outcomes}
353
+ destination={props.summary.destination}
317
354
  disabled={save.isPending}
318
- onChange={(trigger) => change({ trigger })}
355
+ onChange={(outcomes) => change({ outcomes })}
319
356
  />
357
+ ) : (
358
+ <>
359
+ <AutomationOutcomeList
360
+ outcomes={original.outcomes}
361
+ destination={props.summary.destination}
362
+ />
363
+ <p className="m-0 text-xs text-dashboard-text-muted">
364
+ Only {creator} can change where results go.
365
+ </p>
366
+ </>
320
367
  )}
321
- </div>
322
- </AutomationFormSection>
323
- <AutomationFormSection
324
- title="Where results go"
325
- detail="Choose who gets a message after successful work."
326
- >
327
- {fieldError("outcomes") ? (
328
- <p role="alert" className="m-0 text-sm text-rose-300">
329
- {fieldError("outcomes")}
330
- </p>
331
- ) : null}
332
- {owned ? (
333
- <AutomationOutcomeFields
334
- value={draft.outcomes}
335
- original={original.outcomes}
336
- destination={props.summary.destination}
337
- disabled={save.isPending}
338
- onChange={(outcomes) => change({ outcomes })}
339
- />
340
- ) : (
341
- <>
342
- <AutomationOutcomeList
343
- outcomes={original.outcomes}
344
- destination={props.summary.destination}
345
- />
346
- <p className="m-0 text-xs text-dashboard-text-muted">
347
- Only {creator} can change where results go.
348
- </p>
349
- </>
350
- )}
351
- </AutomationFormSection>
352
- <AutomationFormSection
353
- title="Credentials"
354
- detail="Choose which connected accounts Junior can use."
355
- >
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}
368
+ </AutomationFormSection>
369
+ <AutomationFormSection
370
+ changed={changes.credentialMode !== undefined}
371
+ title="Credentials"
372
+ detail="Choose which connected accounts Junior can use."
364
373
  >
365
- {(
366
- [
367
- [
368
- "creator",
369
- owned
370
- ? "Your connected accounts"
371
- : `${creator}’s connected accounts`,
372
- owned
373
- ? "Uses accounts connected by you, the creator."
374
- : `Uses accounts connected by ${creator}. Only ${creator} can turn this on.`,
375
- ],
374
+ <RadioGroup
375
+ aria-label="Credential mode"
376
+ disabled={save.isPending}
377
+ className="divide-y divide-dashboard-border-subtle rounded-md border border-dashboard-border-emphasis"
378
+ onValueChange={(credentialMode: "creator" | "system") =>
379
+ change({ credentialMode })
380
+ }
381
+ value={dropsCreatorCredentials ? "system" : draft.credentialMode}
382
+ >
383
+ {(
376
384
  [
377
- "system",
378
- "System credentials only",
379
- owned
380
- ? "Does not use your connected accounts. Some work may not be possible."
381
- : "Does not use anyone’s connected accounts. Some work may not be possible.",
382
- ],
383
- ] as const
384
- ).map(([value, label, detail]) => (
385
- <label
386
- key={value}
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"
388
- >
389
- <RadioGroupItem
390
- className="mt-0.5"
391
- value={value}
392
- disabled={
393
- value === "creator" &&
394
- !owned &&
395
- (original.credentialMode !== "creator" ||
396
- dropsCreatorCredentials)
397
- }
398
- />
399
- <span>
400
- <span className="block text-sm font-medium">{label}</span>
401
- <span className="mt-1 block text-xs leading-relaxed text-dashboard-text-muted">
402
- {detail}
385
+ [
386
+ "creator",
387
+ owned
388
+ ? "Your connected accounts"
389
+ : `${creator}’s connected accounts`,
390
+ owned
391
+ ? "Uses accounts connected by you, the creator."
392
+ : `Uses accounts connected by ${creator}. Only ${creator} can turn this on.`,
393
+ ],
394
+ [
395
+ "system",
396
+ "System credentials only",
397
+ owned
398
+ ? "Does not use your connected accounts. Some work may not be possible."
399
+ : "Does not use anyone’s connected accounts. Some work may not be possible.",
400
+ ],
401
+ ] as const
402
+ ).map(([value, label, detail]) => (
403
+ <label
404
+ key={value}
405
+ 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"
406
+ >
407
+ <RadioGroupItem
408
+ className="mt-0.5"
409
+ value={value}
410
+ disabled={
411
+ value === "creator" &&
412
+ !owned &&
413
+ (original.credentialMode !== "creator" ||
414
+ dropsCreatorCredentials)
415
+ }
416
+ />
417
+ <span>
418
+ <span className="block text-sm font-medium">{label}</span>
419
+ <span className="mt-1 block text-xs leading-relaxed text-dashboard-text-muted">
420
+ {detail}
421
+ </span>
403
422
  </span>
404
- </span>
405
- </label>
406
- ))}
407
- </RadioGroup>
408
- {dropsCreatorCredentials ? (
409
- <FormNotice title="Saving switches this automation to system credentials.">
410
- Only {creator} can let a changed instruction
411
- {original.kind === "event" ? " or trigger" : ""} use their
412
- connected accounts.
413
- </FormNotice>
414
- ) : null}
415
- {fieldError("credentialMode") ? (
416
- <p role="alert">{fieldError("credentialMode")}</p>
417
- ) : null}
418
- </AutomationFormSection>
419
- </fieldset>
423
+ </label>
424
+ ))}
425
+ </RadioGroup>
426
+ {dropsCreatorCredentials ? (
427
+ <FormNotice title="Saving switches this automation to system credentials.">
428
+ Only {creator} can let a changed instruction
429
+ {original.kind === "event" ? " or trigger" : ""} use their
430
+ connected accounts.
431
+ </FormNotice>
432
+ ) : null}
433
+ {fieldError("credentialMode") ? (
434
+ <p role="alert">{fieldError("credentialMode")}</p>
435
+ ) : null}
436
+ </AutomationFormSection>
437
+ </fieldset>
438
+ <AutomationDraftChanges
439
+ changes={changes}
440
+ disabled={save.isPending}
441
+ onUndo={undo}
442
+ original={original}
443
+ summary={props.summary}
444
+ />
445
+ </div>
420
446
  <p className="mb-5 text-xs leading-relaxed text-dashboard-text-muted sm:hidden">
421
447
  Changes apply to future work. Saving does not run this automation.
422
448
  </p>
@@ -1,11 +1,14 @@
1
1
  import { useState } from "react";
2
2
  import { useQuery } from "@tanstack/react-query";
3
3
  import {
4
+ anyResourceEvents,
4
5
  describeEventTrigger,
6
+ eventTriggerChoices,
5
7
  eventMatchFilterText,
6
8
  eventTriggerProblems,
7
9
  eventTypeText,
8
10
  eventsWithoutMatchField,
11
+ type EventTriggerSource,
9
12
  } from "@sentry/junior-plugin-api";
10
13
  import {
11
14
  automationEventCatalogSchema,
@@ -36,6 +39,33 @@ import { fetchDashboardJson } from "../../http";
36
39
 
37
40
  type Trigger = Extract<AutomationEdit, { kind: "event" }>["trigger"];
38
41
 
42
+ // Select value for a trigger with no identifier, which runs for any resource.
43
+ const ANY_RESOURCE = "*";
44
+
45
+ const NO_SOURCE: EventTriggerSource = {
46
+ resourceTypes: [],
47
+ events: {},
48
+ anyResourceFields: {},
49
+ };
50
+
51
+ /**
52
+ * Say which filter an event needs when a trigger has no identifier, with the
53
+ * filter descriptions, such as "needs a filter: lowercase repository".
54
+ */
55
+ function reachText(
56
+ source: EventTriggerSource,
57
+ eventType: string,
58
+ ): string | undefined {
59
+ const reach = source.events[eventType]?.reach;
60
+ if (typeof reach !== "object") return undefined;
61
+ const filters = reach.anyOf.map(
62
+ (key) => source.anyResourceFields[key]?.description ?? key,
63
+ );
64
+ return filters.length === 1
65
+ ? `needs a filter: ${filters[0]}`
66
+ : `needs one of these filters: ${filters.join("; or ")}`;
67
+ }
68
+
39
69
  /** Render registered Event choices without discarding unsupported saved selectors. */
40
70
  export function AutomationEventFields(props: {
41
71
  value: Trigger;
@@ -55,20 +85,29 @@ export function AutomationEventFields(props: {
55
85
  ),
56
86
  retry: false,
57
87
  });
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,
88
+ const entry = catalog.data?.find(
89
+ (item) => item.namespace === props.value.namespace,
63
90
  );
91
+ const source: EventTriggerSource = entry ?? NO_SOURCE;
92
+ const displayName = entry?.displayName ?? props.value.namespace;
93
+ const anyResource = props.value.resourceType === undefined;
94
+ const choices = eventTriggerChoices(source, props.value);
64
95
  const problems = catalog.data
65
- ? eventTriggerProblems(sourceResourceTypes, props.value)
96
+ ? eventTriggerProblems(source, props.value)
66
97
  : [];
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.
98
+ // A filter that a selected event does not set, or a filter that an event
99
+ // needs, is fixable here. Other problems mean the catalog changed after the
100
+ // trigger was saved.
69
101
  const unsupported =
70
- !resource || problems.some((problem) => problem.reason !== "field_not_set");
71
- const matchFields = Object.entries(resource?.matchFields ?? {});
102
+ !choices ||
103
+ problems.some(
104
+ (problem) =>
105
+ problem.reason !== "field_not_set" &&
106
+ problem.reason !== "filter_required",
107
+ );
108
+ const anyResourceSources =
109
+ catalog.data?.filter((item) => anyResourceEvents(item).length > 0) ?? [];
110
+ const matchFields = Object.entries(choices?.fields ?? {});
72
111
  const shownMatchFields = matchFields.filter(
73
112
  ([key, field]) =>
74
113
  props.value.match?.[key] !== undefined ||
@@ -98,7 +137,8 @@ export function AutomationEventFields(props: {
98
137
  Some saved values are not in the current catalog. You can edit other
99
138
  fields, or select a new resource type to replace the trigger.
100
139
  <SavedTrigger
101
- resourceTypes={sourceResourceTypes}
140
+ displayName={displayName}
141
+ source={source}
102
142
  trigger={props.value}
103
143
  />
104
144
  </FormNotice>
@@ -112,15 +152,20 @@ export function AutomationEventFields(props: {
112
152
  value={
113
153
  unsupported
114
154
  ? ""
115
- : `${props.value.namespace}:${props.value.resourceType}`
155
+ : `${props.value.namespace}:${props.value.resourceType ?? ANY_RESOURCE}`
116
156
  }
117
157
  onChange={(e) => {
118
- const next = catalog.data?.find(
119
- (item) => `${item.namespace}:${item.type}` === e.target.value,
120
- );
121
- if (!next) return;
158
+ const [namespace, type] = e.target.value.split(":");
159
+ if (type === ANY_RESOURCE && namespace) {
160
+ setReplacement({ namespace, label: "", events: [] });
161
+ return;
162
+ }
163
+ const next = catalog.data
164
+ ?.find((item) => item.namespace === namespace)
165
+ ?.resourceTypes.find((item) => item.type === type);
166
+ if (!namespace || !next) return;
122
167
  setReplacement({
123
- namespace: next.namespace,
168
+ namespace,
124
169
  resourceType: next.type,
125
170
  identifier: "",
126
171
  label: "",
@@ -131,14 +176,24 @@ export function AutomationEventFields(props: {
131
176
  <option value="" disabled>
132
177
  Keep saved trigger
133
178
  </option>
134
- {catalog.data?.map((item) => (
179
+ {anyResourceSources.map((item) => (
135
180
  <option
136
- key={`${item.namespace}:${item.type}`}
137
- value={`${item.namespace}:${item.type}`}
181
+ key={`${item.namespace}:${ANY_RESOURCE}`}
182
+ value={`${item.namespace}:${ANY_RESOURCE}`}
138
183
  >
139
- {item.namespace} · {item.type.replaceAll("_", " ")}
184
+ {item.displayName} · any resource
140
185
  </option>
141
186
  ))}
187
+ {catalog.data?.flatMap((item) =>
188
+ item.resourceTypes.map((resourceType) => (
189
+ <option
190
+ key={`${item.namespace}:${resourceType.type}`}
191
+ value={`${item.namespace}:${resourceType.type}`}
192
+ >
193
+ {item.displayName} · {resourceType.type.replaceAll("_", " ")}
194
+ </option>
195
+ )),
196
+ )}
142
197
  </NativeSelect>
143
198
  </Field>
144
199
  <AlertDialog
@@ -166,10 +221,12 @@ export function AutomationEventFields(props: {
166
221
  </AlertDialogFooter>
167
222
  </AlertDialogContent>
168
223
  </AlertDialog>
169
- {!unsupported && resource ? (
224
+ {!unsupported && choices ? (
170
225
  <>
171
226
  <Field>
172
- <FieldLabel htmlFor="trigger-label">Resource name</FieldLabel>
227
+ <FieldLabel htmlFor="trigger-label">
228
+ {anyResource ? "Trigger name" : "Resource name"}
229
+ </FieldLabel>
173
230
  <Input
174
231
  size="comfortable"
175
232
  id="trigger-label"
@@ -179,28 +236,38 @@ export function AutomationEventFields(props: {
179
236
  }
180
237
  />
181
238
  </Field>
182
- <Field>
183
- <FieldLabel htmlFor="trigger-identifier">
184
- Resource identifier
185
- </FieldLabel>
186
- <Input
187
- size="comfortable"
188
- id="trigger-identifier"
189
- value={props.value.identifier}
190
- onChange={(e) =>
191
- props.onChange({ ...props.value, identifier: e.target.value })
192
- }
193
- />
194
- <FieldDescription id="trigger-identifier-help">
195
- Use the exact provider resource identifier, not its display name.
196
- </FieldDescription>
197
- </Field>
239
+ {anyResource ? (
240
+ <p className="m-0 text-xs leading-relaxed text-dashboard-text-muted">
241
+ Runs for every resource that the filters match.
242
+ </p>
243
+ ) : (
244
+ <Field>
245
+ <FieldLabel htmlFor="trigger-identifier">
246
+ Resource identifier
247
+ </FieldLabel>
248
+ <Input
249
+ size="comfortable"
250
+ id="trigger-identifier"
251
+ value={props.value.identifier ?? ""}
252
+ onChange={(e) =>
253
+ props.onChange({
254
+ ...props.value,
255
+ identifier: e.target.value,
256
+ })
257
+ }
258
+ />
259
+ <FieldDescription id="trigger-identifier-help">
260
+ Use the exact provider resource identifier, not its display
261
+ name.
262
+ </FieldDescription>
263
+ </Field>
264
+ )}
198
265
  <fieldset>
199
266
  <legend className="mb-2 text-sm font-semibold">
200
267
  Run on these events
201
268
  </legend>
202
269
  <div className="divide-y divide-dashboard-border-subtle rounded-md border border-dashboard-border-emphasis">
203
- {resource.supportedEvents.map((event) => (
270
+ {choices.events.map((event) => (
204
271
  <label
205
272
  key={event}
206
273
  className="flex min-h-11 cursor-pointer items-start gap-3 px-3 py-3 text-sm"
@@ -220,6 +287,12 @@ export function AutomationEventFields(props: {
220
287
  />
221
288
  <span className="min-w-0 break-words">
222
289
  {eventTypeText(event)}
290
+ {anyResource && reachText(source, event) ? (
291
+ <span className="text-dashboard-text-muted">
292
+ {" "}
293
+ · {reachText(source, event)}
294
+ </span>
295
+ ) : null}
223
296
  </span>
224
297
  </label>
225
298
  ))}
@@ -320,10 +393,11 @@ export function AutomationEventFields(props: {
320
393
 
321
394
  /** Show the saved trigger and its problems in the same words as the details. */
322
395
  function SavedTrigger(props: {
323
- resourceTypes: Parameters<typeof describeEventTrigger>[0];
396
+ displayName: string;
397
+ source: EventTriggerSource;
324
398
  trigger: Trigger;
325
399
  }) {
326
- const description = describeEventTrigger(props.resourceTypes, props.trigger);
400
+ const description = describeEventTrigger(props.source, props.trigger);
327
401
  return (
328
402
  <>
329
403
  <ul className="my-2 list-disc pl-5">
@@ -333,9 +407,9 @@ function SavedTrigger(props: {
333
407
  </ul>
334
408
  <div className="text-xs">
335
409
  <p className="m-0 break-words">
336
- {props.trigger.namespace} ·{" "}
337
- {props.trigger.resourceType.replaceAll("_", " ")} ·{" "}
338
- {props.trigger.identifier}
410
+ {props.displayName} ·{" "}
411
+ {props.trigger.resourceType?.replaceAll("_", " ") ?? "any resource"}
412
+ {props.trigger.identifier ? ` · ${props.trigger.identifier}` : null}
339
413
  </p>
340
414
  <p className="m-0 mt-1 break-words text-dashboard-text-muted">
341
415
  {description.events.join(", ")}
@@ -42,7 +42,7 @@ export function AutomationExecutionStatusChart(props: {
42
42
  <Card className="min-h-[15rem] p-4 sm:p-5">
43
43
  <div>
44
44
  <h2 className="m-0 font-display text-xl font-medium text-dashboard-text">
45
- Executions over time
45
+ Runs over time
46
46
  </h2>
47
47
  <p className="mt-1 mb-0 font-sans text-xs leading-relaxed text-dashboard-text-muted">
48
48
  {bucketUnit === "hour"