@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,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
SegmentedNav,
|
|
5
|
+
SiblingPageLink,
|
|
6
|
+
type SegmentedNavItem,
|
|
7
|
+
} from "../SegmentedNav";
|
|
4
8
|
import {
|
|
5
9
|
cn,
|
|
6
10
|
dashboardContainerClass,
|
|
@@ -8,62 +12,20 @@ import {
|
|
|
8
12
|
} from "../../styles";
|
|
9
13
|
import { SecondaryNavigationPortal } from "./DashboardChrome";
|
|
10
14
|
|
|
11
|
-
export type SecondaryNavigationItem =
|
|
12
|
-
end?: boolean;
|
|
13
|
-
/** Override default NavLink matching when a path family should stay active. */
|
|
14
|
-
isActive?: (pathname: string) => boolean;
|
|
15
|
-
label: string;
|
|
16
|
-
to: string;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
type LinkClassState = { isActive: boolean };
|
|
20
|
-
|
|
21
|
-
function SecondaryNavItem(props: {
|
|
22
|
-
className: (state: LinkClassState) => string;
|
|
23
|
-
item: SecondaryNavigationItem;
|
|
24
|
-
pathname: string;
|
|
25
|
-
search: string;
|
|
26
|
-
}) {
|
|
27
|
-
const { className, item, pathname, search } = props;
|
|
28
|
-
// Keep page filters (range, scope, q) when moving across secondary sections.
|
|
29
|
-
const to = pathWithSearch(item.to, search);
|
|
30
|
-
if (item.isActive) {
|
|
31
|
-
const isActive = item.isActive(pathname);
|
|
32
|
-
return (
|
|
33
|
-
<Link
|
|
34
|
-
aria-current={isActive ? "page" : undefined}
|
|
35
|
-
className={className({ isActive })}
|
|
36
|
-
to={to}
|
|
37
|
-
>
|
|
38
|
-
{item.label}
|
|
39
|
-
</Link>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<NavLink className={className} end={item.end} to={to}>
|
|
45
|
-
{item.label}
|
|
46
|
-
</NavLink>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
15
|
+
export type SecondaryNavigationItem = SegmentedNavItem;
|
|
49
16
|
|
|
50
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Render page navigation above the page, in the desktop chrome, and in the
|
|
19
|
+
* mobile menu. On mobile the links sit at the top of the page and scroll with
|
|
20
|
+
* it. Pass the page as children so the links and the page share one layout
|
|
21
|
+
* row.
|
|
22
|
+
*/
|
|
51
23
|
export function SecondaryNavigation(props: {
|
|
52
24
|
ariaLabel: string;
|
|
25
|
+
children: ReactNode;
|
|
53
26
|
items: SecondaryNavigationItem[];
|
|
54
27
|
}) {
|
|
55
|
-
const
|
|
56
|
-
const desktopLinkClass = ({ isActive }: LinkClassState) =>
|
|
57
|
-
cn(
|
|
58
|
-
"relative flex h-12 shrink-0 items-center px-3 font-display text-xs font-medium no-underline transition-colors after:absolute after:inset-x-3 after:bottom-0 after:h-0.5 after:transition-colors sm:text-sm",
|
|
59
|
-
isActive
|
|
60
|
-
? "text-dashboard-text after:bg-cyan-300"
|
|
61
|
-
: cn(
|
|
62
|
-
"after:bg-transparent hover:bg-white/[0.025]",
|
|
63
|
-
dashboardInteractiveTextClass,
|
|
64
|
-
),
|
|
65
|
-
);
|
|
66
|
-
const mobileLinkClass = ({ isActive }: LinkClassState) =>
|
|
28
|
+
const menuLinkClass = ({ isActive }: { isActive: boolean }) =>
|
|
67
29
|
cn(
|
|
68
30
|
"rounded-lg px-3 py-3 pl-6 font-mono text-sm font-medium no-underline transition-colors",
|
|
69
31
|
isActive
|
|
@@ -72,44 +34,39 @@ export function SecondaryNavigation(props: {
|
|
|
72
34
|
);
|
|
73
35
|
|
|
74
36
|
return (
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
37
|
+
<div className="min-w-0">
|
|
38
|
+
<SecondaryNavigationPortal
|
|
39
|
+
desktop={
|
|
40
|
+
<div className="border-b border-white/[0.06] bg-white/[0.018]">
|
|
41
|
+
<div
|
|
42
|
+
className={cn(
|
|
43
|
+
dashboardContainerClass,
|
|
44
|
+
"min-w-0 px-4 py-2 md:px-8",
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
<SegmentedNav ariaLabel={props.ariaLabel} items={props.items} />
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
}
|
|
51
|
+
mobile={
|
|
78
52
|
<nav
|
|
79
53
|
aria-label={props.ariaLabel}
|
|
80
|
-
className=
|
|
81
|
-
dashboardContainerClass,
|
|
82
|
-
"flex min-w-0 gap-1 overflow-x-auto px-4 [scrollbar-width:none] md:px-8 [&::-webkit-scrollbar]:hidden",
|
|
83
|
-
)}
|
|
54
|
+
className="mt-3 grid gap-1 border-t border-white/[0.07] pt-3"
|
|
84
55
|
>
|
|
85
56
|
{props.items.map((item) => (
|
|
86
|
-
<
|
|
87
|
-
className={
|
|
57
|
+
<SiblingPageLink
|
|
58
|
+
className={menuLinkClass}
|
|
88
59
|
item={item}
|
|
89
60
|
key={item.to}
|
|
90
|
-
pathname={location.pathname}
|
|
91
|
-
search={location.search}
|
|
92
61
|
/>
|
|
93
62
|
))}
|
|
94
63
|
</nav>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
{props.items.map((item) => (
|
|
103
|
-
<SecondaryNavItem
|
|
104
|
-
className={mobileLinkClass}
|
|
105
|
-
item={item}
|
|
106
|
-
key={item.to}
|
|
107
|
-
pathname={location.pathname}
|
|
108
|
-
search={location.search}
|
|
109
|
-
/>
|
|
110
|
-
))}
|
|
111
|
-
</nav>
|
|
112
|
-
}
|
|
113
|
-
/>
|
|
64
|
+
}
|
|
65
|
+
/>
|
|
66
|
+
<div className="px-4 pt-4 sm:px-8 md:hidden">
|
|
67
|
+
<SegmentedNav ariaLabel={props.ariaLabel} fill items={props.items} />
|
|
68
|
+
</div>
|
|
69
|
+
{props.children}
|
|
70
|
+
</div>
|
|
114
71
|
);
|
|
115
72
|
}
|
package/src/client/http.ts
CHANGED
|
@@ -4,11 +4,20 @@ import { recordDashboardServerVersion } from "./dashboard-version";
|
|
|
4
4
|
/** An authenticated dashboard request rejected by the product API. */
|
|
5
5
|
export class DashboardApiError extends Error {
|
|
6
6
|
readonly status: number;
|
|
7
|
+
readonly fields?: Record<string, string[]>;
|
|
8
|
+
readonly code?: string;
|
|
7
9
|
readonly apiError?: string;
|
|
8
10
|
|
|
9
|
-
constructor(
|
|
11
|
+
constructor(
|
|
12
|
+
path: string,
|
|
13
|
+
status: number,
|
|
14
|
+
apiError?: string,
|
|
15
|
+
detail?: { fields?: Record<string, string[]>; code?: string },
|
|
16
|
+
) {
|
|
10
17
|
super(`${path} returned ${status}`);
|
|
11
18
|
this.status = status;
|
|
19
|
+
this.fields = detail?.fields;
|
|
20
|
+
this.code = detail?.code;
|
|
12
21
|
if (apiError?.trim()) this.apiError = apiError.trim();
|
|
13
22
|
}
|
|
14
23
|
}
|
|
@@ -18,13 +27,32 @@ async function throwDashboardApiError(
|
|
|
18
27
|
response: Response,
|
|
19
28
|
): Promise<never> {
|
|
20
29
|
let apiError: string | undefined;
|
|
30
|
+
let fields: Record<string, string[]> | undefined;
|
|
31
|
+
let code: string | undefined;
|
|
21
32
|
try {
|
|
22
|
-
const body = (await response.json()) as {
|
|
33
|
+
const body = (await response.json()) as {
|
|
34
|
+
error?: unknown;
|
|
35
|
+
code?: unknown;
|
|
36
|
+
fields?: unknown;
|
|
37
|
+
};
|
|
23
38
|
if (typeof body.error === "string") apiError = body.error;
|
|
39
|
+
if (typeof body.code === "string") code = body.code;
|
|
40
|
+
if (body.fields && typeof body.fields === "object") {
|
|
41
|
+
fields = Object.fromEntries(
|
|
42
|
+
Object.entries(body.fields).filter(
|
|
43
|
+
(entry): entry is [string, string[]] =>
|
|
44
|
+
Array.isArray(entry[1]) &&
|
|
45
|
+
entry[1].every((value) => typeof value === "string"),
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
24
49
|
} catch {
|
|
25
50
|
// Keep the status-only fallback when the body is not JSON.
|
|
26
51
|
}
|
|
27
|
-
throw new DashboardApiError(path, response.status, apiError
|
|
52
|
+
throw new DashboardApiError(path, response.status, apiError, {
|
|
53
|
+
fields,
|
|
54
|
+
code,
|
|
55
|
+
});
|
|
28
56
|
}
|
|
29
57
|
|
|
30
58
|
function restartDashboardSignIn(): void {
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import { AutomationLifecycle } from "./AutomationLifecycle";
|
|
2
|
+
import { automationOutcomeLabel } from "./automationOutcomes";
|
|
3
|
+
import {
|
|
4
|
+
automationEventConditions,
|
|
5
|
+
automationEventNames,
|
|
6
|
+
automationScheduleLabel,
|
|
7
|
+
formatAutomationRunTime,
|
|
8
|
+
} from "./automationFormat";
|
|
1
9
|
import type { AutomationSummary } from "@sentry/junior/api/schema";
|
|
2
|
-
import { Link } from "react-router";
|
|
3
|
-
import { MapPin } from "lucide-react";
|
|
10
|
+
import { Link, useLocation } from "react-router";
|
|
4
11
|
import { Detail, DetailList } from "../../components/DetailList";
|
|
5
12
|
import { Drawer } from "../../components/Drawer";
|
|
6
13
|
import {
|
|
@@ -17,6 +24,7 @@ export function AutomationDetailsDrawer(props: {
|
|
|
17
24
|
range: TimeRangeDays;
|
|
18
25
|
automation: AutomationSummary | undefined;
|
|
19
26
|
}) {
|
|
27
|
+
const location = useLocation();
|
|
20
28
|
if (!props.automation) return null;
|
|
21
29
|
|
|
22
30
|
const { automation } = props;
|
|
@@ -32,28 +40,25 @@ export function AutomationDetailsDrawer(props: {
|
|
|
32
40
|
) : (
|
|
33
41
|
automation.createdBy
|
|
34
42
|
);
|
|
43
|
+
const conditions =
|
|
44
|
+
automation.kind === "event" ? automationEventConditions(automation) : "";
|
|
35
45
|
const details =
|
|
36
46
|
automation.kind === "scheduled"
|
|
37
47
|
? [
|
|
38
|
-
{ label: "Schedule", value: automation
|
|
48
|
+
{ label: "Schedule", value: automationScheduleLabel(automation) },
|
|
39
49
|
{
|
|
40
50
|
label: "Next run",
|
|
41
|
-
value:
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
value:
|
|
52
|
+
automation.status === "active" && automation.nextRunAt
|
|
53
|
+
? formatAutomationRunTime(automation.nextRunAt)
|
|
54
|
+
: "None",
|
|
44
55
|
},
|
|
45
56
|
]
|
|
46
57
|
: [
|
|
47
|
-
{ label: "
|
|
48
|
-
{ label: "Events", value: automation
|
|
58
|
+
{ label: "Watching", value: automation.resource },
|
|
59
|
+
{ label: "Events", value: automationEventNames(automation) },
|
|
60
|
+
...(conditions ? [{ label: "Conditions", value: conditions }] : []),
|
|
49
61
|
];
|
|
50
|
-
const statusLabel =
|
|
51
|
-
automation.kind === "scheduled"
|
|
52
|
-
? automation.status
|
|
53
|
-
: automation.triggerAvailable
|
|
54
|
-
? "ready"
|
|
55
|
-
: "unavailable";
|
|
56
|
-
|
|
57
62
|
const titleId = "automation-details-drawer-title";
|
|
58
63
|
|
|
59
64
|
return (
|
|
@@ -68,9 +73,8 @@ export function AutomationDetailsDrawer(props: {
|
|
|
68
73
|
>
|
|
69
74
|
{automation.title}
|
|
70
75
|
</h2>
|
|
71
|
-
<div className="mt-1 break-words
|
|
72
|
-
|
|
73
|
-
{formatDate(automation.createdAt)}
|
|
76
|
+
<div className="mt-1 break-words text-sm text-dashboard-text-muted">
|
|
77
|
+
Created by {createdBy} · {automation.destination.label}
|
|
74
78
|
</div>
|
|
75
79
|
</>
|
|
76
80
|
}
|
|
@@ -80,53 +84,70 @@ export function AutomationDetailsDrawer(props: {
|
|
|
80
84
|
>
|
|
81
85
|
<section className="grid gap-5">
|
|
82
86
|
<div>
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
</
|
|
87
|
+
<h3 className="mt-0 mb-2 text-sm font-medium text-dashboard-text-muted">
|
|
88
|
+
What to do
|
|
89
|
+
</h3>
|
|
86
90
|
<TranscriptText text={automation.instruction} />
|
|
87
91
|
</div>
|
|
92
|
+
<AutomationLifecycle
|
|
93
|
+
key={`${automation.kind}:${automation.id}`}
|
|
94
|
+
automation={automation}
|
|
95
|
+
editPath={`/automations/${automation.kind}/${encodeURIComponent(automation.id)}/edit${location.search}`}
|
|
96
|
+
/>
|
|
88
97
|
<DetailList>
|
|
89
98
|
{details.map((detail) => (
|
|
90
99
|
<Detail key={detail.label} label={detail.label}>
|
|
91
100
|
{detail.value}
|
|
92
101
|
</Detail>
|
|
93
102
|
))}
|
|
94
|
-
<Detail label="
|
|
95
|
-
{automation.outcomes.length === 0
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{automation.destination.label} ·{" "}
|
|
107
|
-
{automation.destination.visibility}
|
|
108
|
-
</span>
|
|
109
|
-
</Detail>
|
|
110
|
-
<Detail label="Created">
|
|
111
|
-
{createdBy} · {formatDate(automation.createdAt)}
|
|
112
|
-
</Detail>
|
|
113
|
-
<Detail label="Executions">
|
|
114
|
-
<AutomationExecutionSummary
|
|
115
|
-
range={props.range}
|
|
116
|
-
automation={automation}
|
|
117
|
-
/>
|
|
103
|
+
<Detail label="On success">
|
|
104
|
+
{automation.outcomes.length === 0 ? (
|
|
105
|
+
"No message on success"
|
|
106
|
+
) : (
|
|
107
|
+
<ol className="m-0 pl-4">
|
|
108
|
+
{automation.outcomes.map((outcome, index) => (
|
|
109
|
+
<li key={index}>
|
|
110
|
+
{automationOutcomeLabel(outcome, automation.destination)}
|
|
111
|
+
</li>
|
|
112
|
+
))}
|
|
113
|
+
</ol>
|
|
114
|
+
)}
|
|
118
115
|
</Detail>
|
|
119
116
|
</DetailList>
|
|
120
|
-
{automation.totalRuns > 0
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
</
|
|
117
|
+
{automation.totalRuns > 0 &&
|
|
118
|
+
automation.lastRunStatus !== "failed" &&
|
|
119
|
+
automation.lastRunStatus !== "blocked" ? (
|
|
120
|
+
<Link
|
|
121
|
+
className="w-fit py-2 text-sm text-dashboard-text underline underline-offset-2"
|
|
122
|
+
to={`/automations/${automation.kind}/${encodeURIComponent(automation.id)}/executions`}
|
|
123
|
+
>
|
|
124
|
+
View all runs
|
|
125
|
+
</Link>
|
|
129
126
|
) : null}
|
|
127
|
+
<details>
|
|
128
|
+
<summary className="cursor-pointer py-3 text-sm text-dashboard-text-muted hover:text-dashboard-text focus-visible:outline focus-visible:outline-dashboard-focus">
|
|
129
|
+
More details
|
|
130
|
+
</summary>
|
|
131
|
+
<DetailList>
|
|
132
|
+
<Detail label="Connected accounts">
|
|
133
|
+
{automation.credentialMode === "creator" ? (
|
|
134
|
+
<>
|
|
135
|
+
Uses accounts connected by{" "}
|
|
136
|
+
{automation.ownedByViewer ? "you" : automation.createdBy}.
|
|
137
|
+
</>
|
|
138
|
+
) : (
|
|
139
|
+
"Junior’s accounts only"
|
|
140
|
+
)}
|
|
141
|
+
</Detail>
|
|
142
|
+
<Detail label="Created">{formatDate(automation.createdAt)}</Detail>
|
|
143
|
+
<Detail label="Run history">
|
|
144
|
+
<AutomationExecutionSummary
|
|
145
|
+
range={props.range}
|
|
146
|
+
automation={automation}
|
|
147
|
+
/>
|
|
148
|
+
</Detail>
|
|
149
|
+
</DetailList>
|
|
150
|
+
</details>
|
|
130
151
|
</section>
|
|
131
152
|
</Drawer>
|
|
132
153
|
);
|
|
@@ -169,16 +190,16 @@ function AutomationExecutionSummary(props: {
|
|
|
169
190
|
) : (
|
|
170
191
|
<>{automation.totalRuns} total</>
|
|
171
192
|
)}
|
|
172
|
-
{automation.lastRunAt ? " · Last
|
|
193
|
+
{automation.lastRunAt ? " · Last run " : " · Never run"}
|
|
173
194
|
{automation.lastRunAt && automation.lastConversationId ? (
|
|
174
195
|
<Link
|
|
175
196
|
className="text-dashboard-text underline decoration-white/20 underline-offset-2 hover:decoration-white/60"
|
|
176
197
|
to={conversationPath(automation.lastConversationId)}
|
|
177
198
|
>
|
|
178
|
-
{
|
|
199
|
+
{formatAutomationRunTime(automation.lastRunAt)}
|
|
179
200
|
</Link>
|
|
180
201
|
) : automation.lastRunAt ? (
|
|
181
|
-
|
|
202
|
+
formatAutomationRunTime(automation.lastRunAt)
|
|
182
203
|
) : null}
|
|
183
204
|
</span>
|
|
184
205
|
</div>
|
|
@@ -191,14 +212,3 @@ function formatDate(value: string): string {
|
|
|
191
212
|
timeStyle: "short",
|
|
192
213
|
});
|
|
193
214
|
}
|
|
194
|
-
|
|
195
|
-
function formatRunDate(value: string): string {
|
|
196
|
-
return formatTime(value, {
|
|
197
|
-
day: "numeric",
|
|
198
|
-
hour: "numeric",
|
|
199
|
-
minute: "2-digit",
|
|
200
|
-
month: "short",
|
|
201
|
-
timeZoneName: "short",
|
|
202
|
-
year: "numeric",
|
|
203
|
-
});
|
|
204
|
-
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Link, useLocation, useParams } from "react-router";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { ArrowLeft } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
automationEditSchema,
|
|
6
|
+
type AutomationSummary,
|
|
7
|
+
} from "@sentry/junior/api/schema";
|
|
8
|
+
import { useAutomationData } from "../../api";
|
|
9
|
+
import { fetchDashboardJson } from "../../http";
|
|
10
|
+
import { Button } from "../../components/Button";
|
|
11
|
+
import { FormNotice } from "../../components/FormNotice";
|
|
12
|
+
import { StatusChip } from "../../components/StatusChip";
|
|
13
|
+
import { AutomationsNavigation } from "./AutomationsPageLayout";
|
|
14
|
+
import { AutomationEditor } from "./AutomationEditor";
|
|
15
|
+
import { AutomationEditorLoading } from "./AutomationEditorLoading";
|
|
16
|
+
import { AutomationFormSection } from "./AutomationFormSection";
|
|
17
|
+
import { automationOutcomeLabel } from "./automationOutcomes";
|
|
18
|
+
|
|
19
|
+
/** Load a linkable editor, with readable public values for non-creators. */
|
|
20
|
+
export function AutomationEditPage(props: { enabled: boolean }) {
|
|
21
|
+
const { kind, automationId } = useParams();
|
|
22
|
+
const location = useLocation();
|
|
23
|
+
const summary = useAutomationData(props.enabled, automationId);
|
|
24
|
+
const canEdit =
|
|
25
|
+
summary.data?.ownedByViewer &&
|
|
26
|
+
!(summary.data.kind === "scheduled" && summary.data.status === "completed");
|
|
27
|
+
const edit = useQuery({
|
|
28
|
+
queryKey: ["dashboard", "automations", "edit", kind, automationId],
|
|
29
|
+
enabled: props.enabled && Boolean(canEdit) && summary.data?.kind === kind,
|
|
30
|
+
retry: false,
|
|
31
|
+
queryFn: ({ signal }) =>
|
|
32
|
+
fetchDashboardJson(
|
|
33
|
+
automationEditSchema,
|
|
34
|
+
`/api/automations/${kind}/${encodeURIComponent(automationId!)}/edit`,
|
|
35
|
+
signal,
|
|
36
|
+
),
|
|
37
|
+
});
|
|
38
|
+
const returnPath = `/automations/list${location.search}`;
|
|
39
|
+
const value = summary.data;
|
|
40
|
+
const status =
|
|
41
|
+
value?.kind === "event"
|
|
42
|
+
? value.status === "paused"
|
|
43
|
+
? "paused"
|
|
44
|
+
: value.triggerAvailable
|
|
45
|
+
? "active"
|
|
46
|
+
: "unavailable"
|
|
47
|
+
: value?.status;
|
|
48
|
+
const statusTone =
|
|
49
|
+
status === "active"
|
|
50
|
+
? "success"
|
|
51
|
+
: status === "blocked" || status === "unavailable"
|
|
52
|
+
? "warning"
|
|
53
|
+
: "neutral";
|
|
54
|
+
return (
|
|
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">
|
|
57
|
+
<Link
|
|
58
|
+
to={returnPath}
|
|
59
|
+
className="inline-flex min-h-8 items-center gap-2 text-sm text-dashboard-text-muted no-underline hover:text-dashboard-text"
|
|
60
|
+
>
|
|
61
|
+
<ArrowLeft aria-hidden size={15} />
|
|
62
|
+
All automations
|
|
63
|
+
</Link>
|
|
64
|
+
<div className="mt-4 mb-8 flex flex-wrap items-start justify-between gap-4">
|
|
65
|
+
<div className="min-w-0">
|
|
66
|
+
<h1 className="m-0 break-words font-display text-3xl font-light tracking-tight">
|
|
67
|
+
{value && !canEdit ? value.title : "Edit automation"}
|
|
68
|
+
</h1>
|
|
69
|
+
{value ? (
|
|
70
|
+
<p className="mt-2 mb-0 text-sm text-dashboard-text-muted">
|
|
71
|
+
Created by {value.ownedByViewer ? "you" : value.createdBy} ·{" "}
|
|
72
|
+
{value.destination.label}
|
|
73
|
+
</p>
|
|
74
|
+
) : null}
|
|
75
|
+
</div>
|
|
76
|
+
{value ? (
|
|
77
|
+
<StatusChip tone={statusTone}>
|
|
78
|
+
{status === "unavailable" ? "Trigger unavailable" : status}
|
|
79
|
+
</StatusChip>
|
|
80
|
+
) : null}
|
|
81
|
+
</div>
|
|
82
|
+
{(!value && summary.error) ||
|
|
83
|
+
(!edit.data && edit.error) ||
|
|
84
|
+
(value && value.kind !== kind) ? (
|
|
85
|
+
<FormNotice tone="error" title="Automation could not be loaded.">
|
|
86
|
+
It may no longer be available, or you may not have access.{" "}
|
|
87
|
+
<Button
|
|
88
|
+
onClick={() => {
|
|
89
|
+
void summary.refetch();
|
|
90
|
+
if (canEdit) void edit.refetch();
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
Try again
|
|
94
|
+
</Button>
|
|
95
|
+
</FormNotice>
|
|
96
|
+
) : !value || (canEdit && !edit.data) ? (
|
|
97
|
+
<AutomationEditorLoading />
|
|
98
|
+
) : canEdit && edit.data ? (
|
|
99
|
+
<AutomationEditor
|
|
100
|
+
key={`${kind}:${automationId}`}
|
|
101
|
+
automation={edit.data}
|
|
102
|
+
summary={value}
|
|
103
|
+
returnPath={returnPath}
|
|
104
|
+
/>
|
|
105
|
+
) : (
|
|
106
|
+
<AutomationReadOnly automation={value} />
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
</AutomationsNavigation>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function AutomationReadOnly({ automation }: { automation: AutomationSummary }) {
|
|
114
|
+
return (
|
|
115
|
+
<>
|
|
116
|
+
<p className="mb-6 text-sm text-dashboard-text-muted">
|
|
117
|
+
{automation.ownedByViewer
|
|
118
|
+
? "This automation has completed and cannot be edited. It will not run again."
|
|
119
|
+
: `Only ${automation.createdBy} can edit this automation.`}
|
|
120
|
+
</p>
|
|
121
|
+
<AutomationFormSection title="What to do" detail="The saved instruction.">
|
|
122
|
+
<p className="m-0 whitespace-pre-wrap break-words text-sm leading-relaxed">
|
|
123
|
+
{automation.instruction}
|
|
124
|
+
</p>
|
|
125
|
+
</AutomationFormSection>
|
|
126
|
+
<AutomationFormSection title="When to run" detail="The saved trigger.">
|
|
127
|
+
{automation.kind === "scheduled" ? (
|
|
128
|
+
<p className="m-0 text-sm">
|
|
129
|
+
{automation.schedule} · {automation.timezone}
|
|
130
|
+
</p>
|
|
131
|
+
) : (
|
|
132
|
+
<div className="grid gap-3 text-sm">
|
|
133
|
+
<p className="m-0 break-words">{automation.resource}</p>
|
|
134
|
+
<p className="m-0 break-words">{automation.events.join(", ")}</p>
|
|
135
|
+
{automation.match ? (
|
|
136
|
+
<pre className="whitespace-pre-wrap break-words text-xs">
|
|
137
|
+
{JSON.stringify(automation.match, null, 2)}
|
|
138
|
+
</pre>
|
|
139
|
+
) : null}
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
</AutomationFormSection>
|
|
143
|
+
<AutomationFormSection
|
|
144
|
+
title="Where results go"
|
|
145
|
+
detail="Messages after successful work."
|
|
146
|
+
>
|
|
147
|
+
{automation.outcomes.length ? (
|
|
148
|
+
<ol className="m-0 grid gap-2 pl-5 text-sm">
|
|
149
|
+
{automation.outcomes.map((outcome, index) => (
|
|
150
|
+
<li key={index}>
|
|
151
|
+
{automationOutcomeLabel(outcome, automation.destination)}
|
|
152
|
+
</li>
|
|
153
|
+
))}
|
|
154
|
+
</ol>
|
|
155
|
+
) : (
|
|
156
|
+
<p className="m-0 text-sm">No success message</p>
|
|
157
|
+
)}
|
|
158
|
+
</AutomationFormSection>
|
|
159
|
+
<AutomationFormSection
|
|
160
|
+
title="Credentials"
|
|
161
|
+
detail="The accounts Junior can use."
|
|
162
|
+
>
|
|
163
|
+
<p className="m-0 text-sm">
|
|
164
|
+
{automation.credentialMode === "creator"
|
|
165
|
+
? `Uses ${automation.ownedByViewer ? "your" : `${automation.createdBy}’s`} connected accounts.`
|
|
166
|
+
: "System credentials only."}
|
|
167
|
+
</p>
|
|
168
|
+
</AutomationFormSection>
|
|
169
|
+
</>
|
|
170
|
+
);
|
|
171
|
+
}
|