@sentry/junior-dashboard 0.233.0 → 0.234.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 (39) hide show
  1. package/dist/app.js +89 -15
  2. package/dist/client.js +55 -52
  3. package/dist/index.js +89 -15
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +3 -3
  6. package/src/client/App.tsx +359 -345
  7. package/src/client/api.ts +17 -1
  8. package/src/client/components/ActorAvatar.tsx +37 -0
  9. package/src/client/components/Button.tsx +12 -10
  10. package/src/client/components/Field.tsx +14 -1
  11. package/src/client/components/FormNotice.tsx +40 -0
  12. package/src/client/components/ParticipantAvatarStack.tsx +6 -14
  13. package/src/client/components/README.md +11 -2
  14. package/src/client/components/Select.tsx +18 -0
  15. package/src/client/components/StatusChip.tsx +15 -12
  16. package/src/client/components/TextInput.tsx +25 -5
  17. package/src/client/http.ts +31 -3
  18. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +71 -54
  19. package/src/client/pages/automations/AutomationEditPage.tsx +172 -0
  20. package/src/client/pages/automations/AutomationEditor.tsx +397 -0
  21. package/src/client/pages/automations/AutomationEditorLoading.tsx +18 -0
  22. package/src/client/pages/automations/AutomationEventFields.tsx +264 -0
  23. package/src/client/pages/automations/AutomationFilters.tsx +171 -0
  24. package/src/client/pages/automations/AutomationFormSection.tsx +22 -0
  25. package/src/client/pages/automations/AutomationLifecycle.tsx +146 -0
  26. package/src/client/pages/automations/AutomationOutcomeFields.tsx +177 -0
  27. package/src/client/pages/automations/AutomationRow.tsx +358 -0
  28. package/src/client/pages/automations/AutomationScheduleFields.tsx +328 -0
  29. package/src/client/pages/automations/AutomationsPage.tsx +157 -408
  30. package/src/client/pages/automations/AutomationsPageLayout.tsx +16 -5
  31. package/src/client/pages/automations/README.md +19 -0
  32. package/src/client/pages/automations/automationDraft.ts +110 -0
  33. package/src/client/pages/automations/automationOutcomes.ts +23 -0
  34. package/src/client/pages/dev/ComponentsPage.tsx +50 -4
  35. package/src/client/searchParams.ts +4 -2
  36. package/src/client.tsx +7 -4
  37. package/src/mock-reporting/automation-collection.ts +101 -0
  38. package/src/mock-reporting/fixtures.ts +9 -3
  39. package/src/mock-reporting/routes.ts +14 -9
