@sentry/junior-dashboard 0.270.0 → 0.272.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 +44 -3
- package/dist/client.js +77 -77
- package/dist/index.js +40 -3
- package/dist/tailwind.css +1 -1
- package/package.json +4 -4
- package/src/app.ts +2 -0
- package/src/client/App.tsx +89 -61
- package/src/client/components/ProfileMenu.tsx +0 -24
- package/src/client/components/UserInputForm.tsx +38 -7
- package/src/client/components/layout/PageHeader.tsx +25 -10
- package/src/client/conversations/ConversationPage.tsx +55 -8
- package/src/client/conversations/ConversationTranscript.tsx +74 -5
- package/src/client/conversations/TranscriptRows.tsx +23 -0
- package/src/client/conversations/TranscriptView.tsx +22 -0
- package/src/client/conversations/conversationRoutes.ts +30 -3
- package/src/client/conversations/queries.ts +30 -5
- package/src/client/conversations/transcriptBottomPinning.ts +84 -13
- package/src/client/conversations/transcriptReadPosition.ts +267 -0
- package/src/client/conversations/transcriptRenderModel.ts +16 -2
- package/src/client/pages/automations/AutomationActions.tsx +2 -3
- package/src/client/pages/automations/AutomationEditor.tsx +18 -30
- package/src/client/pages/automations/AutomationOverview.tsx +2 -2
- package/src/client/pages/automations/AutomationVersionsTab.tsx +1 -1
- package/src/client/pages/settings/ConnectionsPage.tsx +198 -0
- package/src/client/pages/settings/McpSettingsPage.tsx +77 -0
- package/src/client/pages/settings/PersonalTokensPage.tsx +198 -0
- package/src/client/pages/{SettingsPage.tsx → settings/SettingsPage.tsx} +115 -61
- package/src/client/pages/settings/SettingsPageLayout.tsx +45 -0
- package/src/client/primaryNav.ts +1 -1
- package/src/client/review-desk/ReviewDeskPage.tsx +185 -242
- package/src/client/review-desk/ReviewDeskSidebar.tsx +259 -0
- package/src/client/review-desk/ReviewItemActions.tsx +1 -10
- package/src/client/review-desk/ReviewItemContext.tsx +108 -0
- package/src/client/review-desk/ReviewItemDetails.tsx +153 -0
- package/src/client/review-desk/ReviewItemView.tsx +159 -0
- package/src/client/review-desk/queries.ts +3 -3
- package/src/client/review-desk/reviewItemFormat.ts +52 -0
- package/src/mock-reporting/read-positions.ts +39 -0
- package/src/mock-reporting/routes.ts +26 -1
- package/src/tailwind.css +1 -1
- package/src/client/components/AGENTS.md +0 -65
- package/src/client/components/charts/AGENTS.md +0 -15
- package/src/client/conversations/AGENTS.md +0 -38
- package/src/client/pages/ConnectionsPage.tsx +0 -207
- package/src/client/pages/PersonalTokensPage.tsx +0 -205
- package/src/client/pages/automations/AGENTS.md +0 -34
- package/src/client/pages/system/AGENTS.md +0 -12
- package/src/client/review-desk/ReviewItemDetail.tsx +0 -123
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
2
|
-
import { KeyRound, Trash2 } from "lucide-react";
|
|
3
|
-
import { useRef, useState } from "react";
|
|
4
|
-
import {
|
|
5
|
-
createdPersonalTokenSchema,
|
|
6
|
-
personalTokenListSchema,
|
|
7
|
-
type PersonalTokenMetadata,
|
|
8
|
-
} from "@sentry/junior/api/schema";
|
|
9
|
-
import { deleteDashboardResource, fetchDashboardJson, post } from "../http";
|
|
10
|
-
import { dashboardContainerClass } from "../styles";
|
|
11
|
-
import { getDashboardAgentName } from "../agentName";
|
|
12
|
-
import { useDocumentTitle } from "../documentTitle";
|
|
13
|
-
import { Button } from "../components/ui/button";
|
|
14
|
-
import { Input } from "../components/ui/input";
|
|
15
|
-
import { InlineError } from "../components/InlineError";
|
|
16
|
-
import { Card } from "../components/layout/Card";
|
|
17
|
-
|
|
18
|
-
const personalTokensQueryKey = ["dashboard", "personal-tokens"] as const;
|
|
19
|
-
|
|
20
|
-
/** Create and revoke personal API tokens for local clients. */
|
|
21
|
-
export function PersonalTokensPage() {
|
|
22
|
-
useDocumentTitle("API tokens");
|
|
23
|
-
const queryClient = useQueryClient();
|
|
24
|
-
const createTokenStarted = useRef(false);
|
|
25
|
-
const [name, setName] = useState("Local agent");
|
|
26
|
-
const cancelTokenListRefetch = () =>
|
|
27
|
-
queryClient.cancelQueries({
|
|
28
|
-
exact: true,
|
|
29
|
-
queryKey: personalTokensQueryKey,
|
|
30
|
-
});
|
|
31
|
-
const tokensQuery = useQuery({
|
|
32
|
-
queryKey: personalTokensQueryKey,
|
|
33
|
-
queryFn: ({ signal }) =>
|
|
34
|
-
fetchDashboardJson(
|
|
35
|
-
personalTokenListSchema,
|
|
36
|
-
"/api/personal-tokens",
|
|
37
|
-
signal,
|
|
38
|
-
),
|
|
39
|
-
retry: false,
|
|
40
|
-
});
|
|
41
|
-
const createTokenMutation = useMutation({
|
|
42
|
-
gcTime: 0,
|
|
43
|
-
mutationFn: (tokenName: string) =>
|
|
44
|
-
post(createdPersonalTokenSchema, "/api/personal-tokens", {
|
|
45
|
-
name: tokenName,
|
|
46
|
-
}),
|
|
47
|
-
onMutate: () => cancelTokenListRefetch(),
|
|
48
|
-
onSuccess: async (created) => {
|
|
49
|
-
const { token: _token, ...metadata } = created;
|
|
50
|
-
await cancelTokenListRefetch();
|
|
51
|
-
queryClient.setQueryData<{ tokens: PersonalTokenMetadata[] }>(
|
|
52
|
-
personalTokensQueryKey,
|
|
53
|
-
(current) => ({
|
|
54
|
-
tokens: [
|
|
55
|
-
metadata,
|
|
56
|
-
...(current?.tokens ?? []).filter(
|
|
57
|
-
(token) => token.id !== metadata.id,
|
|
58
|
-
),
|
|
59
|
-
],
|
|
60
|
-
}),
|
|
61
|
-
);
|
|
62
|
-
},
|
|
63
|
-
onSettled: () => {
|
|
64
|
-
createTokenStarted.current = false;
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
const revokeTokenMutation = useMutation({
|
|
68
|
-
mutationFn: (token: PersonalTokenMetadata) =>
|
|
69
|
-
deleteDashboardResource(
|
|
70
|
-
`/api/personal-tokens/${encodeURIComponent(token.id)}`,
|
|
71
|
-
),
|
|
72
|
-
onMutate: () => cancelTokenListRefetch(),
|
|
73
|
-
onSuccess: async (_result, token) => {
|
|
74
|
-
await cancelTokenListRefetch();
|
|
75
|
-
queryClient.setQueryData<{ tokens: PersonalTokenMetadata[] }>(
|
|
76
|
-
personalTokensQueryKey,
|
|
77
|
-
(current) => ({
|
|
78
|
-
tokens: (current?.tokens ?? []).filter(
|
|
79
|
-
(item) => item.id !== token.id,
|
|
80
|
-
),
|
|
81
|
-
}),
|
|
82
|
-
);
|
|
83
|
-
if (createTokenMutation.data?.id === token.id) {
|
|
84
|
-
createTokenMutation.reset();
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
const createdToken = createTokenMutation.data;
|
|
89
|
-
const tokens = tokensQuery.data?.tokens ?? [];
|
|
90
|
-
const loading = tokensQuery.isPending;
|
|
91
|
-
const busy = createTokenMutation.isPending || revokeTokenMutation.isPending;
|
|
92
|
-
const displayedError =
|
|
93
|
-
(createTokenMutation.isError
|
|
94
|
-
? "Could not create the API token. Try again."
|
|
95
|
-
: revokeTokenMutation.isError
|
|
96
|
-
? "Could not revoke the API token. Try again."
|
|
97
|
-
: undefined) ??
|
|
98
|
-
(!tokensQuery.data && tokensQuery.isError
|
|
99
|
-
? "Could not load API tokens. Try again."
|
|
100
|
-
: undefined);
|
|
101
|
-
|
|
102
|
-
return (
|
|
103
|
-
<div className={`${dashboardContainerClass} px-4 py-8 md:px-8`}>
|
|
104
|
-
<section className="mx-auto w-full max-w-3xl">
|
|
105
|
-
<h1 className="m-0 text-2xl font-bold">Personal API Tokens</h1>
|
|
106
|
-
<p className="mt-2 mb-0 max-w-2xl text-sm text-dashboard-text-muted">
|
|
107
|
-
Use a token to read {getDashboardAgentName()} APIs from a local agent
|
|
108
|
-
or script. Tokens expire after 90 days.
|
|
109
|
-
</p>
|
|
110
|
-
|
|
111
|
-
<Card className="mb-0 mt-6" padding="md" variant="raised">
|
|
112
|
-
{createdToken ? (
|
|
113
|
-
<div className="mt-5 rounded-md border border-emerald-400/40 bg-emerald-400/5 p-3">
|
|
114
|
-
<p className="mt-0 mb-2 text-sm font-semibold">
|
|
115
|
-
Copy this token now. It won't be shown again.
|
|
116
|
-
</p>
|
|
117
|
-
<code className="block overflow-x-auto rounded-md bg-dashboard-ink p-2 text-xs text-emerald-300">
|
|
118
|
-
{createdToken.token}
|
|
119
|
-
</code>
|
|
120
|
-
<Button
|
|
121
|
-
className="mt-3"
|
|
122
|
-
onClick={() => {
|
|
123
|
-
createTokenMutation.reset();
|
|
124
|
-
setName("Local agent");
|
|
125
|
-
}}
|
|
126
|
-
>
|
|
127
|
-
Create another token
|
|
128
|
-
</Button>
|
|
129
|
-
</div>
|
|
130
|
-
) : (
|
|
131
|
-
<div className="mt-5 flex gap-2">
|
|
132
|
-
<Input
|
|
133
|
-
aria-label="Token name"
|
|
134
|
-
className="min-w-0 flex-1"
|
|
135
|
-
maxLength={80}
|
|
136
|
-
onChange={(event) => setName(event.target.value)}
|
|
137
|
-
value={name}
|
|
138
|
-
/>
|
|
139
|
-
<Button
|
|
140
|
-
disabled={loading || busy || !name.trim()}
|
|
141
|
-
onClick={() => {
|
|
142
|
-
if (createTokenStarted.current) return;
|
|
143
|
-
revokeTokenMutation.reset();
|
|
144
|
-
createTokenStarted.current = true;
|
|
145
|
-
createTokenMutation.mutate(name);
|
|
146
|
-
}}
|
|
147
|
-
>
|
|
148
|
-
Create token
|
|
149
|
-
</Button>
|
|
150
|
-
</div>
|
|
151
|
-
)}
|
|
152
|
-
|
|
153
|
-
{displayedError ? (
|
|
154
|
-
<InlineError className="mt-4">{displayedError}</InlineError>
|
|
155
|
-
) : null}
|
|
156
|
-
|
|
157
|
-
<div className="mt-5 space-y-2">
|
|
158
|
-
{loading ? (
|
|
159
|
-
<p className="text-sm text-dashboard-text-muted">
|
|
160
|
-
Loading API tokens…
|
|
161
|
-
</p>
|
|
162
|
-
) : tokens.length === 0 ? (
|
|
163
|
-
<p className="text-sm text-dashboard-text-muted">
|
|
164
|
-
No active API tokens.
|
|
165
|
-
</p>
|
|
166
|
-
) : (
|
|
167
|
-
tokens.map((token) => (
|
|
168
|
-
<div
|
|
169
|
-
className="flex items-center gap-3 rounded-md border border-dashboard-border-strong p-3"
|
|
170
|
-
key={token.id}
|
|
171
|
-
>
|
|
172
|
-
<KeyRound className="text-dashboard-accent" size={16} />
|
|
173
|
-
<div className="min-w-0 flex-1">
|
|
174
|
-
<p className="m-0 truncate text-sm font-semibold">
|
|
175
|
-
{token.name}
|
|
176
|
-
</p>
|
|
177
|
-
<p className="mt-1 mb-0 text-xs text-dashboard-text-muted">
|
|
178
|
-
Ends in {token.tokenSuffix} · Expires{" "}
|
|
179
|
-
{new Date(token.expiresAt).toLocaleDateString()}
|
|
180
|
-
</p>
|
|
181
|
-
</div>
|
|
182
|
-
<Button
|
|
183
|
-
aria-label={`Revoke ${token.name}`}
|
|
184
|
-
disabled={busy}
|
|
185
|
-
onClick={() => {
|
|
186
|
-
if (createTokenMutation.isError) {
|
|
187
|
-
createTokenMutation.reset();
|
|
188
|
-
}
|
|
189
|
-
revokeTokenMutation.mutate(token);
|
|
190
|
-
}}
|
|
191
|
-
size="icon"
|
|
192
|
-
variant="danger"
|
|
193
|
-
type="button"
|
|
194
|
-
>
|
|
195
|
-
<Trash2 size={16} />
|
|
196
|
-
</Button>
|
|
197
|
-
</div>
|
|
198
|
-
))
|
|
199
|
-
)}
|
|
200
|
-
</div>
|
|
201
|
-
</Card>
|
|
202
|
-
</section>
|
|
203
|
-
</div>
|
|
204
|
-
);
|
|
205
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# Automation pages
|
|
2
|
-
|
|
3
|
-
## Boundaries
|
|
4
|
-
|
|
5
|
-
- Core owns validation, edit access, creator-only rules, revision checks,
|
|
6
|
-
Schedule previews, and the Event catalog. The page shows core errors. It
|
|
7
|
-
does not repeat these rules.
|
|
8
|
-
- Preview requests never save or run work.
|
|
9
|
-
- Completed scheduled Automations are read-only.
|
|
10
|
-
|
|
11
|
-
## Editor rules
|
|
12
|
-
|
|
13
|
-
- Send only changed fields. Keep unsupported triggers and stored
|
|
14
|
-
Destinations unless the user replaces them. Keep outcome order.
|
|
15
|
-
- New messages can target only the current Destination or the creator.
|
|
16
|
-
- A public reader sees outcomes as read-only and cannot turn on creator
|
|
17
|
-
credentials. Warn that an instruction or event selector change switches to
|
|
18
|
-
system credentials.
|
|
19
|
-
- Pause, Resume, and Delete are creator-only. Hide them from other viewers.
|
|
20
|
-
- Query refreshes must not replace an unsaved draft. After a stale save, let
|
|
21
|
-
the user keep their changes or load the latest version.
|
|
22
|
-
- Block navigation with unsaved changes.
|
|
23
|
-
- Lifecycle actions and "Make active" read the latest revision first.
|
|
24
|
-
|
|
25
|
-
## Page rules
|
|
26
|
-
|
|
27
|
-
- Each Automation has one page at `/automations/:id`. Keep the list query
|
|
28
|
-
string on the way back. Drop tab-only params: `result`, `run`, `version`.
|
|
29
|
-
- `AutomationRecentRuns` shows terminal runs only. Do not infer queued or
|
|
30
|
-
running work from terminal records.
|
|
31
|
-
- Delete keeps errors in the dialog and blocks dismissal while pending.
|
|
32
|
-
|
|
33
|
-
Browser journeys: `e2e/automation-editor.spec.ts` and
|
|
34
|
-
`e2e/automations.spec.ts`.
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# System pages
|
|
2
|
-
|
|
3
|
-
- The URL selects a Workspace recipe: `/system/workspaces/:id` opens
|
|
4
|
-
`WorkspaceDetails` beside the list.
|
|
5
|
-
- The list report has no snapshots. The detail read adds the current
|
|
6
|
-
snapshot. Keep snapshot loading and errors inside the Snapshot row.
|
|
7
|
-
- Edit and Delete live in the panel header. List rows have no actions.
|
|
8
|
-
- `WorkspaceDeleteDialog` keeps errors in the dialog and blocks dismissal
|
|
9
|
-
while the request runs.
|
|
10
|
-
- Close and Escape return focus to the selected row.
|
|
11
|
-
|
|
12
|
-
Browser coverage: `e2e/system.spec.ts`.
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { Archive } from "lucide-react";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
3
|
-
import { Link } from "react-router";
|
|
4
|
-
import type { ReviewItemPayload } from "@sentry/junior/api/schema";
|
|
5
|
-
|
|
6
|
-
import { StatusChip } from "../components/StatusChip";
|
|
7
|
-
import { DisplayText } from "../display-text/DisplayText";
|
|
8
|
-
import { conversationPath } from "../conversations/conversationRoutes";
|
|
9
|
-
import { automationPath, formatRelativeTime } from "../format";
|
|
10
|
-
import { ReviewItemActions } from "./ReviewItemActions";
|
|
11
|
-
import { useLinkedConversationTitles } from "./queries";
|
|
12
|
-
import { assigneeLabel, reviewItemStatusLabel } from "./reviewItemFormat";
|
|
13
|
-
|
|
14
|
-
function Fact(props: { children: ReactNode; label: string }) {
|
|
15
|
-
return (
|
|
16
|
-
<div className="grid content-start gap-0.5">
|
|
17
|
-
<dt className="text-xs font-medium text-dashboard-text-muted">
|
|
18
|
-
{props.label}
|
|
19
|
-
</dt>
|
|
20
|
-
<dd className="m-0 min-w-0 break-words text-sm">{props.children}</dd>
|
|
21
|
-
</div>
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Show one Review item with its facts and the viewer's actions. */
|
|
26
|
-
export function ReviewItemDetail(props: { item: ReviewItemPayload }) {
|
|
27
|
-
const { item } = props;
|
|
28
|
-
const subject = item.subject;
|
|
29
|
-
const conversationTitles = useLinkedConversationTitles(item.conversationIds);
|
|
30
|
-
const closed = item.status !== "open";
|
|
31
|
-
return (
|
|
32
|
-
<article className="grid min-w-0 content-start gap-5">
|
|
33
|
-
{closed ? (
|
|
34
|
-
// Search mixes open and archived items, so a closed item must be
|
|
35
|
-
// easy to tell apart at the top of the detail.
|
|
36
|
-
<p className="m-0 flex items-center gap-2 rounded-md bg-dashboard-fill-soft px-3 py-2 text-xs font-medium text-dashboard-text-muted">
|
|
37
|
-
<Archive aria-hidden="true" size={14} />
|
|
38
|
-
Archived · {reviewItemStatusLabel(item)}
|
|
39
|
-
</p>
|
|
40
|
-
) : null}
|
|
41
|
-
<header className="grid gap-1.5">
|
|
42
|
-
<div className="flex min-w-0 items-start justify-between gap-3">
|
|
43
|
-
<h2 className="m-0 min-w-0 break-words font-display text-xl font-medium leading-snug">
|
|
44
|
-
{item.title}
|
|
45
|
-
</h2>
|
|
46
|
-
{closed ? null : (
|
|
47
|
-
<StatusChip className="mt-1 shrink-0" size="compact" tone="accent">
|
|
48
|
-
{reviewItemStatusLabel(item)}
|
|
49
|
-
</StatusChip>
|
|
50
|
-
)}
|
|
51
|
-
</div>
|
|
52
|
-
<p className="m-0 text-xs text-dashboard-text-muted">
|
|
53
|
-
Added {formatRelativeTime(item.createdAt)}
|
|
54
|
-
{item.visibility === "private" ? " · Private" : null}
|
|
55
|
-
</p>
|
|
56
|
-
</header>
|
|
57
|
-
<DisplayText text={item.body} />
|
|
58
|
-
<section
|
|
59
|
-
aria-label={item.status === "open" ? "Actions" : "Outcome"}
|
|
60
|
-
className="border-t border-dashboard-border-subtle pt-5"
|
|
61
|
-
>
|
|
62
|
-
<ReviewItemActions item={item} />
|
|
63
|
-
</section>
|
|
64
|
-
<dl className="m-0 grid gap-4 border-t border-dashboard-border-subtle pt-5 sm:grid-cols-2">
|
|
65
|
-
<Fact label="Assignees">
|
|
66
|
-
{item.assignees.length > 0 || item.assignedToEveryone ? (
|
|
67
|
-
assigneeLabel(item)
|
|
68
|
-
) : (
|
|
69
|
-
<StatusChip size="compact" tone="warning">
|
|
70
|
-
Unassigned
|
|
71
|
-
</StatusChip>
|
|
72
|
-
)}
|
|
73
|
-
</Fact>
|
|
74
|
-
{subject ? (
|
|
75
|
-
<Fact label="Subject">
|
|
76
|
-
{subject.url ? (
|
|
77
|
-
<a href={subject.url} rel="noreferrer" target="_blank">
|
|
78
|
-
{subject.title ?? subject.url}
|
|
79
|
-
</a>
|
|
80
|
-
) : (
|
|
81
|
-
(subject.title ?? subject.id)
|
|
82
|
-
)}
|
|
83
|
-
</Fact>
|
|
84
|
-
) : null}
|
|
85
|
-
<Fact label="Added by">
|
|
86
|
-
<Link to={conversationPath(item.addedByConversationId)}>
|
|
87
|
-
Conversation
|
|
88
|
-
</Link>
|
|
89
|
-
{item.automationId ? (
|
|
90
|
-
<>
|
|
91
|
-
{" · "}
|
|
92
|
-
<Link to={automationPath(item.automationId)}>Automation</Link>
|
|
93
|
-
</>
|
|
94
|
-
) : null}
|
|
95
|
-
</Fact>
|
|
96
|
-
{item.conversationIds.length > 0 ? (
|
|
97
|
-
<Fact label="Linked Conversations">
|
|
98
|
-
<ul className="m-0 grid list-none gap-0.5 p-0">
|
|
99
|
-
{item.conversationIds.map((conversationId, index) => {
|
|
100
|
-
const title = conversationTitles[index];
|
|
101
|
-
return (
|
|
102
|
-
<li
|
|
103
|
-
className={
|
|
104
|
-
title ? "truncate" : "truncate font-mono text-xs"
|
|
105
|
-
}
|
|
106
|
-
key={conversationId}
|
|
107
|
-
>
|
|
108
|
-
<Link
|
|
109
|
-
title={conversationId}
|
|
110
|
-
to={conversationPath(conversationId)}
|
|
111
|
-
>
|
|
112
|
-
{title ?? conversationId}
|
|
113
|
-
</Link>
|
|
114
|
-
</li>
|
|
115
|
-
);
|
|
116
|
-
})}
|
|
117
|
-
</ul>
|
|
118
|
-
</Fact>
|
|
119
|
-
) : null}
|
|
120
|
-
</dl>
|
|
121
|
-
</article>
|
|
122
|
-
);
|
|
123
|
-
}
|