@sentry/junior-dashboard 0.266.0 → 0.268.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 +85 -12
- package/dist/client.js +94 -64
- package/dist/index.js +85 -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 +55 -53
- package/src/client/components/DetailsPanel.tsx +12 -13
- package/src/client/components/Drawer.tsx +46 -23
- package/src/client/components/ImageAttachment.tsx +1 -1
- package/src/client/components/ParticipantAvatarStack.tsx +6 -2
- package/src/client/components/README.md +15 -2
- package/src/client/components/UserInputForm.tsx +283 -0
- package/src/client/components/layout/DashboardHeader.tsx +13 -0
- package/src/client/components/ui/alert.tsx +1 -1
- package/src/client/components/ui/button-group.tsx +59 -0
- package/src/client/components/ui/button.tsx +11 -1
- package/src/client/components/ui/dialog.tsx +29 -35
- package/src/client/components/ui/drawer.tsx +140 -0
- package/src/client/components/ui/empty.tsx +30 -1
- package/src/client/components/ui/separator.tsx +22 -0
- package/src/client/components/ui/useSheet.ts +5 -1
- package/src/client/conversations/ComposerImages.tsx +66 -14
- 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/ConversationHomeList.tsx +6 -0
- package/src/client/conversations/ConversationListStatusIcon.tsx +15 -1
- package/src/client/conversations/ConversationPage.tsx +160 -2
- package/src/client/conversations/ConversationSidebar.tsx +6 -0
- package/src/client/conversations/ConversationTranscript.tsx +48 -16
- package/src/client/conversations/ConversationWorkspace.tsx +4 -0
- package/src/client/conversations/DetailsList.tsx +33 -0
- package/src/client/conversations/EventDetails.tsx +46 -19
- package/src/client/conversations/ForkConversationButton.tsx +79 -73
- package/src/client/conversations/PendingMailboxStack.tsx +310 -112
- package/src/client/conversations/README.md +30 -13
- package/src/client/conversations/SubagentTranscriptDrawer.tsx +19 -22
- package/src/client/conversations/TranscriptActivityGroup.tsx +27 -19
- package/src/client/conversations/TranscriptAttachment.tsx +39 -4
- package/src/client/conversations/TranscriptAutomationTriggerView.tsx +14 -19
- package/src/client/conversations/TranscriptFailureView.tsx +10 -13
- package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
- package/src/client/conversations/TranscriptMessageContextView.tsx +12 -1
- package/src/client/conversations/TranscriptMessageView.tsx +10 -1
- package/src/client/conversations/TranscriptSummary.tsx +5 -31
- package/src/client/conversations/TranscriptToolView.tsx +13 -1
- package/src/client/conversations/TranscriptTurnContextView.tsx +174 -161
- package/src/client/conversations/TranscriptView.tsx +7 -3
- package/src/client/conversations/UserInputRequestCard.tsx +39 -0
- package/src/client/conversations/conversationOutbox.ts +46 -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/turnContext.ts +15 -0
- package/src/client/conversations/userInputRequest.ts +56 -0
- package/src/client/format.ts +31 -8
- package/src/client/pages/SettingsPage.tsx +103 -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/pages/code/RepositorySections.tsx +1 -0
- package/src/client/pages/code/RepositoryTabs.tsx +3 -0
- package/src/client/primaryNav.ts +15 -2
- package/src/client/review-desk/ConversationReviewBar.tsx +62 -0
- package/src/client/review-desk/ReviewDeskPage.tsx +277 -0
- package/src/client/review-desk/ReviewItemActions.tsx +165 -0
- package/src/client/review-desk/ReviewItemCard.tsx +47 -0
- package/src/client/review-desk/ReviewItemDetail.tsx +115 -0
- package/src/client/review-desk/queries.ts +164 -0
- package/src/client/review-desk/reviewItemFormat.ts +61 -0
- package/src/client/types.ts +3 -0
- package/src/mock-reporting/fixtures.ts +54 -2
- package/src/mock-reporting/routes.ts +20 -0
- package/src/shell.ts +3 -0
- 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
|
@@ -9,40 +9,22 @@ import { PageHeader } from "../../components/layout/PageHeader";
|
|
|
9
9
|
import { PageLayout } from "../../components/layout/PageLayout";
|
|
10
10
|
import { SecondaryNavigation } from "../../components/layout/SecondaryNavigation";
|
|
11
11
|
|
|
12
|
-
/** Identify
|
|
13
|
-
function isAutomationListPath(pathname: string): boolean {
|
|
14
|
-
const match = matchPath("/automations/:automationId", pathname);
|
|
15
|
-
return Boolean(match && match.params.automationId?.toLowerCase() !== "runs");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Identify Automation routes whose list scrolls on its own beside details. */
|
|
12
|
+
/** Identify Automation routes whose run list scrolls on its own beside run details. */
|
|
19
13
|
export function hasAutomationDetailsFrame(pathname: string): boolean {
|
|
20
14
|
return (
|
|
21
|
-
isAutomationListPath(pathname) ||
|
|
22
15
|
Boolean(matchPath("/automations/runs", pathname)) ||
|
|
23
|
-
Boolean(matchPath("/automations/:automationId/
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function isAutomationListTabPath(pathname: string): boolean {
|
|
28
|
-
return (
|
|
29
|
-
isAutomationListPath(pathname) ||
|
|
30
|
-
Boolean(matchPath("/automations/:automationId/edit", pathname))
|
|
16
|
+
Boolean(matchPath("/automations/:automationId/runs", pathname))
|
|
31
17
|
);
|
|
32
18
|
}
|
|
33
19
|
|
|
34
20
|
const taskNavigationItems = [
|
|
35
21
|
{ end: true, label: "Overview", to: "/automations" },
|
|
36
|
-
{
|
|
37
|
-
isActive: isAutomationListTabPath,
|
|
38
|
-
label: "Automations",
|
|
39
|
-
to: "/automations/list",
|
|
40
|
-
},
|
|
22
|
+
{ label: "Automations", to: "/automations/list" },
|
|
41
23
|
{ label: "Runs", to: "/automations/runs" },
|
|
42
24
|
];
|
|
43
25
|
|
|
44
|
-
/** Keep Automation navigation consistent across
|
|
45
|
-
|
|
26
|
+
/** Keep Automation navigation consistent across the overview, list, and runs. */
|
|
27
|
+
function AutomationsNavigation(props: {
|
|
46
28
|
children: ReactNode;
|
|
47
29
|
className?: string;
|
|
48
30
|
}) {
|
|
@@ -4,7 +4,7 @@ Core owns validation, edit access, creator-only rules, revision checks,
|
|
|
4
4
|
Schedule previews, and the Event catalog. Preview requests cannot save or run
|
|
5
5
|
work. Completed scheduled Automations remain read-only.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Creators and public Destination readers can edit. For a public reader, the
|
|
8
8
|
editor shows outcomes as read-only and blocks turning on creator credential
|
|
9
9
|
use. It warns that an instruction or event selector change switches to system
|
|
10
10
|
credentials. Pause and resume stay creator-only.
|
|
@@ -18,7 +18,8 @@ can keep their changed fields or load the latest saved version. Unchanged fields
|
|
|
18
18
|
come from that version; core checks its revision again on save.
|
|
19
19
|
|
|
20
20
|
The data router blocks navigation with unsaved changes. Browser unload uses the
|
|
21
|
-
native prompt. Save and Cancel
|
|
21
|
+
native prompt. Save and Cancel return to the Automation page and keep its query
|
|
22
|
+
string.
|
|
22
23
|
|
|
23
24
|
Version history lists saved versions, newest first, and marks the active
|
|
24
25
|
version. "Make active" uses the native confirm dialog, like delete. It then
|
|
@@ -29,36 +30,36 @@ shows its error when a public reader cannot make a version active.
|
|
|
29
30
|
Browser behavior lives in `e2e/automation-editor.spec.ts`. API and persistence
|
|
30
31
|
coverage lives in core's `tests/integration/api/automation-edits.test.ts`.
|
|
31
32
|
|
|
32
|
-
## Automation
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
`
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
33
|
+
## Automation page
|
|
34
|
+
|
|
35
|
+
Each Automation has one page at `/automations/:id`. Overview, Runs, and
|
|
36
|
+
Versions are tabs. They use `SecondaryNavigation`, like the repository page.
|
|
37
|
+
`/automations/:id/edit` is edit mode of the Overview tab. The old
|
|
38
|
+
`/executions` route redirects to `/runs` and keeps the query string. List rows
|
|
39
|
+
link to the page. There is no details slideout.
|
|
40
|
+
|
|
41
|
+
The page keeps the list query string. "All automations" returns to the same
|
|
42
|
+
list page and filters. The back link drops params that belong to one tab:
|
|
43
|
+
`result`, `run`, and `version`. One `range` param drives the Overview numbers
|
|
44
|
+
and the Runs chart.
|
|
45
|
+
|
|
46
|
+
The header shows the name, the state, the trigger, the Destination, and the
|
|
47
|
+
creator. `AutomationStateChip` is a live region, so Pause and Resume results
|
|
48
|
+
are announced. `AutomationActions` owns Edit, Pause or Resume, pause help, and
|
|
49
|
+
Delete. Lifecycle actions re-read the revision before they apply. Public
|
|
50
|
+
readers can edit settings. Pause, Resume, and Delete stay creator-only, so
|
|
51
|
+
other viewers do not see them. The About card names the creator-only actions.
|
|
52
|
+
Completed Automations have no Edit action, and `/edit` returns to Overview.
|
|
53
|
+
|
|
54
|
+
Overview shows `AutomationAttention` first when something needs action: a
|
|
55
|
+
block, an unavailable trigger, or a failed last run. The notice has one main
|
|
56
|
+
action, such as Change trigger or Open last run. While it shows, the header
|
|
57
|
+
Edit button becomes a secondary button. The saved settings follow
|
|
58
|
+
in the same `AutomationFormSection` rows as the editor, so edit mode changes
|
|
59
|
+
them in place. The side column shows the next trigger, the last result, runs,
|
|
60
|
+
failures, and spend for the range, and About. Spend is `costUsd` from the
|
|
61
|
+
Automation summary. On small screens, status comes first and About moves below
|
|
62
|
+
the settings.
|
|
62
63
|
|
|
63
64
|
`AutomationRecentRuns` uses the existing execution-history query and cache. It
|
|
64
65
|
shows up to three newest terminal runs, links to their Conversations when one
|
|
@@ -67,21 +68,30 @@ titles that repeat the Automation name. Its loading,
|
|
|
67
68
|
empty, and retry states do not replace Automation settings. It does not infer
|
|
68
69
|
queued or running work from terminal records.
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
scroll region above it owns the summary and settings. Creator, creation date,
|
|
75
|
-
connected accounts, run counts, and version history stay under About this
|
|
76
|
-
Automation. Selection changes remount the content and actions together.
|
|
71
|
+
In edit mode, "Your changes" lists each changed field with the saved value, the
|
|
72
|
+
new value, and its own Undo. It is beside the form on wide screens and below
|
|
73
|
+
the form on small screens when there are changes. Undo remounts the fields, so fields with local state show the saved value again.
|
|
74
|
+
Changed sections get an accent mark. Save and Cancel return to Overview.
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
The Runs tab filters loaded runs by result through the `result` param. Rows
|
|
77
|
+
show the version that ran and the recorded error. A row opens the run details
|
|
78
|
+
panel. The run list scrolls on its own beside it.
|
|
79
|
+
|
|
80
|
+
The Versions tab lists versions and compares the selected one with the version
|
|
81
|
+
before it. The `version` param selects a version; run details link to it.
|
|
82
|
+
Changed fields show the previous value. Instruction changes use a line diff.
|
|
82
83
|
|
|
83
84
|
`AutomationDeleteDialog` names the Automation before deletion. It keeps errors
|
|
84
85
|
and retry inside the dialog and blocks dismissal while the request is pending.
|
|
85
|
-
Cancel returns focus to Delete. Successful deletion
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
Cancel returns focus to Delete. Successful deletion returns to the list.
|
|
87
|
+
Success toasts confirm Save, Pause, Resume, and Delete. Their state belongs to
|
|
88
|
+
the mounted dashboard's Toaster provider.
|
|
89
|
+
|
|
90
|
+
Browser journeys for the page live in `e2e/automations.spec.ts`. Use mock
|
|
91
|
+
reporting to review scheduled and event Automations in both themes at desktop
|
|
92
|
+
and mobile widths. Keep layout checks in manual QA.
|
|
93
|
+
|
|
94
|
+
Filters use a shadcn Popover at 768px and above and the shared BottomSheet below
|
|
95
|
+
that width. Both edit the same URL filters immediately. Active filter chips
|
|
96
|
+
remove one filter without clearing search, scope, or other filters. The mobile
|
|
97
|
+
sheet fits its controls instead of taking the full screen height.
|
|
@@ -69,6 +69,7 @@ export function RecentConversationRow(props: { conversation: Conversation }) {
|
|
|
69
69
|
finishedSinceSeen={false}
|
|
70
70
|
isPrivate={conversation.visibility === "private"}
|
|
71
71
|
status={visualStatusForConversation(conversation)}
|
|
72
|
+
waitingOnViewer={false}
|
|
72
73
|
/>
|
|
73
74
|
</span>
|
|
74
75
|
<div className="min-w-0 truncate font-display text-sm text-dashboard-text">
|
|
@@ -18,6 +18,7 @@ import { Card } from "../../components/layout/Card";
|
|
|
18
18
|
import { ConversationHomeList } from "../../conversations/ConversationHomeList";
|
|
19
19
|
import { buildConversations, getDashboardTimeZone } from "../../format";
|
|
20
20
|
import { useDebouncedValue } from "../../debouncedValue";
|
|
21
|
+
import { useWaitingConversationIds } from "../../review-desk/queries";
|
|
21
22
|
import { CodeActivityChart } from "./CodeActivityChart";
|
|
22
23
|
import { CodeChangeRow } from "./CodeChangeRow";
|
|
23
24
|
import { repositoryActivity } from "./RepositorySections";
|
|
@@ -32,6 +33,7 @@ export function RepositoryConversationsTab(props: { repositoryId: string }) {
|
|
|
32
33
|
const search = useDebouncedValue(query.trim());
|
|
33
34
|
const feed = useCodeRepositoryConversationsData(props.repositoryId, search);
|
|
34
35
|
const conversations = buildConversations(feed.data?.conversations ?? []);
|
|
36
|
+
const waitingConversationIds = useWaitingConversationIds();
|
|
35
37
|
useEffect(() => {
|
|
36
38
|
setPage(1);
|
|
37
39
|
}, [search]);
|
|
@@ -59,6 +61,7 @@ export function RepositoryConversationsTab(props: { repositoryId: string }) {
|
|
|
59
61
|
finishedConversationIds={noFinishedConversations}
|
|
60
62
|
loading={!feed.data}
|
|
61
63
|
timeZone={getDashboardTimeZone()}
|
|
64
|
+
waitingConversationIds={waitingConversationIds}
|
|
62
65
|
/>
|
|
63
66
|
)}
|
|
64
67
|
<PagePagination
|
package/src/client/primaryNav.ts
CHANGED
|
@@ -9,6 +9,7 @@ const AUTH_PRIMARY_NAV_PREFIXES = [
|
|
|
9
9
|
"/memories",
|
|
10
10
|
"/settings",
|
|
11
11
|
"/plugins/",
|
|
12
|
+
"/review",
|
|
12
13
|
] as const;
|
|
13
14
|
|
|
14
15
|
/** True when the path is only available to a signed-in viewer. */
|
|
@@ -24,6 +25,8 @@ export function buildPrimaryNavItems(input: {
|
|
|
24
25
|
loggedIn: boolean;
|
|
25
26
|
pathname: string;
|
|
26
27
|
primaryUserPages: PluginUserPageLink[];
|
|
28
|
+
/** Open Review items assigned to the viewer. */
|
|
29
|
+
reviewCount?: number;
|
|
27
30
|
userPagesPending: boolean;
|
|
28
31
|
}): DashboardHeaderNavItem[] {
|
|
29
32
|
const memoryNavPage = input.primaryUserPages.find(
|
|
@@ -32,8 +35,8 @@ export function buildPrimaryNavItems(input: {
|
|
|
32
35
|
const otherPrimaryUserPages = input.primaryUserPages.filter(
|
|
33
36
|
(page) => !(page.pluginName === "memory" && page.id === "memories"),
|
|
34
37
|
);
|
|
35
|
-
// Auth-only routes already imply the signed-in primary nav. Hold
|
|
36
|
-
// Memories slots while core/user-pages catch up so the header does not jump.
|
|
38
|
+
// Auth-only routes already imply the signed-in primary nav. Hold Review
|
|
39
|
+
// desk, Tasks, and Memories slots while core/user-pages catch up so the header does not jump.
|
|
37
40
|
const reserveAuthPrimaryNav =
|
|
38
41
|
input.loading && isAuthPrimaryNavPath(input.pathname);
|
|
39
42
|
const showTasksNav = input.loggedIn || reserveAuthPrimaryNav;
|
|
@@ -43,6 +46,16 @@ export function buildPrimaryNavItems(input: {
|
|
|
43
46
|
(input.loggedIn && input.userPagesPending);
|
|
44
47
|
|
|
45
48
|
return [
|
|
49
|
+
...(showTasksNav
|
|
50
|
+
? [
|
|
51
|
+
{
|
|
52
|
+
count: input.reviewCount,
|
|
53
|
+
key: "review",
|
|
54
|
+
label: "Review desk",
|
|
55
|
+
to: "/review",
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
: []),
|
|
46
59
|
{ key: "code", label: "Code", to: "/code" },
|
|
47
60
|
...(showTasksNav
|
|
48
61
|
? [{ key: "automations", label: "Automations", to: "/automations" }]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ChevronRight } from "lucide-react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { Drawer } from "../components/Drawer";
|
|
5
|
+
import { ReviewItemCard } from "./ReviewItemCard";
|
|
6
|
+
import { useReviewItemsData } from "./queries";
|
|
7
|
+
|
|
8
|
+
function itemCountLabel(count: number): string {
|
|
9
|
+
return count === 1 ? "1 open review item" : `${count} open review items`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Show how many open Review items are linked to a Conversation, above its
|
|
14
|
+
* message box. The bar opens a side panel where the viewer can act on each.
|
|
15
|
+
*/
|
|
16
|
+
export function ConversationReviewBar(props: { conversationId: string }) {
|
|
17
|
+
const [open, setOpen] = useState(false);
|
|
18
|
+
const items =
|
|
19
|
+
useReviewItemsData({
|
|
20
|
+
archived: false,
|
|
21
|
+
conversationId: props.conversationId,
|
|
22
|
+
view: "all",
|
|
23
|
+
}).data?.items ?? [];
|
|
24
|
+
if (items.length === 0) return null;
|
|
25
|
+
const label = itemCountLabel(items.length);
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
<button
|
|
29
|
+
className="mb-2 flex w-full cursor-pointer items-center justify-between gap-3 rounded-lg border border-violet-300/25 bg-violet-300/[0.06] px-3 py-2 text-left font-sans text-sm text-dashboard-text transition-colors hover:bg-violet-300/[0.1]"
|
|
30
|
+
onClick={() => setOpen(true)}
|
|
31
|
+
type="button"
|
|
32
|
+
>
|
|
33
|
+
<span className="flex items-center gap-2">
|
|
34
|
+
<span
|
|
35
|
+
aria-hidden="true"
|
|
36
|
+
className="size-2 shrink-0 rounded-full bg-violet-300"
|
|
37
|
+
/>
|
|
38
|
+
{label}
|
|
39
|
+
</span>
|
|
40
|
+
<ChevronRight aria-hidden="true" size={16} />
|
|
41
|
+
</button>
|
|
42
|
+
{open ? (
|
|
43
|
+
<Drawer
|
|
44
|
+
closeLabel="Close review items"
|
|
45
|
+
header={
|
|
46
|
+
<h2 className="m-0 font-sans text-base font-semibold">{label}</h2>
|
|
47
|
+
}
|
|
48
|
+
label="Review items"
|
|
49
|
+
onClose={() => setOpen(false)}
|
|
50
|
+
openKey={props.conversationId}
|
|
51
|
+
width="narrow"
|
|
52
|
+
>
|
|
53
|
+
<div className="grid gap-3">
|
|
54
|
+
{items.map((item) => (
|
|
55
|
+
<ReviewItemCard item={item} key={item.id} />
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
</Drawer>
|
|
59
|
+
) : null}
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Link, useSearchParams } from "react-router";
|
|
3
|
+
import type {
|
|
4
|
+
AutomationSummary,
|
|
5
|
+
ReviewItemPayload,
|
|
6
|
+
} from "@sentry/junior/api/schema";
|
|
7
|
+
|
|
8
|
+
import { FilterTabList } from "../components/FilterBar";
|
|
9
|
+
import { InlineError } from "../components/InlineError";
|
|
10
|
+
import { SearchInput } from "../components/SearchInput";
|
|
11
|
+
import { StatusChip } from "../components/StatusChip";
|
|
12
|
+
import { Card } from "../components/layout/Card";
|
|
13
|
+
import { PageHeader } from "../components/layout/PageHeader";
|
|
14
|
+
import { PageLayout } from "../components/layout/PageLayout";
|
|
15
|
+
import { Skeleton } from "../components/ui/skeleton";
|
|
16
|
+
import { automationPath, formatRelativeTime } from "../format";
|
|
17
|
+
import {
|
|
18
|
+
parseSearchParamEnum,
|
|
19
|
+
pathWithSearch,
|
|
20
|
+
useDebouncedSearchParam,
|
|
21
|
+
} from "../searchParams";
|
|
22
|
+
import { cn } from "../styles";
|
|
23
|
+
import { ReviewItemDetail } from "./ReviewItemDetail";
|
|
24
|
+
import {
|
|
25
|
+
REVIEW_DESK_VIEWS,
|
|
26
|
+
useBlockedAutomationsData,
|
|
27
|
+
useReviewItemData,
|
|
28
|
+
useReviewItemsData,
|
|
29
|
+
} from "./queries";
|
|
30
|
+
import { assigneeLabel, reviewItemPath } from "./reviewItemFormat";
|
|
31
|
+
|
|
32
|
+
const VIEW_TABS = [
|
|
33
|
+
{ label: "Mine", value: "mine" },
|
|
34
|
+
{ label: "Everyone", value: "everyone" },
|
|
35
|
+
{ label: "All", value: "all" },
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
const STATE_TABS = [
|
|
39
|
+
{ label: "Open", value: "open" },
|
|
40
|
+
{ label: "Archived", value: "archived" },
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
43
|
+
const ITEM_STATES = ["open", "archived"] as const;
|
|
44
|
+
|
|
45
|
+
const rowClass =
|
|
46
|
+
"grid gap-0.5 rounded-md px-3 py-2 text-inherit no-underline transition-colors";
|
|
47
|
+
|
|
48
|
+
// The list and the detail are already cards, so an empty state is plain
|
|
49
|
+
// text, not a second box.
|
|
50
|
+
function EmptyMessage(props: { children: ReactNode }) {
|
|
51
|
+
return (
|
|
52
|
+
<p className="m-0 px-4 py-8 text-center text-sm text-dashboard-text-muted">
|
|
53
|
+
{props.children}
|
|
54
|
+
</p>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ListGroup(props: { children: ReactNode; title: string }) {
|
|
59
|
+
return (
|
|
60
|
+
<section className="grid gap-1">
|
|
61
|
+
<h2 className="m-0 px-3 font-sans text-xs font-medium text-dashboard-text-muted">
|
|
62
|
+
{props.title}
|
|
63
|
+
</h2>
|
|
64
|
+
<ul className="m-0 grid list-none gap-0.5 p-0">{props.children}</ul>
|
|
65
|
+
</section>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ItemRow(props: {
|
|
70
|
+
item: ReviewItemPayload;
|
|
71
|
+
searchParams: URLSearchParams;
|
|
72
|
+
selected: boolean;
|
|
73
|
+
}) {
|
|
74
|
+
const { item } = props;
|
|
75
|
+
const unassigned = !item.assignedToEveryone && item.assignees.length === 0;
|
|
76
|
+
return (
|
|
77
|
+
<li>
|
|
78
|
+
<Link
|
|
79
|
+
aria-current={props.selected ? "true" : undefined}
|
|
80
|
+
className={cn(
|
|
81
|
+
rowClass,
|
|
82
|
+
props.selected
|
|
83
|
+
? "bg-dashboard-accent/[0.08]"
|
|
84
|
+
: "hover:bg-dashboard-fill-soft",
|
|
85
|
+
)}
|
|
86
|
+
replace
|
|
87
|
+
to={pathWithSearch(reviewItemPath(item.id), props.searchParams)}
|
|
88
|
+
>
|
|
89
|
+
<span className="truncate text-sm font-medium">{item.title}</span>
|
|
90
|
+
<span className="flex min-w-0 items-center gap-2 text-xs text-dashboard-text-muted">
|
|
91
|
+
{unassigned ? (
|
|
92
|
+
<StatusChip size="compact" tone="warning">
|
|
93
|
+
Unassigned
|
|
94
|
+
</StatusChip>
|
|
95
|
+
) : (
|
|
96
|
+
<span className="truncate">{assigneeLabel(item)}</span>
|
|
97
|
+
)}
|
|
98
|
+
<span className="shrink-0">
|
|
99
|
+
{formatRelativeTime(item.closedAt ?? item.updatedAt)}
|
|
100
|
+
</span>
|
|
101
|
+
</span>
|
|
102
|
+
</Link>
|
|
103
|
+
</li>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function BlockedAutomationRow(props: { automation: AutomationSummary }) {
|
|
108
|
+
return (
|
|
109
|
+
<li>
|
|
110
|
+
<Link
|
|
111
|
+
className={cn(rowClass, "hover:bg-dashboard-fill-soft")}
|
|
112
|
+
to={automationPath(props.automation.id)}
|
|
113
|
+
>
|
|
114
|
+
<span className="truncate text-sm font-medium">
|
|
115
|
+
{props.automation.title}
|
|
116
|
+
</span>
|
|
117
|
+
<span className="truncate text-xs text-amber-300">
|
|
118
|
+
{props.automation.statusReason ?? "Blocked"}
|
|
119
|
+
</span>
|
|
120
|
+
</Link>
|
|
121
|
+
</li>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function ReviewListLoading() {
|
|
126
|
+
return (
|
|
127
|
+
<div
|
|
128
|
+
aria-busy="true"
|
|
129
|
+
aria-label="Loading review items"
|
|
130
|
+
className="grid gap-2"
|
|
131
|
+
>
|
|
132
|
+
{[0, 1, 2, 3].map((row) => (
|
|
133
|
+
<div className="grid gap-1.5 px-3 py-2" key={row}>
|
|
134
|
+
<Skeleton className="h-4 w-3/4" />
|
|
135
|
+
<Skeleton className="h-3 w-1/3" />
|
|
136
|
+
</div>
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Render the review desk: open or archived Review items in the Mine,
|
|
144
|
+
* Everyone, and All views, with the selected item beside the list.
|
|
145
|
+
*/
|
|
146
|
+
export function ReviewDeskPage() {
|
|
147
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
148
|
+
const view =
|
|
149
|
+
parseSearchParamEnum(searchParams.get("view"), REVIEW_DESK_VIEWS) ?? "mine";
|
|
150
|
+
const state =
|
|
151
|
+
parseSearchParamEnum(searchParams.get("state"), ITEM_STATES) ?? "open";
|
|
152
|
+
// The selected item can be missing from the new list, so a tab change
|
|
153
|
+
// drops it and the first item of the new list shows.
|
|
154
|
+
function selectTab(key: "state" | "view", value: string, initial: string) {
|
|
155
|
+
setSearchParams(
|
|
156
|
+
(current) => {
|
|
157
|
+
const next = new URLSearchParams(current);
|
|
158
|
+
next.delete("item");
|
|
159
|
+
if (value === initial) next.delete(key);
|
|
160
|
+
else next.set(key, value);
|
|
161
|
+
return next;
|
|
162
|
+
},
|
|
163
|
+
{ replace: true },
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const [search, setSearch, q] = useDebouncedSearchParam();
|
|
167
|
+
const archived = state === "archived";
|
|
168
|
+
const items = useReviewItemsData({ archived, q, view });
|
|
169
|
+
const showBlocked = !archived && view !== "everyone" && !q;
|
|
170
|
+
const blocked = useBlockedAutomationsData(showBlocked);
|
|
171
|
+
const list = items.data?.items ?? [];
|
|
172
|
+
const selectedId = searchParams.get("item") ?? list[0]?.id;
|
|
173
|
+
const listed = list.find((item) => item.id === selectedId);
|
|
174
|
+
// A link can select an item that is not in the current view.
|
|
175
|
+
const direct = useReviewItemData(listed ? undefined : selectedId);
|
|
176
|
+
const selected = listed ?? direct.data;
|
|
177
|
+
const actions = list.filter((item) => item.kind === "action");
|
|
178
|
+
const notices = list.filter((item) => item.kind === "notice");
|
|
179
|
+
const blockedAutomations = showBlocked
|
|
180
|
+
? (blocked.data?.automations ?? [])
|
|
181
|
+
: [];
|
|
182
|
+
const empty =
|
|
183
|
+
items.data && list.length === 0 && blockedAutomations.length === 0;
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<PageLayout>
|
|
187
|
+
<PageHeader
|
|
188
|
+
description="Items that need a person, and who can act on them."
|
|
189
|
+
title="Review desk"
|
|
190
|
+
/>
|
|
191
|
+
<div className="flex min-w-0 flex-wrap items-center gap-3">
|
|
192
|
+
<FilterTabList
|
|
193
|
+
ariaLabel="Assignee view"
|
|
194
|
+
items={VIEW_TABS}
|
|
195
|
+
onChange={(value) => selectTab("view", value, "mine")}
|
|
196
|
+
value={view}
|
|
197
|
+
/>
|
|
198
|
+
<FilterTabList
|
|
199
|
+
ariaLabel="Item state"
|
|
200
|
+
items={STATE_TABS}
|
|
201
|
+
onChange={(value) => selectTab("state", value, "open")}
|
|
202
|
+
value={state}
|
|
203
|
+
/>
|
|
204
|
+
<SearchInput
|
|
205
|
+
className="ml-auto w-full sm:w-64"
|
|
206
|
+
label="Search review items"
|
|
207
|
+
onChange={setSearch}
|
|
208
|
+
placeholder="Search items"
|
|
209
|
+
value={search}
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
<div className="grid min-w-0 gap-4 lg:grid-cols-[minmax(0,22rem)_minmax(0,1fr)]">
|
|
213
|
+
<Card className="grid content-start gap-4 py-3" padding="none">
|
|
214
|
+
{items.error ? (
|
|
215
|
+
<InlineError className="px-3">
|
|
216
|
+
Could not load review items.
|
|
217
|
+
</InlineError>
|
|
218
|
+
) : !items.data ? (
|
|
219
|
+
<ReviewListLoading />
|
|
220
|
+
) : empty ? (
|
|
221
|
+
<EmptyMessage>
|
|
222
|
+
{archived ? "No archived items." : "Nothing needs you right now."}
|
|
223
|
+
</EmptyMessage>
|
|
224
|
+
) : (
|
|
225
|
+
<>
|
|
226
|
+
{actions.length > 0 ? (
|
|
227
|
+
<ListGroup title={archived ? "Questions" : "Needs an answer"}>
|
|
228
|
+
{actions.map((item) => (
|
|
229
|
+
<ItemRow
|
|
230
|
+
item={item}
|
|
231
|
+
key={item.id}
|
|
232
|
+
searchParams={searchParams}
|
|
233
|
+
selected={item.id === selected?.id}
|
|
234
|
+
/>
|
|
235
|
+
))}
|
|
236
|
+
</ListGroup>
|
|
237
|
+
) : null}
|
|
238
|
+
{notices.length > 0 ? (
|
|
239
|
+
<ListGroup title="Notices">
|
|
240
|
+
{notices.map((item) => (
|
|
241
|
+
<ItemRow
|
|
242
|
+
item={item}
|
|
243
|
+
key={item.id}
|
|
244
|
+
searchParams={searchParams}
|
|
245
|
+
selected={item.id === selected?.id}
|
|
246
|
+
/>
|
|
247
|
+
))}
|
|
248
|
+
</ListGroup>
|
|
249
|
+
) : null}
|
|
250
|
+
{blockedAutomations.length > 0 ? (
|
|
251
|
+
<ListGroup title="Blocked Automations">
|
|
252
|
+
{blockedAutomations.map((automation) => (
|
|
253
|
+
<BlockedAutomationRow
|
|
254
|
+
automation={automation}
|
|
255
|
+
key={automation.id}
|
|
256
|
+
/>
|
|
257
|
+
))}
|
|
258
|
+
</ListGroup>
|
|
259
|
+
) : null}
|
|
260
|
+
</>
|
|
261
|
+
)}
|
|
262
|
+
</Card>
|
|
263
|
+
<Card className="min-w-0" padding="md">
|
|
264
|
+
{selected ? (
|
|
265
|
+
// A key per item resets the answer form and its send lock, so
|
|
266
|
+
// one item's answer can never reach another item.
|
|
267
|
+
<ReviewItemDetail item={selected} key={selected.id} />
|
|
268
|
+
) : direct.error ? (
|
|
269
|
+
<InlineError>Review item not found.</InlineError>
|
|
270
|
+
) : (
|
|
271
|
+
<EmptyMessage>Select an item to see it here.</EmptyMessage>
|
|
272
|
+
)}
|
|
273
|
+
</Card>
|
|
274
|
+
</div>
|
|
275
|
+
</PageLayout>
|
|
276
|
+
);
|
|
277
|
+
}
|