@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.
- package/dist/app.js +89 -15
- package/dist/client.js +55 -52
- package/dist/index.js +89 -15
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- package/src/client/App.tsx +359 -345
- package/src/client/api.ts +17 -1
- package/src/client/components/ActorAvatar.tsx +37 -0
- package/src/client/components/Button.tsx +12 -10
- package/src/client/components/Field.tsx +14 -1
- package/src/client/components/FormNotice.tsx +40 -0
- package/src/client/components/ParticipantAvatarStack.tsx +6 -14
- package/src/client/components/README.md +11 -2
- package/src/client/components/Select.tsx +18 -0
- package/src/client/components/StatusChip.tsx +15 -12
- package/src/client/components/TextInput.tsx +25 -5
- package/src/client/http.ts +31 -3
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +71 -54
- package/src/client/pages/automations/AutomationEditPage.tsx +172 -0
- package/src/client/pages/automations/AutomationEditor.tsx +397 -0
- package/src/client/pages/automations/AutomationEditorLoading.tsx +18 -0
- package/src/client/pages/automations/AutomationEventFields.tsx +264 -0
- package/src/client/pages/automations/AutomationFilters.tsx +171 -0
- package/src/client/pages/automations/AutomationFormSection.tsx +22 -0
- package/src/client/pages/automations/AutomationLifecycle.tsx +146 -0
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +177 -0
- package/src/client/pages/automations/AutomationRow.tsx +358 -0
- package/src/client/pages/automations/AutomationScheduleFields.tsx +328 -0
- package/src/client/pages/automations/AutomationsPage.tsx +157 -408
- package/src/client/pages/automations/AutomationsPageLayout.tsx +16 -5
- package/src/client/pages/automations/README.md +19 -0
- package/src/client/pages/automations/automationDraft.ts +110 -0
- package/src/client/pages/automations/automationOutcomes.ts +23 -0
- package/src/client/pages/dev/ComponentsPage.tsx +50 -4
- package/src/client/searchParams.ts +4 -2
- package/src/client.tsx +7 -4
- package/src/mock-reporting/automation-collection.ts +101 -0
- package/src/mock-reporting/fixtures.ts +9 -3
- 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
|
+
}
|