@sentry/junior-dashboard 0.253.0 → 0.255.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/README.md +4 -0
- package/dist/app.js +7 -15
- package/dist/client.js +35 -35
- package/dist/index.js +7 -15
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- package/src/client/App.tsx +3 -3
- package/src/client/api.ts +6 -8
- package/src/client/conversations/ConversationComposer.tsx +32 -34
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +4 -4
- package/src/client/pages/automations/AutomationEditPage.tsx +7 -9
- package/src/client/pages/automations/AutomationEditor.tsx +1 -1
- package/src/client/pages/automations/AutomationExecutionsPage.tsx +3 -8
- package/src/client/pages/automations/AutomationLifecycle.tsx +2 -2
- package/src/client/pages/automations/AutomationRow.tsx +2 -2
- package/src/client/pages/automations/AutomationScheduleFields.tsx +1 -1
- package/src/client/pages/automations/AutomationVersionsPage.tsx +6 -10
- package/src/client/pages/automations/AutomationsPage.tsx +1 -1
- package/src/mock-reporting/fixtures.ts +1 -4
- package/src/mock-reporting/routes.ts +3 -9
|
@@ -16,8 +16,6 @@ import { Button } from "../components/Button";
|
|
|
16
16
|
import { useDashboardOnline } from "../connection";
|
|
17
17
|
import { cn, dashboardComposerSurfaceClass } from "../styles";
|
|
18
18
|
|
|
19
|
-
const MOBILE_COMPOSER_MAX_HEIGHT_PX = 112;
|
|
20
|
-
const DESKTOP_MEDIA_QUERY = "(min-width: 768px)";
|
|
21
19
|
const DRAFT_STORAGE_PREFIX = "junior:dashboard:conversation-draft:";
|
|
22
20
|
// Keep keystrokes off the storage path. Mobile Safari pays for every write.
|
|
23
21
|
const DRAFT_STORAGE_DEBOUNCE_MS = 250;
|
|
@@ -137,35 +135,31 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
137
135
|
};
|
|
138
136
|
}, [storageKey]);
|
|
139
137
|
|
|
138
|
+
// Grow with wrapped and explicit lines. CSS `max-height` caps the box, and
|
|
139
|
+
// longer drafts scroll inside it. Width changes rewrap the text, so measure
|
|
140
|
+
// again when the textarea gets narrower or wider.
|
|
140
141
|
useEffect(() => {
|
|
141
142
|
const textarea = textareaRef.current;
|
|
142
143
|
if (!textarea) return;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
textarea
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
textarea.style.height = next;
|
|
144
|
+
syncComposerHeight(textarea);
|
|
145
|
+
let width = textarea.clientWidth;
|
|
146
|
+
let frame: number | undefined;
|
|
147
|
+
const observer = new ResizeObserver(() => {
|
|
148
|
+
if (textarea.clientWidth === width) return;
|
|
149
|
+
width = textarea.clientWidth;
|
|
150
|
+
// A height change inside this callback starts a ResizeObserver loop
|
|
151
|
+
// error. Measure in the next frame instead.
|
|
152
|
+
if (frame !== undefined) window.cancelAnimationFrame(frame);
|
|
153
|
+
frame = window.requestAnimationFrame(() => {
|
|
154
|
+
frame = undefined;
|
|
155
|
+
syncComposerHeight(textarea);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
observer.observe(textarea);
|
|
159
|
+
return () => {
|
|
160
|
+
observer.disconnect();
|
|
161
|
+
if (frame !== undefined) window.cancelAnimationFrame(frame);
|
|
163
162
|
};
|
|
164
|
-
|
|
165
|
-
syncHeight();
|
|
166
|
-
const media = window.matchMedia(DESKTOP_MEDIA_QUERY);
|
|
167
|
-
media.addEventListener("change", syncHeight);
|
|
168
|
-
return () => media.removeEventListener("change", syncHeight);
|
|
169
163
|
}, []);
|
|
170
164
|
|
|
171
165
|
const updateCanSend = (text: string) => {
|
|
@@ -210,12 +204,7 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
210
204
|
};
|
|
211
205
|
|
|
212
206
|
const syncTextareaHeight = () => {
|
|
213
|
-
|
|
214
|
-
if (!textarea || window.matchMedia(DESKTOP_MEDIA_QUERY).matches) return;
|
|
215
|
-
const previous = textarea.style.height;
|
|
216
|
-
textarea.style.height = "auto";
|
|
217
|
-
const next = `${Math.min(textarea.scrollHeight, MOBILE_COMPOSER_MAX_HEIGHT_PX)}px`;
|
|
218
|
-
textarea.style.height = previous === next ? previous : next;
|
|
207
|
+
if (textareaRef.current) syncComposerHeight(textareaRef.current);
|
|
219
208
|
};
|
|
220
209
|
|
|
221
210
|
const setMessage = (text: string) => {
|
|
@@ -431,7 +420,7 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
431
420
|
autoCapitalize="off"
|
|
432
421
|
autoComplete="off"
|
|
433
422
|
autoCorrect="off"
|
|
434
|
-
className="min-h-12 max-h-28 w-full resize-none overflow-y-auto border-0 bg-transparent px-3 py-3 font-sans text-base leading-relaxed text-dashboard-text outline-none placeholder:text-dashboard-text-muted/65 md:min-h-14 md:max-h-
|
|
423
|
+
className="min-h-12 max-h-28 w-full resize-none overflow-y-auto border-0 bg-transparent px-3 py-3 font-sans text-base leading-relaxed text-dashboard-text outline-none placeholder:text-dashboard-text-muted/65 md:min-h-14 md:max-h-[min(calc(12lh_+_1.5rem),40dvh)] md:px-4 md:py-3"
|
|
435
424
|
disabled={Boolean(props.restoreDraftOnError && createPending)}
|
|
436
425
|
enterKeyHint="send"
|
|
437
426
|
id={id}
|
|
@@ -514,6 +503,15 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
514
503
|
);
|
|
515
504
|
});
|
|
516
505
|
|
|
506
|
+
/** Fit the textarea to its text. CSS `max-height` sets the scroll limit. */
|
|
507
|
+
function syncComposerHeight(textarea: HTMLTextAreaElement): void {
|
|
508
|
+
// Measure from auto height so the box can also shrink after deletes.
|
|
509
|
+
const previous = textarea.style.height;
|
|
510
|
+
textarea.style.height = "auto";
|
|
511
|
+
const next = `${textarea.scrollHeight}px`;
|
|
512
|
+
textarea.style.height = previous === next ? previous : next;
|
|
513
|
+
}
|
|
514
|
+
|
|
517
515
|
function emptyDraft(): ConversationDraft {
|
|
518
516
|
return {
|
|
519
517
|
idempotencyKey: crypto.randomUUID(),
|
|
@@ -93,7 +93,7 @@ export function AutomationDetailsDrawer(props: {
|
|
|
93
93
|
<AutomationLifecycle
|
|
94
94
|
key={`${automation.kind}:${automation.id}`}
|
|
95
95
|
automation={automation}
|
|
96
|
-
editPath={`/automations/${
|
|
96
|
+
editPath={`/automations/${encodeURIComponent(automation.id)}/edit${location.search}`}
|
|
97
97
|
/>
|
|
98
98
|
<DetailList>
|
|
99
99
|
{details.map((detail) => (
|
|
@@ -121,14 +121,14 @@ export function AutomationDetailsDrawer(props: {
|
|
|
121
121
|
automation.lastRunStatus !== "blocked" ? (
|
|
122
122
|
<Link
|
|
123
123
|
className="w-fit py-2 text-sm text-dashboard-text underline underline-offset-2"
|
|
124
|
-
to={`/automations/${
|
|
124
|
+
to={`/automations/${encodeURIComponent(automation.id)}/executions`}
|
|
125
125
|
>
|
|
126
126
|
View all runs
|
|
127
127
|
</Link>
|
|
128
128
|
) : null}
|
|
129
129
|
<Link
|
|
130
130
|
className="w-fit py-2 text-sm text-dashboard-text underline underline-offset-2"
|
|
131
|
-
to={`/automations/${
|
|
131
|
+
to={`/automations/${encodeURIComponent(automation.id)}/versions${location.search}`}
|
|
132
132
|
>
|
|
133
133
|
Version history
|
|
134
134
|
</Link>
|
|
@@ -172,7 +172,7 @@ function AutomationExecutionSummary(props: {
|
|
|
172
172
|
const { range, automation } = props;
|
|
173
173
|
const runCount = automation.runs[range];
|
|
174
174
|
const rangeLabel = timeRangeLabel(range);
|
|
175
|
-
const executionsPath = `/automations/${
|
|
175
|
+
const executionsPath = `/automations/${encodeURIComponent(automation.id)}/executions`;
|
|
176
176
|
return (
|
|
177
177
|
<div className="text-sm text-dashboard-text-muted">
|
|
178
178
|
{automation.totalRuns > 0 ? (
|
|
@@ -19,20 +19,20 @@ import { AutomationOutcomeList } from "./AutomationOutcomeFields";
|
|
|
19
19
|
|
|
20
20
|
/** Load a linkable editor. Owners and public readers can edit until work completes. */
|
|
21
21
|
export function AutomationEditPage(props: { enabled: boolean }) {
|
|
22
|
-
const {
|
|
22
|
+
const { automationId } = useParams();
|
|
23
23
|
const location = useLocation();
|
|
24
24
|
const summary = useAutomationData(props.enabled, automationId);
|
|
25
25
|
const canEdit =
|
|
26
26
|
summary.data &&
|
|
27
27
|
!(summary.data.kind === "scheduled" && summary.data.status === "completed");
|
|
28
28
|
const edit = useQuery({
|
|
29
|
-
queryKey: ["dashboard", "automations", "edit",
|
|
30
|
-
enabled: props.enabled && Boolean(canEdit)
|
|
29
|
+
queryKey: ["dashboard", "automations", "edit", automationId],
|
|
30
|
+
enabled: props.enabled && Boolean(canEdit),
|
|
31
31
|
retry: false,
|
|
32
32
|
queryFn: ({ signal }) =>
|
|
33
33
|
fetchDashboardJson(
|
|
34
34
|
automationEditSchema,
|
|
35
|
-
`/api/automations/${
|
|
35
|
+
`/api/automations/${encodeURIComponent(automationId!)}/edit`,
|
|
36
36
|
signal,
|
|
37
37
|
),
|
|
38
38
|
});
|
|
@@ -84,7 +84,7 @@ export function AutomationEditPage(props: { enabled: boolean }) {
|
|
|
84
84
|
{value ? (
|
|
85
85
|
<div className="flex items-center gap-4">
|
|
86
86
|
<Link
|
|
87
|
-
to={`/automations/${
|
|
87
|
+
to={`/automations/${encodeURIComponent(value.id)}/versions${location.search}`}
|
|
88
88
|
className="text-sm text-dashboard-text-muted underline underline-offset-2 hover:text-dashboard-text"
|
|
89
89
|
>
|
|
90
90
|
Version history
|
|
@@ -95,9 +95,7 @@ export function AutomationEditPage(props: { enabled: boolean }) {
|
|
|
95
95
|
</div>
|
|
96
96
|
) : null}
|
|
97
97
|
</div>
|
|
98
|
-
{(!value && summary.error) ||
|
|
99
|
-
(!edit.data && edit.error) ||
|
|
100
|
-
(value && value.kind !== kind) ? (
|
|
98
|
+
{(!value && summary.error) || (!edit.data && edit.error) ? (
|
|
101
99
|
<FormNotice tone="error" title="Automation could not be loaded.">
|
|
102
100
|
It may no longer be available, or you may not have access.{" "}
|
|
103
101
|
<Button
|
|
@@ -113,7 +111,7 @@ export function AutomationEditPage(props: { enabled: boolean }) {
|
|
|
113
111
|
<AutomationEditorLoading />
|
|
114
112
|
) : canEdit && edit.data ? (
|
|
115
113
|
<AutomationEditor
|
|
116
|
-
key={
|
|
114
|
+
key={automationId}
|
|
117
115
|
automation={edit.data}
|
|
118
116
|
summary={value}
|
|
119
117
|
returnPath={returnPath}
|
|
@@ -55,7 +55,7 @@ export function AutomationEditor(props: {
|
|
|
55
55
|
useEffect(() => {
|
|
56
56
|
if (blocker.state === "blocked") discardNotice.current?.focus();
|
|
57
57
|
}, [blocker.state]);
|
|
58
|
-
const url = `/api/automations/${
|
|
58
|
+
const url = `/api/automations/${encodeURIComponent(original.id)}`;
|
|
59
59
|
useEffect(() => {
|
|
60
60
|
const prevent = (event: BeforeUnloadEvent) => {
|
|
61
61
|
if (dirty && !allowLeave.current) {
|
|
@@ -42,20 +42,15 @@ const EXECUTION_GRID = "grid-cols-[minmax(0,1fr)_5.5rem_5.5rem_5.5rem_auto]";
|
|
|
42
42
|
|
|
43
43
|
/** Render one automation's terminal executions as a conversation-style list. */
|
|
44
44
|
export function AutomationExecutionsPage(props: { enabled: boolean }) {
|
|
45
|
-
const { automationId
|
|
45
|
+
const { automationId } = useParams();
|
|
46
46
|
const [searchParams] = useSearchParams();
|
|
47
|
-
const
|
|
48
|
-
const query = useAutomationExecutionsData(
|
|
49
|
-
props.enabled && Boolean(automationId && taskKind),
|
|
50
|
-
taskKind,
|
|
51
|
-
automationId,
|
|
52
|
-
);
|
|
47
|
+
const query = useAutomationExecutionsData(props.enabled, automationId);
|
|
53
48
|
const backTo = pathWithSearch(
|
|
54
49
|
automationId ? automationPath(automationId) : "/automations/list",
|
|
55
50
|
searchParams,
|
|
56
51
|
);
|
|
57
52
|
|
|
58
|
-
if (!automationId
|
|
53
|
+
if (!automationId) {
|
|
59
54
|
return <Navigate replace to="/automations" />;
|
|
60
55
|
}
|
|
61
56
|
if (!query.data && !query.error) {
|
|
@@ -30,7 +30,7 @@ export function AutomationLifecycle({
|
|
|
30
30
|
const action = paused || blocked ? "resume" : "pause";
|
|
31
31
|
const mutation = useMutation({
|
|
32
32
|
mutationFn: async () => {
|
|
33
|
-
const path = `/api/automations/${
|
|
33
|
+
const path = `/api/automations/${encodeURIComponent(automation.id)}`;
|
|
34
34
|
const current = await fetchDashboardJson(
|
|
35
35
|
automationEditSchema,
|
|
36
36
|
`${path}/edit`,
|
|
@@ -114,7 +114,7 @@ export function AutomationLifecycle({
|
|
|
114
114
|
: ""}{" "}
|
|
115
115
|
<Link
|
|
116
116
|
className="text-dashboard-text underline underline-offset-2"
|
|
117
|
-
to={`/automations/${
|
|
117
|
+
to={`/automations/${encodeURIComponent(automation.id)}/executions`}
|
|
118
118
|
>
|
|
119
119
|
View run history
|
|
120
120
|
</Link>
|
|
@@ -108,7 +108,7 @@ export function AutomationRow(props: {
|
|
|
108
108
|
{automation.ownedByViewer ? (
|
|
109
109
|
<div className="-mr-2 -mt-1.5 shrink-0">
|
|
110
110
|
<AutomationActions
|
|
111
|
-
editPath={`/automations/${
|
|
111
|
+
editPath={`/automations/${encodeURIComponent(automation.id)}/edit${location.search}`}
|
|
112
112
|
deleting={props.deleting}
|
|
113
113
|
onDelete={props.onDelete}
|
|
114
114
|
title={automation.title}
|
|
@@ -208,7 +208,7 @@ function AutomationTiming({ automation }: { automation: AutomationSummary }) {
|
|
|
208
208
|
{lastRunFailed ? (
|
|
209
209
|
<Link
|
|
210
210
|
className="text-amber-300 underline"
|
|
211
|
-
to={`/automations/${
|
|
211
|
+
to={`/automations/${encodeURIComponent(automation.id)}/executions`}
|
|
212
212
|
>
|
|
213
213
|
Last run {automation.lastRunStatus}
|
|
214
214
|
{automation.lastRunAt
|
|
@@ -43,7 +43,7 @@ export function AutomationScheduleFields(props: {
|
|
|
43
43
|
queryFn: () =>
|
|
44
44
|
post(
|
|
45
45
|
automationSchedulePreviewSchema,
|
|
46
|
-
`/api/automations
|
|
46
|
+
`/api/automations/${encodeURIComponent(props.automation.id)}/preview`,
|
|
47
47
|
previewValue,
|
|
48
48
|
),
|
|
49
49
|
});
|
|
@@ -28,20 +28,16 @@ const DESCRIPTION = "Saved settings for this automation, newest first.";
|
|
|
28
28
|
|
|
29
29
|
/** Load one Automation's versions with its summary for labels and access. */
|
|
30
30
|
export function AutomationVersionsPage(props: { enabled: boolean }) {
|
|
31
|
-
const { automationId
|
|
31
|
+
const { automationId } = useParams();
|
|
32
32
|
const [searchParams] = useSearchParams();
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const versions = useAutomationVersionsData(enabled, taskKind, automationId);
|
|
37
|
-
if (!automationId || !taskKind) {
|
|
33
|
+
const summary = useAutomationData(props.enabled, automationId);
|
|
34
|
+
const versions = useAutomationVersionsData(props.enabled, automationId);
|
|
35
|
+
if (!automationId) {
|
|
38
36
|
return <Navigate replace to="/automations" />;
|
|
39
37
|
}
|
|
40
38
|
const backTo = pathWithSearch(automationPath(automationId), searchParams);
|
|
41
39
|
const error = summary.error ?? versions.error;
|
|
42
|
-
const notFound =
|
|
43
|
-
(error instanceof DashboardApiError && error.status === 404) ||
|
|
44
|
-
(summary.data && summary.data.kind !== taskKind);
|
|
40
|
+
const notFound = error instanceof DashboardApiError && error.status === 404;
|
|
45
41
|
if (error || notFound) {
|
|
46
42
|
return (
|
|
47
43
|
<>
|
|
@@ -97,7 +93,7 @@ function AutomationVersionsView(props: {
|
|
|
97
93
|
const canActivate = !(
|
|
98
94
|
automation.kind === "scheduled" && automation.status === "completed"
|
|
99
95
|
);
|
|
100
|
-
const path = `/api/automations/${
|
|
96
|
+
const path = `/api/automations/${encodeURIComponent(automation.id)}`;
|
|
101
97
|
const activate = useMutation({
|
|
102
98
|
mutationFn: async (version: number) => {
|
|
103
99
|
// The user chose an exact saved version, so the latest revision is safe here.
|
|
@@ -158,7 +158,7 @@ export function AutomationsPage(props: {
|
|
|
158
158
|
const deletion = useMutation({
|
|
159
159
|
mutationFn: async (automation: AutomationSummary) => {
|
|
160
160
|
await deleteDashboardResource(
|
|
161
|
-
`/api/automations/${
|
|
161
|
+
`/api/automations/${encodeURIComponent(automation.id)}`,
|
|
162
162
|
);
|
|
163
163
|
},
|
|
164
164
|
onSuccess: async () => {
|
|
@@ -3031,13 +3031,10 @@ function mockStatusDays(
|
|
|
3031
3031
|
|
|
3032
3032
|
/** Build mock terminal executions for one viewer-visible task. */
|
|
3033
3033
|
export function readMockAutomationExecutions(
|
|
3034
|
-
kind: "scheduled" | "event",
|
|
3035
3034
|
id: string,
|
|
3036
3035
|
nowMs = NOW_MS,
|
|
3037
3036
|
): AutomationExecutionList | undefined {
|
|
3038
|
-
const task = mockTasks().find(
|
|
3039
|
-
(candidate) => candidate.kind === kind && candidate.id === id,
|
|
3040
|
-
);
|
|
3037
|
+
const task = mockTasks().find((candidate) => candidate.id === id);
|
|
3041
3038
|
if (!task) return undefined;
|
|
3042
3039
|
if (task.totalRuns === 0) {
|
|
3043
3040
|
return {
|
|
@@ -292,10 +292,7 @@ export function createMockReportingApi(): Hono<{
|
|
|
292
292
|
app.get("/automations/runs", () => {
|
|
293
293
|
const automations = readMockAutomationList().automations;
|
|
294
294
|
const runs = automations.flatMap((automation) => {
|
|
295
|
-
const report = readMockAutomationExecutions(
|
|
296
|
-
automation.kind,
|
|
297
|
-
automation.id,
|
|
298
|
-
);
|
|
295
|
+
const report = readMockAutomationExecutions(automation.id);
|
|
299
296
|
return (report?.executions ?? []).map((run) => ({
|
|
300
297
|
...run,
|
|
301
298
|
kind: automation.kind,
|
|
@@ -318,15 +315,12 @@ export function createMockReportingApi(): Hono<{
|
|
|
318
315
|
? jsonResponse(automationSummarySchema, automation)
|
|
319
316
|
: errorResponse("Automation not found.", 404);
|
|
320
317
|
});
|
|
321
|
-
app.get("/automations/:
|
|
318
|
+
app.get("/automations/:id/executions", (c) => {
|
|
322
319
|
const params = automationParamsSchema.safeParse(c.req.param());
|
|
323
320
|
if (!params.success) {
|
|
324
321
|
return errorResponse("Invalid route parameters.", 400);
|
|
325
322
|
}
|
|
326
|
-
const report = readMockAutomationExecutions(
|
|
327
|
-
params.data.kind,
|
|
328
|
-
params.data.id,
|
|
329
|
-
);
|
|
323
|
+
const report = readMockAutomationExecutions(params.data.id);
|
|
330
324
|
return report
|
|
331
325
|
? jsonResponse(automationExecutionListSchema, report)
|
|
332
326
|
: errorResponse("Task not found.", 404);
|