@sentry/junior-dashboard 0.233.0 → 0.235.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 +178 -29
- package/dist/client.js +53 -50
- package/dist/index.js +178 -29
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- package/src/assets/junior-avatar.png +0 -0
- package/src/assets/junior-favicon.png +0 -0
- package/src/assets.ts +1 -0
- package/src/client/App.tsx +372 -345
- package/src/client/api.ts +60 -1
- package/src/client/components/ActorAvatar.tsx +37 -0
- package/src/client/components/Button.tsx +12 -10
- package/src/client/components/Field.tsx +14 -1
- package/src/client/components/FormNotice.tsx +40 -0
- package/src/client/components/ParticipantAvatarStack.tsx +6 -14
- package/src/client/components/README.md +19 -6
- package/src/client/components/SegmentedNav.tsx +167 -0
- package/src/client/components/SegmentedTabs.tsx +24 -5
- package/src/client/components/Select.tsx +18 -0
- package/src/client/components/StatusChip.tsx +15 -12
- package/src/client/components/TextInput.tsx +25 -5
- package/src/client/components/layout/SecondaryNavigation.tsx +40 -83
- package/src/client/http.ts +31 -3
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +78 -68
- package/src/client/pages/automations/AutomationEditPage.tsx +171 -0
- package/src/client/pages/automations/AutomationEditor.tsx +397 -0
- package/src/client/pages/automations/AutomationEditorLoading.tsx +18 -0
- package/src/client/pages/automations/AutomationEventFields.tsx +264 -0
- package/src/client/pages/automations/AutomationFilters.tsx +179 -0
- package/src/client/pages/automations/AutomationFormSection.tsx +22 -0
- package/src/client/pages/automations/AutomationLifecycle.tsx +146 -0
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +177 -0
- package/src/client/pages/automations/AutomationRow.tsx +343 -0
- package/src/client/pages/automations/AutomationScheduleFields.tsx +328 -0
- package/src/client/pages/automations/AutomationsPage.tsx +186 -439
- package/src/client/pages/automations/AutomationsPageLayout.tsx +19 -7
- package/src/client/pages/automations/README.md +19 -0
- package/src/client/pages/automations/automationDraft.ts +110 -0
- package/src/client/pages/automations/automationFormat.ts +43 -0
- package/src/client/pages/automations/automationOutcomes.ts +23 -0
- package/src/client/pages/code/CodeChangeRow.tsx +41 -0
- package/src/client/pages/code/CodePage.tsx +18 -93
- package/src/client/pages/code/CodeSummaryCards.tsx +53 -0
- package/src/client/pages/code/RepositoryPage.tsx +227 -0
- package/src/client/pages/code/RepositorySections.tsx +108 -0
- package/src/client/pages/code/RepositoryTabs.tsx +146 -0
- package/src/client/pages/code/codeRepositoryRoutes.ts +18 -0
- package/src/client/pages/dev/ComponentsPage.tsx +55 -26
- package/src/client/pages/dev/SegmentedFixtures.tsx +58 -0
- package/src/client/pages/memory/MemoryPageLayout.tsx +12 -13
- package/src/client/pages/system/SystemNavigation.tsx +6 -2
- package/src/client/pages/system/SystemPageLayout.tsx +4 -6
- package/src/client/searchParams.ts +4 -2
- package/src/client.tsx +7 -4
- package/src/mock-reporting/automation-collection.ts +101 -0
- package/src/mock-reporting/code-repository.ts +77 -0
- package/src/mock-reporting/fixtures.ts +9 -3
- package/src/mock-reporting/routes.ts +39 -17
- package/src/shell.ts +21 -5
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { CodeRepositoryReport } from "@sentry/junior/api/schema";
|
|
3
|
+
import { ArrowLeft, ExternalLink } from "lucide-react";
|
|
4
|
+
import { Link, useParams } from "react-router";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
useCodeRepositoryConversationsData,
|
|
8
|
+
useCodeRepositoryData,
|
|
9
|
+
} from "../../api";
|
|
10
|
+
import { ButtonLink } from "../../components/Button";
|
|
11
|
+
import { EmptyTelemetry } from "../../components/EmptyTelemetry";
|
|
12
|
+
import { PageRouteLoading } from "../../components/PageRouteLoading";
|
|
13
|
+
import {
|
|
14
|
+
timeRangeBucketUnit,
|
|
15
|
+
TimeRangeSelector,
|
|
16
|
+
type TimeRangeDays,
|
|
17
|
+
} from "../../components/controls/TimeRangeSelector";
|
|
18
|
+
import { PageHeader } from "../../components/layout/PageHeader";
|
|
19
|
+
import { PageLayout } from "../../components/layout/PageLayout";
|
|
20
|
+
import { SecondaryNavigation } from "../../components/layout/SecondaryNavigation";
|
|
21
|
+
import { buildConversations } from "../../format";
|
|
22
|
+
import { CodeActivityChart } from "./CodeActivityChart";
|
|
23
|
+
import { CodeChangeRow } from "./CodeChangeRow";
|
|
24
|
+
import { CodeSummaryCards } from "./CodeSummaryCards";
|
|
25
|
+
import {
|
|
26
|
+
codeRepositoryPath,
|
|
27
|
+
type CodeRepositoryTab,
|
|
28
|
+
} from "./codeRepositoryRoutes";
|
|
29
|
+
import {
|
|
30
|
+
RecentConversationRow,
|
|
31
|
+
RepositorySection,
|
|
32
|
+
repositoryActivity,
|
|
33
|
+
} from "./RepositorySections";
|
|
34
|
+
import {
|
|
35
|
+
RepositoryChangesTab,
|
|
36
|
+
RepositoryConversationsTab,
|
|
37
|
+
} from "./RepositoryTabs";
|
|
38
|
+
|
|
39
|
+
const RECENT_ROWS = 5;
|
|
40
|
+
const DESCRIPTION = "Conversations and code changes from Junior.";
|
|
41
|
+
|
|
42
|
+
/** Inline link style used by `ConversationSummary` location links. */
|
|
43
|
+
const inlineLinkClass =
|
|
44
|
+
"font-semibold text-dashboard-text underline decoration-white/20 underline-offset-2 transition-colors hover:decoration-white/60";
|
|
45
|
+
|
|
46
|
+
/** Render one repository tab under the shared header and tab bar. */
|
|
47
|
+
export function RepositoryPage(props: { tab: CodeRepositoryTab }) {
|
|
48
|
+
const params = useParams();
|
|
49
|
+
const repositoryId = params.repositoryId;
|
|
50
|
+
const [range, setRange] = useState<TimeRangeDays>(30);
|
|
51
|
+
const query = useCodeRepositoryData(repositoryId);
|
|
52
|
+
if (!query.data && !query.error) {
|
|
53
|
+
return (
|
|
54
|
+
<PageRouteLoading
|
|
55
|
+
description={DESCRIPTION}
|
|
56
|
+
label="Loading repository"
|
|
57
|
+
title="Repository"
|
|
58
|
+
variant="stats"
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const data = query.data;
|
|
63
|
+
if (!data) {
|
|
64
|
+
return (
|
|
65
|
+
<PageLayout>
|
|
66
|
+
<BackToCode />
|
|
67
|
+
<EmptyTelemetry>
|
|
68
|
+
This repository is unavailable. It may not have any code changes from
|
|
69
|
+
Junior.
|
|
70
|
+
</EmptyTelemetry>
|
|
71
|
+
</PageLayout>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const repository = data.repository;
|
|
75
|
+
return (
|
|
76
|
+
<SecondaryNavigation
|
|
77
|
+
ariaLabel="Repository navigation"
|
|
78
|
+
items={[
|
|
79
|
+
{
|
|
80
|
+
end: true,
|
|
81
|
+
label: "Overview",
|
|
82
|
+
to: codeRepositoryPath(repository.id),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
label: "Conversations",
|
|
86
|
+
to: codeRepositoryPath(repository.id, "conversations"),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
label: "Changes",
|
|
90
|
+
to: codeRepositoryPath(repository.id, "changes"),
|
|
91
|
+
},
|
|
92
|
+
]}
|
|
93
|
+
>
|
|
94
|
+
<PageLayout>
|
|
95
|
+
<BackToCode />
|
|
96
|
+
<PageHeader
|
|
97
|
+
actions={
|
|
98
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
99
|
+
<TimeRangeSelector onChange={setRange} value={range} />
|
|
100
|
+
{repository.url ? (
|
|
101
|
+
<ButtonLink
|
|
102
|
+
rel="noopener noreferrer"
|
|
103
|
+
target="_blank"
|
|
104
|
+
to={repository.url}
|
|
105
|
+
>
|
|
106
|
+
Open repository
|
|
107
|
+
<ExternalLink
|
|
108
|
+
aria-hidden="true"
|
|
109
|
+
className="size-3.5 shrink-0"
|
|
110
|
+
/>
|
|
111
|
+
</ButtonLink>
|
|
112
|
+
) : null}
|
|
113
|
+
</div>
|
|
114
|
+
}
|
|
115
|
+
description={<RepositoryDescription data={data} />}
|
|
116
|
+
title={repository.name}
|
|
117
|
+
/>
|
|
118
|
+
{props.tab === "overview" ? (
|
|
119
|
+
<RepositoryOverview data={data} range={range} />
|
|
120
|
+
) : null}
|
|
121
|
+
{props.tab === "conversations" ? (
|
|
122
|
+
<RepositoryConversationsTab repositoryId={repository.id} />
|
|
123
|
+
) : null}
|
|
124
|
+
{props.tab === "changes" ? (
|
|
125
|
+
<RepositoryChangesTab data={data} range={range} />
|
|
126
|
+
) : null}
|
|
127
|
+
</PageLayout>
|
|
128
|
+
</SecondaryNavigation>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function BackToCode() {
|
|
133
|
+
return (
|
|
134
|
+
<Link
|
|
135
|
+
className="flex w-fit items-center gap-2 font-display text-sm font-medium text-dashboard-text-muted no-underline transition-colors hover:text-dashboard-text"
|
|
136
|
+
to="/code"
|
|
137
|
+
>
|
|
138
|
+
<ArrowLeft aria-hidden="true" size={15} strokeWidth={1.8} />
|
|
139
|
+
Back to code
|
|
140
|
+
</Link>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Page description, then each Workspace that includes the repository. */
|
|
145
|
+
function RepositoryDescription(props: { data: CodeRepositoryReport }) {
|
|
146
|
+
const workspaces = props.data.workspaces;
|
|
147
|
+
if (workspaces.length === 0) return DESCRIPTION;
|
|
148
|
+
return (
|
|
149
|
+
<>
|
|
150
|
+
{DESCRIPTION} / Workspaces{" "}
|
|
151
|
+
{workspaces.map((workspace, index) => (
|
|
152
|
+
<span key={workspace.id}>
|
|
153
|
+
{index > 0 ? ", " : null}
|
|
154
|
+
<Link
|
|
155
|
+
className={inlineLinkClass}
|
|
156
|
+
to={`/system/workspaces/${encodeURIComponent(workspace.id)}`}
|
|
157
|
+
>
|
|
158
|
+
{workspace.name}
|
|
159
|
+
</Link>
|
|
160
|
+
</span>
|
|
161
|
+
))}
|
|
162
|
+
</>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function RepositoryOverview(props: {
|
|
167
|
+
data: CodeRepositoryReport;
|
|
168
|
+
range: TimeRangeDays;
|
|
169
|
+
}) {
|
|
170
|
+
const data = props.data;
|
|
171
|
+
const conversationsQuery = useCodeRepositoryConversationsData(
|
|
172
|
+
data.repository.id,
|
|
173
|
+
);
|
|
174
|
+
const conversations = buildConversations(
|
|
175
|
+
conversationsQuery.data?.conversations ?? [],
|
|
176
|
+
).slice(0, RECENT_ROWS);
|
|
177
|
+
const changes = data.changes.slice(0, RECENT_ROWS);
|
|
178
|
+
return (
|
|
179
|
+
<>
|
|
180
|
+
<CodeSummaryCards summary={data.summary} />
|
|
181
|
+
<div className="grid gap-3 lg:grid-cols-2">
|
|
182
|
+
<RepositorySection
|
|
183
|
+
title="Recent conversations"
|
|
184
|
+
viewAllTo={codeRepositoryPath(data.repository.id, "conversations")}
|
|
185
|
+
>
|
|
186
|
+
{conversations.length > 0 ? (
|
|
187
|
+
conversations.map((conversation) => (
|
|
188
|
+
<RecentConversationRow
|
|
189
|
+
conversation={conversation}
|
|
190
|
+
key={conversation.id}
|
|
191
|
+
/>
|
|
192
|
+
))
|
|
193
|
+
) : (
|
|
194
|
+
<div className="p-4">
|
|
195
|
+
<EmptyTelemetry>
|
|
196
|
+
{conversationsQuery.error
|
|
197
|
+
? "Conversations are unavailable. Try refreshing the dashboard."
|
|
198
|
+
: conversationsQuery.data
|
|
199
|
+
? "No conversations are linked to this repository yet."
|
|
200
|
+
: "Loading conversations…"}
|
|
201
|
+
</EmptyTelemetry>
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
</RepositorySection>
|
|
205
|
+
<RepositorySection
|
|
206
|
+
title="Recent changes"
|
|
207
|
+
viewAllTo={codeRepositoryPath(data.repository.id, "changes")}
|
|
208
|
+
>
|
|
209
|
+
{changes.length > 0 ? (
|
|
210
|
+
changes.map((change) => (
|
|
211
|
+
<CodeChangeRow change={change} key={change.id} />
|
|
212
|
+
))
|
|
213
|
+
) : (
|
|
214
|
+
<div className="p-4">
|
|
215
|
+
<EmptyTelemetry>No code changes are recorded yet.</EmptyTelemetry>
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
</RepositorySection>
|
|
219
|
+
</div>
|
|
220
|
+
<CodeActivityChart
|
|
221
|
+
bucketUnit={timeRangeBucketUnit(props.range)}
|
|
222
|
+
days={repositoryActivity(data, props.range)}
|
|
223
|
+
range={props.range}
|
|
224
|
+
/>
|
|
225
|
+
</>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { CodeRepositoryReport } from "@sentry/junior/api/schema";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { Link } from "react-router";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
selectTimeSeries,
|
|
7
|
+
type TimeRangeDays,
|
|
8
|
+
} from "../../components/controls/TimeRangeSelector";
|
|
9
|
+
import { Card } from "../../components/layout/Card";
|
|
10
|
+
import { SectionHeader } from "../../components/layout/SectionHeader";
|
|
11
|
+
import { SectionTitle } from "../../components/layout/SectionTitle";
|
|
12
|
+
import { ConversationSidebarAnnotations } from "../../conversations/ConversationMeta";
|
|
13
|
+
import { ConversationListStatusIcon } from "../../conversations/ConversationListStatusIcon";
|
|
14
|
+
import { conversationPath } from "../../conversations/conversationRoutes";
|
|
15
|
+
import {
|
|
16
|
+
conversationDisplayTitle,
|
|
17
|
+
formatRelativeTime,
|
|
18
|
+
slackLocationLabel,
|
|
19
|
+
visualStatusForConversation,
|
|
20
|
+
} from "../../format";
|
|
21
|
+
import type { Conversation } from "../../types";
|
|
22
|
+
|
|
23
|
+
/** Titled repository card with an optional "View all" link to its tab. */
|
|
24
|
+
export function RepositorySection(props: {
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
title: string;
|
|
27
|
+
viewAllTo?: string;
|
|
28
|
+
}) {
|
|
29
|
+
return (
|
|
30
|
+
<Card as="section" className="mb-0" variant="section">
|
|
31
|
+
<SectionHeader
|
|
32
|
+
actions={
|
|
33
|
+
props.viewAllTo ? (
|
|
34
|
+
<Link
|
|
35
|
+
className="text-sm text-dashboard-text underline underline-offset-2"
|
|
36
|
+
to={props.viewAllTo}
|
|
37
|
+
>
|
|
38
|
+
View all
|
|
39
|
+
</Link>
|
|
40
|
+
) : undefined
|
|
41
|
+
}
|
|
42
|
+
>
|
|
43
|
+
<SectionTitle>{props.title}</SectionTitle>
|
|
44
|
+
</SectionHeader>
|
|
45
|
+
{props.children}
|
|
46
|
+
</Card>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Two-line conversation row built from the sidebar row parts: status icon and
|
|
52
|
+
* title, then plugin annotations or the location.
|
|
53
|
+
*/
|
|
54
|
+
export function RecentConversationRow(props: { conversation: Conversation }) {
|
|
55
|
+
const conversation = props.conversation;
|
|
56
|
+
const title = conversationDisplayTitle(conversation);
|
|
57
|
+
const location = slackLocationLabel(conversation, { includeId: false });
|
|
58
|
+
const hasAnnotations = Boolean(conversation.sidebarAnnotations?.length);
|
|
59
|
+
return (
|
|
60
|
+
<div className="relative min-w-0 border-b border-dashboard-border-subtle px-4 py-3 transition-colors last:border-b-0 hover:bg-dashboard-fill-soft">
|
|
61
|
+
<Link
|
|
62
|
+
aria-label={`Open ${title}`}
|
|
63
|
+
className="absolute inset-0 z-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-dashboard-focus"
|
|
64
|
+
to={conversationPath(conversation.id)}
|
|
65
|
+
/>
|
|
66
|
+
<div className="pointer-events-none relative z-[1] grid min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-x-2">
|
|
67
|
+
<span className="mt-1.5 grid size-3 shrink-0 place-items-center">
|
|
68
|
+
<ConversationListStatusIcon
|
|
69
|
+
finishedSinceSeen={false}
|
|
70
|
+
isPrivate={conversation.visibility === "private"}
|
|
71
|
+
status={visualStatusForConversation(conversation)}
|
|
72
|
+
/>
|
|
73
|
+
</span>
|
|
74
|
+
<div className="min-w-0 truncate font-display text-sm text-dashboard-text">
|
|
75
|
+
{title}
|
|
76
|
+
</div>
|
|
77
|
+
<span className="whitespace-nowrap font-mono text-xs text-dashboard-text-muted">
|
|
78
|
+
{formatRelativeTime(conversation.lastSeenAt)}
|
|
79
|
+
</span>
|
|
80
|
+
{/* Keep the meta line at text height so annotation chips do not make
|
|
81
|
+
this row taller than a code change row beside it. */}
|
|
82
|
+
<div className="col-start-2 col-end-4 mt-1 flex h-4 min-w-0 items-center gap-1.5 font-mono text-xs text-dashboard-text-muted">
|
|
83
|
+
{hasAnnotations ? (
|
|
84
|
+
<ConversationSidebarAnnotations
|
|
85
|
+
annotations={conversation.sidebarAnnotations}
|
|
86
|
+
/>
|
|
87
|
+
) : location ? (
|
|
88
|
+
<span className="truncate">{location}</span>
|
|
89
|
+
) : null}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Chart buckets for the selected range from a repository report. */
|
|
97
|
+
export function repositoryActivity(
|
|
98
|
+
data: CodeRepositoryReport,
|
|
99
|
+
range: TimeRangeDays,
|
|
100
|
+
) {
|
|
101
|
+
return selectTimeSeries({
|
|
102
|
+
days: data.activityDays,
|
|
103
|
+
hours: data.activityHours,
|
|
104
|
+
sixHours: data.activitySixHours,
|
|
105
|
+
range,
|
|
106
|
+
emptySixHour: (date) => ({ closed: 0, created: 0, date, merged: 0 }),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useDeferredValue, useEffect, useState } from "react";
|
|
2
|
+
import type { CodeRepositoryReport } from "@sentry/junior/api/schema";
|
|
3
|
+
|
|
4
|
+
import { useCodeRepositoryConversationsData } from "../../api";
|
|
5
|
+
import { EmptyTelemetry } from "../../components/EmptyTelemetry";
|
|
6
|
+
import { FilterTabList } from "../../components/FilterBar";
|
|
7
|
+
import {
|
|
8
|
+
PagePagination,
|
|
9
|
+
pageCount,
|
|
10
|
+
pageItems,
|
|
11
|
+
} from "../../components/Pagination";
|
|
12
|
+
import { SearchInput } from "../../components/SearchInput";
|
|
13
|
+
import {
|
|
14
|
+
timeRangeBucketUnit,
|
|
15
|
+
type TimeRangeDays,
|
|
16
|
+
} from "../../components/controls/TimeRangeSelector";
|
|
17
|
+
import { Card } from "../../components/layout/Card";
|
|
18
|
+
import { ConversationHomeList } from "../../conversations/ConversationHomeList";
|
|
19
|
+
import { buildConversations, getDashboardTimeZone } from "../../format";
|
|
20
|
+
import { CodeActivityChart } from "./CodeActivityChart";
|
|
21
|
+
import { CodeChangeRow } from "./CodeChangeRow";
|
|
22
|
+
import { repositoryActivity } from "./RepositorySections";
|
|
23
|
+
|
|
24
|
+
const PAGE_SIZE = 25;
|
|
25
|
+
const noFinishedConversations = new Set<string>();
|
|
26
|
+
|
|
27
|
+
/** Conversations linked to the repository, in the home list layout. */
|
|
28
|
+
export function RepositoryConversationsTab(props: { repositoryId: string }) {
|
|
29
|
+
const [query, setQuery] = useState("");
|
|
30
|
+
const [page, setPage] = useState(1);
|
|
31
|
+
const search = useDeferredValue(query.trim());
|
|
32
|
+
const feed = useCodeRepositoryConversationsData(props.repositoryId, search);
|
|
33
|
+
const conversations = buildConversations(feed.data?.conversations ?? []);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setPage(1);
|
|
36
|
+
}, [search]);
|
|
37
|
+
return (
|
|
38
|
+
<section aria-label="Conversations" className="grid gap-3">
|
|
39
|
+
<SearchInput
|
|
40
|
+
className="w-full sm:ml-auto sm:w-72"
|
|
41
|
+
label="Search conversations in this repository"
|
|
42
|
+
onChange={setQuery}
|
|
43
|
+
placeholder="Search conversations…"
|
|
44
|
+
value={query}
|
|
45
|
+
/>
|
|
46
|
+
{feed.error ? (
|
|
47
|
+
<EmptyTelemetry>
|
|
48
|
+
Conversations are unavailable. Try refreshing the dashboard.
|
|
49
|
+
</EmptyTelemetry>
|
|
50
|
+
) : (
|
|
51
|
+
<ConversationHomeList
|
|
52
|
+
conversations={pageItems(conversations, page, PAGE_SIZE)}
|
|
53
|
+
emptyLabel={
|
|
54
|
+
search
|
|
55
|
+
? "No conversations match this search."
|
|
56
|
+
: "No conversations are linked to this repository yet."
|
|
57
|
+
}
|
|
58
|
+
finishedConversationIds={noFinishedConversations}
|
|
59
|
+
loading={!feed.data}
|
|
60
|
+
timeZone={getDashboardTimeZone()}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
<PagePagination
|
|
64
|
+
className="pt-1"
|
|
65
|
+
onPageChange={setPage}
|
|
66
|
+
page={page}
|
|
67
|
+
pageCount={pageCount(conversations.length, PAGE_SIZE)}
|
|
68
|
+
pageSize={PAGE_SIZE}
|
|
69
|
+
total={conversations.length}
|
|
70
|
+
/>
|
|
71
|
+
</section>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Recent code changes in the repository, filtered by state and search. */
|
|
76
|
+
export function RepositoryChangesTab(props: {
|
|
77
|
+
data: CodeRepositoryReport;
|
|
78
|
+
range: TimeRangeDays;
|
|
79
|
+
}) {
|
|
80
|
+
const [state, setState] = useState("");
|
|
81
|
+
const [query, setQuery] = useState("");
|
|
82
|
+
const [page, setPage] = useState(1);
|
|
83
|
+
const all = props.data.changes;
|
|
84
|
+
const needle = query.trim().toLowerCase();
|
|
85
|
+
const changes = all.filter(
|
|
86
|
+
(change) =>
|
|
87
|
+
(!state || change.state === state) &&
|
|
88
|
+
(!needle ||
|
|
89
|
+
change.title?.toLowerCase().includes(needle) ||
|
|
90
|
+
`#${change.number}`.includes(needle)),
|
|
91
|
+
);
|
|
92
|
+
const count = (value: string) =>
|
|
93
|
+
all.filter((change) => change.state === value).length;
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
setPage(1);
|
|
96
|
+
}, [state, needle]);
|
|
97
|
+
return (
|
|
98
|
+
<>
|
|
99
|
+
<CodeActivityChart
|
|
100
|
+
bucketUnit={timeRangeBucketUnit(props.range)}
|
|
101
|
+
days={repositoryActivity(props.data, props.range)}
|
|
102
|
+
range={props.range}
|
|
103
|
+
/>
|
|
104
|
+
<section aria-label="Changes" className="grid gap-4">
|
|
105
|
+
<div className="flex flex-wrap items-end justify-between gap-4">
|
|
106
|
+
<FilterTabList
|
|
107
|
+
ariaLabel="Change state"
|
|
108
|
+
items={[
|
|
109
|
+
{ count: all.length, label: "All", value: "" },
|
|
110
|
+
{ count: count("open"), label: "Open", value: "open" },
|
|
111
|
+
{ count: count("merged"), label: "Merged", value: "merged" },
|
|
112
|
+
{ count: count("closed"), label: "Closed", value: "closed" },
|
|
113
|
+
]}
|
|
114
|
+
onChange={setState}
|
|
115
|
+
value={state}
|
|
116
|
+
/>
|
|
117
|
+
<SearchInput
|
|
118
|
+
className="w-full max-w-md sm:w-auto"
|
|
119
|
+
label="Search changes"
|
|
120
|
+
onChange={setQuery}
|
|
121
|
+
placeholder="Title or number"
|
|
122
|
+
value={query}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
<Card>
|
|
126
|
+
{changes.length === 0 ? (
|
|
127
|
+
<div className="p-4">
|
|
128
|
+
<EmptyTelemetry>No changes match these filters.</EmptyTelemetry>
|
|
129
|
+
</div>
|
|
130
|
+
) : (
|
|
131
|
+
pageItems(changes, page, PAGE_SIZE).map((change) => (
|
|
132
|
+
<CodeChangeRow change={change} key={change.id} />
|
|
133
|
+
))
|
|
134
|
+
)}
|
|
135
|
+
</Card>
|
|
136
|
+
<PagePagination
|
|
137
|
+
onPageChange={setPage}
|
|
138
|
+
page={page}
|
|
139
|
+
pageCount={pageCount(changes.length, PAGE_SIZE)}
|
|
140
|
+
pageSize={PAGE_SIZE}
|
|
141
|
+
total={changes.length}
|
|
142
|
+
/>
|
|
143
|
+
</section>
|
|
144
|
+
</>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Repository page tabs. Overview is the bare repository path. */
|
|
2
|
+
export type CodeRepositoryTab = "overview" | "conversations" | "changes";
|
|
3
|
+
|
|
4
|
+
/** Tab order in the repository page navigation. */
|
|
5
|
+
export const codeRepositoryTabs: readonly CodeRepositoryTab[] = [
|
|
6
|
+
"overview",
|
|
7
|
+
"conversations",
|
|
8
|
+
"changes",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
/** Build the dashboard path for one repository page tab. */
|
|
12
|
+
export function codeRepositoryPath(
|
|
13
|
+
repositoryId: string,
|
|
14
|
+
tab: CodeRepositoryTab = "overview",
|
|
15
|
+
): string {
|
|
16
|
+
const base = `/code/${encodeURIComponent(repositoryId)}`;
|
|
17
|
+
return tab === "overview" ? base : `${base}/${tab}`;
|
|
18
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { FormNotice } from "../../components/FormNotice";
|
|
2
|
+
import { Select } from "../../components/Select";
|
|
1
3
|
import { ObjectFixtures } from "./ObjectFixtures";
|
|
2
4
|
import { ChartFixtures } from "./ChartFixtures";
|
|
5
|
+
import { SegmentedNavFixture, SegmentedTabsFixture } from "./SegmentedFixtures";
|
|
3
6
|
import {
|
|
4
7
|
ConversationFixture,
|
|
5
8
|
MessageAttachmentsFixture,
|
|
@@ -33,11 +36,11 @@ import { DashboardHeader } from "../../components/layout/DashboardHeader";
|
|
|
33
36
|
import { PageHeader } from "../../components/layout/PageHeader";
|
|
34
37
|
import { MetricList, MetricValue } from "../../components/Metric";
|
|
35
38
|
import { PageContentSkeleton } from "../../components/PageContentSkeleton";
|
|
39
|
+
import { ActorAvatar } from "../../components/ActorAvatar";
|
|
36
40
|
import { ParticipantAvatarStack } from "../../components/ParticipantAvatarStack";
|
|
37
41
|
import { StatCard } from "../../components/metrics/StatCard";
|
|
38
42
|
import { StatusChip } from "../../components/StatusChip";
|
|
39
43
|
import { StatusDot } from "../../components/StatusDot";
|
|
40
|
-
import { SegmentedTabs } from "../../components/SegmentedTabs";
|
|
41
44
|
import { TextArea, TextInput } from "../../components/TextInput";
|
|
42
45
|
import { TranscriptMarkdown } from "../../conversations/TranscriptMarkdown";
|
|
43
46
|
import { TranscriptText } from "../../conversations/TranscriptText";
|
|
@@ -300,7 +303,6 @@ function FoundationsGalleryPage() {
|
|
|
300
303
|
const [navigationOpen, setNavigationOpen] = useState(false);
|
|
301
304
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
302
305
|
const [nestedDrawerOpen, setNestedDrawerOpen] = useState(false);
|
|
303
|
-
const [tab, setTab] = useState<"details" | "memories" | "usage">("details");
|
|
304
306
|
|
|
305
307
|
return (
|
|
306
308
|
<GalleryShell
|
|
@@ -308,6 +310,34 @@ function FoundationsGalleryPage() {
|
|
|
308
310
|
sectionId="foundations"
|
|
309
311
|
title="Foundations"
|
|
310
312
|
>
|
|
313
|
+
<Fixture title="Editor controls">
|
|
314
|
+
<Field
|
|
315
|
+
label="Instruction"
|
|
316
|
+
htmlFor="gallery-instruction"
|
|
317
|
+
error="Enter an instruction."
|
|
318
|
+
>
|
|
319
|
+
<TextArea
|
|
320
|
+
prose
|
|
321
|
+
id="gallery-instruction"
|
|
322
|
+
aria-invalid
|
|
323
|
+
aria-describedby="gallery-instruction-error"
|
|
324
|
+
/>
|
|
325
|
+
</Field>
|
|
326
|
+
<Field label="Repeat" htmlFor="gallery-repeat">
|
|
327
|
+
<TextInput
|
|
328
|
+
size="comfortable"
|
|
329
|
+
aria-label="Resource identifier"
|
|
330
|
+
defaultValue="getsentry/junior#42"
|
|
331
|
+
/>
|
|
332
|
+
<Select id="gallery-repeat">
|
|
333
|
+
<option>Every week</option>
|
|
334
|
+
</Select>
|
|
335
|
+
</Field>
|
|
336
|
+
<Button tone="primary">Save changes</Button>
|
|
337
|
+
<FormNotice title="This automation changed while you were editing.">
|
|
338
|
+
Your edits are still here. Review the latest version before saving.
|
|
339
|
+
</FormNotice>
|
|
340
|
+
</Fixture>
|
|
311
341
|
<Fixture title="Conversation navigation">
|
|
312
342
|
<DashboardChromeProvider>
|
|
313
343
|
<DashboardHeader
|
|
@@ -323,26 +353,10 @@ function FoundationsGalleryPage() {
|
|
|
323
353
|
</DashboardChromeProvider>
|
|
324
354
|
</Fixture>
|
|
325
355
|
<Fixture title="Segmented tabs">
|
|
326
|
-
<
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
{ label: "Memories", value: "memories" },
|
|
331
|
-
{ label: "Usage", value: "usage" },
|
|
332
|
-
]}
|
|
333
|
-
label="Conversation panels"
|
|
334
|
-
onChange={setTab}
|
|
335
|
-
value={tab}
|
|
336
|
-
>
|
|
337
|
-
<p className="m-0 text-sm leading-relaxed text-dashboard-text-muted">
|
|
338
|
-
{tab === "details"
|
|
339
|
-
? "Conversation summary and linked work."
|
|
340
|
-
: tab === "memories"
|
|
341
|
-
? "What Junior learned from this conversation."
|
|
342
|
-
: "Time, tokens, and cost for this conversation."}
|
|
343
|
-
</p>
|
|
344
|
-
</SegmentedTabs>
|
|
345
|
-
</div>
|
|
356
|
+
<SegmentedTabsFixture />
|
|
357
|
+
</Fixture>
|
|
358
|
+
<Fixture title="Segmented page links">
|
|
359
|
+
<SegmentedNavFixture />
|
|
346
360
|
</Fixture>
|
|
347
361
|
<Fixture title="Narrow details drawer">
|
|
348
362
|
<Button onClick={() => setDrawerOpen(true)}>Open details</Button>
|
|
@@ -563,19 +577,34 @@ function FoundationsGalleryPage() {
|
|
|
563
577
|
/>
|
|
564
578
|
</div>
|
|
565
579
|
</Fixture>
|
|
566
|
-
<Fixture title="Status
|
|
567
|
-
<div className="flex flex-wrap items-center gap-
|
|
580
|
+
<Fixture title="Status labels">
|
|
581
|
+
<div className="flex flex-wrap items-center gap-x-5 gap-y-3">
|
|
568
582
|
<StatusChip tone="neutral">private</StatusChip>
|
|
569
583
|
<StatusChip tone="success">completed</StatusChip>
|
|
570
584
|
<StatusChip tone="danger">failed</StatusChip>
|
|
571
585
|
<StatusChip tone="warning">blocked</StatusChip>
|
|
572
586
|
<StatusChip tone="info">preference</StatusChip>
|
|
573
587
|
<StatusChip tone="accent">knowledge</StatusChip>
|
|
574
|
-
<StatusChip size="compact" tone="
|
|
575
|
-
|
|
588
|
+
<StatusChip size="compact" tone="warning">
|
|
589
|
+
Trigger unavailable
|
|
576
590
|
</StatusChip>
|
|
577
591
|
</div>
|
|
578
592
|
</Fixture>
|
|
593
|
+
<Fixture title="Actor avatars">
|
|
594
|
+
<div className="flex items-center gap-4">
|
|
595
|
+
<ActorAvatar
|
|
596
|
+
name="Ada Lovelace"
|
|
597
|
+
imageUrl="/_junior/dashboard/avatar.png"
|
|
598
|
+
size="detail"
|
|
599
|
+
/>
|
|
600
|
+
<ActorAvatar name="Grace Hopper" size="detail" />
|
|
601
|
+
<ActorAvatar
|
|
602
|
+
name="Alan Turing"
|
|
603
|
+
imageUrl="/missing-avatar.png"
|
|
604
|
+
size="list"
|
|
605
|
+
/>
|
|
606
|
+
</div>
|
|
607
|
+
</Fixture>
|
|
579
608
|
<Fixture title="Participant avatars">
|
|
580
609
|
<div className="flex flex-wrap items-center gap-8">
|
|
581
610
|
<ParticipantAvatarStack
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { SegmentedNav } from "../../components/SegmentedNav";
|
|
4
|
+
import { SegmentedTabs } from "../../components/SegmentedTabs";
|
|
5
|
+
|
|
6
|
+
/** Show panel tabs switching the panel below them. */
|
|
7
|
+
export function SegmentedTabsFixture() {
|
|
8
|
+
const [tab, setTab] = useState<"details" | "memories" | "usage">("details");
|
|
9
|
+
return (
|
|
10
|
+
<div className="max-w-md">
|
|
11
|
+
<SegmentedTabs
|
|
12
|
+
items={[
|
|
13
|
+
{ label: "Details", value: "details" },
|
|
14
|
+
{ label: "Memories", value: "memories" },
|
|
15
|
+
{ label: "Usage", value: "usage" },
|
|
16
|
+
]}
|
|
17
|
+
label="Conversation panels"
|
|
18
|
+
onChange={setTab}
|
|
19
|
+
value={tab}
|
|
20
|
+
>
|
|
21
|
+
<p className="m-0 text-sm leading-relaxed text-dashboard-text-muted">
|
|
22
|
+
{tab === "details"
|
|
23
|
+
? "Conversation summary and linked work."
|
|
24
|
+
: tab === "memories"
|
|
25
|
+
? "What Junior learned from this conversation."
|
|
26
|
+
: "Time, tokens, and cost for this conversation."}
|
|
27
|
+
</p>
|
|
28
|
+
</SegmentedTabs>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PAGE_LINKS = [
|
|
34
|
+
"Overview",
|
|
35
|
+
"People",
|
|
36
|
+
"Locations",
|
|
37
|
+
"Workspaces",
|
|
38
|
+
"Plugins",
|
|
39
|
+
].map((label) => ({
|
|
40
|
+
isActive: () => label === "Overview",
|
|
41
|
+
label,
|
|
42
|
+
to: `#${label.toLowerCase()}`,
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
/** Show page links at label width, filling a row, and overflowing a row. */
|
|
46
|
+
export function SegmentedNavFixture() {
|
|
47
|
+
return (
|
|
48
|
+
<div className="grid gap-4">
|
|
49
|
+
<SegmentedNav ariaLabel="Label width" items={PAGE_LINKS} />
|
|
50
|
+
<div className="max-w-md">
|
|
51
|
+
<SegmentedNav ariaLabel="Fill" fill items={PAGE_LINKS.slice(0, 2)} />
|
|
52
|
+
</div>
|
|
53
|
+
<div className="max-w-xs">
|
|
54
|
+
<SegmentedNav ariaLabel="Overflow" fill items={PAGE_LINKS} />
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|