@@ -0,0 +1,328 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { CalendarClock } from "lucide-react";
4
+ import {
5
+ automationScheduleIntentSchema,
6
+ automationSchedulePreviewSchema,
7
+ type AutomationEdit,
8
+ } from "@sentry/junior/api/schema";
9
+ import { Field } from "../../components/Field";
10
+ import { Select } from "../../components/Select";
11
+ import { TextInput } from "../../components/TextInput";
12
+ import { Button } from "../../components/Button";
13
+ import { post, DashboardApiError } from "../../http";
14
+ import { cn } from "../../styles";
15
+ import {
16
+ scheduleDraft,
17
+ weekdays,
18
+ type AutomationScheduleDraft,
19
+ } from "./automationDraft";
20
+
21
+ /** Edit calendar fields and preview the next run without saving. */
22
+ export function AutomationScheduleFields(props: {
23
+ automation: Extract<AutomationEdit, { kind: "scheduled" }>;
24
+ value?: AutomationScheduleDraft;
25
+ onChange(value: AutomationScheduleDraft | undefined): void;
26
+ }) {
27
+ const value = props.value ?? scheduleDraft(props.automation);
28
+ const [previewValue, setPreviewValue] = useState(props.value);
29
+ useEffect(() => {
30
+ const timer = setTimeout(() => setPreviewValue(props.value), 300);
31
+ return () => clearTimeout(timer);
32
+ }, [props.value]);
33
+ const parsed = automationScheduleIntentSchema.safeParse(previewValue);
34
+ const preview = useQuery({
35
+ queryKey: [
36
+ "dashboard",
37
+ "automation-schedule-preview",
38
+ props.automation.id,
39
+ previewValue,
40
+ ],
41
+ enabled: Boolean(previewValue) && parsed.success,
42
+ retry: false,
43
+ queryFn: () =>
44
+ post(
45
+ automationSchedulePreviewSchema,
46
+ `/api/automations/scheduled/${encodeURIComponent(props.automation.id)}/preview`,
47
+ previewValue,
48
+ ),
49
+ });
50
+ const frequency = value.kind === "recurring" ? value.frequency : "once";
51
+ const time = value.kind === "recurring" ? value.time : value.timing.time;
52
+ const nextRun = props.value
53
+ ? preview.data?.nextRunAtMs
54
+ : props.automation.nextRunAtMs;
55
+ const waiting = props.value !== previewValue || preview.isFetching;
56
+ const issue =
57
+ preview.error instanceof DashboardApiError
58
+ ? preview.error.apiError
59
+ : preview.error
60
+ ? "Schedule preview could not be loaded. Try again."
61
+ : props.value && !parsed.success && !waiting
62
+ ? parsed.error.issues[0]?.message
63
+ : undefined;
64
+ return (
65
+ <>
66
+ <div className="grid grid-cols-1 items-start gap-4 sm:grid-cols-2">
67
+ <Field label="Repeat" htmlFor="schedule-frequency">
68
+ <Select
69
+ id="schedule-frequency"
70
+ value={frequency}
71
+ onChange={(e) => {
72
+ const frequency = e.target.value;
73
+ if (
74
+ frequency !== "once" &&
75
+ frequency !== "daily" &&
76
+ frequency !== "weekly" &&
77
+ frequency !== "monthly" &&
78
+ frequency !== "yearly"
79
+ )
80
+ return;
81
+ props.onChange(
82
+ frequency === "once"
83
+ ? {
84
+ kind: "one_off",
85
+ timezone:
86
+ value.timezone ?? props.automation.schedule.timezone,
87
+ timing: { type: "at", date: "", time },
88
+ }
89
+ : {
90
+ kind: "recurring",
91
+ timezone: value.timezone,
92
+ time,
93
+ frequency,
94
+ interval: 1,
95
+ weekdays: frequency === "weekly" ? ["monday"] : undefined,
96
+ dayOfMonth: ["monthly", "yearly"].includes(frequency)
97
+ ? 1
98
+ : undefined,
99
+ month: frequency === "yearly" ? 1 : undefined,
100
+ },
101
+ );
102
+ }}
103
+ >
104
+ <option value="once">Once</option>
105
+ <option value="daily">Every day</option>
106
+ <option value="weekly">Every week</option>
107
+ <option value="monthly">Every month</option>
108
+ <option value="yearly">Every year</option>
109
+ </Select>
110
+ </Field>
111
+ <Field label="Time" htmlFor="schedule-time">
112
+ <TextInput
113
+ size="comfortable"
114
+ id="schedule-time"
115
+ type="time"
116
+ value={time}
117
+ onChange={(e) =>
118
+ props.onChange(
119
+ value.kind === "recurring"
120
+ ? { ...value, time: e.target.value }
121
+ : {
122
+ ...value,
123
+ timing: {
124
+ type: "at",
125
+ date: value.timing.date,
126
+ time: e.target.value,
127
+ },
128
+ },
129
+ )
130
+ }
131
+ />
132
+ </Field>
133
+ </div>
134
+ {value.kind === "recurring" ? (
135
+ <>
136
+ {value.frequency === "weekly" ? (
137
+ <fieldset>
138
+ <legend className="mb-2 text-sm font-semibold">On</legend>
139
+ <div className="grid grid-cols-7 gap-1.5">
140
+ {[...weekdays.slice(1), weekdays[0]].map((day) => (
141
+ <button
142
+ type="button"
143
+ key={day}
144
+ aria-label={day}
145
+ aria-pressed={value.weekdays?.includes(day) ?? false}
146
+ onClick={() =>
147
+ props.onChange({
148
+ ...value,
149
+ weekdays: value.weekdays?.includes(day)
150
+ ? value.weekdays.filter((d) => d !== day)
151
+ : [...(value.weekdays ?? []), day],
152
+ })
153
+ }
154
+ className={cn(
155
+ "min-h-11 cursor-pointer rounded border px-1 text-xs font-medium capitalize focus-visible:outline-2 focus-visible:outline-dashboard-focus sm:min-h-10 sm:text-sm",
156
+ value.weekdays?.includes(day)
157
+ ? "border-dashboard-focus/50 bg-dashboard-focus/10 text-dashboard-focus"
158
+ : "border-dashboard-border-emphasis text-dashboard-text-muted hover:bg-dashboard-fill-hover",
159
+ )}
160
+ >
161
+ {day.slice(0, 3)}
162
+ </button>
163
+ ))}
164
+ </div>
165
+ </fieldset>
166
+ ) : null}
167
+ <div className="grid grid-cols-1 items-start gap-4 sm:grid-cols-2">
168
+ <Field
169
+ label="Interval"
170
+ htmlFor="schedule-interval"
171
+ help="Number of calendar periods between runs."
172
+ >
173
+ <TextInput
174
+ size="comfortable"
175
+ id="schedule-interval"
176
+ type="number"
177
+ min={1}
178
+ max={365}
179
+ value={value.interval ?? 1}
180
+ onChange={(e) =>
181
+ props.onChange({ ...value, interval: e.target.valueAsNumber })
182
+ }
183
+ />
184
+ </Field>
185
+ <Field label="Start date" htmlFor="schedule-start">
186
+ <TextInput
187
+ size="comfortable"
188
+ id="schedule-start"
189
+ type="date"
190
+ value={value.startDate ?? ""}
191
+ onChange={(e) =>
192
+ props.onChange({
193
+ ...value,
194
+ startDate: e.target.value || undefined,
195
+ })
196
+ }
197
+ />
198
+ </Field>
199
+ </div>
200
+ {value.frequency === "monthly" || value.frequency === "yearly" ? (
201
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
202
+ <Field label="Day of month" htmlFor="schedule-day">
203
+ <TextInput
204
+ size="comfortable"
205
+ id="schedule-day"
206
+ type="number"
207
+ min={1}
208
+ max={31}
209
+ value={value.dayOfMonth ?? 1}
210
+ onChange={(e) =>
211
+ props.onChange({
212
+ ...value,
213
+ dayOfMonth: e.target.valueAsNumber,
214
+ })
215
+ }
216
+ />
217
+ </Field>
218
+ {value.frequency === "yearly" ? (
219
+ <Field label="Month" htmlFor="schedule-month">
220
+ <Select
221
+ id="schedule-month"
222
+ value={value.month ?? 1}
223
+ onChange={(e) =>
224
+ props.onChange({
225
+ ...value,
226
+ month: Number(e.target.value),
227
+ })
228
+ }
229
+ >
230
+ {Array.from({ length: 12 }, (_, index) => (
231
+ <option key={index} value={index + 1}>
232
+ {new Intl.DateTimeFormat("en", {
233
+ month: "long",
234
+ timeZone: "UTC",
235
+ }).format(new Date(Date.UTC(2026, index, 1)))}
236
+ </option>
237
+ ))}
238
+ </Select>
239
+ </Field>
240
+ ) : null}
241
+ </div>
242
+ ) : null}
243
+ </>
244
+ ) : (
245
+ <Field label="Date" htmlFor="schedule-date">
246
+ <TextInput
247
+ size="comfortable"
248
+ id="schedule-date"
249
+ type="date"
250
+ value={value.timing.date}
251
+ onChange={(e) =>
252
+ props.onChange({
253
+ ...value,
254
+ timing: { type: "at", time, date: e.target.value },
255
+ })
256
+ }
257
+ />
258
+ </Field>
259
+ )}
260
+ <Field
261
+ label="Timezone"
262
+ htmlFor="schedule-timezone"
263
+ help="Use an IANA timezone. The schedule follows local time, including daylight saving time."
264
+ >
265
+ <TextInput
266
+ size="comfortable"
267
+ id="schedule-timezone"
268
+ list="automation-timezones"
269
+ value={value.timezone ?? ""}
270
+ onChange={(e) =>
271
+ props.onChange({ ...value, timezone: e.target.value })
272
+ }
273
+ />
274
+ <datalist id="automation-timezones">
275
+ {["UTC", ...Intl.supportedValuesOf("timeZone")].map((zone) => (
276
+ <option key={zone}>{zone}</option>
277
+ ))}
278
+ </datalist>
279
+ </Field>
280
+ <div
281
+ role="status"
282
+ className="flex gap-3 rounded-md bg-dashboard-fill-soft p-3 text-sm leading-relaxed"
283
+ >
284
+ <CalendarClock
285
+ aria-hidden
286
+ size={17}
287
+ className="mt-0.5 shrink-0 text-dashboard-text-muted"
288
+ />
289
+ <div>
290
+ {waiting ? (
291
+ "Checking schedule…"
292
+ ) : issue ? (
293
+ <>
294
+ <span className="text-rose-300">{issue}</span>
295
+ {preview.error ? (
296
+ <Button onClick={() => void preview.refetch()}>
297
+ Try again
298
+ </Button>
299
+ ) : null}
300
+ </>
301
+ ) : nextRun ? (
302
+ <>
303
+ Next run{" "}
304
+ {new Intl.DateTimeFormat(undefined, {
305
+ year: "numeric",
306
+ month: "short",
307
+ day: "numeric",
308
+ hour: "numeric",
309
+ minute: "2-digit",
310
+ timeZoneName: "short",
311
+ timeZone: props.value
312
+ ? (preview.data?.schedule.timezone ?? "UTC")
313
+ : props.automation.schedule.timezone,
314
+ }).format(nextRun)}
315
+ </>
316
+ ) : (
317
+ "No next run."
318
+ )}
319
+ </div>
320
+ </div>
321
+ {props.value ? (
322
+ <Button className="w-fit" onClick={() => props.onChange(undefined)}>
323
+ Keep saved schedule
324
+ </Button>
325
+ ) : null}
326
+ </>
327
+ );
328
+ }