@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.
@@ -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
- const syncHeight = () => {
145
- if (window.matchMedia(DESKTOP_MEDIA_QUERY).matches) {
146
- if (textarea.style.height) textarea.style.height = "";
147
- return;
148
- }
149
- // Measure from auto height only when the value changed. Avoid layout work
150
- // on parent re-renders that leave the draft text alone.
151
- const previous = textarea.style.height;
152
- textarea.style.height = "auto";
153
- const nextHeight = Math.min(
154
- textarea.scrollHeight,
155
- MOBILE_COMPOSER_MAX_HEIGHT_PX,
156
- );
157
- const next = `${nextHeight}px`;
158
- if (previous === next) {
159
- textarea.style.height = previous;
160
- return;
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
- const textarea = textareaRef.current;
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-none md:overflow-visible md:px-4 md:py-3"
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/${automation.kind}/${encodeURIComponent(automation.id)}/edit${location.search}`}
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/${automation.kind}/${encodeURIComponent(automation.id)}/executions`}
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/${automation.kind}/${encodeURIComponent(automation.id)}/versions${location.search}`}
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/${automation.kind}/${encodeURIComponent(automation.id)}/executions`;
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 { kind, automationId } = useParams();
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", kind, automationId],
30
- enabled: props.enabled && Boolean(canEdit) && summary.data?.kind === kind,
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/${kind}/${encodeURIComponent(automationId!)}/edit`,
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/${value.kind}/${encodeURIComponent(value.id)}/versions${location.search}`}
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={`${kind}:${automationId}`}
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/${original.kind}/${encodeURIComponent(original.id)}`;
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, kind } = useParams();
45
+ const { automationId } = useParams();
46
46
  const [searchParams] = useSearchParams();
47
- const taskKind = kind === "scheduled" || kind === "event" ? kind : undefined;
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 || !taskKind) {
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/${automation.kind}/${encodeURIComponent(automation.id)}`;
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/${automation.kind}/${encodeURIComponent(automation.id)}/executions`}
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/${automation.kind}/${encodeURIComponent(automation.id)}/edit${location.search}`}
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/${automation.kind}/${encodeURIComponent(automation.id)}/executions`}
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/scheduled/${encodeURIComponent(props.automation.id)}/preview`,
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, kind } = useParams();
31
+ const { automationId } = useParams();
32
32
  const [searchParams] = useSearchParams();
33
- const taskKind = kind === "scheduled" || kind === "event" ? kind : undefined;
34
- const enabled = props.enabled && Boolean(automationId && taskKind);
35
- const summary = useAutomationData(enabled, automationId);
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/${automation.kind}/${encodeURIComponent(automation.id)}`;
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/${automation.kind}/${encodeURIComponent(automation.id)}`,
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/:kind/:id/executions", (c) => {
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);