@sentry/junior-dashboard 0.234.0 → 0.236.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 +100 -18
- package/dist/client.js +47 -46
- package/dist/index.d.ts +3 -1
- package/dist/index.js +100 -18
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- package/src/app.ts +7 -1
- package/src/assets/junior-avatar.png +0 -0
- package/src/assets/junior-favicon.png +0 -0
- package/src/assets.ts +1 -0
- package/src/client/App.tsx +35 -0
- package/src/client/api.ts +63 -0
- package/src/client/components/README.md +8 -4
- package/src/client/components/SegmentedNav.tsx +167 -0
- package/src/client/components/SegmentedTabs.tsx +24 -5
- package/src/client/components/layout/SecondaryNavigation.tsx +40 -83
- package/src/client/conversations/ConversationEventLog.tsx +45 -5
- package/src/client/conversations/ConversationForkLinks.tsx +51 -0
- package/src/client/conversations/ConversationHeader.tsx +12 -1
- package/src/client/conversations/ConversationPage.tsx +72 -2
- package/src/client/conversations/ForkConversationButton.tsx +125 -0
- package/src/client/conversations/TranscriptMessageView.tsx +28 -7
- package/src/client/conversations/TranscriptView.tsx +4 -0
- package/src/client/conversations/conversationRoutes.ts +10 -0
- package/src/client/conversations/queries.ts +45 -0
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +28 -27
- package/src/client/pages/automations/AutomationEditPage.tsx +23 -24
- package/src/client/pages/automations/AutomationEditor.tsx +66 -13
- package/src/client/pages/automations/AutomationFilters.tsx +22 -14
- package/src/client/pages/automations/AutomationLifecycle.tsx +6 -4
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +18 -0
- package/src/client/pages/automations/AutomationRow.tsx +172 -187
- package/src/client/pages/automations/AutomationVersionsPage.tsx +356 -0
- package/src/client/pages/automations/AutomationsPage.tsx +40 -42
- package/src/client/pages/automations/AutomationsPageLayout.tsx +6 -5
- package/src/client/pages/automations/README.md +14 -3
- package/src/client/pages/automations/automationFormat.ts +43 -0
- package/src/client/pages/code/CodeChangeRow.tsx +41 -0
- package/src/client/pages/code/CodePage.tsx +18 -93
- package/src/client/pages/code/CodeSummaryCards.tsx +53 -0
- package/src/client/pages/code/RepositoryPage.tsx +227 -0
- package/src/client/pages/code/RepositorySections.tsx +108 -0
- package/src/client/pages/code/RepositoryTabs.tsx +146 -0
- package/src/client/pages/code/codeRepositoryRoutes.ts +18 -0
- package/src/client/pages/dev/ComponentsPage.tsx +5 -22
- package/src/client/pages/dev/SegmentedFixtures.tsx +58 -0
- package/src/client/pages/memory/MemoryPageLayout.tsx +12 -13
- package/src/client/pages/system/SystemNavigation.tsx +6 -2
- package/src/client/pages/system/SystemPageLayout.tsx +4 -6
- package/src/mock-reporting/code-repository.ts +77 -0
- package/src/mock-reporting/fixtures.ts +8 -2
- package/src/mock-reporting/routes.ts +25 -8
- package/src/shell.ts +21 -5
|
@@ -14,15 +14,15 @@ import { AutomationsNavigation } from "./AutomationsPageLayout";
|
|
|
14
14
|
import { AutomationEditor } from "./AutomationEditor";
|
|
15
15
|
import { AutomationEditorLoading } from "./AutomationEditorLoading";
|
|
16
16
|
import { AutomationFormSection } from "./AutomationFormSection";
|
|
17
|
-
import {
|
|
17
|
+
import { AutomationOutcomeList } from "./AutomationOutcomeFields";
|
|
18
18
|
|
|
19
|
-
/** Load a linkable editor
|
|
19
|
+
/** Load a linkable editor. Owners and public readers can edit until work completes. */
|
|
20
20
|
export function AutomationEditPage(props: { enabled: boolean }) {
|
|
21
21
|
const { kind, automationId } = useParams();
|
|
22
22
|
const location = useLocation();
|
|
23
23
|
const summary = useAutomationData(props.enabled, automationId);
|
|
24
24
|
const canEdit =
|
|
25
|
-
summary.data
|
|
25
|
+
summary.data &&
|
|
26
26
|
!(summary.data.kind === "scheduled" && summary.data.status === "completed");
|
|
27
27
|
const edit = useQuery({
|
|
28
28
|
queryKey: ["dashboard", "automations", "edit", kind, automationId],
|
|
@@ -52,9 +52,8 @@ export function AutomationEditPage(props: { enabled: boolean }) {
|
|
|
52
52
|
? "warning"
|
|
53
53
|
: "neutral";
|
|
54
54
|
return (
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
<div className="mx-auto w-full min-w-0 max-w-5xl px-5 pt-7 sm:px-8 sm:pt-9">
|
|
55
|
+
<AutomationsNavigation>
|
|
56
|
+
<div className="mx-auto w-full min-w-0 max-w-5xl px-4 pt-7 sm:px-8 sm:pt-9">
|
|
58
57
|
<Link
|
|
59
58
|
to={returnPath}
|
|
60
59
|
className="inline-flex min-h-8 items-center gap-2 text-sm text-dashboard-text-muted no-underline hover:text-dashboard-text"
|
|
@@ -75,9 +74,17 @@ export function AutomationEditPage(props: { enabled: boolean }) {
|
|
|
75
74
|
) : null}
|
|
76
75
|
</div>
|
|
77
76
|
{value ? (
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
<div className="flex items-center gap-4">
|
|
78
|
+
<Link
|
|
79
|
+
to={`/automations/${value.kind}/${encodeURIComponent(value.id)}/versions${location.search}`}
|
|
80
|
+
className="text-sm text-dashboard-text-muted underline underline-offset-2 hover:text-dashboard-text"
|
|
81
|
+
>
|
|
82
|
+
Version history
|
|
83
|
+
</Link>
|
|
84
|
+
<StatusChip tone={statusTone}>
|
|
85
|
+
{status === "unavailable" ? "Trigger unavailable" : status}
|
|
86
|
+
</StatusChip>
|
|
87
|
+
</div>
|
|
81
88
|
) : null}
|
|
82
89
|
</div>
|
|
83
90
|
{(!value && summary.error) ||
|
|
@@ -107,7 +114,7 @@ export function AutomationEditPage(props: { enabled: boolean }) {
|
|
|
107
114
|
<AutomationReadOnly automation={value} />
|
|
108
115
|
)}
|
|
109
116
|
</div>
|
|
110
|
-
|
|
117
|
+
</AutomationsNavigation>
|
|
111
118
|
);
|
|
112
119
|
}
|
|
113
120
|
|
|
@@ -115,9 +122,8 @@ function AutomationReadOnly({ automation }: { automation: AutomationSummary }) {
|
|
|
115
122
|
return (
|
|
116
123
|
<>
|
|
117
124
|
<p className="mb-6 text-sm text-dashboard-text-muted">
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
: `Only ${automation.createdBy} can edit this automation.`}
|
|
125
|
+
This automation has completed and cannot be edited. It will not run
|
|
126
|
+
again.
|
|
121
127
|
</p>
|
|
122
128
|
<AutomationFormSection title="What to do" detail="The saved instruction.">
|
|
123
129
|
<p className="m-0 whitespace-pre-wrap break-words text-sm leading-relaxed">
|
|
@@ -145,17 +151,10 @@ function AutomationReadOnly({ automation }: { automation: AutomationSummary }) {
|
|
|
145
151
|
title="Where results go"
|
|
146
152
|
detail="Messages after successful work."
|
|
147
153
|
>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
{automationOutcomeLabel(outcome, automation.destination)}
|
|
153
|
-
</li>
|
|
154
|
-
))}
|
|
155
|
-
</ol>
|
|
156
|
-
) : (
|
|
157
|
-
<p className="m-0 text-sm">No success message</p>
|
|
158
|
-
)}
|
|
154
|
+
<AutomationOutcomeList
|
|
155
|
+
outcomes={automation.outcomes}
|
|
156
|
+
destination={automation.destination}
|
|
157
|
+
/>
|
|
159
158
|
</AutomationFormSection>
|
|
160
159
|
<AutomationFormSection
|
|
161
160
|
title="Credentials"
|
|
@@ -16,7 +16,10 @@ import { DashboardApiError, fetchDashboardJson, patch } from "../../http";
|
|
|
16
16
|
import { AutomationFormSection } from "./AutomationFormSection";
|
|
17
17
|
import { AutomationScheduleFields } from "./AutomationScheduleFields";
|
|
18
18
|
import { AutomationEventFields } from "./AutomationEventFields";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
AutomationOutcomeFields,
|
|
21
|
+
AutomationOutcomeList,
|
|
22
|
+
} from "./AutomationOutcomeFields";
|
|
20
23
|
import {
|
|
21
24
|
automationDraftChanges,
|
|
22
25
|
createAutomationDraft,
|
|
@@ -110,6 +113,14 @@ export function AutomationEditor(props: {
|
|
|
110
113
|
.flatMap(([, messages]) => messages)
|
|
111
114
|
.join(" ") || undefined;
|
|
112
115
|
const triggerField = original.kind === "scheduled" ? "schedule" : "trigger";
|
|
116
|
+
const owned = original.ownedByViewer;
|
|
117
|
+
const creator = props.summary.createdBy;
|
|
118
|
+
// Core switches a non-creator's execution change to system credentials.
|
|
119
|
+
const dropsCreatorCredentials =
|
|
120
|
+
!owned &&
|
|
121
|
+
original.credentialMode === "creator" &&
|
|
122
|
+
(changes.instruction !== undefined ||
|
|
123
|
+
(original.kind === "event" && changes.trigger !== undefined));
|
|
113
124
|
const triggerError = fieldError(triggerField);
|
|
114
125
|
const inputState = (key: string) => ({
|
|
115
126
|
"aria-invalid": Boolean(fieldError(key)) || undefined,
|
|
@@ -223,6 +234,13 @@ export function AutomationEditor(props: {
|
|
|
223
234
|
</FormNotice>
|
|
224
235
|
</div>
|
|
225
236
|
) : null}
|
|
237
|
+
{owned ? null : (
|
|
238
|
+
<p className="mt-0 mb-6 max-w-2xl text-sm leading-relaxed text-dashboard-text-muted">
|
|
239
|
+
{creator} created this automation. You can edit it because it is in a
|
|
240
|
+
public channel. Only {creator} can change where results go or turn on
|
|
241
|
+
their connected accounts. Version history keeps every saved change.
|
|
242
|
+
</p>
|
|
243
|
+
)}
|
|
226
244
|
<fieldset disabled={save.isPending} className="min-w-0">
|
|
227
245
|
<AutomationFormSection
|
|
228
246
|
title="What to do"
|
|
@@ -300,12 +318,24 @@ export function AutomationEditor(props: {
|
|
|
300
318
|
{fieldError("outcomes")}
|
|
301
319
|
</p>
|
|
302
320
|
) : null}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
321
|
+
{owned ? (
|
|
322
|
+
<AutomationOutcomeFields
|
|
323
|
+
value={draft.outcomes}
|
|
324
|
+
original={original.outcomes}
|
|
325
|
+
destination={props.summary.destination}
|
|
326
|
+
onChange={(outcomes) => change({ outcomes })}
|
|
327
|
+
/>
|
|
328
|
+
) : (
|
|
329
|
+
<>
|
|
330
|
+
<AutomationOutcomeList
|
|
331
|
+
outcomes={original.outcomes}
|
|
332
|
+
destination={props.summary.destination}
|
|
333
|
+
/>
|
|
334
|
+
<p className="m-0 text-xs text-dashboard-text-muted">
|
|
335
|
+
Only {creator} can change where results go.
|
|
336
|
+
</p>
|
|
337
|
+
</>
|
|
338
|
+
)}
|
|
309
339
|
</AutomationFormSection>
|
|
310
340
|
<AutomationFormSection
|
|
311
341
|
title="Credentials"
|
|
@@ -317,25 +347,41 @@ export function AutomationEditor(props: {
|
|
|
317
347
|
[
|
|
318
348
|
[
|
|
319
349
|
"creator",
|
|
320
|
-
|
|
321
|
-
|
|
350
|
+
owned
|
|
351
|
+
? "Your connected accounts"
|
|
352
|
+
: `${creator}’s connected accounts`,
|
|
353
|
+
owned
|
|
354
|
+
? "Uses accounts connected by you, the creator."
|
|
355
|
+
: `Uses accounts connected by ${creator}. Only ${creator} can turn this on.`,
|
|
322
356
|
],
|
|
323
357
|
[
|
|
324
358
|
"system",
|
|
325
359
|
"System credentials only",
|
|
326
|
-
|
|
360
|
+
owned
|
|
361
|
+
? "Does not use your connected accounts. Some work may not be possible."
|
|
362
|
+
: "Does not use anyone’s connected accounts. Some work may not be possible.",
|
|
327
363
|
],
|
|
328
364
|
] as const
|
|
329
365
|
).map(([value, label, detail]) => (
|
|
330
366
|
<label
|
|
331
367
|
key={value}
|
|
332
|
-
className="flex cursor-pointer items-start gap-3 px-4 py-4 has-checked:bg-dashboard-fill-soft"
|
|
368
|
+
className="flex cursor-pointer items-start gap-3 px-4 py-4 has-checked:bg-dashboard-fill-soft has-disabled:cursor-not-allowed has-disabled:opacity-60"
|
|
333
369
|
>
|
|
334
370
|
<input
|
|
335
371
|
type="radio"
|
|
336
372
|
name="credentials"
|
|
337
373
|
className="mt-0.5 size-4 shrink-0 accent-dashboard-focus"
|
|
338
|
-
checked={
|
|
374
|
+
checked={
|
|
375
|
+
dropsCreatorCredentials
|
|
376
|
+
? value === "system"
|
|
377
|
+
: draft.credentialMode === value
|
|
378
|
+
}
|
|
379
|
+
disabled={
|
|
380
|
+
value === "creator" &&
|
|
381
|
+
!owned &&
|
|
382
|
+
(original.credentialMode !== "creator" ||
|
|
383
|
+
dropsCreatorCredentials)
|
|
384
|
+
}
|
|
339
385
|
onChange={() => change({ credentialMode: value })}
|
|
340
386
|
/>
|
|
341
387
|
<span>
|
|
@@ -347,6 +393,13 @@ export function AutomationEditor(props: {
|
|
|
347
393
|
</label>
|
|
348
394
|
))}
|
|
349
395
|
</fieldset>
|
|
396
|
+
{dropsCreatorCredentials ? (
|
|
397
|
+
<FormNotice title="Saving switches this automation to system credentials.">
|
|
398
|
+
Only {creator} can let a changed instruction
|
|
399
|
+
{original.kind === "event" ? " or trigger" : ""} use their
|
|
400
|
+
connected accounts.
|
|
401
|
+
</FormNotice>
|
|
402
|
+
) : null}
|
|
350
403
|
{fieldError("credentialMode") ? (
|
|
351
404
|
<p role="alert">{fieldError("credentialMode")}</p>
|
|
352
405
|
) : null}
|
|
@@ -355,7 +408,7 @@ export function AutomationEditor(props: {
|
|
|
355
408
|
<p className="mb-5 text-xs leading-relaxed text-dashboard-text-muted sm:hidden">
|
|
356
409
|
Changes apply to future work. Saving does not run this automation.
|
|
357
410
|
</p>
|
|
358
|
-
<div className="sticky bottom-0 z-20 -mx-
|
|
411
|
+
<div className="sticky bottom-0 z-20 -mx-4 border-t border-dashboard-border-emphasis bg-dashboard-bg px-4 py-4 sm:-mx-8 sm:px-8">
|
|
359
412
|
<div className="flex items-center justify-between gap-3">
|
|
360
413
|
<div className="min-w-0 flex-1" role="status">
|
|
361
414
|
<p className="m-0 text-xs font-medium sm:text-sm">
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
AutomationListQuery,
|
|
7
7
|
} from "@sentry/junior/api/schema";
|
|
8
8
|
import { Button, ToggleButton } from "../../components/Button";
|
|
9
|
+
import { DirectorySortSelect } from "../../components/controls/DirectorySortSelect";
|
|
9
10
|
import { FilterGroup } from "../../components/FilterBar";
|
|
10
11
|
import { SearchInput } from "../../components/SearchInput";
|
|
11
12
|
|
|
@@ -25,7 +26,6 @@ export function AutomationFilters(props: {
|
|
|
25
26
|
Boolean(filters.creator),
|
|
26
27
|
Boolean(filters.destination),
|
|
27
28
|
filters.state !== "all",
|
|
28
|
-
filters.sort !== "newest",
|
|
29
29
|
].filter(Boolean).length;
|
|
30
30
|
return (
|
|
31
31
|
<div className="grid gap-3">
|
|
@@ -76,12 +76,12 @@ export function AutomationFilters(props: {
|
|
|
76
76
|
<div
|
|
77
77
|
id={controlsId}
|
|
78
78
|
className={cn(
|
|
79
|
-
"grid-cols-2 items-end gap-3 lg:grid-cols-
|
|
79
|
+
"grid-cols-2 items-end gap-3 lg:grid-cols-3 xl:grid-cols-[auto_repeat(3,minmax(0,1fr))]",
|
|
80
80
|
expanded ? "grid" : "hidden",
|
|
81
81
|
)}
|
|
82
82
|
>
|
|
83
83
|
<FilterGroup
|
|
84
|
-
className="col-span-2 lg:col-span-
|
|
84
|
+
className="col-span-2 lg:col-span-3 xl:col-span-1"
|
|
85
85
|
label="Type"
|
|
86
86
|
>
|
|
87
87
|
{(["all", "scheduled", "event"] as const).map((value) => (
|
|
@@ -123,22 +123,30 @@ export function AutomationFilters(props: {
|
|
|
123
123
|
]}
|
|
124
124
|
onChange={(value) => onChange("state", value)}
|
|
125
125
|
/>
|
|
126
|
-
<AutomationFilterSelect
|
|
127
|
-
label="Sort"
|
|
128
|
-
ariaLabel="Sort automations"
|
|
129
|
-
value={filters.sort}
|
|
130
|
-
onChange={(value) => onChange("sort", value)}
|
|
131
|
-
options={[
|
|
132
|
-
{ value: "newest", label: "Newest first" },
|
|
133
|
-
{ value: "oldest", label: "Oldest first" },
|
|
134
|
-
{ value: "title", label: "Title" },
|
|
135
|
-
]}
|
|
136
|
-
/>
|
|
137
126
|
</div>
|
|
138
127
|
</div>
|
|
139
128
|
);
|
|
140
129
|
}
|
|
141
130
|
|
|
131
|
+
/** Keep list order visible beside the result count instead of inside Filters. */
|
|
132
|
+
export function AutomationSort(props: {
|
|
133
|
+
value: AutomationListQuery["sort"];
|
|
134
|
+
onChange(value: string): void;
|
|
135
|
+
}) {
|
|
136
|
+
return (
|
|
137
|
+
<DirectorySortSelect
|
|
138
|
+
ariaLabel="Sort automations"
|
|
139
|
+
onChange={props.onChange}
|
|
140
|
+
options={[
|
|
141
|
+
{ value: "newest", label: "Recently created" },
|
|
142
|
+
{ value: "oldest", label: "Oldest created" },
|
|
143
|
+
{ value: "title", label: "Title A–Z" },
|
|
144
|
+
]}
|
|
145
|
+
value={props.value}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
142
150
|
function AutomationFilterSelect(props: {
|
|
143
151
|
label: string;
|
|
144
152
|
ariaLabel?: string;
|
|
@@ -9,7 +9,7 @@ import { StatusChip } from "../../components/StatusChip";
|
|
|
9
9
|
import { FormNotice } from "../../components/FormNotice";
|
|
10
10
|
import { DashboardApiError, fetchDashboardJson, post } from "../../http";
|
|
11
11
|
|
|
12
|
-
/** Explain lifecycle separately from the last execution and
|
|
12
|
+
/** Explain lifecycle separately from the last execution. Pause and resume are creator-only. */
|
|
13
13
|
export function AutomationLifecycle({
|
|
14
14
|
automation,
|
|
15
15
|
editPath,
|
|
@@ -23,7 +23,9 @@ export function AutomationLifecycle({
|
|
|
23
23
|
const blocked = automation.status === "blocked";
|
|
24
24
|
const unavailable =
|
|
25
25
|
automation.kind === "event" && !automation.triggerAvailable;
|
|
26
|
-
|
|
26
|
+
// Public readers can edit settings, but only the creator controls lifecycle.
|
|
27
|
+
const canEdit = !completed;
|
|
28
|
+
const canPause = automation.ownedByViewer && !completed;
|
|
27
29
|
const blockReason =
|
|
28
30
|
automation.kind === "scheduled" ? automation.statusReason : undefined;
|
|
29
31
|
const action = paused || blocked ? "resume" : "pause";
|
|
@@ -78,7 +80,7 @@ export function AutomationLifecycle({
|
|
|
78
80
|
<ButtonLink className="min-h-11" to={editPath}>
|
|
79
81
|
{canEdit ? "Edit automation" : "View settings"}
|
|
80
82
|
</ButtonLink>
|
|
81
|
-
{
|
|
83
|
+
{canPause ? (
|
|
82
84
|
<Button
|
|
83
85
|
className="min-h-11"
|
|
84
86
|
disabled={mutation.isPending}
|
|
@@ -119,7 +121,7 @@ export function AutomationLifecycle({
|
|
|
119
121
|
</Link>
|
|
120
122
|
</p>
|
|
121
123
|
) : null}
|
|
122
|
-
{
|
|
124
|
+
{canPause ? (
|
|
123
125
|
<details className="text-dashboard-text-muted">
|
|
124
126
|
<summary className="w-fit cursor-pointer py-2 text-xs hover:text-dashboard-text focus-visible:outline focus-visible:outline-dashboard-focus">
|
|
125
127
|
About pausing
|
|
@@ -9,6 +9,24 @@ import { cn } from "../../styles";
|
|
|
9
9
|
import type { AutomationDraft } from "./automationDraft";
|
|
10
10
|
import { automationOutcomeLabel } from "./automationOutcomes";
|
|
11
11
|
|
|
12
|
+
/** Show saved outcomes in order for a reader who cannot change them. */
|
|
13
|
+
export function AutomationOutcomeList(props: {
|
|
14
|
+
outcomes: AutomationEdit["outcomes"];
|
|
15
|
+
destination: AutomationSummary["destination"];
|
|
16
|
+
}) {
|
|
17
|
+
if (!props.outcomes.length)
|
|
18
|
+
return <p className="m-0 text-sm">No success message</p>;
|
|
19
|
+
return (
|
|
20
|
+
<ol className="m-0 grid gap-2 pl-5 text-sm">
|
|
21
|
+
{props.outcomes.map((outcome, index) => (
|
|
22
|
+
<li key={index}>
|
|
23
|
+
{automationOutcomeLabel(outcome, props.destination)}
|
|
24
|
+
</li>
|
|
25
|
+
))}
|
|
26
|
+
</ol>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
/** Keep outcome order and offer only supported message Destinations. */
|
|
13
31
|
export function AutomationOutcomeFields(props: {
|
|
14
32
|
value: AutomationDraft["outcomes"];
|