@sentry/junior-dashboard 0.269.0 → 0.271.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 +2 -2
- package/dist/client.js +78 -75
- package/dist/index.js +2 -2
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/client/App.tsx +103 -65
- package/src/client/components/AGENTS.md +30 -59
- package/src/client/components/AutomationCard.tsx +2 -1
- package/src/client/components/ProfileMenu.tsx +0 -24
- package/src/client/components/UserInputForm.tsx +143 -39
- package/src/client/components/layout/PageHeader.tsx +25 -10
- package/src/client/conversations/AGENTS.md +12 -37
- package/src/client/conversations/ConversationBrief.tsx +55 -36
- package/src/client/conversations/ConversationComposer.tsx +27 -8
- package/src/client/conversations/ConversationEventLog.tsx +1 -0
- package/src/client/conversations/ConversationHomeList.tsx +43 -26
- package/src/client/conversations/ConversationPage.tsx +137 -40
- package/src/client/conversations/ConversationSectionHeading.tsx +44 -0
- package/src/client/conversations/ConversationSidebar.tsx +55 -25
- package/src/client/conversations/ConversationTranscript.tsx +3 -2
- package/src/client/conversations/ConversationWatches.tsx +4 -1
- package/src/client/conversations/ConversationWorkspace.tsx +43 -25
- package/src/client/conversations/EventDetails.tsx +6 -9
- package/src/client/conversations/PendingMailboxStack.tsx +5 -2
- package/src/client/conversations/TranscriptContextEventView.tsx +3 -4
- package/src/client/conversations/TranscriptEventView.tsx +9 -3
- package/src/client/conversations/TranscriptMessageView.tsx +6 -5
- package/src/client/conversations/TranscriptReasoningView.tsx +4 -5
- package/src/client/conversations/TranscriptRows.tsx +6 -1
- package/src/client/conversations/TranscriptStructuredEventView.tsx +5 -4
- package/src/client/conversations/TranscriptToolView.tsx +2 -1
- package/src/client/conversations/TranscriptView.tsx +6 -6
- package/src/client/conversations/conversationOutbox.ts +13 -3
- package/src/client/conversations/conversationRoutes.ts +30 -3
- package/src/client/conversations/conversationSections.ts +36 -8
- package/src/client/conversations/eventTranscript.ts +1 -0
- package/src/client/conversations/transcriptBottomPinning.ts +171 -81
- package/src/client/conversations/transcriptScrollAnchor.ts +101 -0
- package/src/client/conversations/useCollapsedConversationSections.ts +48 -0
- package/src/client/display-text/DisplayText.tsx +68 -0
- package/src/client/display-text/emoji.ts +54 -0
- package/src/client/{conversations/transcriptMarkdownLinks.ts → display-text/links.ts} +16 -3
- package/src/client/{conversations/TranscriptMarkdown.tsx → display-text/markdown.tsx} +83 -19
- package/src/client/display-text/mentions.tsx +148 -0
- package/src/client/display-text/plainText.ts +29 -0
- package/src/client/pages/automations/AGENTS.md +6 -25
- package/src/client/pages/automations/AutomationFilters.tsx +6 -6
- package/src/client/pages/automations/AutomationOverview.tsx +2 -2
- package/src/client/pages/automations/AutomationVersionsTab.tsx +12 -8
- package/src/client/pages/code/RepositoryTabs.tsx +6 -1
- package/src/client/pages/memory/MemoryDetails.tsx +5 -13
- 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} +54 -62
- package/src/client/pages/settings/SettingsPageLayout.tsx +45 -0
- package/src/client/primaryNav.ts +1 -1
- package/src/client/review-desk/ConversationReviewBar.tsx +0 -1
- package/src/client/review-desk/ReviewDeskPage.tsx +202 -241
- package/src/client/review-desk/ReviewDeskSidebar.tsx +253 -0
- package/src/client/review-desk/ReviewItemActions.tsx +6 -2
- package/src/client/review-desk/ReviewItemCard.tsx +58 -17
- 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/UnsentAnswers.tsx +148 -0
- package/src/client/review-desk/queries.ts +4 -2
- package/src/client/review-desk/reviewItemFormat.ts +83 -1
- package/src/client/components/charts/AGENTS.md +0 -15
- package/src/client/conversations/TranscriptText.tsx +0 -47
- package/src/client/conversations/conversationActivityPreview.ts +0 -16
- package/src/client/conversations/transcriptMentions.tsx +0 -134
- package/src/client/pages/ConnectionsPage.tsx +0 -207
- package/src/client/pages/PersonalTokensPage.tsx +0 -205
- package/src/client/pages/system/AGENTS.md +0 -12
- package/src/client/review-desk/ReviewItemDetail.tsx +0 -123
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
2
|
-
import { Plug } from "lucide-react";
|
|
3
|
-
import type { z } from "zod";
|
|
4
|
-
import {
|
|
5
|
-
connectionListSchema,
|
|
6
|
-
disconnectConnectionResponseSchema,
|
|
7
|
-
startConnectionResponseSchema,
|
|
8
|
-
type connectionSchema,
|
|
9
|
-
} from "@sentry/junior/api/schema";
|
|
10
|
-
import { del, fetchDashboardJson, post } from "../http";
|
|
11
|
-
import { dashboardContainerClass } from "../styles";
|
|
12
|
-
import { getDashboardAgentName } from "../agentName";
|
|
13
|
-
import { useDocumentTitle } from "../documentTitle";
|
|
14
|
-
import { Button } from "../components/ui/button";
|
|
15
|
-
import { InlineError } from "../components/InlineError";
|
|
16
|
-
import { Card } from "../components/layout/Card";
|
|
17
|
-
|
|
18
|
-
type Connection = z.infer<typeof connectionSchema>;
|
|
19
|
-
|
|
20
|
-
const connectionsQueryKey = ["dashboard", "connections"] as const;
|
|
21
|
-
|
|
22
|
-
function connectionPath(provider: string): string {
|
|
23
|
-
return `/api/connections/${encodeURIComponent(provider)}`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Connect and disconnect the viewer's plugin accounts. */
|
|
27
|
-
export function ConnectionsPage() {
|
|
28
|
-
useDocumentTitle("Connected accounts");
|
|
29
|
-
const queryClient = useQueryClient();
|
|
30
|
-
const connectionsQuery = useQuery({
|
|
31
|
-
queryKey: connectionsQueryKey,
|
|
32
|
-
queryFn: ({ signal }) =>
|
|
33
|
-
fetchDashboardJson(connectionListSchema, "/api/connections", signal),
|
|
34
|
-
// OAuth finishes in another tab. Refresh when the viewer returns.
|
|
35
|
-
refetchOnWindowFocus: true,
|
|
36
|
-
retry: false,
|
|
37
|
-
});
|
|
38
|
-
const connectMutation = useMutation({
|
|
39
|
-
mutationFn: (input: { provider: string; popup: Window | null }) =>
|
|
40
|
-
post(startConnectionResponseSchema, connectionPath(input.provider), {}),
|
|
41
|
-
onSuccess: ({ authorizationUrl }, { popup }) => {
|
|
42
|
-
// A blocked popup returns null, so use this tab. A tab the viewer
|
|
43
|
-
// closed means they cancelled; keep them on the dashboard.
|
|
44
|
-
if (!popup) {
|
|
45
|
-
window.location.assign(authorizationUrl);
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
if (popup.closed) return;
|
|
49
|
-
popup.opener = null;
|
|
50
|
-
popup.location.href = authorizationUrl;
|
|
51
|
-
},
|
|
52
|
-
onError: (_error, { popup }) => {
|
|
53
|
-
popup?.close();
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
const disconnectMutation = useMutation({
|
|
57
|
-
mutationFn: (connection: Connection) =>
|
|
58
|
-
del(
|
|
59
|
-
disconnectConnectionResponseSchema,
|
|
60
|
-
connectionPath(connection.provider),
|
|
61
|
-
),
|
|
62
|
-
onSuccess: (_result, connection) => {
|
|
63
|
-
queryClient.setQueryData<{ connections: Connection[] }>(
|
|
64
|
-
connectionsQueryKey,
|
|
65
|
-
(current) =>
|
|
66
|
-
current && {
|
|
67
|
-
connections: current.connections.map((item) =>
|
|
68
|
-
item.provider === connection.provider
|
|
69
|
-
? {
|
|
70
|
-
provider: item.provider,
|
|
71
|
-
displayName: item.displayName,
|
|
72
|
-
description: item.description,
|
|
73
|
-
connected: false,
|
|
74
|
-
}
|
|
75
|
-
: item,
|
|
76
|
-
),
|
|
77
|
-
},
|
|
78
|
-
);
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
const connections = connectionsQuery.data?.connections ?? [];
|
|
82
|
-
const busy = connectMutation.isPending || disconnectMutation.isPending;
|
|
83
|
-
const pendingProvider = connectMutation.isPending
|
|
84
|
-
? connectMutation.variables?.provider
|
|
85
|
-
: disconnectMutation.isPending
|
|
86
|
-
? disconnectMutation.variables?.provider
|
|
87
|
-
: undefined;
|
|
88
|
-
const listFailed = !connectionsQuery.data && connectionsQuery.isError;
|
|
89
|
-
const displayedError = connectMutation.isError
|
|
90
|
-
? "Could not start the connection. Try again."
|
|
91
|
-
: disconnectMutation.isError
|
|
92
|
-
? "Could not disconnect the account. Try again."
|
|
93
|
-
: listFailed
|
|
94
|
-
? "Could not load connected accounts. Try again."
|
|
95
|
-
: undefined;
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<div className={`${dashboardContainerClass} px-4 py-8 md:px-8`}>
|
|
99
|
-
<section className="mx-auto w-full max-w-3xl">
|
|
100
|
-
<h1 className="m-0 text-2xl font-bold">Connected accounts</h1>
|
|
101
|
-
<p className="mt-2 mb-0 max-w-2xl text-sm text-dashboard-text-muted">
|
|
102
|
-
Connect an account so {getDashboardAgentName()} can act as you in
|
|
103
|
-
conversations you start here.
|
|
104
|
-
</p>
|
|
105
|
-
|
|
106
|
-
<Card className="mb-0 mt-6" padding="md" variant="raised">
|
|
107
|
-
{displayedError ? (
|
|
108
|
-
<InlineError className={listFailed ? undefined : "mb-4"}>
|
|
109
|
-
{displayedError}
|
|
110
|
-
</InlineError>
|
|
111
|
-
) : null}
|
|
112
|
-
{listFailed ? null : (
|
|
113
|
-
<div className="space-y-2">
|
|
114
|
-
{connectionsQuery.isPending ? (
|
|
115
|
-
<p className="m-0 text-sm text-dashboard-text-muted">
|
|
116
|
-
Loading connected accounts…
|
|
117
|
-
</p>
|
|
118
|
-
) : connections.length === 0 ? (
|
|
119
|
-
<p className="m-0 text-sm text-dashboard-text-muted">
|
|
120
|
-
No installed plugins support account connections.
|
|
121
|
-
</p>
|
|
122
|
-
) : (
|
|
123
|
-
connections.map((connection) => (
|
|
124
|
-
<div
|
|
125
|
-
className="flex items-center gap-3 rounded-md border border-dashboard-border-strong p-3"
|
|
126
|
-
key={connection.provider}
|
|
127
|
-
>
|
|
128
|
-
<Plug
|
|
129
|
-
className="shrink-0 text-dashboard-accent"
|
|
130
|
-
size={16}
|
|
131
|
-
/>
|
|
132
|
-
<div className="min-w-0 flex-1">
|
|
133
|
-
<p className="m-0 truncate text-sm font-semibold">
|
|
134
|
-
{connection.displayName}
|
|
135
|
-
</p>
|
|
136
|
-
<p className="mt-1 mb-0 line-clamp-2 break-words text-xs text-dashboard-text-muted">
|
|
137
|
-
{connection.connected ? (
|
|
138
|
-
connection.account ? (
|
|
139
|
-
<>
|
|
140
|
-
Connected as{" "}
|
|
141
|
-
{connection.account.url ? (
|
|
142
|
-
<a
|
|
143
|
-
className="text-dashboard-text"
|
|
144
|
-
href={connection.account.url}
|
|
145
|
-
rel="noreferrer"
|
|
146
|
-
target="_blank"
|
|
147
|
-
>
|
|
148
|
-
{connection.account.label}
|
|
149
|
-
</a>
|
|
150
|
-
) : (
|
|
151
|
-
connection.account.label
|
|
152
|
-
)}
|
|
153
|
-
</>
|
|
154
|
-
) : (
|
|
155
|
-
"Connected"
|
|
156
|
-
)
|
|
157
|
-
) : (
|
|
158
|
-
connection.description
|
|
159
|
-
)}
|
|
160
|
-
</p>
|
|
161
|
-
</div>
|
|
162
|
-
{connection.connected ? (
|
|
163
|
-
<Button
|
|
164
|
-
aria-busy={pendingProvider === connection.provider}
|
|
165
|
-
aria-label={`Disconnect ${connection.displayName}`}
|
|
166
|
-
disabled={busy}
|
|
167
|
-
onClick={() => {
|
|
168
|
-
connectMutation.reset();
|
|
169
|
-
disconnectMutation.mutate(connection);
|
|
170
|
-
}}
|
|
171
|
-
variant="danger"
|
|
172
|
-
>
|
|
173
|
-
{pendingProvider === connection.provider
|
|
174
|
-
? "Disconnecting…"
|
|
175
|
-
: "Disconnect"}
|
|
176
|
-
</Button>
|
|
177
|
-
) : (
|
|
178
|
-
<Button
|
|
179
|
-
aria-busy={pendingProvider === connection.provider}
|
|
180
|
-
aria-label={`Connect ${connection.displayName}`}
|
|
181
|
-
disabled={busy}
|
|
182
|
-
onClick={() => {
|
|
183
|
-
disconnectMutation.reset();
|
|
184
|
-
// Open the tab during the click so popup blockers
|
|
185
|
-
// allow it, then point it at the provider.
|
|
186
|
-
const popup = window.open("about:blank", "_blank");
|
|
187
|
-
connectMutation.mutate({
|
|
188
|
-
provider: connection.provider,
|
|
189
|
-
popup,
|
|
190
|
-
});
|
|
191
|
-
}}
|
|
192
|
-
>
|
|
193
|
-
{pendingProvider === connection.provider
|
|
194
|
-
? "Connecting…"
|
|
195
|
-
: "Connect"}
|
|
196
|
-
</Button>
|
|
197
|
-
)}
|
|
198
|
-
</div>
|
|
199
|
-
))
|
|
200
|
-
)}
|
|
201
|
-
</div>
|
|
202
|
-
)}
|
|
203
|
-
</Card>
|
|
204
|
-
</section>
|
|
205
|
-
</div>
|
|
206
|
-
);
|
|
207
|
-
}
|
|
@@ -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,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 { TranscriptMarkdown } from "../conversations/TranscriptMarkdown";
|
|
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
|
-
<TranscriptMarkdown 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
|
-
}
|