@sentry/junior-dashboard 0.234.0 → 0.236.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 +100 -18
- package/dist/client.js +47 -46
- package/dist/index.d.ts +3 -1
- package/dist/index.js +100 -18
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- package/src/app.ts +7 -1
- 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 +35 -0
- package/src/client/api.ts +63 -0
- package/src/client/components/README.md +8 -4
- package/src/client/components/SegmentedNav.tsx +167 -0
- package/src/client/components/SegmentedTabs.tsx +24 -5
- package/src/client/components/layout/SecondaryNavigation.tsx +40 -83
- package/src/client/conversations/ConversationEventLog.tsx +45 -5
- package/src/client/conversations/ConversationForkLinks.tsx +51 -0
- package/src/client/conversations/ConversationHeader.tsx +12 -1
- package/src/client/conversations/ConversationPage.tsx +72 -2
- package/src/client/conversations/ForkConversationButton.tsx +125 -0
- package/src/client/conversations/TranscriptMessageView.tsx +28 -7
- package/src/client/conversations/TranscriptView.tsx +4 -0
- package/src/client/conversations/conversationRoutes.ts +10 -0
- package/src/client/conversations/queries.ts +45 -0
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +28 -27
- package/src/client/pages/automations/AutomationEditPage.tsx +23 -24
- package/src/client/pages/automations/AutomationEditor.tsx +66 -13
- package/src/client/pages/automations/AutomationFilters.tsx +22 -14
- package/src/client/pages/automations/AutomationLifecycle.tsx +6 -4
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +18 -0
- package/src/client/pages/automations/AutomationRow.tsx +172 -187
- package/src/client/pages/automations/AutomationVersionsPage.tsx +356 -0
- package/src/client/pages/automations/AutomationsPage.tsx +40 -42
- package/src/client/pages/automations/AutomationsPageLayout.tsx +6 -5
- package/src/client/pages/automations/README.md +14 -3
- package/src/client/pages/automations/automationFormat.ts +43 -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 +5 -22
- 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/mock-reporting/code-repository.ts +77 -0
- package/src/mock-reporting/fixtures.ts +8 -2
- package/src/mock-reporting/routes.ts +25 -8
- package/src/shell.ts +21 -5
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import type { CodeOverviewReport } from "@sentry/junior/api/schema";
|
|
3
|
-
import {
|
|
3
|
+
import { Link } from "react-router";
|
|
4
4
|
import { useCodeOverviewData } from "../../api";
|
|
5
|
-
import { formatDuration } from "../../components/Duration";
|
|
6
5
|
import { EmptyTelemetry } from "../../components/EmptyTelemetry";
|
|
7
6
|
import { PageRouteLoading } from "../../components/PageRouteLoading";
|
|
8
|
-
import { StatusChip } from "../../components/StatusChip";
|
|
9
7
|
import {
|
|
10
8
|
selectTimeSeries,
|
|
11
9
|
timeRangeBucketUnit,
|
|
@@ -14,29 +12,14 @@ import {
|
|
|
14
12
|
import { Card } from "../../components/layout/Card";
|
|
15
13
|
import { PageHeader } from "../../components/layout/PageHeader";
|
|
16
14
|
import { PageLayout } from "../../components/layout/PageLayout";
|
|
17
|
-
import { StatCard } from "../../components/metrics/StatCard";
|
|
18
|
-
import { formatCompactNumber, formatCostSummary } from "../../format";
|
|
19
15
|
import { CodeActivityChart } from "./CodeActivityChart";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function costUsd(value: number | undefined): string {
|
|
30
|
-
return (
|
|
31
|
-
formatCostSummary(value === undefined ? undefined : { total: value }) || "—"
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function stateTone(state: "closed" | "merged" | "open") {
|
|
36
|
-
if (state === "merged") return "success" as const;
|
|
37
|
-
if (state === "open") return "info" as const;
|
|
38
|
-
return "neutral" as const;
|
|
39
|
-
}
|
|
16
|
+
import { CodeChangeRow } from "./CodeChangeRow";
|
|
17
|
+
import {
|
|
18
|
+
CodeSummaryCards,
|
|
19
|
+
formatCodeCost,
|
|
20
|
+
formatMergeRate,
|
|
21
|
+
} from "./CodeSummaryCards";
|
|
22
|
+
import { codeRepositoryPath } from "./codeRepositoryRoutes";
|
|
40
23
|
|
|
41
24
|
/** Render code analytics and recent code changes. */
|
|
42
25
|
export function CodePage() {
|
|
@@ -77,32 +60,7 @@ function CodeOverview(props: {
|
|
|
77
60
|
const data = props.data;
|
|
78
61
|
return (
|
|
79
62
|
<>
|
|
80
|
-
<
|
|
81
|
-
<StatCard
|
|
82
|
-
detail="In the last 30 days"
|
|
83
|
-
icon={GitPullRequest}
|
|
84
|
-
label="Created"
|
|
85
|
-
value={formatCompactNumber(data.summary.created)}
|
|
86
|
-
/>
|
|
87
|
-
<StatCard
|
|
88
|
-
detail="Share of completed changes that merged"
|
|
89
|
-
icon={LibraryBig}
|
|
90
|
-
label="Merge rate"
|
|
91
|
-
value={mergeRate(data.summary.mergeRate)}
|
|
92
|
-
/>
|
|
93
|
-
<StatCard
|
|
94
|
-
detail="Median time from open to merge in the last 30 days"
|
|
95
|
-
icon={Timer}
|
|
96
|
-
label="Median merge time"
|
|
97
|
-
value={medianMergeTime(data.summary.medianMergeTimeMs)}
|
|
98
|
-
/>
|
|
99
|
-
<StatCard
|
|
100
|
-
detail="Conversation cost for changes opened in the last 30 days"
|
|
101
|
-
icon={Coins}
|
|
102
|
-
label="Cost"
|
|
103
|
-
value={costUsd(data.summary.costUsd)}
|
|
104
|
-
/>
|
|
105
|
-
</div>
|
|
63
|
+
<CodeSummaryCards summary={data.summary} />
|
|
106
64
|
<CodeActivityChart
|
|
107
65
|
bucketUnit={timeRangeBucketUnit(props.range)}
|
|
108
66
|
days={selectTimeSeries({
|
|
@@ -160,18 +118,12 @@ function CodeOverview(props: {
|
|
|
160
118
|
>
|
|
161
119
|
<td className="min-w-0 px-4 py-3">
|
|
162
120
|
<div className="truncate font-display text-sm text-dashboard-text">
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
>
|
|
170
|
-
{repository.name}
|
|
171
|
-
</a>
|
|
172
|
-
) : (
|
|
173
|
-
repository.name
|
|
174
|
-
)}
|
|
121
|
+
<Link
|
|
122
|
+
className="text-inherit no-underline hover:text-cyan-100"
|
|
123
|
+
to={codeRepositoryPath(repository.id)}
|
|
124
|
+
>
|
|
125
|
+
{repository.name}
|
|
126
|
+
</Link>
|
|
175
127
|
</div>
|
|
176
128
|
<div className="mt-1 truncate font-mono text-xs text-dashboard-text-muted">
|
|
177
129
|
{repository.provider}
|
|
@@ -181,10 +133,10 @@ function CodeOverview(props: {
|
|
|
181
133
|
{repository.created}
|
|
182
134
|
</td>
|
|
183
135
|
<td className="px-4 py-3 text-right font-mono text-sm whitespace-nowrap text-dashboard-text">
|
|
184
|
-
{
|
|
136
|
+
{formatMergeRate(repository.mergeRate)}
|
|
185
137
|
</td>
|
|
186
138
|
<td className="px-4 py-3 text-right font-mono text-sm whitespace-nowrap text-dashboard-text">
|
|
187
|
-
{
|
|
139
|
+
{formatCodeCost(repository.medianCostUsd)}
|
|
188
140
|
</td>
|
|
189
141
|
</tr>
|
|
190
142
|
))}
|
|
@@ -200,34 +152,7 @@ function CodeOverview(props: {
|
|
|
200
152
|
</div>
|
|
201
153
|
<div>
|
|
202
154
|
{data.changes.map((change) => (
|
|
203
|
-
<
|
|
204
|
-
className="flex min-w-0 items-center justify-between gap-4 border-b border-dashboard-border-subtle px-4 py-3 last:border-b-0"
|
|
205
|
-
key={change.id}
|
|
206
|
-
>
|
|
207
|
-
<div className="min-w-0">
|
|
208
|
-
<div className="truncate font-display text-sm text-dashboard-text">
|
|
209
|
-
{change.url ? (
|
|
210
|
-
<a
|
|
211
|
-
className="text-inherit no-underline hover:text-cyan-100"
|
|
212
|
-
href={change.url}
|
|
213
|
-
rel="noreferrer"
|
|
214
|
-
target="_blank"
|
|
215
|
-
>
|
|
216
|
-
{change.title ??
|
|
217
|
-
`${change.repository} #${change.number}`}
|
|
218
|
-
</a>
|
|
219
|
-
) : (
|
|
220
|
-
(change.title ?? `${change.repository} #${change.number}`)
|
|
221
|
-
)}
|
|
222
|
-
</div>
|
|
223
|
-
<div className="mt-1 truncate font-mono text-xs text-dashboard-text-muted">
|
|
224
|
-
{change.repository} #{change.number} · {change.provider}
|
|
225
|
-
</div>
|
|
226
|
-
</div>
|
|
227
|
-
<StatusChip size="compact" tone={stateTone(change.state)}>
|
|
228
|
-
{change.state}
|
|
229
|
-
</StatusChip>
|
|
230
|
-
</div>
|
|
155
|
+
<CodeChangeRow change={change} key={change.id} />
|
|
231
156
|
))}
|
|
232
157
|
</div>
|
|
233
158
|
</Card>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { CodeOverviewReport } from "@sentry/junior/api/schema";
|
|
2
|
+
import { Coins, GitPullRequest, LibraryBig, Timer } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { formatDuration } from "../../components/Duration";
|
|
5
|
+
import { StatCard } from "../../components/metrics/StatCard";
|
|
6
|
+
import { formatCompactNumber, formatCostSummary } from "../../format";
|
|
7
|
+
|
|
8
|
+
/** Format a merge rate, or a dash when no change has finished. */
|
|
9
|
+
export function formatMergeRate(value: number | undefined): string {
|
|
10
|
+
return value === undefined ? "—" : `${Math.round(value * 100)}%`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Format a code cost, or a dash when no cost is recorded. */
|
|
14
|
+
export function formatCodeCost(value: number | undefined): string {
|
|
15
|
+
return (
|
|
16
|
+
formatCostSummary(value === undefined ? undefined : { total: value }) || "—"
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Show the 30-day code change summary cards. */
|
|
21
|
+
export function CodeSummaryCards(props: {
|
|
22
|
+
summary: CodeOverviewReport["summary"];
|
|
23
|
+
}) {
|
|
24
|
+
const summary = props.summary;
|
|
25
|
+
return (
|
|
26
|
+
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
|
27
|
+
<StatCard
|
|
28
|
+
detail="In the last 30 days"
|
|
29
|
+
icon={GitPullRequest}
|
|
30
|
+
label="Created"
|
|
31
|
+
value={formatCompactNumber(summary.created)}
|
|
32
|
+
/>
|
|
33
|
+
<StatCard
|
|
34
|
+
detail="Share of completed changes that merged"
|
|
35
|
+
icon={LibraryBig}
|
|
36
|
+
label="Merge rate"
|
|
37
|
+
value={formatMergeRate(summary.mergeRate)}
|
|
38
|
+
/>
|
|
39
|
+
<StatCard
|
|
40
|
+
detail="Median time from open to merge in the last 30 days"
|
|
41
|
+
icon={Timer}
|
|
42
|
+
label="Median merge time"
|
|
43
|
+
value={formatDuration(summary.medianMergeTimeMs) || "—"}
|
|
44
|
+
/>
|
|
45
|
+
<StatCard
|
|
46
|
+
detail="Conversation cost for changes opened in the last 30 days"
|
|
47
|
+
icon={Coins}
|
|
48
|
+
label="Cost"
|
|
49
|
+
value={formatCodeCost(summary.costUsd)}
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -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
|
+
}
|