@sentry/junior-dashboard 0.265.0 → 0.267.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 +49 -12
- package/dist/client.js +94 -63
- package/dist/index.js +49 -12
- package/dist/tailwind.css +1 -1
- package/package.json +11 -5
- package/src/app.ts +0 -2
- package/src/auth.ts +4 -7
- package/src/client/App.tsx +35 -53
- package/src/client/components/ActorAvatar.tsx +10 -22
- package/src/client/components/ConnectionBanner.tsx +4 -3
- package/src/client/components/CopyButton.tsx +3 -4
- package/src/client/components/DetailsPanel.tsx +12 -13
- package/src/client/components/Drawer.tsx +46 -23
- package/src/client/components/EmptyTelemetry.tsx +5 -3
- package/src/client/components/ImageAttachment.tsx +49 -63
- package/src/client/components/InlineError.tsx +8 -4
- package/src/client/components/Notice.tsx +19 -26
- package/src/client/components/Pagination.tsx +26 -26
- package/src/client/components/ParticipantAvatarStack.tsx +13 -10
- package/src/client/components/README.md +28 -3
- package/src/client/components/SearchInput.tsx +51 -41
- package/src/client/components/VersionDriftBanner.tsx +9 -7
- package/src/client/components/charts/CacheWrites.tsx +52 -43
- package/src/client/components/ui/alert.tsx +64 -0
- package/src/client/components/ui/avatar.tsx +53 -0
- package/src/client/components/ui/button-group.tsx +59 -0
- package/src/client/components/ui/button.tsx +2 -1
- package/src/client/components/ui/dialog.tsx +60 -0
- package/src/client/components/ui/drawer.tsx +140 -0
- package/src/client/components/ui/empty.tsx +61 -0
- package/src/client/components/ui/input-group.tsx +96 -0
- package/src/client/components/ui/pagination.tsx +60 -0
- package/src/client/components/ui/separator.tsx +22 -0
- package/src/client/components/ui/spinner.tsx +21 -0
- package/src/client/components/ui/table.tsx +22 -1
- package/src/client/components/ui/tooltip.tsx +1 -1
- package/src/client/components/ui/useSheet.ts +5 -1
- package/src/client/conversations/ComposerImages.tsx +66 -14
- package/src/client/conversations/ConversationChildAgents.tsx +5 -5
- package/src/client/conversations/ConversationComposer.tsx +84 -12
- package/src/client/conversations/ConversationDetails.tsx +3 -27
- package/src/client/conversations/ConversationEventDetails.tsx +26 -10
- package/src/client/conversations/ConversationHeader.tsx +5 -4
- package/src/client/conversations/ConversationHeaderActions.tsx +7 -11
- package/src/client/conversations/ConversationPage.tsx +156 -2
- package/src/client/conversations/ConversationSidebar.tsx +5 -4
- package/src/client/conversations/ConversationTranscript.tsx +48 -16
- package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
- package/src/client/conversations/ConversationWatches.tsx +67 -59
- package/src/client/conversations/DetailsList.tsx +77 -29
- package/src/client/conversations/EventDetails.tsx +46 -19
- package/src/client/conversations/EventUsage.tsx +99 -74
- package/src/client/conversations/ForkConversationButton.tsx +79 -73
- package/src/client/conversations/PendingMailboxStack.tsx +312 -119
- package/src/client/conversations/README.md +30 -10
- package/src/client/conversations/ShareConversationButton.tsx +3 -4
- package/src/client/conversations/SubagentTranscriptDrawer.tsx +19 -22
- package/src/client/conversations/TranscriptActivityGroup.tsx +22 -19
- package/src/client/conversations/TranscriptAttachment.tsx +39 -4
- package/src/client/conversations/TranscriptAutomationTriggerView.tsx +14 -19
- package/src/client/conversations/TranscriptEventView.tsx +35 -16
- package/src/client/conversations/TranscriptFailureView.tsx +10 -13
- package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
- package/src/client/conversations/TranscriptMessageContextView.tsx +31 -20
- package/src/client/conversations/TranscriptMessageView.tsx +7 -1
- package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
- package/src/client/conversations/TranscriptSummary.tsx +13 -7
- package/src/client/conversations/TranscriptTurnContextView.tsx +78 -56
- package/src/client/conversations/TranscriptView.tsx +7 -3
- package/src/client/conversations/UserInputRequestCard.tsx +244 -0
- package/src/client/conversations/conversationOutbox.ts +46 -0
- package/src/client/conversations/eventLog.ts +1 -0
- package/src/client/conversations/eventTime.ts +13 -1
- package/src/client/conversations/eventTranscript.ts +12 -5
- package/src/client/conversations/queries.ts +127 -26
- package/src/client/conversations/transcriptMarkdownLinks.ts +5 -367
- package/src/client/conversations/transcriptMentions.tsx +18 -5
- package/src/client/conversations/userInputRequest.ts +56 -0
- package/src/client/format.ts +10 -3
- package/src/client/pages/automations/{AutomationLifecycle.tsx → AutomationActions.tsx} +60 -69
- package/src/client/pages/automations/AutomationDraftChanges.tsx +171 -0
- package/src/client/pages/automations/AutomationEditor.tsx +182 -156
- package/src/client/pages/automations/AutomationEventFields.tsx +119 -45
- package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +1 -1
- package/src/client/pages/automations/AutomationFormSection.tsx +20 -2
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +1 -1
- package/src/client/pages/automations/AutomationOverview.tsx +238 -0
- package/src/client/pages/automations/AutomationPage.tsx +343 -0
- package/src/client/pages/automations/AutomationRecentRuns.tsx +3 -2
- package/src/client/pages/automations/AutomationRow.tsx +11 -13
- package/src/client/pages/automations/AutomationRunDetails.tsx +4 -2
- package/src/client/pages/automations/{AutomationExecutionsPage.tsx → AutomationRunsTab.tsx} +94 -103
- package/src/client/pages/automations/AutomationStatus.tsx +164 -108
- package/src/client/pages/automations/AutomationVersionsTab.tsx +518 -0
- package/src/client/pages/automations/AutomationsPage.tsx +10 -98
- package/src/client/pages/automations/AutomationsPageLayout.tsx +5 -23
- package/src/client/pages/automations/README.md +56 -46
- package/src/client/types.ts +3 -0
- package/src/mock-reporting/fixtures.ts +44 -2
- package/src/mock-reporting/watches.ts +1 -1
- package/src/client/conversations/transcriptStyles.ts +0 -10
- package/src/client/pages/automations/AutomationDetails.tsx +0 -193
- package/src/client/pages/automations/AutomationEditPage.tsx +0 -176
- package/src/client/pages/automations/AutomationVersionsPage.tsx +0 -390
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.267.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -29,16 +29,20 @@
|
|
|
29
29
|
"cn": "^0.5.0",
|
|
30
30
|
"hono": "^4.13.11",
|
|
31
31
|
"lucide-react": "^1.17.0",
|
|
32
|
-
"nitro": "3.0.260522-beta",
|
|
33
32
|
"react": "^19.2.6",
|
|
34
33
|
"react-dom": "^19.2.6",
|
|
35
34
|
"react-is": "^19.2.6",
|
|
35
|
+
"react-markdown": "^10.1.0",
|
|
36
36
|
"react-router": "^7.18.2",
|
|
37
37
|
"recharts": "3.8.0",
|
|
38
|
+
"remark-breaks": "^4.0.0",
|
|
39
|
+
"remark-gfm": "^4.0.1",
|
|
38
40
|
"shiki": "4.1.0",
|
|
39
41
|
"zod": "^4.5.4",
|
|
40
|
-
"@sentry/junior": "0.
|
|
41
|
-
|
|
42
|
+
"@sentry/junior-plugin-api": "0.267.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@sentry/junior": "0.267.0"
|
|
42
46
|
},
|
|
43
47
|
"devDependencies": {
|
|
44
48
|
"@oxlint/plugins": "1.79.0",
|
|
@@ -49,6 +53,7 @@
|
|
|
49
53
|
"@types/react-dom": "^19.2.3",
|
|
50
54
|
"@vitest/coverage-v8": "4.1.12",
|
|
51
55
|
"eslint": "^10.8.0",
|
|
56
|
+
"nitro": "3.0.260522-beta",
|
|
52
57
|
"oxlint": "^1.79.0",
|
|
53
58
|
"shadcn": "4.23.0",
|
|
54
59
|
"tailwindcss": "^4.3.0",
|
|
@@ -56,7 +61,8 @@
|
|
|
56
61
|
"tsup": "^8.5.1",
|
|
57
62
|
"tw-animate-css": "^1.4.0",
|
|
58
63
|
"typescript": "^7.0.2",
|
|
59
|
-
"vitest": "^4.1.12"
|
|
64
|
+
"vitest": "^4.1.12",
|
|
65
|
+
"@sentry/junior": "0.267.0"
|
|
60
66
|
},
|
|
61
67
|
"scripts": {
|
|
62
68
|
"build": "pnpm --filter @sentry/junior-plugin-api build && pnpm --filter @sentry/junior build && pnpm run build:client && pnpm run build:css && pnpm run build:server",
|
package/src/app.ts
CHANGED
|
@@ -126,8 +126,6 @@ function resolveDashboardOptions(
|
|
|
126
126
|
): DashboardRuntimeOptions {
|
|
127
127
|
return {
|
|
128
128
|
...options,
|
|
129
|
-
authRequired:
|
|
130
|
-
options.authRequired ?? readEnvFlag("JUNIOR_DASHBOARD_AUTH_REQUIRED"),
|
|
131
129
|
mockConversations:
|
|
132
130
|
options.mockConversations ??
|
|
133
131
|
readEnvFlag("JUNIOR_DASHBOARD_MOCK_CONVERSATIONS"),
|
package/src/auth.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isAllowedSignInEmail } from "@sentry/junior/api";
|
|
1
2
|
import { dashboardSessionSchema } from "./api/schema";
|
|
2
3
|
import type { DashboardSession } from "./api/schema";
|
|
3
4
|
|
|
@@ -34,13 +35,9 @@ export function dashboardSessionIsAuthorized(
|
|
|
34
35
|
session: DashboardSession,
|
|
35
36
|
allowedDomains: string[],
|
|
36
37
|
): boolean {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return Boolean(
|
|
41
|
-
session.user.emailVerified &&
|
|
42
|
-
domain !== undefined &&
|
|
43
|
-
allowedDomains.includes(domain),
|
|
38
|
+
return (
|
|
39
|
+
session.user.emailVerified === true &&
|
|
40
|
+
isAllowedSignInEmail(session.user.email, allowedDomains)
|
|
44
41
|
);
|
|
45
42
|
}
|
|
46
43
|
|
package/src/client/App.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { AutomationEditPage } from "./pages/automations/AutomationEditPage";
|
|
2
1
|
import { useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { Navigate, Route, Routes, useLocation } from "react-router";
|
|
2
|
+
import { Navigate, Route, Routes, useLocation, useParams } from "react-router";
|
|
4
3
|
|
|
5
4
|
import {
|
|
6
5
|
useConversationsData,
|
|
@@ -21,6 +20,7 @@ import {
|
|
|
21
20
|
import { DashboardHeader } from "./components/layout/DashboardHeader";
|
|
22
21
|
import { VisualViewportShell } from "./components/layout/VisualViewportShell";
|
|
23
22
|
import {
|
|
23
|
+
automationPath,
|
|
24
24
|
buildConversations,
|
|
25
25
|
conversationDetailTitle,
|
|
26
26
|
conversationDisplayTitle,
|
|
@@ -52,9 +52,8 @@ import {
|
|
|
52
52
|
isMemoryLibraryPath,
|
|
53
53
|
MemoryRouteLoading,
|
|
54
54
|
} from "./pages/memory/MemoryPageLayout";
|
|
55
|
-
import {
|
|
55
|
+
import { AutomationPage } from "./pages/automations/AutomationPage";
|
|
56
56
|
import { AutomationRunsPage } from "./pages/automations/AutomationRunsPage";
|
|
57
|
-
import { AutomationVersionsPage } from "./pages/automations/AutomationVersionsPage";
|
|
58
57
|
import { AutomationsPage } from "./pages/automations/AutomationsPage";
|
|
59
58
|
import {
|
|
60
59
|
AutomationsPageLayout,
|
|
@@ -207,48 +206,40 @@ export function DashboardShell() {
|
|
|
207
206
|
<Routes>
|
|
208
207
|
<Route element={<LegacyAutomationsRedirect />} path="/tasks" />
|
|
209
208
|
<Route element={<LegacyAutomationsRedirect />} path="/tasks/*" />
|
|
209
|
+
<Route
|
|
210
|
+
element={<LegacyAutomationExecutionsRedirect />}
|
|
211
|
+
path="/automations/:automationId/executions"
|
|
212
|
+
/>
|
|
210
213
|
<Route
|
|
211
214
|
element={
|
|
212
|
-
loading ? (
|
|
213
|
-
<
|
|
214
|
-
<AutomationsRouteLoading
|
|
215
|
-
description="Terminal runs for one scheduled or event automation."
|
|
216
|
-
label="Loading automation executions"
|
|
217
|
-
title="Automation executions"
|
|
218
|
-
variant="list"
|
|
219
|
-
/>
|
|
220
|
-
</AutomationsPageLayout>
|
|
221
|
-
) : loggedIn ? (
|
|
222
|
-
<AutomationsPageLayout>
|
|
223
|
-
<AutomationExecutionsPage enabled={loggedIn} />
|
|
224
|
-
</AutomationsPageLayout>
|
|
215
|
+
loading || loggedIn ? (
|
|
216
|
+
<AutomationPage enabled={loggedIn} tab="runs" />
|
|
225
217
|
) : (
|
|
226
218
|
<Navigate replace to="/" />
|
|
227
219
|
)
|
|
228
220
|
}
|
|
229
|
-
path="/automations/:automationId/
|
|
221
|
+
path="/automations/:automationId/runs"
|
|
230
222
|
/>
|
|
231
223
|
<Route
|
|
232
224
|
element={
|
|
233
|
-
loading ? (
|
|
234
|
-
<
|
|
235
|
-
<AutomationsRouteLoading
|
|
236
|
-
description="Saved settings for this automation, newest first."
|
|
237
|
-
label="Loading version history"
|
|
238
|
-
title="Version history"
|
|
239
|
-
variant="list"
|
|
240
|
-
/>
|
|
241
|
-
</AutomationsPageLayout>
|
|
242
|
-
) : loggedIn ? (
|
|
243
|
-
<AutomationsPageLayout>
|
|
244
|
-
<AutomationVersionsPage enabled={loggedIn} />
|
|
245
|
-
</AutomationsPageLayout>
|
|
225
|
+
loading || loggedIn ? (
|
|
226
|
+
<AutomationPage enabled={loggedIn} tab="versions" />
|
|
246
227
|
) : (
|
|
247
228
|
<Navigate replace to="/" />
|
|
248
229
|
)
|
|
249
230
|
}
|
|
250
231
|
path="/automations/:automationId/versions"
|
|
251
232
|
/>
|
|
233
|
+
<Route
|
|
234
|
+
element={
|
|
235
|
+
loading || loggedIn ? (
|
|
236
|
+
<AutomationPage enabled={loggedIn} tab="edit" />
|
|
237
|
+
) : (
|
|
238
|
+
<Navigate replace to="/" />
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
path="/automations/:automationId/edit"
|
|
242
|
+
/>
|
|
252
243
|
<Route
|
|
253
244
|
element={
|
|
254
245
|
loading ? (
|
|
@@ -292,30 +283,9 @@ export function DashboardShell() {
|
|
|
292
283
|
path="/automations/list"
|
|
293
284
|
/>
|
|
294
285
|
<Route
|
|
295
|
-
path="/automations/:automationId/edit"
|
|
296
286
|
element={
|
|
297
287
|
loading || loggedIn ? (
|
|
298
|
-
<
|
|
299
|
-
) : (
|
|
300
|
-
<Navigate replace to="/" />
|
|
301
|
-
)
|
|
302
|
-
}
|
|
303
|
-
/>
|
|
304
|
-
<Route
|
|
305
|
-
element={
|
|
306
|
-
loading ? (
|
|
307
|
-
<AutomationsPageLayout>
|
|
308
|
-
<AutomationsRouteLoading
|
|
309
|
-
description="Find and manage automations across your linked workspaces."
|
|
310
|
-
label="Loading automations"
|
|
311
|
-
title="All automations"
|
|
312
|
-
variant="list"
|
|
313
|
-
/>
|
|
314
|
-
</AutomationsPageLayout>
|
|
315
|
-
) : loggedIn ? (
|
|
316
|
-
<AutomationsPageLayout>
|
|
317
|
-
<AutomationsPage enabled={loggedIn} view="list" />
|
|
318
|
-
</AutomationsPageLayout>
|
|
288
|
+
<AutomationPage enabled={loggedIn} tab="overview" />
|
|
319
289
|
) : (
|
|
320
290
|
<Navigate replace to="/" />
|
|
321
291
|
)
|
|
@@ -661,6 +631,18 @@ function LegacyAutomationsRedirect() {
|
|
|
661
631
|
);
|
|
662
632
|
}
|
|
663
633
|
|
|
634
|
+
/** Execution history became the Runs tab of the Automation page. */
|
|
635
|
+
function LegacyAutomationExecutionsRedirect() {
|
|
636
|
+
const location = useLocation();
|
|
637
|
+
const { automationId } = useParams();
|
|
638
|
+
return (
|
|
639
|
+
<Navigate
|
|
640
|
+
replace
|
|
641
|
+
to={`${automationPath(automationId ?? "", "runs")}${location.search}${location.hash}`}
|
|
642
|
+
/>
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
664
646
|
function LegacySystemRedirect(props: { section: "locations" | "people" }) {
|
|
665
647
|
const location = useLocation();
|
|
666
648
|
const legacyPrefix = `/${props.section}`;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { cn } from "../styles";
|
|
1
|
+
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
|
3
2
|
|
|
4
3
|
/** Show an actor's image, with initials when the image is missing or fails. */
|
|
5
4
|
export function ActorAvatar(props: {
|
|
@@ -7,7 +6,6 @@ export function ActorAvatar(props: {
|
|
|
7
6
|
imageUrl?: string;
|
|
8
7
|
size: "detail" | "list";
|
|
9
8
|
}) {
|
|
10
|
-
const [failedUrl, setFailedUrl] = useState<string>();
|
|
11
9
|
const words = props.name.trim().split(/\s+/).filter(Boolean);
|
|
12
10
|
// The compact size fits one initial at the 12px type floor.
|
|
13
11
|
const initials =
|
|
@@ -17,24 +15,14 @@ export function ActorAvatar(props: {
|
|
|
17
15
|
? `${words[0]![0]}${words.at(-1)![0]}`
|
|
18
16
|
: props.name.slice(0, 2);
|
|
19
17
|
return (
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
{initials.toUpperCase()}
|
|
28
|
-
|
|
29
|
-
<img
|
|
30
|
-
alt=""
|
|
31
|
-
className="absolute inset-0 size-full object-cover"
|
|
32
|
-
loading="lazy"
|
|
33
|
-
onError={() => setFailedUrl(props.imageUrl)}
|
|
34
|
-
referrerPolicy="no-referrer"
|
|
35
|
-
src={props.imageUrl}
|
|
36
|
-
/>
|
|
37
|
-
) : null}
|
|
38
|
-
</span>
|
|
18
|
+
<Avatar aria-hidden="true" size={props.size === "list" ? "sm" : "default"}>
|
|
19
|
+
<AvatarImage
|
|
20
|
+
alt=""
|
|
21
|
+
loading="lazy"
|
|
22
|
+
referrerPolicy="no-referrer"
|
|
23
|
+
src={props.imageUrl}
|
|
24
|
+
/>
|
|
25
|
+
<AvatarFallback>{initials.toUpperCase()}</AvatarFallback>
|
|
26
|
+
</Avatar>
|
|
39
27
|
);
|
|
40
28
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useDashboardOnline } from "../connection";
|
|
2
|
+
import { Alert } from "./ui/alert";
|
|
2
3
|
|
|
3
4
|
/** Explain that cached data remains visible while network reads are paused. */
|
|
4
5
|
export function ConnectionBanner() {
|
|
@@ -6,12 +7,12 @@ export function ConnectionBanner() {
|
|
|
6
7
|
if (online) return null;
|
|
7
8
|
|
|
8
9
|
return (
|
|
9
|
-
<
|
|
10
|
+
<Alert
|
|
10
11
|
aria-live="polite"
|
|
11
|
-
className="border-t border-amber-300/15 bg-amber-300/[0.07] px-3 py-1.5 text-center font-sans text-xs text-amber-100/80"
|
|
12
|
+
className="block rounded-none border-0 border-t border-amber-300/15 bg-amber-300/[0.07] px-3 py-1.5 text-center font-sans text-xs text-amber-100/80"
|
|
12
13
|
role="status"
|
|
13
14
|
>
|
|
14
15
|
You’re offline. Drafts stay on this device.
|
|
15
|
-
</
|
|
16
|
+
</Alert>
|
|
16
17
|
);
|
|
17
18
|
}
|
|
@@ -44,16 +44,15 @@ export function CopyButton(props: {
|
|
|
44
44
|
|
|
45
45
|
if (props.layout === "menu") {
|
|
46
46
|
return (
|
|
47
|
-
<
|
|
47
|
+
<Button
|
|
48
48
|
aria-label={label}
|
|
49
|
-
className="flex w-full cursor-pointer items-center gap-2.5 rounded-md border-0 bg-transparent px-2.5 py-2 text-left text-sm font-semibold text-dashboard-text transition-colors hover:bg-dashboard-fill-strong focus-visible:bg-dashboard-fill-strong focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
50
49
|
disabled={!props.getText || status === "copying"}
|
|
51
50
|
onClick={() => void copyText()}
|
|
52
|
-
|
|
51
|
+
size="menu"
|
|
53
52
|
>
|
|
54
53
|
<Icon aria-hidden="true" size={16} strokeWidth={2} />
|
|
55
54
|
<span>{label}</span>
|
|
56
|
-
</
|
|
55
|
+
</Button>
|
|
57
56
|
);
|
|
58
57
|
}
|
|
59
58
|
|
|
@@ -110,20 +110,19 @@ export function DetailsPanel(props: {
|
|
|
110
110
|
className="grid min-h-0 min-w-0 grid-rows-[auto_minmax(0,1fr)] border-l border-dashboard-border bg-dashboard-bg-elevated"
|
|
111
111
|
>
|
|
112
112
|
<header className="flex items-center justify-between gap-3 border-b border-dashboard-border py-2 pl-5 pr-3">
|
|
113
|
-
<div className="
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
<div className="min-w-0 flex-1">{props.title}</div>
|
|
114
|
+
{/* Keep actions beside close, so they do not move with the title width. */}
|
|
115
|
+
<div className="flex shrink-0 items-center gap-0.5">
|
|
116
|
+
{props.actions}
|
|
117
|
+
<Button
|
|
118
|
+
ref={closeRef}
|
|
119
|
+
aria-label={closeLabel}
|
|
120
|
+
onClick={onClose}
|
|
121
|
+
size="icon"
|
|
122
|
+
>
|
|
123
|
+
<X aria-hidden="true" size={15} />
|
|
124
|
+
</Button>
|
|
118
125
|
</div>
|
|
119
|
-
<Button
|
|
120
|
-
ref={closeRef}
|
|
121
|
-
aria-label={closeLabel}
|
|
122
|
-
onClick={onClose}
|
|
123
|
-
size="icon"
|
|
124
|
-
>
|
|
125
|
-
<X aria-hidden="true" size={15} />
|
|
126
|
-
</Button>
|
|
127
126
|
</header>
|
|
128
127
|
<div className="min-h-0 overflow-y-auto overscroll-contain">
|
|
129
128
|
{props.children}
|
|
@@ -1,20 +1,31 @@
|
|
|
1
|
+
import { X } from "lucide-react";
|
|
1
2
|
import { useRef, type ReactNode } from "react";
|
|
2
3
|
|
|
3
4
|
import { cn } from "../styles";
|
|
4
|
-
import {
|
|
5
|
+
import { Button } from "./ui/button";
|
|
6
|
+
import {
|
|
7
|
+
Drawer as DrawerRoot,
|
|
8
|
+
DrawerClose,
|
|
9
|
+
DrawerContent as DrawerPopup,
|
|
10
|
+
} from "./ui/drawer";
|
|
5
11
|
import { useSheet } from "./ui/useSheet";
|
|
6
12
|
|
|
7
13
|
type DrawerProps = {
|
|
8
14
|
actions?: ReactNode;
|
|
9
15
|
children: ReactNode;
|
|
10
16
|
closeLabel: string;
|
|
17
|
+
/** Remove the body inset, so content such as the event log meets the edges. */
|
|
18
|
+
flush?: boolean;
|
|
11
19
|
header: ReactNode;
|
|
12
20
|
onClose(): void;
|
|
13
21
|
openKey: string;
|
|
14
22
|
width?: "default" | "wide" | "narrow";
|
|
15
23
|
} & ({ titleId: string; label?: never } | { label: string; titleId?: never });
|
|
16
24
|
|
|
17
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Render a side drawer with shared modal focus and dismissal behavior.
|
|
27
|
+
* A drawer opened from inside another drawer stacks in front of it.
|
|
28
|
+
*/
|
|
18
29
|
export function Drawer(props: DrawerProps) {
|
|
19
30
|
return <DrawerContent key={props.openKey} {...props} />;
|
|
20
31
|
}
|
|
@@ -23,37 +34,49 @@ function DrawerContent(props: DrawerProps) {
|
|
|
23
34
|
const closeRef = useRef<HTMLButtonElement>(null);
|
|
24
35
|
const sheet = useSheet(props.onClose, closeRef);
|
|
25
36
|
return (
|
|
26
|
-
<
|
|
27
|
-
<
|
|
37
|
+
<DrawerRoot {...sheet.rootProps} swipeDirection="right">
|
|
38
|
+
<DrawerPopup
|
|
28
39
|
{...sheet.contentProps}
|
|
29
40
|
aria-label={props.label}
|
|
30
41
|
aria-labelledby={props.titleId}
|
|
31
|
-
|
|
32
|
-
closeButtonProps={{
|
|
33
|
-
ref: closeRef,
|
|
34
|
-
"aria-label": props.closeLabel,
|
|
35
|
-
className:
|
|
36
|
-
"right-4 top-[max(0.75rem,env(safe-area-inset-top))] md:right-5",
|
|
37
|
-
}}
|
|
42
|
+
data-dashboard-modal=""
|
|
38
43
|
className={cn(
|
|
39
|
-
"
|
|
44
|
+
"data-[swipe-axis=x]:[--drawer-content-width:100%] data-[swipe-axis=x]:sm:[--drawer-content-width:100%]",
|
|
40
45
|
props.width === "wide"
|
|
41
|
-
? "data-[
|
|
46
|
+
? "data-[swipe-axis=x]:md:[--drawer-content-width:min(760px,94vw)]"
|
|
42
47
|
: props.width === "narrow"
|
|
43
|
-
? "data-[
|
|
44
|
-
: "data-[
|
|
48
|
+
? "data-[swipe-axis=x]:md:[--drawer-content-width:min(440px,94vw)]"
|
|
49
|
+
: "data-[swipe-axis=x]:md:[--drawer-content-width:min(560px,94vw)]",
|
|
45
50
|
)}
|
|
46
51
|
>
|
|
47
|
-
<
|
|
52
|
+
<header className="flex shrink-0 items-start gap-3 border-b border-dashboard-border bg-dashboard-bg-elevated px-4 py-3 pt-[max(0.75rem,env(safe-area-inset-top))] md:px-5">
|
|
48
53
|
<div className="min-w-0 flex-1">{props.header}</div>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
<div className="flex shrink-0 items-start gap-1.5">
|
|
55
|
+
{props.actions}
|
|
56
|
+
<DrawerClose
|
|
57
|
+
render={
|
|
58
|
+
<Button
|
|
59
|
+
ref={closeRef}
|
|
60
|
+
aria-label={props.closeLabel}
|
|
61
|
+
size="icon"
|
|
62
|
+
/>
|
|
63
|
+
}
|
|
64
|
+
>
|
|
65
|
+
<X aria-hidden="true" size={15} strokeWidth={2.25} />
|
|
66
|
+
</DrawerClose>
|
|
67
|
+
</div>
|
|
68
|
+
</header>
|
|
69
|
+
<div
|
|
70
|
+
className={cn(
|
|
71
|
+
"min-h-0 flex-1 overflow-auto",
|
|
72
|
+
props.flush
|
|
73
|
+
? "pb-[env(safe-area-inset-bottom)]"
|
|
74
|
+
: "px-4 py-4 pb-[max(1rem,env(safe-area-inset-bottom))] md:px-5",
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
54
77
|
{props.children}
|
|
55
78
|
</div>
|
|
56
|
-
</
|
|
57
|
-
</
|
|
79
|
+
</DrawerPopup>
|
|
80
|
+
</DrawerRoot>
|
|
58
81
|
);
|
|
59
82
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
|
|
3
|
+
import { Empty, EmptyDescription } from "./ui/empty";
|
|
4
|
+
|
|
3
5
|
/** Render the dashboard empty-state block with quiet warning context. */
|
|
4
6
|
export function EmptyTelemetry(props: { children: ReactNode }) {
|
|
5
7
|
return (
|
|
6
|
-
<
|
|
7
|
-
{props.children}
|
|
8
|
-
</
|
|
8
|
+
<Empty>
|
|
9
|
+
<EmptyDescription>{props.children}</EmptyDescription>
|
|
10
|
+
</Empty>
|
|
9
11
|
);
|
|
10
12
|
}
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import { ExternalLink, X } from "lucide-react";
|
|
2
2
|
import {
|
|
3
3
|
type MouseEvent,
|
|
4
|
-
useEffect,
|
|
5
|
-
useId,
|
|
6
|
-
useRef,
|
|
7
4
|
useState,
|
|
8
5
|
type ComponentPropsWithoutRef,
|
|
9
6
|
} from "react";
|
|
10
7
|
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from "../bodyScrollLock";
|
|
8
|
+
import { Button } from "./ui/button";
|
|
9
|
+
import { Dialog, DialogClose, DialogContent, DialogTitle } from "./ui/dialog";
|
|
10
|
+
import { useSheet } from "./ui/useSheet";
|
|
15
11
|
|
|
16
12
|
/** Return true when an image link click should open the inline preview. */
|
|
17
13
|
export function shouldPreviewImageAttachment(
|
|
@@ -45,9 +41,6 @@ export function ImageAttachment(
|
|
|
45
41
|
triggerClassName?: string;
|
|
46
42
|
} & Omit<ComponentPropsWithoutRef<"img">, "alt" | "className" | "src">,
|
|
47
43
|
) {
|
|
48
|
-
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
49
|
-
const bodyScrollLockedRef = useRef(false);
|
|
50
|
-
const titleId = useId();
|
|
51
44
|
const [open, setOpen] = useState(false);
|
|
52
45
|
const {
|
|
53
46
|
context,
|
|
@@ -58,20 +51,6 @@ export function ImageAttachment(
|
|
|
58
51
|
...imageProps
|
|
59
52
|
} = props;
|
|
60
53
|
|
|
61
|
-
const unlockBodyScroll = () => {
|
|
62
|
-
if (!bodyScrollLockedRef.current) return;
|
|
63
|
-
releaseBodyScrollLock();
|
|
64
|
-
bodyScrollLockedRef.current = false;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const close = () => {
|
|
68
|
-
dialogRef.current?.close();
|
|
69
|
-
setOpen(false);
|
|
70
|
-
unlockBodyScroll();
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
useEffect(() => () => unlockBodyScroll(), []);
|
|
74
|
-
|
|
75
54
|
return (
|
|
76
55
|
<>
|
|
77
56
|
<a
|
|
@@ -80,12 +59,7 @@ export function ImageAttachment(
|
|
|
80
59
|
onClick={(event) => {
|
|
81
60
|
if (!shouldPreviewImageAttachment(event)) return;
|
|
82
61
|
event.preventDefault();
|
|
83
|
-
const dialog = dialogRef.current;
|
|
84
|
-
if (!dialog || dialog.open) return;
|
|
85
|
-
acquireBodyScrollLock();
|
|
86
|
-
bodyScrollLockedRef.current = true;
|
|
87
62
|
setOpen(true);
|
|
88
|
-
dialog.showModal();
|
|
89
63
|
}}
|
|
90
64
|
rel="noreferrer"
|
|
91
65
|
target="_blank"
|
|
@@ -97,62 +71,74 @@ export function ImageAttachment(
|
|
|
97
71
|
src={src}
|
|
98
72
|
/>
|
|
99
73
|
</a>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
74
|
+
{open ? (
|
|
75
|
+
<ImagePreview
|
|
76
|
+
context={context}
|
|
77
|
+
filename={filename}
|
|
78
|
+
onClose={() => setOpen(false)}
|
|
79
|
+
src={src}
|
|
80
|
+
/>
|
|
81
|
+
) : null}
|
|
82
|
+
</>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Show one image above the page. `useSheet` shares the document scroll lock
|
|
88
|
+
* with sheets and keeps Escape and backdrop dismissal on the front modal.
|
|
89
|
+
*/
|
|
90
|
+
function ImagePreview(props: {
|
|
91
|
+
context?: string;
|
|
92
|
+
filename: string;
|
|
93
|
+
onClose(): void;
|
|
94
|
+
src: string;
|
|
95
|
+
}) {
|
|
96
|
+
const sheet = useSheet(props.onClose);
|
|
97
|
+
return (
|
|
98
|
+
<Dialog {...sheet.rootProps}>
|
|
99
|
+
<DialogContent
|
|
100
|
+
{...sheet.contentProps}
|
|
101
|
+
className="top-0 left-0 h-dvh w-screen max-w-none translate-x-0 translate-y-0 gap-0 overflow-hidden rounded-none border-0 bg-dashboard-ink p-0 shadow-none max-h-none sm:top-0 sm:translate-y-0 md:top-1/2 md:left-1/2 md:h-[min(90dvh,960px)] md:w-[min(92vw,1440px)] md:-translate-x-1/2 md:-translate-y-1/2 md:rounded-lg md:border"
|
|
111
102
|
>
|
|
112
103
|
<div className="grid h-full min-h-0 grid-rows-[auto_minmax(0,1fr)] bg-dashboard-bg-elevated">
|
|
113
104
|
<header className="grid grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-1.5 border-b border-dashboard-border-strong bg-dashboard-surface-raised px-3 py-2 pt-[max(0.5rem,env(safe-area-inset-top))] md:px-4">
|
|
114
105
|
<div className="min-w-0">
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
{filename}
|
|
120
|
-
</div>
|
|
121
|
-
{context ? (
|
|
106
|
+
<DialogTitle className="truncate font-mono text-xs leading-normal font-normal">
|
|
107
|
+
{props.filename}
|
|
108
|
+
</DialogTitle>
|
|
109
|
+
{props.context ? (
|
|
122
110
|
<div className="truncate font-mono text-2xs text-dashboard-text-muted">
|
|
123
|
-
{context}
|
|
111
|
+
{props.context}
|
|
124
112
|
</div>
|
|
125
113
|
) : null}
|
|
126
114
|
</div>
|
|
127
115
|
<a
|
|
128
116
|
className="inline-flex min-h-10 items-center gap-1.5 rounded-md px-2.5 text-xs font-medium text-dashboard-text-muted no-underline transition-colors hover:bg-dashboard-fill-hover hover:text-dashboard-text"
|
|
129
|
-
href={src}
|
|
117
|
+
href={props.src}
|
|
130
118
|
rel="noreferrer"
|
|
131
119
|
target="_blank"
|
|
132
120
|
>
|
|
133
121
|
<ExternalLink aria-hidden="true" size={14} />
|
|
134
122
|
<span className="max-sm:sr-only">Open raw file</span>
|
|
135
123
|
</a>
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
124
|
+
<DialogClose
|
|
125
|
+
render={
|
|
126
|
+
<Button className="h-10 gap-1.5 border-0 bg-transparent px-2.5 text-xs text-dashboard-text-muted hover:bg-dashboard-fill-hover hover:text-dashboard-text" />
|
|
127
|
+
}
|
|
140
128
|
>
|
|
141
129
|
<X aria-hidden="true" size={16} />
|
|
142
130
|
<span>Close</span>
|
|
143
|
-
</
|
|
131
|
+
</DialogClose>
|
|
144
132
|
</header>
|
|
145
133
|
<div className="grid min-h-0 place-items-center overflow-auto p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] md:p-6">
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
/>
|
|
152
|
-
) : null}
|
|
134
|
+
<img
|
|
135
|
+
alt={props.filename}
|
|
136
|
+
className="max-h-full max-w-full object-contain"
|
|
137
|
+
src={props.src}
|
|
138
|
+
/>
|
|
153
139
|
</div>
|
|
154
140
|
</div>
|
|
155
|
-
</
|
|
156
|
-
|
|
141
|
+
</DialogContent>
|
|
142
|
+
</Dialog>
|
|
157
143
|
);
|
|
158
144
|
}
|