@sentry/junior-dashboard 0.233.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 +178 -29
- package/dist/client.js +53 -50
- package/dist/index.js +178 -29
- 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 +372 -345
- package/src/client/api.ts +60 -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 +19 -6
- package/src/client/components/SegmentedNav.tsx +167 -0
- package/src/client/components/SegmentedTabs.tsx +24 -5
- 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/components/layout/SecondaryNavigation.tsx +40 -83
- package/src/client/http.ts +31 -3
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +78 -68
- package/src/client/pages/automations/AutomationEditPage.tsx +171 -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 +179 -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 +343 -0
- package/src/client/pages/automations/AutomationScheduleFields.tsx +328 -0
- package/src/client/pages/automations/AutomationsPage.tsx +186 -439
- package/src/client/pages/automations/AutomationsPageLayout.tsx +19 -7
- package/src/client/pages/automations/README.md +19 -0
- package/src/client/pages/automations/automationDraft.ts +110 -0
- package/src/client/pages/automations/automationFormat.ts +43 -0
- package/src/client/pages/automations/automationOutcomes.ts +23 -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 +55 -26
- 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/client/searchParams.ts +4 -2
- package/src/client.tsx +7 -4
- package/src/mock-reporting/automation-collection.ts +101 -0
- package/src/mock-reporting/code-repository.ts +77 -0
- package/src/mock-reporting/fixtures.ts +9 -3
- package/src/mock-reporting/routes.ts +39 -17
- package/src/shell.ts +21 -5
|
@@ -1,27 +1,22 @@
|
|
|
1
|
-
import { useEffect
|
|
2
|
-
import {
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useLocation,
|
|
4
|
+
useNavigate,
|
|
5
|
+
useParams,
|
|
6
|
+
useSearchParams,
|
|
7
|
+
} from "react-router";
|
|
3
8
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
4
|
-
import type { AutomationSummary } from "@sentry/junior/api/schema";
|
|
5
9
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "lucide-react";
|
|
14
|
-
import { useAutomationsData } from "../../api";
|
|
15
|
-
import { Button, ToggleButton } from "../../components/Button";
|
|
16
|
-
import { FilterBar, FilterGroup } from "../../components/FilterBar";
|
|
10
|
+
automationListQuerySchema,
|
|
11
|
+
type AutomationSummary,
|
|
12
|
+
} from "@sentry/junior/api/schema";
|
|
13
|
+
import { Globe2, ListChecks, LockKeyhole, UserRound } from "lucide-react";
|
|
14
|
+
import { useAutomationsData, useAutomationData } from "../../api";
|
|
15
|
+
import { Button } from "../../components/Button";
|
|
16
|
+
import { AutomationFilters, AutomationSort } from "./AutomationFilters";
|
|
17
17
|
import { InlineError } from "../../components/InlineError";
|
|
18
18
|
import { PageContentSkeleton } from "../../components/PageContentSkeleton";
|
|
19
|
-
import {
|
|
20
|
-
pageCount,
|
|
21
|
-
pageItems,
|
|
22
|
-
PagePagination,
|
|
23
|
-
} from "../../components/Pagination";
|
|
24
|
-
import { SelectableRow } from "../../components/SelectableRow";
|
|
19
|
+
import { pageCount, PagePagination } from "../../components/Pagination";
|
|
25
20
|
import {
|
|
26
21
|
selectTimeSeries,
|
|
27
22
|
timeRangeBucketUnit,
|
|
@@ -31,33 +26,19 @@ import { Card } from "../../components/layout/Card";
|
|
|
31
26
|
import { PageHeader } from "../../components/layout/PageHeader";
|
|
32
27
|
import { StatCard } from "../../components/metrics/StatCard";
|
|
33
28
|
import { deleteDashboardResource } from "../../http";
|
|
34
|
-
import {
|
|
29
|
+
import { automationPath } from "../../format";
|
|
35
30
|
import {
|
|
36
31
|
pathWithSearch,
|
|
37
32
|
useDebouncedSearchParam,
|
|
38
33
|
useSearchParamEnum,
|
|
39
34
|
} from "../../searchParams";
|
|
40
|
-
import {
|
|
35
|
+
import { AutomationRow } from "./AutomationRow";
|
|
41
36
|
import { AutomationCostChart } from "./AutomationCostChart";
|
|
42
37
|
import { AutomationDetailsDrawer } from "./AutomationDetailsDrawer";
|
|
43
38
|
import { AutomationExecutionChart } from "./AutomationExecutionChart";
|
|
44
39
|
|
|
45
|
-
const AUTOMATION_PAGE_SIZE = 25;
|
|
46
40
|
const AUTOMATION_RANGE_OPTIONS = ["1", "7", "30", "90"] as const;
|
|
47
41
|
|
|
48
|
-
type AutomationFilter = "all" | AutomationSummary["kind"];
|
|
49
|
-
type AutomationScope = "mine" | "public";
|
|
50
|
-
|
|
51
|
-
const AUTOMATION_FILTERS = [
|
|
52
|
-
"all",
|
|
53
|
-
"scheduled",
|
|
54
|
-
"event",
|
|
55
|
-
] as const satisfies readonly AutomationFilter[];
|
|
56
|
-
const AUTOMATION_SCOPES = [
|
|
57
|
-
"mine",
|
|
58
|
-
"public",
|
|
59
|
-
] as const satisfies readonly AutomationScope[];
|
|
60
|
-
|
|
61
42
|
function parseTaskRange(value: string): TimeRangeDays {
|
|
62
43
|
const days = Number(value);
|
|
63
44
|
return (
|
|
@@ -65,24 +46,6 @@ function parseTaskRange(value: string): TimeRangeDays {
|
|
|
65
46
|
) as TimeRangeDays;
|
|
66
47
|
}
|
|
67
48
|
|
|
68
|
-
function formatDate(value: string): string {
|
|
69
|
-
return formatTime(value, {
|
|
70
|
-
dateStyle: "medium",
|
|
71
|
-
timeStyle: "short",
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function formatRunDate(value: string): string {
|
|
76
|
-
return formatTime(value, {
|
|
77
|
-
day: "numeric",
|
|
78
|
-
hour: "numeric",
|
|
79
|
-
minute: "2-digit",
|
|
80
|
-
month: "short",
|
|
81
|
-
timeZoneName: "short",
|
|
82
|
-
year: "numeric",
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
49
|
const EMPTY_TASKS: AutomationSummary[] = [];
|
|
87
50
|
|
|
88
51
|
/** Render viewer-owned and public-workspace automations in one native view. */
|
|
@@ -102,71 +65,89 @@ export function AutomationsPage(props: {
|
|
|
102
65
|
const range = parseTaskRange(rangeParam);
|
|
103
66
|
const setRange = (value: TimeRangeDays) =>
|
|
104
67
|
setRangeParam(String(value) as (typeof AUTOMATION_RANGE_OPTIONS)[number]);
|
|
105
|
-
const [
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
68
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
69
|
+
const [searchText, setSearchText] = useDebouncedSearchParam("q", {
|
|
70
|
+
resetPage: true,
|
|
71
|
+
});
|
|
72
|
+
// Ignore only invalid URL values. One bad field must not reset other filters.
|
|
73
|
+
const filters = automationListQuerySchema.parse(
|
|
74
|
+
Object.fromEntries(
|
|
75
|
+
// Zod owns this property name; it is not a Junior domain symbol.
|
|
76
|
+
// oxlint-disable-next-line anti-slop/no-shape-in-symbol-names
|
|
77
|
+
Object.entries(automationListQuerySchema.shape).flatMap(
|
|
78
|
+
([key, schema]) => {
|
|
79
|
+
const parsed = schema.safeParse(searchParams.get(key) ?? undefined);
|
|
80
|
+
return parsed.success && parsed.data !== undefined
|
|
81
|
+
? [[key, parsed.data]]
|
|
82
|
+
: [];
|
|
83
|
+
},
|
|
84
|
+
),
|
|
85
|
+
),
|
|
109
86
|
);
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
"mine",
|
|
113
|
-
AUTOMATION_SCOPES,
|
|
87
|
+
const request = new URLSearchParams(
|
|
88
|
+
Object.entries(filters).map(([key, value]) => [key, String(value)]),
|
|
114
89
|
);
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
90
|
+
const query = useAutomationsData(
|
|
91
|
+
props.enabled,
|
|
92
|
+
props.view === "list" ? request.toString() : "",
|
|
93
|
+
);
|
|
94
|
+
const detail = useAutomationData(props.enabled, automationId);
|
|
95
|
+
const setFilter = (key: string, value: string) =>
|
|
96
|
+
setSearchParams(
|
|
97
|
+
(current) => {
|
|
98
|
+
const next = new URLSearchParams(current);
|
|
99
|
+
next.delete("page");
|
|
100
|
+
if (value) next.set(key, value);
|
|
101
|
+
else next.delete(key);
|
|
102
|
+
return next;
|
|
103
|
+
},
|
|
104
|
+
{ replace: true },
|
|
105
|
+
);
|
|
106
|
+
const setPage = (page: number) =>
|
|
107
|
+
setSearchParams((current) => {
|
|
108
|
+
const next = new URLSearchParams(current);
|
|
109
|
+
if (page === 1) next.delete("page");
|
|
110
|
+
else next.set("page", String(page));
|
|
111
|
+
return next;
|
|
112
|
+
});
|
|
119
113
|
const listPath = "/automations/list";
|
|
120
114
|
const tasksPath = (pathname: string) =>
|
|
121
115
|
pathWithSearch(pathname, location.search);
|
|
122
116
|
const selectedTaskPath = (id: string) => tasksPath(automationPath(id));
|
|
123
117
|
const automations = query.data?.automations ?? EMPTY_TASKS;
|
|
124
|
-
const mineCount =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
).length;
|
|
133
|
-
const scopedTasks = useMemo(
|
|
134
|
-
() =>
|
|
135
|
-
automations.filter((automation) =>
|
|
136
|
-
scope === "mine"
|
|
137
|
-
? automation.ownedByViewer
|
|
138
|
-
: automation.destination.visibility === "public",
|
|
139
|
-
),
|
|
140
|
-
[scope, automations],
|
|
141
|
-
);
|
|
142
|
-
const visibleTasks = useMemo(
|
|
143
|
-
() =>
|
|
144
|
-
scopedTasks.filter(
|
|
145
|
-
(automation) => filter === "all" || automation.kind === filter,
|
|
146
|
-
),
|
|
147
|
-
[filter, scopedTasks],
|
|
148
|
-
);
|
|
149
|
-
const visibleTaskCount = visibleTasks.length;
|
|
150
|
-
const totalPages = pageCount(visibleTaskCount, AUTOMATION_PAGE_SIZE);
|
|
151
|
-
const pagedTasks = useMemo(
|
|
152
|
-
() =>
|
|
153
|
-
props.view === "list"
|
|
154
|
-
? pageItems(visibleTasks, page, AUTOMATION_PAGE_SIZE)
|
|
155
|
-
: visibleTasks,
|
|
156
|
-
[page, props.view, visibleTasks],
|
|
157
|
-
);
|
|
158
|
-
const selectedTask = useMemo(
|
|
159
|
-
() => automations.find((automation) => automation.id === automationId),
|
|
160
|
-
[automationId, automations],
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
useEffect(() => {
|
|
164
|
-
setPage(1);
|
|
165
|
-
}, [filter, scope, searchQuery, props.view]);
|
|
166
|
-
|
|
118
|
+
const mineCount = query.data?.counts.mine ?? 0;
|
|
119
|
+
const publicCount = query.data?.counts.public ?? 0;
|
|
120
|
+
const privateCount = query.data?.counts.private ?? 0;
|
|
121
|
+
const visibleTaskCount = query.data?.total ?? 0;
|
|
122
|
+
const totalPages = pageCount(visibleTaskCount, filters.pageSize);
|
|
123
|
+
const page = query.data?.page ?? filters.page;
|
|
124
|
+
const selectedTask = detail.data;
|
|
125
|
+
// A deletion or a shared URL can leave the requested page past the last page.
|
|
167
126
|
useEffect(() => {
|
|
168
|
-
if (
|
|
169
|
-
|
|
127
|
+
if (
|
|
128
|
+
props.view !== "list" ||
|
|
129
|
+
!query.data ||
|
|
130
|
+
query.isPlaceholderData ||
|
|
131
|
+
query.isFetching ||
|
|
132
|
+
query.data.page === filters.page
|
|
133
|
+
)
|
|
134
|
+
return;
|
|
135
|
+
setSearchParams(
|
|
136
|
+
(current) => {
|
|
137
|
+
const next = new URLSearchParams(current);
|
|
138
|
+
next.set("page", String(query.data.page));
|
|
139
|
+
return next;
|
|
140
|
+
},
|
|
141
|
+
{ replace: true },
|
|
142
|
+
);
|
|
143
|
+
}, [
|
|
144
|
+
filters.page,
|
|
145
|
+
props.view,
|
|
146
|
+
query.data,
|
|
147
|
+
query.isFetching,
|
|
148
|
+
query.isPlaceholderData,
|
|
149
|
+
setSearchParams,
|
|
150
|
+
]);
|
|
170
151
|
const deletion = useMutation({
|
|
171
152
|
mutationFn: async (automation: AutomationSummary) => {
|
|
172
153
|
await deleteDashboardResource(
|
|
@@ -199,30 +180,62 @@ export function AutomationsPage(props: {
|
|
|
199
180
|
|
|
200
181
|
return (
|
|
201
182
|
<>
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
183
|
+
{location.state?.automationSaved ? (
|
|
184
|
+
<p role="status" className="m-0 text-sm text-emerald-300">
|
|
185
|
+
Automation saved. Changes apply to future work.
|
|
186
|
+
</p>
|
|
187
|
+
) : null}
|
|
188
|
+
{props.view === "overview" ? (
|
|
189
|
+
<PageHeader
|
|
190
|
+
description="Scheduled and event-driven work created by users."
|
|
191
|
+
onRangeChange={setRange}
|
|
192
|
+
range={range}
|
|
193
|
+
title="Automations"
|
|
194
|
+
/>
|
|
195
|
+
) : (
|
|
196
|
+
<h2 className="m-0 font-display text-2xl font-light tracking-tight text-dashboard-text sm:text-3xl">
|
|
197
|
+
All automations
|
|
198
|
+
</h2>
|
|
199
|
+
)}
|
|
200
|
+
{props.view === "list" ? (
|
|
201
|
+
<AutomationFilters
|
|
202
|
+
filters={filters}
|
|
203
|
+
data={query.data}
|
|
204
|
+
onChange={setFilter}
|
|
205
|
+
searchText={searchText}
|
|
206
|
+
onSearch={setSearchText}
|
|
207
|
+
/>
|
|
208
|
+
) : null}
|
|
212
209
|
{loading ? (
|
|
213
210
|
<PageContentSkeleton
|
|
214
211
|
label="Loading automations"
|
|
215
212
|
variant={props.view === "overview" ? "stats" : "list"}
|
|
216
213
|
/>
|
|
217
214
|
) : null}
|
|
218
|
-
{
|
|
215
|
+
{query.error ? (
|
|
216
|
+
<InlineError>
|
|
217
|
+
Automations could not be loaded.{" "}
|
|
218
|
+
<Button onClick={() => void query.refetch()}>Try again</Button>
|
|
219
|
+
</InlineError>
|
|
220
|
+
) : null}
|
|
221
|
+
{automationId && detail.isPending ? (
|
|
222
|
+
<p role="status">Loading automation details…</p>
|
|
223
|
+
) : null}
|
|
224
|
+
{automationId && detail.error ? (
|
|
225
|
+
<InlineError>
|
|
226
|
+
Automation details could not be loaded. It may be unavailable or you
|
|
227
|
+
may not have access.{" "}
|
|
228
|
+
<Button onClick={() => void detail.refetch()}>Try again</Button>
|
|
229
|
+
</InlineError>
|
|
230
|
+
) : null}
|
|
231
|
+
{query.data && props.view === "overview" ? (
|
|
219
232
|
<>
|
|
220
233
|
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
|
221
234
|
<StatCard
|
|
222
235
|
detail="All automations you can access"
|
|
223
236
|
icon={ListChecks}
|
|
224
237
|
label="Total automations"
|
|
225
|
-
value={
|
|
238
|
+
value={query.data?.counts.all ?? 0}
|
|
226
239
|
/>
|
|
227
240
|
<StatCard
|
|
228
241
|
detail="Created by you"
|
|
@@ -259,348 +272,82 @@ export function AutomationsPage(props: {
|
|
|
259
272
|
) : null}
|
|
260
273
|
</>
|
|
261
274
|
) : null}
|
|
262
|
-
{
|
|
275
|
+
{query.data && props.view === "list" ? (
|
|
263
276
|
<>
|
|
264
|
-
|
|
265
|
-
<
|
|
266
|
-
<AutomationExecutionChart
|
|
267
|
-
bucketUnit={timeRangeBucketUnit(range)}
|
|
268
|
-
days={executionSeries}
|
|
269
|
-
range={range}
|
|
270
|
-
/>
|
|
271
|
-
<AutomationCostChart
|
|
272
|
-
bucketUnit={timeRangeBucketUnit(range)}
|
|
273
|
-
days={executionSeries}
|
|
274
|
-
range={range}
|
|
275
|
-
/>
|
|
276
|
-
</section>
|
|
277
|
-
) : null}
|
|
278
|
-
<FilterBar
|
|
279
|
-
search={{
|
|
280
|
-
label: "Search automations",
|
|
281
|
-
onChange: setSearchText,
|
|
282
|
-
placeholder: "Title",
|
|
283
|
-
value: searchText,
|
|
284
|
-
}}
|
|
285
|
-
>
|
|
286
|
-
<FilterGroup label="Scope">
|
|
287
|
-
<ToggleButton
|
|
288
|
-
className="inline-flex items-center gap-1.5"
|
|
289
|
-
onClick={() => setScope("mine")}
|
|
290
|
-
pressed={scope === "mine"}
|
|
291
|
-
variant="pill"
|
|
292
|
-
>
|
|
293
|
-
<UserRound aria-hidden="true" size={13} />
|
|
294
|
-
Mine <span className="opacity-65">{mineCount}</span>
|
|
295
|
-
</ToggleButton>
|
|
296
|
-
<ToggleButton
|
|
297
|
-
className="inline-flex items-center gap-1.5"
|
|
298
|
-
onClick={() => setScope("public")}
|
|
299
|
-
pressed={scope === "public"}
|
|
300
|
-
variant="pill"
|
|
301
|
-
>
|
|
302
|
-
<Globe2 aria-hidden="true" size={13} />
|
|
303
|
-
Public <span className="opacity-65">{publicCount}</span>
|
|
304
|
-
</ToggleButton>
|
|
305
|
-
</FilterGroup>
|
|
306
|
-
<FilterGroup label="Type">
|
|
307
|
-
{(["all", "scheduled", "event"] as const).map((kind) => (
|
|
308
|
-
<ToggleButton
|
|
309
|
-
key={kind}
|
|
310
|
-
onClick={() => setFilter(kind)}
|
|
311
|
-
pressed={filter === kind}
|
|
312
|
-
variant="pill"
|
|
313
|
-
>
|
|
314
|
-
{kind}
|
|
315
|
-
</ToggleButton>
|
|
316
|
-
))}
|
|
317
|
-
</FilterGroup>
|
|
318
|
-
</FilterBar>
|
|
319
|
-
<div className="flex flex-wrap items-baseline justify-between gap-x-5 gap-y-1 border-b border-white/[0.07] pb-3">
|
|
320
|
-
<p className="m-0 font-display text-lg text-dashboard-text">
|
|
277
|
+
<div className="flex flex-wrap items-center justify-between gap-x-5 gap-y-2">
|
|
278
|
+
<p className="m-0 text-xs text-dashboard-text-muted">
|
|
321
279
|
{visibleTaskCount}{" "}
|
|
322
280
|
{visibleTaskCount === 1 ? "automation" : "automations"}
|
|
281
|
+
{query.isFetching ? (
|
|
282
|
+
<span className="ml-3" role="status">
|
|
283
|
+
Updating results…
|
|
284
|
+
</span>
|
|
285
|
+
) : null}
|
|
323
286
|
</p>
|
|
324
|
-
<
|
|
325
|
-
{
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
</p>
|
|
287
|
+
<AutomationSort
|
|
288
|
+
value={filters.sort}
|
|
289
|
+
onChange={(value) => setFilter("sort", value)}
|
|
290
|
+
/>
|
|
329
291
|
</div>
|
|
330
|
-
{query.error ? (
|
|
331
|
-
<Card padding="md">
|
|
332
|
-
<InlineError>
|
|
333
|
-
Automations could not be loaded. Try again.
|
|
334
|
-
</InlineError>
|
|
335
|
-
</Card>
|
|
336
|
-
) : visibleTaskCount === 0 ? (
|
|
292
|
+
{!query.error && visibleTaskCount === 0 ? (
|
|
337
293
|
<Card padding="md">
|
|
338
294
|
<p className="m-0 text-sm text-dashboard-text-muted">
|
|
339
|
-
|
|
295
|
+
No automations matched these filters.
|
|
340
296
|
</p>
|
|
341
297
|
</Card>
|
|
342
298
|
) : (
|
|
343
|
-
<
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
window.confirm(
|
|
358
|
-
`Delete this ${automation.kind} automation?`,
|
|
359
|
-
)
|
|
360
|
-
) {
|
|
361
|
-
deletion.mutate(automation);
|
|
362
|
-
}
|
|
363
|
-
}}
|
|
364
|
-
onSelect={() =>
|
|
365
|
-
navigate(
|
|
366
|
-
automationId === automation.id
|
|
367
|
-
? tasksPath(listPath)
|
|
368
|
-
: 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?`,
|
|
369
313
|
)
|
|
314
|
+
) {
|
|
315
|
+
deletion.mutate(automation);
|
|
370
316
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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>
|
|
379
331
|
)}
|
|
380
332
|
<PagePagination
|
|
381
333
|
onPageChange={setPage}
|
|
382
334
|
page={page}
|
|
383
335
|
pageCount={totalPages}
|
|
384
|
-
pageSize={
|
|
336
|
+
pageSize={filters.pageSize}
|
|
385
337
|
total={visibleTaskCount}
|
|
386
338
|
/>
|
|
387
|
-
{query.data?.truncated ? (
|
|
388
|
-
<p className="m-0 text-center text-xs text-dashboard-text-muted">
|
|
389
|
-
Showing up to 100 recent automations in each scope.
|
|
390
|
-
</p>
|
|
391
|
-
) : null}
|
|
392
339
|
{deletion.error ? (
|
|
393
340
|
<InlineError className="text-center">
|
|
394
341
|
The automation could not be deleted. Try again.
|
|
395
342
|
</InlineError>
|
|
396
343
|
) : null}
|
|
397
|
-
<AutomationDetailsDrawer
|
|
398
|
-
onClose={() => navigate(tasksPath(listPath))}
|
|
399
|
-
range={range}
|
|
400
|
-
automation={selectedTask}
|
|
401
|
-
/>
|
|
402
344
|
</>
|
|
403
345
|
) : null}
|
|
346
|
+
<AutomationDetailsDrawer
|
|
347
|
+
onClose={() => navigate(tasksPath(listPath))}
|
|
348
|
+
range={range}
|
|
349
|
+
automation={selectedTask}
|
|
350
|
+
/>
|
|
404
351
|
</>
|
|
405
352
|
);
|
|
406
353
|
}
|
|
407
|
-
|
|
408
|
-
function emptyText(input: {
|
|
409
|
-
filter: AutomationFilter;
|
|
410
|
-
mineCount: number;
|
|
411
|
-
publicCount: number;
|
|
412
|
-
scope: AutomationScope;
|
|
413
|
-
search: string;
|
|
414
|
-
}): string {
|
|
415
|
-
if (input.search || input.filter !== "all") {
|
|
416
|
-
return "No automations matched these filters.";
|
|
417
|
-
}
|
|
418
|
-
if (input.scope === "mine" && input.mineCount === 0) {
|
|
419
|
-
return "You have no active or completed automations.";
|
|
420
|
-
}
|
|
421
|
-
if (input.scope === "public" && input.publicCount === 0) {
|
|
422
|
-
return "No automations are assigned to public destinations in your linked workspaces.";
|
|
423
|
-
}
|
|
424
|
-
return "No automations are available.";
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function AutomationListHeader() {
|
|
428
|
-
return (
|
|
429
|
-
<div
|
|
430
|
-
aria-hidden="true"
|
|
431
|
-
className="hidden grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)_minmax(0,1fr)_4.5rem_7.5rem_auto_auto] items-center gap-3 border-b border-white/[0.07] px-4 py-2.5 text-left font-mono text-xs uppercase tracking-[0.12em] text-dashboard-text-muted lg:grid"
|
|
432
|
-
>
|
|
433
|
-
<span>Automation</span>
|
|
434
|
-
<span>Destination</span>
|
|
435
|
-
<span>Trigger</span>
|
|
436
|
-
<span>Runs</span>
|
|
437
|
-
<span>Last run</span>
|
|
438
|
-
<span aria-hidden="true" className="size-8" />
|
|
439
|
-
<span aria-hidden="true" className="size-9" />
|
|
440
|
-
</div>
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function TaskRow(props: {
|
|
445
|
-
deleting: boolean;
|
|
446
|
-
onDelete(): void;
|
|
447
|
-
onSelect(): void;
|
|
448
|
-
range: TimeRangeDays;
|
|
449
|
-
selected: boolean;
|
|
450
|
-
automation: AutomationSummary;
|
|
451
|
-
}) {
|
|
452
|
-
const { range, automation } = props;
|
|
453
|
-
const runCount = automation.runs[range];
|
|
454
|
-
return (
|
|
455
|
-
<article role="listitem">
|
|
456
|
-
<SelectableRow
|
|
457
|
-
className="grid grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-3 px-4 py-3 md:grid-cols-[repeat(2,minmax(0,1fr))_auto_auto] lg:grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)_minmax(0,1fr)_4.5rem_7.5rem_auto_auto]"
|
|
458
|
-
onSelect={props.onSelect}
|
|
459
|
-
selected={props.selected}
|
|
460
|
-
>
|
|
461
|
-
<button
|
|
462
|
-
aria-expanded={props.selected}
|
|
463
|
-
className="flex min-w-0 cursor-pointer items-center gap-3 border-0 bg-transparent p-0 text-left"
|
|
464
|
-
onClick={props.onSelect}
|
|
465
|
-
type="button"
|
|
466
|
-
>
|
|
467
|
-
<TaskSourceMark automation={automation} />
|
|
468
|
-
<span className="min-w-0">
|
|
469
|
-
<span className="block truncate font-display text-base font-medium text-dashboard-text">
|
|
470
|
-
{automation.title}
|
|
471
|
-
</span>
|
|
472
|
-
<span className="mt-1 block truncate font-mono text-xs uppercase tracking-[0.08em] text-dashboard-text-muted">
|
|
473
|
-
<span className="md:hidden">{automation.destination.label}</span>
|
|
474
|
-
<span className="hidden md:inline">
|
|
475
|
-
{formatDate(automation.createdAt)}
|
|
476
|
-
</span>
|
|
477
|
-
</span>
|
|
478
|
-
</span>
|
|
479
|
-
</button>
|
|
480
|
-
<div className="hidden min-w-0 md:block">
|
|
481
|
-
<div className="truncate text-sm font-medium text-dashboard-text">
|
|
482
|
-
{automation.destination.label}
|
|
483
|
-
</div>
|
|
484
|
-
<div className="mt-1 font-mono text-xs uppercase tracking-[0.08em] text-dashboard-text-muted">
|
|
485
|
-
{automation.destination.visibility}
|
|
486
|
-
</div>
|
|
487
|
-
</div>
|
|
488
|
-
<div className="hidden min-w-0 lg:block">
|
|
489
|
-
<div className="truncate text-sm text-dashboard-text">
|
|
490
|
-
{automation.kind === "scheduled"
|
|
491
|
-
? automation.schedule
|
|
492
|
-
: automation.resource}
|
|
493
|
-
</div>
|
|
494
|
-
<div className="mt-1 truncate font-mono text-xs text-dashboard-text-muted">
|
|
495
|
-
{automation.kind === "scheduled"
|
|
496
|
-
? automation.nextRunAt
|
|
497
|
-
? `Next ${formatRunDate(automation.nextRunAt)}`
|
|
498
|
-
: "No next run"
|
|
499
|
-
: automation.events.join(", ")}
|
|
500
|
-
</div>
|
|
501
|
-
</div>
|
|
502
|
-
<div className="hidden min-w-0 lg:block">
|
|
503
|
-
<div className="truncate text-sm font-medium text-dashboard-text">
|
|
504
|
-
{runCount}
|
|
505
|
-
</div>
|
|
506
|
-
</div>
|
|
507
|
-
<div className="hidden min-w-0 lg:block">
|
|
508
|
-
<div className="truncate text-sm text-dashboard-text">
|
|
509
|
-
{automation.lastRunAt
|
|
510
|
-
? formatRelativeTime(automation.lastRunAt)
|
|
511
|
-
: "Never"}
|
|
512
|
-
</div>
|
|
513
|
-
<div className="mt-1 truncate font-mono text-xs text-dashboard-text-muted">
|
|
514
|
-
{automation.lastRunAt
|
|
515
|
-
? formatRunDate(automation.lastRunAt)
|
|
516
|
-
: "No executions"}
|
|
517
|
-
</div>
|
|
518
|
-
</div>
|
|
519
|
-
<button
|
|
520
|
-
aria-expanded={props.selected}
|
|
521
|
-
aria-label={`View automation details: ${automation.title}`}
|
|
522
|
-
className="grid size-8 cursor-pointer place-items-center rounded border border-transparent bg-transparent text-dashboard-text-muted transition-colors hover:border-white/10 hover:bg-white/[0.04] hover:text-dashboard-text"
|
|
523
|
-
onClick={props.onSelect}
|
|
524
|
-
type="button"
|
|
525
|
-
>
|
|
526
|
-
<ChevronRight
|
|
527
|
-
aria-hidden="true"
|
|
528
|
-
className={cn(
|
|
529
|
-
"transition-transform",
|
|
530
|
-
props.selected && "translate-x-0.5 text-cyan-200",
|
|
531
|
-
)}
|
|
532
|
-
size={16}
|
|
533
|
-
/>
|
|
534
|
-
</button>
|
|
535
|
-
{automation.ownedByViewer ? (
|
|
536
|
-
<Button
|
|
537
|
-
aria-label={`Delete: ${automation.title}`}
|
|
538
|
-
disabled={props.deleting}
|
|
539
|
-
onClick={props.onDelete}
|
|
540
|
-
size="icon"
|
|
541
|
-
title="Delete automation"
|
|
542
|
-
>
|
|
543
|
-
<Trash2 aria-hidden="true" size={15} />
|
|
544
|
-
</Button>
|
|
545
|
-
) : (
|
|
546
|
-
<span aria-hidden="true" className="size-8" />
|
|
547
|
-
)}
|
|
548
|
-
</SelectableRow>
|
|
549
|
-
</article>
|
|
550
|
-
);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
function TaskSourceMark(props: { automation: AutomationSummary }) {
|
|
554
|
-
const { automation } = props;
|
|
555
|
-
if (automation.kind === "scheduled") {
|
|
556
|
-
return (
|
|
557
|
-
<div
|
|
558
|
-
aria-label="Scheduled automation"
|
|
559
|
-
className="grid size-9 shrink-0 place-items-center rounded border border-white/[0.08] bg-white/[0.03] text-cyan-300/75"
|
|
560
|
-
role="img"
|
|
561
|
-
title="Scheduled automation"
|
|
562
|
-
>
|
|
563
|
-
<CalendarClock aria-hidden="true" size={16} />
|
|
564
|
-
</div>
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
const source = automation.source.trim();
|
|
568
|
-
const sourceKey = source.toLowerCase();
|
|
569
|
-
const isGitHub = sourceKey === "github";
|
|
570
|
-
const sourceLabel = isGitHub
|
|
571
|
-
? "GitHub"
|
|
572
|
-
: sourceKey === "pagerduty"
|
|
573
|
-
? "PagerDuty"
|
|
574
|
-
: source;
|
|
575
|
-
const sourceMark =
|
|
576
|
-
sourceKey === "pagerduty" ? "PD" : source.slice(0, 2).toUpperCase();
|
|
577
|
-
return (
|
|
578
|
-
<div
|
|
579
|
-
aria-label={`${sourceLabel} event automation`}
|
|
580
|
-
className="grid size-9 shrink-0 place-items-center rounded border border-white/[0.08] bg-white/[0.03] text-cyan-300/75"
|
|
581
|
-
role="img"
|
|
582
|
-
title={`${sourceLabel} event automation`}
|
|
583
|
-
>
|
|
584
|
-
{isGitHub ? (
|
|
585
|
-
<GitHubMark />
|
|
586
|
-
) : (
|
|
587
|
-
<span className="font-mono text-xs font-semibold uppercase tracking-[0.08em]">
|
|
588
|
-
{sourceMark}
|
|
589
|
-
</span>
|
|
590
|
-
)}
|
|
591
|
-
</div>
|
|
592
|
-
);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
function GitHubMark() {
|
|
596
|
-
return (
|
|
597
|
-
<svg
|
|
598
|
-
aria-hidden="true"
|
|
599
|
-
className="size-[18px]"
|
|
600
|
-
fill="currentColor"
|
|
601
|
-
viewBox="0 0 24 24"
|
|
602
|
-
>
|
|
603
|
-
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.418-1.305.762-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.793 1.23 1.1-.306 2.28-.459 3.45-.465 1.17.006 2.35.159 3.45.465 2.79-1.552 3.795-1.23 3.795-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.435.375.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
|
604
|
-
</svg>
|
|
605
|
-
);
|
|
606
|
-
}
|