@sentry/junior-dashboard 0.264.0 → 0.265.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/LICENSE +201 -0
- package/README.md +2 -5
- package/dist/app.js +119 -35
- package/dist/client.js +145 -136
- package/dist/index.d.ts +0 -1
- package/dist/index.js +119 -35
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/api/schema.ts +0 -1
- package/src/app.ts +5 -20
- package/src/client/App.tsx +29 -31
- package/src/client/api.ts +7 -0
- package/src/client/components/BottomSheet.tsx +12 -2
- package/src/client/components/CopyButton.tsx +83 -0
- package/src/client/components/DetailsPanel.tsx +133 -0
- package/src/client/components/Drawer.tsx +15 -24
- package/src/client/components/FilterBar.tsx +16 -31
- package/src/client/components/Metric.tsx +33 -23
- package/src/client/components/PageContentSkeleton.tsx +1 -1
- package/src/client/components/Pagination.tsx +1 -1
- package/src/client/components/ParticipantAvatarStack.tsx +40 -36
- package/src/client/components/ProfileMenu.tsx +27 -93
- package/src/client/components/README.md +86 -77
- package/src/client/components/SegmentedNav.tsx +1 -1
- package/src/client/components/ShimmerText.tsx +2 -6
- package/src/client/components/ThemeSwitcher.tsx +19 -15
- package/src/client/components/charts/ActivityChart.tsx +173 -288
- package/src/client/components/charts/CacheWrites.tsx +63 -50
- package/src/client/components/charts/InputCacheChart.tsx +137 -131
- package/src/client/components/charts/README.md +4 -4
- package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
- package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
- package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
- package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
- package/src/client/components/layout/DashboardHeader.tsx +44 -90
- package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
- package/src/client/components/layout/VisualViewportShell.tsx +2 -2
- package/src/client/components/ui/accordion.tsx +77 -0
- package/src/client/components/ui/alert-dialog.tsx +89 -0
- package/src/client/components/ui/button.tsx +85 -0
- package/src/client/components/ui/chart.tsx +74 -0
- package/src/client/components/ui/checkbox.tsx +26 -0
- package/src/client/components/ui/collapsible.tsx +22 -0
- package/src/client/components/ui/field.tsx +49 -0
- package/src/client/components/ui/input.tsx +35 -0
- package/src/client/components/ui/native-select.tsx +57 -0
- package/src/client/components/ui/popover.tsx +67 -0
- package/src/client/components/ui/radio-group.tsx +32 -0
- package/src/client/components/ui/sheet.tsx +13 -3
- package/src/client/components/ui/skeleton.tsx +19 -0
- package/src/client/components/ui/table.tsx +80 -0
- package/src/client/components/ui/tabs.tsx +75 -0
- package/src/client/components/ui/textarea.tsx +17 -0
- package/src/client/components/ui/toast.tsx +215 -0
- package/src/client/components/ui/toggle-group.tsx +36 -0
- package/src/client/components/ui/toggle.tsx +37 -0
- package/src/client/components/ui/tooltip.tsx +168 -0
- package/src/client/components/ui/trace.tsx +96 -0
- package/src/client/conversations/ChatLayout.tsx +7 -3
- package/src/client/conversations/ConversationBrief.tsx +17 -6
- package/src/client/conversations/ConversationComposer.tsx +26 -12
- package/src/client/conversations/ConversationDetails.tsx +147 -62
- package/src/client/conversations/ConversationEventDetails.tsx +141 -0
- package/src/client/conversations/ConversationEventLog.tsx +150 -129
- package/src/client/conversations/ConversationHeader.tsx +8 -3
- package/src/client/conversations/ConversationHeaderActions.tsx +90 -82
- package/src/client/conversations/ConversationHomeList.tsx +1 -1
- package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
- package/src/client/conversations/ConversationMeta.tsx +55 -88
- package/src/client/conversations/ConversationPage.tsx +220 -101
- package/src/client/conversations/ConversationSidebar.tsx +48 -29
- package/src/client/conversations/ConversationWatches.tsx +165 -0
- package/src/client/conversations/ConversationWorkspace.tsx +12 -22
- package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
- package/src/client/conversations/DetailsList.tsx +124 -0
- package/src/client/conversations/EventDetails.tsx +42 -8
- package/src/client/conversations/ForkConversationButton.tsx +27 -16
- package/src/client/conversations/ObjectCard.tsx +20 -8
- package/src/client/conversations/PendingMailboxStack.tsx +52 -33
- package/src/client/conversations/README.md +86 -13
- package/src/client/conversations/ShareConversationButton.tsx +25 -13
- package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
- package/src/client/conversations/ToolFrame.tsx +36 -52
- package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
- package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
- package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
- package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
- package/src/client/conversations/TranscriptLoading.tsx +1 -1
- package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
- package/src/client/conversations/TranscriptMessageContextView.tsx +8 -2
- package/src/client/conversations/TranscriptMessageView.tsx +6 -1
- package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
- package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
- package/src/client/conversations/TranscriptToolView.tsx +1 -1
- package/src/client/conversations/TranscriptTurnContextView.tsx +99 -27
- package/src/client/conversations/TranscriptView.tsx +63 -27
- package/src/client/conversations/eventLog.ts +57 -0
- package/src/client/conversations/eventTime.ts +60 -0
- package/src/client/conversations/queries.ts +29 -0
- package/src/client/conversations/transcriptBottomPinning.ts +34 -0
- package/src/client/format.ts +5 -0
- package/src/client/pages/ConnectionsPage.tsx +2 -2
- package/src/client/pages/PersonalTokensPage.tsx +4 -4
- package/src/client/pages/SettingsPage.tsx +9 -10
- package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
- package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
- package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
- package/src/client/pages/automations/AutomationDetails.tsx +193 -0
- package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
- package/src/client/pages/automations/AutomationEditor.tsx +41 -29
- package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
- package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
- package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
- package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
- package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
- package/src/client/pages/automations/AutomationFilters.tsx +189 -74
- package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
- package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
- package/src/client/pages/automations/AutomationRow.tsx +12 -138
- package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
- package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
- package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
- package/src/client/pages/automations/AutomationStatus.tsx +201 -0
- package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
- package/src/client/pages/automations/AutomationsPage.tsx +80 -68
- package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
- package/src/client/pages/automations/README.md +57 -0
- package/src/client/pages/automations/TriggerFilters.tsx +22 -0
- package/src/client/pages/automations/automationFormat.ts +37 -23
- package/src/client/pages/code/CodeActivityChart.tsx +59 -101
- package/src/client/pages/code/CodePage.tsx +52 -55
- package/src/client/pages/code/RepositoryPage.tsx +6 -8
- package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
- package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
- package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
- package/src/client/pages/memory/MemoryDetails.tsx +218 -0
- package/src/client/pages/memory/MemoryPage.tsx +168 -112
- package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
- package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
- package/src/client/pages/memory/memoryRecord.ts +1 -1
- package/src/client/pages/people/ContributionGrid.tsx +23 -16
- package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
- package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
- package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
- package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
- package/src/client/pages/system/GuardianActivity.tsx +1 -2
- package/src/client/pages/system/PluginBarChart.tsx +106 -87
- package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
- package/src/client/pages/system/README.md +23 -0
- package/src/client/pages/system/SystemNavigation.tsx +5 -1
- package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
- package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
- package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
- package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
- package/src/client/pages/system/WorkspaceList.tsx +20 -30
- package/src/client/pages/system/WorkspacesPage.tsx +117 -119
- package/src/client/pages/system/sandboxSize.ts +8 -0
- package/src/client/pages/system/workspaceQueries.ts +53 -0
- package/src/client/pages/user/PluginUserPage.tsx +51 -2
- package/src/client/pages/user/pluginUserPageData.ts +1 -6
- package/src/client/styles.ts +4 -0
- package/src/client.tsx +7 -1
- package/src/mock-reporting/automation-collection.ts +4 -2
- package/src/mock-reporting/fixtures.ts +23 -7
- package/src/mock-reporting/routes.ts +12 -0
- package/src/mock-reporting/watches.ts +94 -0
- package/src/shell.ts +4 -9
- package/src/tailwind.css +0 -10
- package/src/client/components/Button.tsx +0 -118
- package/src/client/components/Field.tsx +0 -53
- package/src/client/components/SegmentedTabs.tsx +0 -119
- package/src/client/components/Select.tsx +0 -52
- package/src/client/components/Skeleton.tsx +0 -17
- package/src/client/components/TextInput.tsx +0 -58
- package/src/client/components/Tooltip.tsx +0 -216
- package/src/client/components/charts/useChartLayout.ts +0 -27
- package/src/client/components/ui/sheet.LICENSE +0 -21
- package/src/client/conversations/ConversationDetailsFrame.tsx +0 -119
- package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
- package/src/client/pages/dev/ChartFixtures.tsx +0 -325
- package/src/client/pages/dev/ComponentsPage.tsx +0 -899
- package/src/client/pages/dev/ConversationFixture.tsx +0 -231
- package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
- package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
- package/src/client/pages/dev/SelectFixtures.tsx +0 -62
- package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
- package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
- package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
- package/src/client/pages/system/SnapshotSummary.tsx +0 -78
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { AutomationSummary } from "@sentry/junior/api/schema";
|
|
2
|
+
import { Link } from "react-router";
|
|
3
|
+
|
|
4
|
+
import { useAutomationExecutionsData } from "../../api";
|
|
5
|
+
import { Button } from "../../components/ui/button";
|
|
6
|
+
import { InlineError } from "../../components/InlineError";
|
|
7
|
+
import { Skeleton } from "../../components/ui/skeleton";
|
|
8
|
+
import { StatusChip } from "../../components/StatusChip";
|
|
9
|
+
import { conversationPath } from "../../conversations/conversationRoutes";
|
|
10
|
+
import {
|
|
11
|
+
formatCostSummary,
|
|
12
|
+
formatRelativeTime,
|
|
13
|
+
formatRuntime,
|
|
14
|
+
} from "../../format";
|
|
15
|
+
import { dashboardInlineLinkClass } from "../../styles";
|
|
16
|
+
import { formatAutomationRunTime } from "./automationFormat";
|
|
17
|
+
|
|
18
|
+
/** Preview the newest terminal runs without leaving the Automation list. */
|
|
19
|
+
export function AutomationRecentRuns({
|
|
20
|
+
automation,
|
|
21
|
+
}: {
|
|
22
|
+
automation: AutomationSummary;
|
|
23
|
+
}) {
|
|
24
|
+
const query = useAutomationExecutionsData(true, automation.id);
|
|
25
|
+
const runs = query.data?.executions.slice(0, 3) ?? [];
|
|
26
|
+
return (
|
|
27
|
+
<section aria-label="Recent runs" className="grid min-w-0 gap-2.5">
|
|
28
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
29
|
+
<h3 className="m-0 text-xs font-medium text-dashboard-text-muted">
|
|
30
|
+
Recent runs
|
|
31
|
+
</h3>
|
|
32
|
+
{automation.totalRuns > 0 || runs.length > 0 ? (
|
|
33
|
+
<Link
|
|
34
|
+
className={`${dashboardInlineLinkClass} text-xs`}
|
|
35
|
+
to={`/automations/${encodeURIComponent(automation.id)}/executions`}
|
|
36
|
+
>
|
|
37
|
+
View all runs
|
|
38
|
+
</Link>
|
|
39
|
+
) : null}
|
|
40
|
+
</div>
|
|
41
|
+
{query.isPending ? (
|
|
42
|
+
<div aria-busy="true" role="status" className="grid gap-2">
|
|
43
|
+
<span className="sr-only">Loading recent runs</span>
|
|
44
|
+
{Array.from({ length: 3 }, (_, index) => (
|
|
45
|
+
<Skeleton className="h-14 rounded-md" key={index} />
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
) : null}
|
|
49
|
+
{query.error ? (
|
|
50
|
+
<InlineError>
|
|
51
|
+
Recent runs could not be loaded.{" "}
|
|
52
|
+
<Button onClick={() => void query.refetch()}>Try again</Button>
|
|
53
|
+
</InlineError>
|
|
54
|
+
) : null}
|
|
55
|
+
{query.data && runs.length === 0 ? (
|
|
56
|
+
<p className="m-0 text-sm text-dashboard-text-muted">
|
|
57
|
+
{automation.totalRuns === 0
|
|
58
|
+
? "No runs yet. Results will appear here after the first run."
|
|
59
|
+
: "No recent runs are available. Older run details may have expired."}
|
|
60
|
+
</p>
|
|
61
|
+
) : null}
|
|
62
|
+
{runs.length > 0 ? (
|
|
63
|
+
<ol className="m-0 list-none divide-y divide-dashboard-border-subtle p-0">
|
|
64
|
+
{runs.map((run) => {
|
|
65
|
+
const metrics = [
|
|
66
|
+
run.durationMs !== undefined
|
|
67
|
+
? formatRuntime(run.durationMs)
|
|
68
|
+
: undefined,
|
|
69
|
+
run.costUsd !== undefined
|
|
70
|
+
? formatCostSummary({ total: run.costUsd })
|
|
71
|
+
: undefined,
|
|
72
|
+
]
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.join(" · ");
|
|
75
|
+
const title =
|
|
76
|
+
run.title !== automation.title ? run.title : undefined;
|
|
77
|
+
const content = (
|
|
78
|
+
<>
|
|
79
|
+
<div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-1">
|
|
80
|
+
<StatusChip
|
|
81
|
+
size="compact"
|
|
82
|
+
tone={
|
|
83
|
+
run.status === "completed"
|
|
84
|
+
? "success"
|
|
85
|
+
: run.status === "failed"
|
|
86
|
+
? "danger"
|
|
87
|
+
: "warning"
|
|
88
|
+
}
|
|
89
|
+
>
|
|
90
|
+
{run.status}
|
|
91
|
+
</StatusChip>
|
|
92
|
+
<time
|
|
93
|
+
className="text-xs text-dashboard-text-muted"
|
|
94
|
+
dateTime={run.executedAt}
|
|
95
|
+
title={formatAutomationRunTime(run.executedAt)}
|
|
96
|
+
>
|
|
97
|
+
{formatRelativeTime(run.executedAt)}
|
|
98
|
+
</time>
|
|
99
|
+
</div>
|
|
100
|
+
{title || metrics ? (
|
|
101
|
+
<div className="flex min-w-0 items-baseline justify-between gap-3 text-xs text-dashboard-text-muted">
|
|
102
|
+
{title ? (
|
|
103
|
+
<p
|
|
104
|
+
className="m-0 min-w-0 truncate text-dashboard-text"
|
|
105
|
+
title={title}
|
|
106
|
+
>
|
|
107
|
+
{title}
|
|
108
|
+
</p>
|
|
109
|
+
) : null}
|
|
110
|
+
{metrics ? (
|
|
111
|
+
<p className="m-0 shrink-0 tabular-nums">{metrics}</p>
|
|
112
|
+
) : null}
|
|
113
|
+
</div>
|
|
114
|
+
) : null}
|
|
115
|
+
{!run.conversationId ? (
|
|
116
|
+
<p className="m-0 text-xs text-dashboard-text-muted">
|
|
117
|
+
No Conversation recorded
|
|
118
|
+
</p>
|
|
119
|
+
) : null}
|
|
120
|
+
</>
|
|
121
|
+
);
|
|
122
|
+
const rowClass = "grid min-w-0 gap-1 rounded-md px-2 py-2.5";
|
|
123
|
+
return (
|
|
124
|
+
<li key={run.executionId}>
|
|
125
|
+
{run.conversationId ? (
|
|
126
|
+
<Link
|
|
127
|
+
aria-label={`Open ${run.status} run from ${formatAutomationRunTime(run.executedAt)}`}
|
|
128
|
+
className={`${rowClass} text-inherit no-underline transition-colors hover:bg-dashboard-fill-hover focus-visible:outline-2 focus-visible:outline-dashboard-focus`}
|
|
129
|
+
to={conversationPath(run.conversationId)}
|
|
130
|
+
>
|
|
131
|
+
{content}
|
|
132
|
+
</Link>
|
|
133
|
+
) : (
|
|
134
|
+
<div className={rowClass}>{content}</div>
|
|
135
|
+
)}
|
|
136
|
+
</li>
|
|
137
|
+
);
|
|
138
|
+
})}
|
|
139
|
+
</ol>
|
|
140
|
+
) : null}
|
|
141
|
+
</section>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
@@ -1,38 +1,28 @@
|
|
|
1
1
|
import type { AutomationSummary } from "@sentry/junior/api/schema";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
MoreHorizontal,
|
|
6
|
-
Trash2,
|
|
7
|
-
Zap,
|
|
8
|
-
} from "lucide-react";
|
|
9
|
-
import { type ReactNode, useEffect, useId, useRef, useState } from "react";
|
|
10
|
-
import { Link, useLocation } from "react-router";
|
|
2
|
+
import { CalendarClock, LockKeyhole, Zap } from "lucide-react";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
import { Link } from "react-router";
|
|
11
5
|
import { ActorAvatar } from "../../components/ActorAvatar";
|
|
12
6
|
import { SelectableRow } from "../../components/SelectableRow";
|
|
13
7
|
import { StatusChip } from "../../components/StatusChip";
|
|
14
8
|
import { formatRelativeTime, peoplePath } from "../../format";
|
|
9
|
+
import { eventTriggerText } from "@sentry/junior-plugin-api";
|
|
15
10
|
import {
|
|
16
|
-
automationEventConditions,
|
|
17
|
-
automationEventNames,
|
|
18
11
|
automationScheduleLabel,
|
|
19
12
|
formatAutomationRunTime,
|
|
20
13
|
} from "./automationFormat";
|
|
21
14
|
|
|
22
15
|
/**
|
|
23
16
|
* 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
|
|
25
|
-
* the details
|
|
17
|
+
* it, and when it runs. Instructions, timezones, and event filters stay in
|
|
18
|
+
* the details panel.
|
|
26
19
|
*/
|
|
27
20
|
export function AutomationRow(props: {
|
|
28
|
-
deleting: boolean;
|
|
29
|
-
onDelete(): void;
|
|
30
21
|
onSelect(): void;
|
|
31
22
|
selected: boolean;
|
|
32
23
|
automation: AutomationSummary;
|
|
33
24
|
}) {
|
|
34
25
|
const { automation } = props;
|
|
35
|
-
const location = useLocation();
|
|
36
26
|
const status = automationStatus(automation);
|
|
37
27
|
const creator = (
|
|
38
28
|
<>
|
|
@@ -47,6 +37,7 @@ export function AutomationRow(props: {
|
|
|
47
37
|
return (
|
|
48
38
|
<article
|
|
49
39
|
className="min-w-0 rounded-lg border border-dashboard-border-subtle bg-dashboard-fill-faint"
|
|
40
|
+
data-automation-id={automation.id}
|
|
50
41
|
role="listitem"
|
|
51
42
|
>
|
|
52
43
|
<SelectableRow
|
|
@@ -105,16 +96,6 @@ export function AutomationRow(props: {
|
|
|
105
96
|
<AutomationTiming automation={automation} />
|
|
106
97
|
</div>
|
|
107
98
|
</div>
|
|
108
|
-
{automation.ownedByViewer ? (
|
|
109
|
-
<div className="-mr-2 -mt-1.5 shrink-0">
|
|
110
|
-
<AutomationActions
|
|
111
|
-
editPath={`/automations/${encodeURIComponent(automation.id)}/edit${location.search}`}
|
|
112
|
-
deleting={props.deleting}
|
|
113
|
-
onDelete={props.onDelete}
|
|
114
|
-
title={automation.title}
|
|
115
|
-
/>
|
|
116
|
-
</div>
|
|
117
|
-
) : null}
|
|
118
99
|
</SelectableRow>
|
|
119
100
|
</article>
|
|
120
101
|
);
|
|
@@ -126,7 +107,9 @@ function automationStatus(automation: AutomationSummary): string | undefined {
|
|
|
126
107
|
return automation.status === "active" ? undefined : automation.status;
|
|
127
108
|
}
|
|
128
109
|
if (automation.status !== "active") return automation.status;
|
|
129
|
-
return automation.
|
|
110
|
+
return automation.trigger.problems.length === 0
|
|
111
|
+
? undefined
|
|
112
|
+
: "Trigger unavailable";
|
|
130
113
|
}
|
|
131
114
|
|
|
132
115
|
/** Combine the trigger and Destination into one muted line under the title. */
|
|
@@ -140,15 +123,11 @@ function AutomationSummaryLine({
|
|
|
140
123
|
const trigger =
|
|
141
124
|
automation.kind === "scheduled"
|
|
142
125
|
? automation.schedule
|
|
143
|
-
:
|
|
144
|
-
const conditions =
|
|
145
|
-
automation.kind === "event" ? automationEventConditions(automation) : "";
|
|
126
|
+
: eventTriggerText(automation.resource, automation.trigger);
|
|
146
127
|
const triggerDetail =
|
|
147
128
|
automation.kind === "scheduled"
|
|
148
129
|
? automationScheduleLabel(automation)
|
|
149
|
-
:
|
|
150
|
-
? `${trigger} · ${conditions}`
|
|
151
|
-
: trigger;
|
|
130
|
+
: trigger;
|
|
152
131
|
return (
|
|
153
132
|
<span className="mt-1 flex min-w-0 items-center gap-1.5 text-xs text-dashboard-text-muted">
|
|
154
133
|
<Icon
|
|
@@ -236,108 +215,3 @@ function MetaItem(props: { children: ReactNode; title?: string }) {
|
|
|
236
215
|
</span>
|
|
237
216
|
);
|
|
238
217
|
}
|
|
239
|
-
|
|
240
|
-
function AutomationActions(props: {
|
|
241
|
-
editPath: string;
|
|
242
|
-
deleting: boolean;
|
|
243
|
-
onDelete(): void;
|
|
244
|
-
title: string;
|
|
245
|
-
}) {
|
|
246
|
-
const [open, setOpen] = useState(false);
|
|
247
|
-
const root = useRef<HTMLDivElement>(null);
|
|
248
|
-
const trigger = useRef<HTMLButtonElement>(null);
|
|
249
|
-
const item = useRef<HTMLButtonElement>(null);
|
|
250
|
-
const menuId = useId();
|
|
251
|
-
|
|
252
|
-
useEffect(() => {
|
|
253
|
-
if (!open) return;
|
|
254
|
-
item.current?.focus();
|
|
255
|
-
function closeOutside(event: PointerEvent) {
|
|
256
|
-
if (!root.current?.contains(event.target as Node)) setOpen(false);
|
|
257
|
-
}
|
|
258
|
-
document.addEventListener("pointerdown", closeOutside);
|
|
259
|
-
return () => document.removeEventListener("pointerdown", closeOutside);
|
|
260
|
-
}, [open]);
|
|
261
|
-
|
|
262
|
-
function close() {
|
|
263
|
-
setOpen(false);
|
|
264
|
-
trigger.current?.focus();
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return (
|
|
268
|
-
<div
|
|
269
|
-
className="relative"
|
|
270
|
-
onBlur={(event) => {
|
|
271
|
-
if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
|
|
272
|
-
}}
|
|
273
|
-
onKeyDown={(event) => {
|
|
274
|
-
if (event.key === "Escape") {
|
|
275
|
-
event.stopPropagation();
|
|
276
|
-
close();
|
|
277
|
-
} else if (
|
|
278
|
-
["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)
|
|
279
|
-
) {
|
|
280
|
-
event.preventDefault();
|
|
281
|
-
setOpen(true);
|
|
282
|
-
const items = Array.from(
|
|
283
|
-
root.current?.querySelectorAll<HTMLElement>('[role="menuitem"]') ??
|
|
284
|
-
[],
|
|
285
|
-
);
|
|
286
|
-
const index = items.indexOf(document.activeElement as HTMLElement);
|
|
287
|
-
const next =
|
|
288
|
-
event.key === "Home"
|
|
289
|
-
? 0
|
|
290
|
-
: event.key === "End"
|
|
291
|
-
? items.length - 1
|
|
292
|
-
: (index + (event.key === "ArrowUp" ? -1 : 1) + items.length) %
|
|
293
|
-
items.length;
|
|
294
|
-
items[next]?.focus();
|
|
295
|
-
}
|
|
296
|
-
}}
|
|
297
|
-
ref={root}
|
|
298
|
-
>
|
|
299
|
-
<button
|
|
300
|
-
aria-controls={open ? menuId : undefined}
|
|
301
|
-
aria-expanded={open}
|
|
302
|
-
aria-haspopup="menu"
|
|
303
|
-
aria-label={`Actions: ${props.title}`}
|
|
304
|
-
className="grid size-11 cursor-pointer place-items-center rounded-md border-0 bg-transparent text-dashboard-text-muted hover:bg-dashboard-fill-hover hover:text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus disabled:cursor-not-allowed disabled:opacity-50 xl:size-9"
|
|
305
|
-
disabled={props.deleting}
|
|
306
|
-
onClick={() => setOpen(!open)}
|
|
307
|
-
ref={trigger}
|
|
308
|
-
type="button"
|
|
309
|
-
>
|
|
310
|
-
<MoreHorizontal aria-hidden="true" size={18} />
|
|
311
|
-
</button>
|
|
312
|
-
{open ? (
|
|
313
|
-
<div
|
|
314
|
-
aria-label={`Actions: ${props.title}`}
|
|
315
|
-
className="absolute right-0 top-full z-20 min-w-36 rounded-md border border-dashboard-border bg-dashboard-surface-raised p-1 shadow-lg"
|
|
316
|
-
id={menuId}
|
|
317
|
-
onClick={(event) => event.stopPropagation()}
|
|
318
|
-
role="menu"
|
|
319
|
-
>
|
|
320
|
-
<Link
|
|
321
|
-
role="menuitem"
|
|
322
|
-
to={props.editPath}
|
|
323
|
-
className="flex min-h-11 items-center rounded-md px-3 text-sm text-dashboard-text no-underline hover:bg-dashboard-fill-hover focus-visible:outline focus-visible:outline-dashboard-focus"
|
|
324
|
-
>
|
|
325
|
-
Edit automation
|
|
326
|
-
</Link>
|
|
327
|
-
<button
|
|
328
|
-
className="flex min-h-11 w-full cursor-pointer items-center gap-2 rounded-md border-0 bg-transparent px-3 text-sm text-rose-300 hover:bg-dashboard-fill-hover focus-visible:bg-dashboard-fill-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
|
|
329
|
-
onClick={() => {
|
|
330
|
-
close();
|
|
331
|
-
props.onDelete();
|
|
332
|
-
}}
|
|
333
|
-
ref={item}
|
|
334
|
-
role="menuitem"
|
|
335
|
-
type="button"
|
|
336
|
-
>
|
|
337
|
-
<Trash2 aria-hidden="true" size={15} /> Delete
|
|
338
|
-
</button>
|
|
339
|
-
</div>
|
|
340
|
-
) : null}
|
|
341
|
-
</div>
|
|
342
|
-
);
|
|
343
|
-
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import type { AutomationRun } from "@sentry/junior/api/schema";
|
|
2
|
+
import { MessageSquareText } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
useRef,
|
|
5
|
+
type KeyboardEvent,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
type RefObject,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { Link, useSearchParams } from "react-router";
|
|
10
|
+
|
|
11
|
+
import { DetailsPanel } from "../../components/DetailsPanel";
|
|
12
|
+
import { PageContentSkeleton } from "../../components/PageContentSkeleton";
|
|
13
|
+
import { StatusChip } from "../../components/StatusChip";
|
|
14
|
+
import { PageLayout } from "../../components/layout/PageLayout";
|
|
15
|
+
import { buttonVariants } from "../../components/ui/button";
|
|
16
|
+
import {
|
|
17
|
+
Tooltip,
|
|
18
|
+
TooltipContent,
|
|
19
|
+
TooltipTrigger,
|
|
20
|
+
} from "../../components/ui/tooltip";
|
|
21
|
+
import { conversationPath } from "../../conversations/conversationRoutes";
|
|
22
|
+
import {
|
|
23
|
+
automationPath,
|
|
24
|
+
formatCompactNumber,
|
|
25
|
+
formatCostSummary,
|
|
26
|
+
formatRelativeTime,
|
|
27
|
+
formatRuntime,
|
|
28
|
+
} from "../../format";
|
|
29
|
+
import { cn, dashboardInlineLinkClass } from "../../styles";
|
|
30
|
+
import {
|
|
31
|
+
automationRunStatusTone,
|
|
32
|
+
automationRunTitle,
|
|
33
|
+
formatAutomationRunTime,
|
|
34
|
+
} from "./automationFormat";
|
|
35
|
+
|
|
36
|
+
/** Search param that owns the selected run, so a run link can be shared. */
|
|
37
|
+
export const AUTOMATION_RUN_PARAM = "run";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Read the run that the URL selects, and change it. Closing returns focus to
|
|
41
|
+
* the run row. Up and Down move the selection while details are open.
|
|
42
|
+
*/
|
|
43
|
+
export function useAutomationRunSelection(runIds: readonly string[]) {
|
|
44
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
45
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
46
|
+
const runId = searchParams.get(AUTOMATION_RUN_PARAM) ?? undefined;
|
|
47
|
+
const select = (id: string | undefined) =>
|
|
48
|
+
setSearchParams(
|
|
49
|
+
(current) => {
|
|
50
|
+
const next = new URLSearchParams(current);
|
|
51
|
+
if (id) next.set(AUTOMATION_RUN_PARAM, id);
|
|
52
|
+
else next.delete(AUTOMATION_RUN_PARAM);
|
|
53
|
+
return next;
|
|
54
|
+
},
|
|
55
|
+
{ replace: true },
|
|
56
|
+
);
|
|
57
|
+
const rowButton = (id: string) =>
|
|
58
|
+
listRef.current?.querySelector<HTMLButtonElement>(
|
|
59
|
+
`[data-run-id="${CSS.escape(id)}"] button`,
|
|
60
|
+
);
|
|
61
|
+
const close = () => {
|
|
62
|
+
const trigger = runId ? rowButton(runId) : undefined;
|
|
63
|
+
select(undefined);
|
|
64
|
+
(trigger ?? listRef.current)?.focus({ preventScroll: true });
|
|
65
|
+
};
|
|
66
|
+
const toggle = (id: string) => select(id === runId ? undefined : id);
|
|
67
|
+
const onRowsKeyDown = (event: KeyboardEvent<HTMLElement>) => {
|
|
68
|
+
if (!runId || (event.key !== "ArrowDown" && event.key !== "ArrowUp")) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Read the focused row. The URL can lag behind fast key repeats.
|
|
72
|
+
const current = (event.target as Element)
|
|
73
|
+
.closest("[data-run-id]")
|
|
74
|
+
?.getAttribute("data-run-id");
|
|
75
|
+
const index = current ? runIds.indexOf(current) : -1;
|
|
76
|
+
const next = runIds[index + (event.key === "ArrowDown" ? 1 : -1)];
|
|
77
|
+
if (index === -1 || !next) return;
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
select(next);
|
|
80
|
+
rowButton(next)?.focus();
|
|
81
|
+
};
|
|
82
|
+
return { close, listRef, onRowsKeyDown, runId, toggle };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Scroll a run list on its own, beside the selected run's details. */
|
|
86
|
+
export function AutomationRunsFrame(props: {
|
|
87
|
+
children: ReactNode;
|
|
88
|
+
details: ReactNode;
|
|
89
|
+
label: string;
|
|
90
|
+
listRef: RefObject<HTMLDivElement | null>;
|
|
91
|
+
}) {
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
className={cn(
|
|
95
|
+
"grid h-full min-h-0 min-w-0",
|
|
96
|
+
props.details ? "xl:grid-cols-[minmax(0,1fr)_25rem]" : undefined,
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
99
|
+
<div
|
|
100
|
+
aria-label={props.label}
|
|
101
|
+
className="min-h-0 overflow-y-auto overscroll-contain outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
|
|
102
|
+
ref={props.listRef}
|
|
103
|
+
role="region"
|
|
104
|
+
tabIndex={0}
|
|
105
|
+
>
|
|
106
|
+
<PageLayout>{props.children}</PageLayout>
|
|
107
|
+
</div>
|
|
108
|
+
{props.details}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Open a run's Conversation from its row without opening run details. */
|
|
114
|
+
export function RunConversationLink(props: { conversationId?: string }) {
|
|
115
|
+
if (!props.conversationId) return <span aria-hidden="true" />;
|
|
116
|
+
return (
|
|
117
|
+
<Tooltip>
|
|
118
|
+
<TooltipTrigger
|
|
119
|
+
render={
|
|
120
|
+
<Link
|
|
121
|
+
aria-label="Open conversation"
|
|
122
|
+
className={cn(buttonVariants({ size: "icon" }), "size-8")}
|
|
123
|
+
to={conversationPath(props.conversationId)}
|
|
124
|
+
>
|
|
125
|
+
<MessageSquareText aria-hidden="true" size={15} />
|
|
126
|
+
</Link>
|
|
127
|
+
}
|
|
128
|
+
/>
|
|
129
|
+
<TooltipContent size="compact">Open conversation</TooltipContent>
|
|
130
|
+
</Tooltip>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Show what one run did, what it cost, and why it failed. */
|
|
135
|
+
export function AutomationRunDetails(props: {
|
|
136
|
+
loading: boolean;
|
|
137
|
+
onClose(): void;
|
|
138
|
+
/** Undefined while runs load, or when the selected run is not loaded. */
|
|
139
|
+
run: AutomationRun | undefined;
|
|
140
|
+
/** The executions page already names its Automation. */
|
|
141
|
+
showAutomation: boolean;
|
|
142
|
+
}) {
|
|
143
|
+
const { run } = props;
|
|
144
|
+
const title = run ? automationRunTitle(run, run.automationTitle) : "Run";
|
|
145
|
+
return (
|
|
146
|
+
<DetailsPanel
|
|
147
|
+
actions={
|
|
148
|
+
run?.conversationId ? (
|
|
149
|
+
<Link
|
|
150
|
+
className={cn(buttonVariants(), "shrink-0 whitespace-nowrap")}
|
|
151
|
+
to={conversationPath(run.conversationId)}
|
|
152
|
+
>
|
|
153
|
+
Open conversation
|
|
154
|
+
</Link>
|
|
155
|
+
) : undefined
|
|
156
|
+
}
|
|
157
|
+
label="Run details"
|
|
158
|
+
onClose={props.onClose}
|
|
159
|
+
sheetTitle={title}
|
|
160
|
+
title={
|
|
161
|
+
<h2 className="m-0 line-clamp-2 break-words font-display text-base font-medium text-dashboard-text">
|
|
162
|
+
{title}
|
|
163
|
+
</h2>
|
|
164
|
+
}
|
|
165
|
+
>
|
|
166
|
+
<div className="p-4 [overflow-wrap:anywhere]">
|
|
167
|
+
{run ? (
|
|
168
|
+
<AutomationRunContent
|
|
169
|
+
run={run}
|
|
170
|
+
showAutomation={props.showAutomation}
|
|
171
|
+
/>
|
|
172
|
+
) : props.loading ? (
|
|
173
|
+
<PageContentSkeleton label="Loading run details" variant="panel" />
|
|
174
|
+
) : (
|
|
175
|
+
<p className="m-0 text-sm text-dashboard-text-muted">
|
|
176
|
+
This run is not in the loaded runs.
|
|
177
|
+
</p>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</DetailsPanel>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function AutomationRunContent(props: {
|
|
185
|
+
run: AutomationRun;
|
|
186
|
+
showAutomation: boolean;
|
|
187
|
+
}) {
|
|
188
|
+
const { run } = props;
|
|
189
|
+
const versionsPath = `${automationPath(run.automationId)}/versions`;
|
|
190
|
+
return (
|
|
191
|
+
<div className="grid min-w-0 gap-3">
|
|
192
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 px-1">
|
|
193
|
+
<StatusChip tone={automationRunStatusTone(run.status)}>
|
|
194
|
+
{run.status}
|
|
195
|
+
</StatusChip>
|
|
196
|
+
<time
|
|
197
|
+
className="text-xs text-dashboard-text-muted"
|
|
198
|
+
dateTime={run.executedAt}
|
|
199
|
+
title={formatAutomationRunTime(run.executedAt)}
|
|
200
|
+
>
|
|
201
|
+
{formatRelativeTime(run.executedAt)}
|
|
202
|
+
</time>
|
|
203
|
+
</div>
|
|
204
|
+
{run.error ? (
|
|
205
|
+
<RunSection title="Error">
|
|
206
|
+
<p className="m-0 whitespace-pre-wrap font-mono text-xs leading-relaxed text-rose-300">
|
|
207
|
+
{run.error}
|
|
208
|
+
</p>
|
|
209
|
+
</RunSection>
|
|
210
|
+
) : null}
|
|
211
|
+
<RunSection title="Run">
|
|
212
|
+
<dl className={runFactsClass}>
|
|
213
|
+
<RunFact label="Trigger">
|
|
214
|
+
{run.kind === "scheduled" ? "Schedule" : "Event"}
|
|
215
|
+
</RunFact>
|
|
216
|
+
{run.scheduledFor ? (
|
|
217
|
+
<RunFact label="Scheduled for">
|
|
218
|
+
{formatAutomationRunTime(run.scheduledFor)}
|
|
219
|
+
</RunFact>
|
|
220
|
+
) : null}
|
|
221
|
+
{run.startedAt ? (
|
|
222
|
+
<RunFact label="Started">
|
|
223
|
+
{formatAutomationRunTime(run.startedAt)}
|
|
224
|
+
</RunFact>
|
|
225
|
+
) : null}
|
|
226
|
+
<RunFact label="Finished">
|
|
227
|
+
{formatAutomationRunTime(run.executedAt)}
|
|
228
|
+
</RunFact>
|
|
229
|
+
<RunFact label="Duration">
|
|
230
|
+
{formatRuntime(run.durationMs) || "—"}
|
|
231
|
+
</RunFact>
|
|
232
|
+
<RunFact label="Tokens">
|
|
233
|
+
{run.totalTokens === undefined
|
|
234
|
+
? "—"
|
|
235
|
+
: formatCompactNumber(run.totalTokens)}
|
|
236
|
+
</RunFact>
|
|
237
|
+
<RunFact label="Cost">
|
|
238
|
+
{run.costUsd === undefined
|
|
239
|
+
? "—"
|
|
240
|
+
: formatCostSummary({ total: run.costUsd })}
|
|
241
|
+
</RunFact>
|
|
242
|
+
</dl>
|
|
243
|
+
</RunSection>
|
|
244
|
+
<RunSection title="Setup">
|
|
245
|
+
<dl className={runFactsClass}>
|
|
246
|
+
{props.showAutomation ? (
|
|
247
|
+
<RunFact label="Automation">
|
|
248
|
+
<Link
|
|
249
|
+
className={dashboardInlineLinkClass}
|
|
250
|
+
to={automationPath(run.automationId)}
|
|
251
|
+
>
|
|
252
|
+
{run.automationTitle}
|
|
253
|
+
</Link>
|
|
254
|
+
</RunFact>
|
|
255
|
+
) : null}
|
|
256
|
+
{run.automationVersion ? (
|
|
257
|
+
<RunFact label="Version">
|
|
258
|
+
<Link className={dashboardInlineLinkClass} to={versionsPath}>
|
|
259
|
+
Version {run.automationVersion}
|
|
260
|
+
</Link>
|
|
261
|
+
</RunFact>
|
|
262
|
+
) : null}
|
|
263
|
+
{run.credentialMode ? (
|
|
264
|
+
<RunFact label="Accounts">
|
|
265
|
+
{run.credentialMode === "creator"
|
|
266
|
+
? "Creator’s connected accounts"
|
|
267
|
+
: "Junior’s accounts only"}
|
|
268
|
+
</RunFact>
|
|
269
|
+
) : null}
|
|
270
|
+
<RunFact label="Run ID">
|
|
271
|
+
<span className="font-mono">{run.executionId}</span>
|
|
272
|
+
</RunFact>
|
|
273
|
+
</dl>
|
|
274
|
+
</RunSection>
|
|
275
|
+
{run.conversationId ? null : (
|
|
276
|
+
<p className="m-0 px-1 text-xs text-dashboard-text-muted">
|
|
277
|
+
No Conversation was recorded for this run.
|
|
278
|
+
</p>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Match the Summary tab cards in conversation details.
|
|
285
|
+
const runFactsClass = "m-0 grid min-w-0 gap-2 text-xs leading-snug";
|
|
286
|
+
|
|
287
|
+
function RunSection(props: { children: ReactNode; title: string }) {
|
|
288
|
+
return (
|
|
289
|
+
<section className="grid min-w-0 gap-2.5 rounded-lg border border-dashboard-border-subtle bg-dashboard-surface-panel p-3">
|
|
290
|
+
<h3 className="m-0 font-sans text-xs font-medium text-dashboard-text-muted">
|
|
291
|
+
{props.title}
|
|
292
|
+
</h3>
|
|
293
|
+
{props.children}
|
|
294
|
+
</section>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function RunFact(props: { label: string; children: ReactNode }) {
|
|
299
|
+
return (
|
|
300
|
+
<div className="flex min-w-0 items-baseline justify-between gap-4">
|
|
301
|
+
<dt className="shrink-0 text-dashboard-text-muted">{props.label}</dt>
|
|
302
|
+
<dd className="m-0 min-w-0 break-words text-right tabular-nums text-dashboard-text">
|
|
303
|
+
{props.children}
|
|
304
|
+
</dd>
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|