@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.
- package/dist/app.js +49 -12
- package/dist/client.js +94 -63
- package/dist/index.js +49 -12
- package/dist/tailwind.css +1 -1
- package/package.json +11 -5
- package/src/app.ts +0 -2
- package/src/auth.ts +4 -7
- package/src/client/App.tsx +35 -53
- package/src/client/components/ActorAvatar.tsx +10 -22
- package/src/client/components/ConnectionBanner.tsx +4 -3
- package/src/client/components/CopyButton.tsx +3 -4
- package/src/client/components/DetailsPanel.tsx +12 -13
- package/src/client/components/Drawer.tsx +46 -23
- package/src/client/components/EmptyTelemetry.tsx +5 -3
- package/src/client/components/ImageAttachment.tsx +49 -63
- package/src/client/components/InlineError.tsx +8 -4
- package/src/client/components/Notice.tsx +19 -26
- package/src/client/components/Pagination.tsx +26 -26
- package/src/client/components/ParticipantAvatarStack.tsx +13 -10
- package/src/client/components/README.md +28 -3
- package/src/client/components/SearchInput.tsx +51 -41
- package/src/client/components/VersionDriftBanner.tsx +9 -7
- package/src/client/components/charts/CacheWrites.tsx +52 -43
- package/src/client/components/ui/alert.tsx +64 -0
- package/src/client/components/ui/avatar.tsx +53 -0
- package/src/client/components/ui/button-group.tsx +59 -0
- package/src/client/components/ui/button.tsx +2 -1
- package/src/client/components/ui/dialog.tsx +60 -0
- package/src/client/components/ui/drawer.tsx +140 -0
- package/src/client/components/ui/empty.tsx +61 -0
- package/src/client/components/ui/input-group.tsx +96 -0
- package/src/client/components/ui/pagination.tsx +60 -0
- package/src/client/components/ui/separator.tsx +22 -0
- package/src/client/components/ui/spinner.tsx +21 -0
- package/src/client/components/ui/table.tsx +22 -1
- package/src/client/components/ui/tooltip.tsx +1 -1
- package/src/client/components/ui/useSheet.ts +5 -1
- package/src/client/conversations/ComposerImages.tsx +66 -14
- package/src/client/conversations/ConversationChildAgents.tsx +5 -5
- package/src/client/conversations/ConversationComposer.tsx +84 -12
- package/src/client/conversations/ConversationDetails.tsx +3 -27
- package/src/client/conversations/ConversationEventDetails.tsx +26 -10
- package/src/client/conversations/ConversationHeader.tsx +5 -4
- package/src/client/conversations/ConversationHeaderActions.tsx +7 -11
- package/src/client/conversations/ConversationPage.tsx +156 -2
- package/src/client/conversations/ConversationSidebar.tsx +5 -4
- package/src/client/conversations/ConversationTranscript.tsx +48 -16
- package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
- package/src/client/conversations/ConversationWatches.tsx +67 -59
- package/src/client/conversations/DetailsList.tsx +77 -29
- package/src/client/conversations/EventDetails.tsx +46 -19
- package/src/client/conversations/EventUsage.tsx +99 -74
- package/src/client/conversations/ForkConversationButton.tsx +79 -73
- package/src/client/conversations/PendingMailboxStack.tsx +312 -119
- package/src/client/conversations/README.md +30 -10
- package/src/client/conversations/ShareConversationButton.tsx +3 -4
- package/src/client/conversations/SubagentTranscriptDrawer.tsx +19 -22
- package/src/client/conversations/TranscriptActivityGroup.tsx +22 -19
- package/src/client/conversations/TranscriptAttachment.tsx +39 -4
- package/src/client/conversations/TranscriptAutomationTriggerView.tsx +14 -19
- package/src/client/conversations/TranscriptEventView.tsx +35 -16
- package/src/client/conversations/TranscriptFailureView.tsx +10 -13
- package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
- package/src/client/conversations/TranscriptMessageContextView.tsx +31 -20
- package/src/client/conversations/TranscriptMessageView.tsx +7 -1
- package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
- package/src/client/conversations/TranscriptSummary.tsx +13 -7
- package/src/client/conversations/TranscriptTurnContextView.tsx +78 -56
- package/src/client/conversations/TranscriptView.tsx +7 -3
- package/src/client/conversations/UserInputRequestCard.tsx +244 -0
- package/src/client/conversations/conversationOutbox.ts +46 -0
- package/src/client/conversations/eventLog.ts +1 -0
- package/src/client/conversations/eventTime.ts +13 -1
- package/src/client/conversations/eventTranscript.ts +12 -5
- package/src/client/conversations/queries.ts +127 -26
- package/src/client/conversations/transcriptMarkdownLinks.ts +5 -367
- package/src/client/conversations/transcriptMentions.tsx +18 -5
- package/src/client/conversations/userInputRequest.ts +56 -0
- package/src/client/format.ts +10 -3
- package/src/client/pages/automations/{AutomationLifecycle.tsx → AutomationActions.tsx} +60 -69
- package/src/client/pages/automations/AutomationDraftChanges.tsx +171 -0
- package/src/client/pages/automations/AutomationEditor.tsx +182 -156
- package/src/client/pages/automations/AutomationEventFields.tsx +119 -45
- package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +1 -1
- package/src/client/pages/automations/AutomationFormSection.tsx +20 -2
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +1 -1
- package/src/client/pages/automations/AutomationOverview.tsx +238 -0
- package/src/client/pages/automations/AutomationPage.tsx +343 -0
- package/src/client/pages/automations/AutomationRecentRuns.tsx +3 -2
- package/src/client/pages/automations/AutomationRow.tsx +11 -13
- package/src/client/pages/automations/AutomationRunDetails.tsx +4 -2
- package/src/client/pages/automations/{AutomationExecutionsPage.tsx → AutomationRunsTab.tsx} +94 -103
- package/src/client/pages/automations/AutomationStatus.tsx +164 -108
- package/src/client/pages/automations/AutomationVersionsTab.tsx +518 -0
- package/src/client/pages/automations/AutomationsPage.tsx +10 -98
- package/src/client/pages/automations/AutomationsPageLayout.tsx +5 -23
- package/src/client/pages/automations/README.md +56 -46
- package/src/client/types.ts +3 -0
- package/src/mock-reporting/fixtures.ts +44 -2
- package/src/mock-reporting/watches.ts +1 -1
- package/src/client/conversations/transcriptStyles.ts +0 -10
- package/src/client/pages/automations/AutomationDetails.tsx +0 -193
- package/src/client/pages/automations/AutomationEditPage.tsx +0 -176
- 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
|
-
<
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
<
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
346
|
+
{fieldError("outcomes")}
|
|
305
347
|
</p>
|
|
306
348
|
) : null}
|
|
307
|
-
{
|
|
308
|
-
<
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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={(
|
|
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
|
-
</
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
{
|
|
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
|
-
</
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
</
|
|
419
|
-
|
|
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
|
|
59
|
-
|
|
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(
|
|
96
|
+
? eventTriggerProblems(source, props.value)
|
|
66
97
|
: [];
|
|
67
|
-
// A filter that a selected event does not set
|
|
68
|
-
// problems mean the catalog changed after the
|
|
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
|
-
!
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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
|
-
{
|
|
179
|
+
{anyResourceSources.map((item) => (
|
|
135
180
|
<option
|
|
136
|
-
key={`${item.namespace}:${
|
|
137
|
-
value={`${item.namespace}:${
|
|
181
|
+
key={`${item.namespace}:${ANY_RESOURCE}`}
|
|
182
|
+
value={`${item.namespace}:${ANY_RESOURCE}`}
|
|
138
183
|
>
|
|
139
|
-
{item.
|
|
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 &&
|
|
224
|
+
{!unsupported && choices ? (
|
|
170
225
|
<>
|
|
171
226
|
<Field>
|
|
172
|
-
<FieldLabel htmlFor="trigger-label">
|
|
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
|
-
|
|
183
|
-
<
|
|
184
|
-
|
|
185
|
-
</
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
396
|
+
displayName: string;
|
|
397
|
+
source: EventTriggerSource;
|
|
324
398
|
trigger: Trigger;
|
|
325
399
|
}) {
|
|
326
|
-
const description = describeEventTrigger(props.
|
|
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.
|
|
337
|
-
{props.trigger.resourceType
|
|
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
|
-
|
|
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"
|