@sentry/junior-dashboard 0.234.0 → 0.235.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 +91 -16
- package/dist/client.js +43 -43
- package/dist/index.js +91 -16
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- 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 +13 -0
- package/src/client/api.ts +43 -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/pages/automations/AutomationDetailsDrawer.tsx +14 -21
- package/src/client/pages/automations/AutomationEditPage.tsx +3 -4
- package/src/client/pages/automations/AutomationEditor.tsx +1 -1
- package/src/client/pages/automations/AutomationFilters.tsx +22 -14
- package/src/client/pages/automations/AutomationRow.tsx +172 -187
- package/src/client/pages/automations/AutomationsPage.tsx +40 -42
- package/src/client/pages/automations/AutomationsPageLayout.tsx +6 -5
- 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/routes.ts +25 -8
- package/src/shell.ts +21 -5
|
@@ -6,38 +6,24 @@ import {
|
|
|
6
6
|
Trash2,
|
|
7
7
|
Zap,
|
|
8
8
|
} from "lucide-react";
|
|
9
|
-
import { useEffect, useId, useRef, useState } from "react";
|
|
9
|
+
import { type ReactNode, useEffect, useId, useRef, useState } from "react";
|
|
10
10
|
import { Link, useLocation } from "react-router";
|
|
11
11
|
import { ActorAvatar } from "../../components/ActorAvatar";
|
|
12
12
|
import { SelectableRow } from "../../components/SelectableRow";
|
|
13
13
|
import { StatusChip } from "../../components/StatusChip";
|
|
14
|
-
import { formatRelativeTime,
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export function AutomationListHeader() {
|
|
22
|
-
return (
|
|
23
|
-
<div
|
|
24
|
-
aria-hidden="true"
|
|
25
|
-
className={cn(
|
|
26
|
-
"hidden items-center gap-4 border-b border-dashboard-border-subtle px-4 py-2.5 text-xs text-dashboard-text-muted xl:grid",
|
|
27
|
-
desktopColumns,
|
|
28
|
-
)}
|
|
29
|
-
>
|
|
30
|
-
<span>Automation</span>
|
|
31
|
-
<span>Creator</span>
|
|
32
|
-
<span>Trigger</span>
|
|
33
|
-
<span>Destination</span>
|
|
34
|
-
<span>Last run</span>
|
|
35
|
-
<span className="sr-only">Actions</span>
|
|
36
|
-
</div>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
14
|
+
import { formatRelativeTime, peoplePath } from "../../format";
|
|
15
|
+
import {
|
|
16
|
+
automationEventConditions,
|
|
17
|
+
automationEventNames,
|
|
18
|
+
automationScheduleLabel,
|
|
19
|
+
formatAutomationRunTime,
|
|
20
|
+
} from "./automationFormat";
|
|
39
21
|
|
|
40
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Show one automation as a compact card: what it is, where it posts, who owns
|
|
24
|
+
* it, and when it runs. Instructions, timezones, and event conditions stay in
|
|
25
|
+
* the details drawer.
|
|
26
|
+
*/
|
|
41
27
|
export function AutomationRow(props: {
|
|
42
28
|
deleting: boolean;
|
|
43
29
|
onDelete(): void;
|
|
@@ -47,6 +33,7 @@ export function AutomationRow(props: {
|
|
|
47
33
|
}) {
|
|
48
34
|
const { automation } = props;
|
|
49
35
|
const location = useLocation();
|
|
36
|
+
const status = automationStatus(automation);
|
|
50
37
|
const creator = (
|
|
51
38
|
<>
|
|
52
39
|
<ActorAvatar
|
|
@@ -58,197 +45,195 @@ export function AutomationRow(props: {
|
|
|
58
45
|
</>
|
|
59
46
|
);
|
|
60
47
|
return (
|
|
61
|
-
<article
|
|
48
|
+
<article
|
|
49
|
+
className="min-w-0 rounded-lg border border-dashboard-border-subtle bg-dashboard-fill-faint"
|
|
50
|
+
role="listitem"
|
|
51
|
+
>
|
|
62
52
|
<SelectableRow
|
|
63
|
-
className=
|
|
64
|
-
"relative grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] items-center gap-x-3 gap-y-2 px-4 py-3 xl:gap-x-4",
|
|
65
|
-
desktopColumns,
|
|
66
|
-
)}
|
|
53
|
+
className="flex items-start gap-3 rounded-lg px-4 py-3.5"
|
|
67
54
|
onSelect={props.onSelect}
|
|
68
55
|
selected={props.selected}
|
|
69
56
|
>
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
<span
|
|
78
|
-
className="block truncate font-display text-base font-medium text-dashboard-text"
|
|
79
|
-
title={automation.title}
|
|
80
|
-
>
|
|
81
|
-
{automation.title}
|
|
82
|
-
</span>
|
|
83
|
-
<span
|
|
84
|
-
className="mt-1 block truncate text-xs text-dashboard-text-muted"
|
|
85
|
-
title={automation.instruction}
|
|
57
|
+
<div className="min-w-0 flex-1">
|
|
58
|
+
<button
|
|
59
|
+
aria-expanded={props.selected}
|
|
60
|
+
aria-label={`View automation details: ${automation.title}`}
|
|
61
|
+
className="block w-full min-w-0 cursor-pointer rounded border-0 bg-transparent p-0 text-left focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
|
|
62
|
+
onClick={props.onSelect}
|
|
63
|
+
type="button"
|
|
86
64
|
>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
65
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
66
|
+
<span
|
|
67
|
+
className="truncate font-display text-base font-medium text-dashboard-text"
|
|
68
|
+
title={automation.title}
|
|
69
|
+
>
|
|
70
|
+
{automation.title}
|
|
71
|
+
</span>
|
|
72
|
+
{status ? (
|
|
73
|
+
<StatusChip
|
|
74
|
+
className="shrink-0"
|
|
75
|
+
size="compact"
|
|
76
|
+
tone={
|
|
77
|
+
status === "completed" || status === "paused"
|
|
78
|
+
? "neutral"
|
|
79
|
+
: "warning"
|
|
80
|
+
}
|
|
81
|
+
>
|
|
82
|
+
{status}
|
|
83
|
+
</StatusChip>
|
|
84
|
+
) : null}
|
|
105
85
|
</span>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
</span>
|
|
128
|
-
<span
|
|
129
|
-
className="text-xs text-dashboard-text-muted xl:text-sm xl:text-dashboard-text"
|
|
130
|
-
title={
|
|
131
|
-
automation.lastRunAt
|
|
132
|
-
? formatTime(automation.lastRunAt, {
|
|
133
|
-
dateStyle: "medium",
|
|
134
|
-
timeStyle: "short",
|
|
135
|
-
})
|
|
136
|
-
: undefined
|
|
137
|
-
}
|
|
138
|
-
>
|
|
139
|
-
{automation.lastRunAt
|
|
140
|
-
? formatRelativeTime(automation.lastRunAt)
|
|
141
|
-
: "Never"}
|
|
142
|
-
</span>
|
|
143
|
-
{automation.lastRunStatus === "failed" ||
|
|
144
|
-
automation.lastRunStatus === "blocked" ? (
|
|
145
|
-
<Link
|
|
146
|
-
className="block text-xs text-amber-300 underline"
|
|
147
|
-
to={`/automations/${automation.kind}/${encodeURIComponent(automation.id)}/executions`}
|
|
148
|
-
>
|
|
149
|
-
Last run {automation.lastRunStatus}
|
|
150
|
-
</Link>
|
|
151
|
-
) : null}
|
|
86
|
+
<AutomationSummaryLine automation={automation} />
|
|
87
|
+
</button>
|
|
88
|
+
<div className="mt-2.5 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-dashboard-text-muted">
|
|
89
|
+
{automation.createdByEmail ? (
|
|
90
|
+
<Link
|
|
91
|
+
className="inline-flex min-w-0 max-w-full items-center gap-2 rounded text-dashboard-text no-underline hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
|
|
92
|
+
title={automation.createdBy}
|
|
93
|
+
to={peoplePath(automation.createdByEmail)}
|
|
94
|
+
>
|
|
95
|
+
{creator}
|
|
96
|
+
</Link>
|
|
97
|
+
) : (
|
|
98
|
+
<span
|
|
99
|
+
className="inline-flex min-w-0 max-w-full items-center gap-2 text-dashboard-text"
|
|
100
|
+
title={automation.createdBy}
|
|
101
|
+
>
|
|
102
|
+
{creator}
|
|
103
|
+
</span>
|
|
104
|
+
)}
|
|
105
|
+
<AutomationTiming automation={automation} />
|
|
106
|
+
</div>
|
|
152
107
|
</div>
|
|
153
|
-
|
|
154
|
-
|
|
108
|
+
{automation.ownedByViewer ? (
|
|
109
|
+
<div className="-mr-2 -mt-1.5 shrink-0">
|
|
155
110
|
<AutomationActions
|
|
156
111
|
editPath={`/automations/${automation.kind}/${encodeURIComponent(automation.id)}/edit${location.search}`}
|
|
157
112
|
deleting={props.deleting}
|
|
158
113
|
onDelete={props.onDelete}
|
|
159
114
|
title={automation.title}
|
|
160
115
|
/>
|
|
161
|
-
|
|
162
|
-
|
|
116
|
+
</div>
|
|
117
|
+
) : null}
|
|
163
118
|
</SelectableRow>
|
|
164
119
|
</article>
|
|
165
120
|
);
|
|
166
121
|
}
|
|
167
122
|
|
|
168
|
-
|
|
123
|
+
/** Return the one state worth flagging in the list, if the automation is not simply active. */
|
|
124
|
+
function automationStatus(automation: AutomationSummary): string | undefined {
|
|
125
|
+
if (automation.kind === "scheduled") {
|
|
126
|
+
return automation.status === "active" ? undefined : automation.status;
|
|
127
|
+
}
|
|
128
|
+
if (automation.status === "paused") return "paused";
|
|
129
|
+
return automation.triggerAvailable ? undefined : "Trigger unavailable";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Combine the trigger and Destination into one muted line under the title. */
|
|
133
|
+
function AutomationSummaryLine({
|
|
134
|
+
automation,
|
|
135
|
+
}: {
|
|
136
|
+
automation: AutomationSummary;
|
|
137
|
+
}) {
|
|
169
138
|
const scheduled = automation.kind === "scheduled";
|
|
170
139
|
const Icon = scheduled ? CalendarClock : Zap;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
let status: string | undefined;
|
|
174
|
-
let conditions: string | undefined;
|
|
175
|
-
|
|
176
|
-
if (automation.kind === "scheduled") {
|
|
177
|
-
summary = automation.schedule.includes(automation.timezone)
|
|
140
|
+
const trigger =
|
|
141
|
+
automation.kind === "scheduled"
|
|
178
142
|
? automation.schedule
|
|
179
|
-
: `${automation.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
month: "short",
|
|
189
|
-
day: "numeric",
|
|
190
|
-
hour: "numeric",
|
|
191
|
-
minute: "2-digit",
|
|
192
|
-
timeZone: automation.timezone,
|
|
193
|
-
timeZoneName: "short",
|
|
194
|
-
});
|
|
195
|
-
detail = `Next ${nextRun}`;
|
|
196
|
-
}
|
|
197
|
-
} else {
|
|
198
|
-
summary = automation.resource;
|
|
199
|
-
detail = automation.events
|
|
200
|
-
.map((event) => event.replaceAll(/[._]/g, " "))
|
|
201
|
-
.join(", ");
|
|
202
|
-
status =
|
|
203
|
-
automation.status === "paused"
|
|
204
|
-
? "paused"
|
|
205
|
-
: automation.triggerAvailable
|
|
206
|
-
? undefined
|
|
207
|
-
: "Trigger unavailable";
|
|
208
|
-
conditions = Object.entries(automation.match ?? {})
|
|
209
|
-
.map(
|
|
210
|
-
([field, value]) =>
|
|
211
|
-
`${field}: ${Array.isArray(value) ? value.join(" or ") : String(value)}`,
|
|
212
|
-
)
|
|
213
|
-
.join(" · ");
|
|
214
|
-
}
|
|
143
|
+
: `${automation.resource} · ${automationEventNames(automation)}`;
|
|
144
|
+
const conditions =
|
|
145
|
+
automation.kind === "event" ? automationEventConditions(automation) : "";
|
|
146
|
+
const triggerDetail =
|
|
147
|
+
automation.kind === "scheduled"
|
|
148
|
+
? automationScheduleLabel(automation)
|
|
149
|
+
: conditions
|
|
150
|
+
? `${trigger} · ${conditions}`
|
|
151
|
+
: trigger;
|
|
215
152
|
return (
|
|
216
|
-
<
|
|
153
|
+
<span className="mt-1 flex min-w-0 items-center gap-1.5 text-xs text-dashboard-text-muted">
|
|
217
154
|
<Icon
|
|
218
155
|
aria-label={
|
|
219
156
|
scheduled
|
|
220
157
|
? "Scheduled automation"
|
|
221
158
|
: `${automation.source} event automation`
|
|
222
159
|
}
|
|
223
|
-
className="
|
|
160
|
+
className="shrink-0"
|
|
224
161
|
role="img"
|
|
225
|
-
size={
|
|
162
|
+
size={13}
|
|
226
163
|
/>
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
164
|
+
<span className="min-w-0 truncate" title={triggerDetail}>
|
|
165
|
+
{trigger}
|
|
166
|
+
</span>
|
|
167
|
+
<span aria-hidden="true" className="shrink-0 opacity-45">
|
|
168
|
+
·
|
|
169
|
+
</span>
|
|
170
|
+
{automation.destination.visibility === "private" ? (
|
|
171
|
+
<LockKeyhole
|
|
172
|
+
aria-label="Private destination"
|
|
173
|
+
className="shrink-0"
|
|
174
|
+
role="img"
|
|
175
|
+
size={12}
|
|
176
|
+
/>
|
|
177
|
+
) : null}
|
|
178
|
+
<span
|
|
179
|
+
className="min-w-0 max-w-[45%] truncate"
|
|
180
|
+
title={automation.destination.label}
|
|
181
|
+
>
|
|
182
|
+
{automation.destination.label}
|
|
183
|
+
</span>
|
|
184
|
+
</span>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Show next and last run as short relative times after the creator. */
|
|
189
|
+
function AutomationTiming({ automation }: { automation: AutomationSummary }) {
|
|
190
|
+
const nextRunAt =
|
|
191
|
+
automation.kind === "scheduled" && automation.status === "active"
|
|
192
|
+
? automation.nextRunAt
|
|
193
|
+
: undefined;
|
|
194
|
+
const lastRunTitle = automation.lastRunAt
|
|
195
|
+
? formatAutomationRunTime(automation.lastRunAt)
|
|
196
|
+
: undefined;
|
|
197
|
+
const lastRunFailed =
|
|
198
|
+
automation.lastRunStatus === "failed" ||
|
|
199
|
+
automation.lastRunStatus === "blocked";
|
|
200
|
+
return (
|
|
201
|
+
<>
|
|
202
|
+
{nextRunAt ? (
|
|
203
|
+
<MetaItem title={formatAutomationRunTime(nextRunAt)}>
|
|
204
|
+
Next run {formatRelativeTime(nextRunAt)}
|
|
205
|
+
</MetaItem>
|
|
206
|
+
) : null}
|
|
207
|
+
<MetaItem title={lastRunTitle}>
|
|
208
|
+
{lastRunFailed ? (
|
|
209
|
+
<Link
|
|
210
|
+
className="text-amber-300 underline"
|
|
211
|
+
to={`/automations/${automation.kind}/${encodeURIComponent(automation.id)}/executions`}
|
|
246
212
|
>
|
|
247
|
-
{
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
213
|
+
Last run {automation.lastRunStatus}
|
|
214
|
+
{automation.lastRunAt
|
|
215
|
+
? ` ${formatRelativeTime(automation.lastRunAt)}`
|
|
216
|
+
: null}
|
|
217
|
+
</Link>
|
|
218
|
+
) : automation.lastRunAt ? (
|
|
219
|
+
`Last run ${formatRelativeTime(automation.lastRunAt)}`
|
|
220
|
+
) : (
|
|
221
|
+
"Never run"
|
|
222
|
+
)}
|
|
223
|
+
</MetaItem>
|
|
224
|
+
</>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Keep each separator with the item after it so wrapped lines never end in a dot. */
|
|
229
|
+
function MetaItem(props: { children: ReactNode; title?: string }) {
|
|
230
|
+
return (
|
|
231
|
+
<span className="whitespace-nowrap" title={props.title}>
|
|
232
|
+
<span aria-hidden="true" className="mr-2 opacity-45">
|
|
233
|
+
·
|
|
234
|
+
</span>
|
|
235
|
+
{props.children}
|
|
236
|
+
</span>
|
|
252
237
|
);
|
|
253
238
|
}
|
|
254
239
|
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import { Globe2, ListChecks, LockKeyhole, UserRound } from "lucide-react";
|
|
14
14
|
import { useAutomationsData, useAutomationData } from "../../api";
|
|
15
15
|
import { Button } from "../../components/Button";
|
|
16
|
-
import { AutomationFilters } from "./AutomationFilters";
|
|
16
|
+
import { AutomationFilters, AutomationSort } from "./AutomationFilters";
|
|
17
17
|
import { InlineError } from "../../components/InlineError";
|
|
18
18
|
import { PageContentSkeleton } from "../../components/PageContentSkeleton";
|
|
19
19
|
import { pageCount, PagePagination } from "../../components/Pagination";
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
useDebouncedSearchParam,
|
|
33
33
|
useSearchParamEnum,
|
|
34
34
|
} from "../../searchParams";
|
|
35
|
-
import { AutomationRow
|
|
35
|
+
import { AutomationRow } from "./AutomationRow";
|
|
36
36
|
import { AutomationCostChart } from "./AutomationCostChart";
|
|
37
37
|
import { AutomationDetailsDrawer } from "./AutomationDetailsDrawer";
|
|
38
38
|
import { AutomationExecutionChart } from "./AutomationExecutionChart";
|
|
@@ -274,16 +274,20 @@ export function AutomationsPage(props: {
|
|
|
274
274
|
) : null}
|
|
275
275
|
{query.data && props.view === "list" ? (
|
|
276
276
|
<>
|
|
277
|
-
<div className="flex flex-wrap items-
|
|
277
|
+
<div className="flex flex-wrap items-center justify-between gap-x-5 gap-y-2">
|
|
278
278
|
<p className="m-0 text-xs text-dashboard-text-muted">
|
|
279
279
|
{visibleTaskCount}{" "}
|
|
280
280
|
{visibleTaskCount === 1 ? "automation" : "automations"}
|
|
281
|
-
</p>
|
|
282
|
-
<p className="m-0 text-xs text-dashboard-text-muted">
|
|
283
281
|
{query.isFetching ? (
|
|
284
|
-
<span role="status">
|
|
282
|
+
<span className="ml-3" role="status">
|
|
283
|
+
Updating results…
|
|
284
|
+
</span>
|
|
285
285
|
) : null}
|
|
286
286
|
</p>
|
|
287
|
+
<AutomationSort
|
|
288
|
+
value={filters.sort}
|
|
289
|
+
onChange={(value) => setFilter("sort", value)}
|
|
290
|
+
/>
|
|
287
291
|
</div>
|
|
288
292
|
{!query.error && visibleTaskCount === 0 ? (
|
|
289
293
|
<Card padding="md">
|
|
@@ -292,44 +296,38 @@ export function AutomationsPage(props: {
|
|
|
292
296
|
</p>
|
|
293
297
|
</Card>
|
|
294
298
|
) : (
|
|
295
|
-
<
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
key={key}
|
|
310
|
-
onDelete={() => {
|
|
311
|
-
if (
|
|
312
|
-
window.confirm(
|
|
313
|
-
`Delete this ${automation.kind} automation?`,
|
|
314
|
-
)
|
|
315
|
-
) {
|
|
316
|
-
deletion.mutate(automation);
|
|
317
|
-
}
|
|
318
|
-
}}
|
|
319
|
-
onSelect={() =>
|
|
320
|
-
navigate(
|
|
321
|
-
automationId === automation.id
|
|
322
|
-
? tasksPath(listPath)
|
|
323
|
-
: selectedTaskPath(automation.id),
|
|
299
|
+
<div className="grid grid-cols-[minmax(0,1fr)] gap-2" role="list">
|
|
300
|
+
{automations.map((automation) => {
|
|
301
|
+
const key = `${automation.kind}:${automation.id}`;
|
|
302
|
+
return (
|
|
303
|
+
<AutomationRow
|
|
304
|
+
deleting={
|
|
305
|
+
deletion.isPending &&
|
|
306
|
+
deletion.variables?.id === automation.id
|
|
307
|
+
}
|
|
308
|
+
key={key}
|
|
309
|
+
onDelete={() => {
|
|
310
|
+
if (
|
|
311
|
+
window.confirm(
|
|
312
|
+
`Delete this ${automation.kind} automation?`,
|
|
324
313
|
)
|
|
314
|
+
) {
|
|
315
|
+
deletion.mutate(automation);
|
|
325
316
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
317
|
+
}}
|
|
318
|
+
onSelect={() =>
|
|
319
|
+
navigate(
|
|
320
|
+
automationId === automation.id
|
|
321
|
+
? tasksPath(listPath)
|
|
322
|
+
: selectedTaskPath(automation.id),
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
selected={automationId === automation.id}
|
|
326
|
+
automation={automation}
|
|
327
|
+
/>
|
|
328
|
+
);
|
|
329
|
+
})}
|
|
330
|
+
</div>
|
|
333
331
|
)}
|
|
334
332
|
<PagePagination
|
|
335
333
|
onPageChange={setPage}
|
|
@@ -27,22 +27,23 @@ const taskNavigationItems = [
|
|
|
27
27
|
];
|
|
28
28
|
|
|
29
29
|
/** Keep Automation navigation consistent across lists, details, and editing. */
|
|
30
|
-
export function AutomationsNavigation() {
|
|
30
|
+
export function AutomationsNavigation(props: { children: ReactNode }) {
|
|
31
31
|
return (
|
|
32
32
|
<SecondaryNavigation
|
|
33
33
|
ariaLabel="Automations navigation"
|
|
34
34
|
items={taskNavigationItems}
|
|
35
|
-
|
|
35
|
+
>
|
|
36
|
+
{props.children}
|
|
37
|
+
</SecondaryNavigation>
|
|
36
38
|
);
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/** Place the shared secondary navigation above one Automations page. */
|
|
40
42
|
export function AutomationsPageLayout(props: { children: ReactNode }) {
|
|
41
43
|
return (
|
|
42
|
-
|
|
43
|
-
<AutomationsNavigation />
|
|
44
|
+
<AutomationsNavigation>
|
|
44
45
|
<PageLayout>{props.children}</PageLayout>
|
|
45
|
-
|
|
46
|
+
</AutomationsNavigation>
|
|
46
47
|
);
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AutomationSummary } from "@sentry/junior/api/schema";
|
|
2
|
+
import { formatTime } from "../../format";
|
|
3
|
+
|
|
4
|
+
type ScheduledAutomation = Extract<AutomationSummary, { kind: "scheduled" }>;
|
|
5
|
+
type EventAutomation = Extract<AutomationSummary, { kind: "event" }>;
|
|
6
|
+
|
|
7
|
+
/** Show the schedule with its timezone unless the schedule text already names it. */
|
|
8
|
+
export function automationScheduleLabel(
|
|
9
|
+
automation: ScheduledAutomation,
|
|
10
|
+
): string {
|
|
11
|
+
return automation.schedule.includes(automation.timezone)
|
|
12
|
+
? automation.schedule
|
|
13
|
+
: `${automation.schedule} (${automation.timezone})`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Name event triggers in readable words. */
|
|
17
|
+
export function automationEventNames(automation: EventAutomation): string {
|
|
18
|
+
return automation.events
|
|
19
|
+
.map((event) => event.replaceAll(/[._]/g, " "))
|
|
20
|
+
.join(", ");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Summarize event match conditions as one readable line. */
|
|
24
|
+
export function automationEventConditions(automation: EventAutomation): string {
|
|
25
|
+
return Object.entries(automation.match ?? {})
|
|
26
|
+
.map(
|
|
27
|
+
([field, value]) =>
|
|
28
|
+
`${field}: ${Array.isArray(value) ? value.join(" or ") : String(value)}`,
|
|
29
|
+
)
|
|
30
|
+
.join(" · ");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Format a run time with date, time, and zone for details and tooltips. */
|
|
34
|
+
export function formatAutomationRunTime(value: string): string {
|
|
35
|
+
return formatTime(value, {
|
|
36
|
+
day: "numeric",
|
|
37
|
+
hour: "numeric",
|
|
38
|
+
minute: "2-digit",
|
|
39
|
+
month: "short",
|
|
40
|
+
timeZoneName: "short",
|
|
41
|
+
year: "numeric",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CodeChangeSummaryReport } from "@sentry/junior/api/schema";
|
|
2
|
+
|
|
3
|
+
import { StatusChip } from "../../components/StatusChip";
|
|
4
|
+
|
|
5
|
+
function stateTone(state: CodeChangeSummaryReport["state"]) {
|
|
6
|
+
if (state === "merged") return "success" as const;
|
|
7
|
+
if (state === "open") return "info" as const;
|
|
8
|
+
return "neutral" as const;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Show one code change with its title, repository reference, and state. */
|
|
12
|
+
export function CodeChangeRow(props: { change: CodeChangeSummaryReport }) {
|
|
13
|
+
const change = props.change;
|
|
14
|
+
const title = change.title ?? `${change.repository} #${change.number}`;
|
|
15
|
+
return (
|
|
16
|
+
<div className="flex min-w-0 items-center justify-between gap-4 border-b border-dashboard-border-subtle px-4 py-3 last:border-b-0">
|
|
17
|
+
<div className="min-w-0">
|
|
18
|
+
<div className="truncate font-display text-sm text-dashboard-text">
|
|
19
|
+
{change.url ? (
|
|
20
|
+
<a
|
|
21
|
+
className="text-inherit no-underline hover:text-cyan-100"
|
|
22
|
+
href={change.url}
|
|
23
|
+
rel="noreferrer"
|
|
24
|
+
target="_blank"
|
|
25
|
+
>
|
|
26
|
+
{title}
|
|
27
|
+
</a>
|
|
28
|
+
) : (
|
|
29
|
+
title
|
|
30
|
+
)}
|
|
31
|
+
</div>
|
|
32
|
+
<div className="mt-1 truncate font-mono text-xs text-dashboard-text-muted">
|
|
33
|
+
{change.repository} #{change.number}
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
<StatusChip size="compact" tone={stateTone(change.state)}>
|
|
37
|
+
{change.state}
|
|
38
|
+
</StatusChip>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|