@git-snitch/renderer 0.0.11 → 0.0.12
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/charts.d.ts +40 -1
- package/dist/charts.d.ts.map +1 -1
- package/dist/charts.js +59 -0
- package/dist/layout.js +3 -3
- package/dist/template/report-template.html +14 -14
- package/package.json +3 -3
- package/src/ai-usage.tsx +22 -33
- package/src/charts.tsx +166 -1
- package/src/layout.tsx +3 -3
- package/src/scan-routes.tsx +36 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@git-snitch/renderer",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"react-dom": "^19.2.5",
|
|
36
36
|
"recharts": "3.8.0",
|
|
37
37
|
"vite": "^8.0.8",
|
|
38
|
-
"@git-snitch/core": "0.0.
|
|
39
|
-
"@git-snitch/ui": "0.0.
|
|
38
|
+
"@git-snitch/core": "0.0.12",
|
|
39
|
+
"@git-snitch/ui": "0.0.12"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@testing-library/dom": "^10.4.1",
|
package/src/ai-usage.tsx
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Card, CardContent, CardHeader, CardTitle } from "@git-snitch/ui/components/card";
|
|
2
|
-
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@git-snitch/ui/components/table";
|
|
3
2
|
|
|
3
|
+
import type { ColumnDef } from "@tanstack/react-table";
|
|
4
|
+
import { useMemo } from "react";
|
|
4
5
|
import type { AiUsageBreakdownItem, AiUsageSummary, ReportAiUsageProjectSummary } from "@git-snitch/core";
|
|
5
6
|
|
|
6
7
|
import { EmptyState } from "./empty-state.js";
|
|
8
|
+
import { DataTable } from "./tables.js";
|
|
7
9
|
|
|
8
10
|
type AiUsagePanelProps = {
|
|
9
11
|
readonly title?: string;
|
|
@@ -41,41 +43,28 @@ function SummaryMetric({ label, value, description }: { readonly label: string;
|
|
|
41
43
|
);
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
const breakdownColumns: ColumnDef<AiUsageBreakdownItem>[] = [
|
|
47
|
+
{ accessorKey: "key", header: "Name", cell: ({ row }) => <span className="font-medium text-foreground">{row.original.key}</span> },
|
|
48
|
+
{ accessorKey: "records", header: "Messages", cell: ({ row }) => formatNumber(row.original.records) },
|
|
49
|
+
{ accessorKey: "tokens.total", header: "Tokens", cell: ({ row }) => formatAiUsageTokens(row.original.tokens.total) },
|
|
50
|
+
{ accessorKey: "cost", header: "Cost", cell: ({ row }) => formatAiUsageCost(row.original.cost) },
|
|
51
|
+
];
|
|
52
|
+
|
|
44
53
|
function BreakdownTable({ title, rows, emptyDescription }: { readonly title: string; readonly rows: readonly AiUsageBreakdownItem[]; readonly emptyDescription: string }) {
|
|
45
|
-
const
|
|
54
|
+
const columns = useMemo(() => breakdownColumns, []);
|
|
46
55
|
|
|
47
56
|
return (
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<TableRow className="hover:bg-transparent">
|
|
60
|
-
<TableHead className="bg-muted/30 px-3 py-2 text-[0.7rem] uppercase tracking-[0.18em] text-muted-foreground">Name</TableHead>
|
|
61
|
-
<TableHead className="bg-muted/30 px-3 py-2 text-[0.7rem] uppercase tracking-[0.18em] text-muted-foreground">Messages</TableHead>
|
|
62
|
-
<TableHead className="bg-muted/30 px-3 py-2 text-[0.7rem] uppercase tracking-[0.18em] text-muted-foreground">Tokens</TableHead>
|
|
63
|
-
<TableHead className="bg-muted/30 px-3 py-2 text-[0.7rem] uppercase tracking-[0.18em] text-muted-foreground">Cost</TableHead>
|
|
64
|
-
</TableRow>
|
|
65
|
-
</TableHeader>
|
|
66
|
-
<TableBody>
|
|
67
|
-
{visibleRows.map((row) => (
|
|
68
|
-
<TableRow key={row.key}>
|
|
69
|
-
<TableCell className="px-3 py-3 text-sm font-medium text-foreground">{row.key}</TableCell>
|
|
70
|
-
<TableCell className="px-3 py-3 text-sm text-muted-foreground">{formatNumber(row.records)}</TableCell>
|
|
71
|
-
<TableCell className="px-3 py-3 text-sm text-muted-foreground">{formatNumber(row.tokens.total)}</TableCell>
|
|
72
|
-
<TableCell className="px-3 py-3 text-sm text-muted-foreground">{formatAiUsageCost(row.cost)}</TableCell>
|
|
73
|
-
</TableRow>
|
|
74
|
-
))}
|
|
75
|
-
</TableBody>
|
|
76
|
-
</Table>
|
|
77
|
-
)}
|
|
78
|
-
</div>
|
|
57
|
+
<DataTable
|
|
58
|
+
ariaLabel={title}
|
|
59
|
+
data={rows}
|
|
60
|
+
columns={columns}
|
|
61
|
+
search={{ placeholder: "Search", toText: (row) => row.key }}
|
|
62
|
+
exportConfig={{
|
|
63
|
+
filename: `${title.toLowerCase().replace(/\s+/g, "-")}.csv`,
|
|
64
|
+
mapRow: (row) => ({ name: row.key, messages: row.records, tokens: row.tokens.total, cost: row.cost }),
|
|
65
|
+
}}
|
|
66
|
+
emptyState={{ title: `${title} unavailable`, description: emptyDescription }}
|
|
67
|
+
/>
|
|
79
68
|
);
|
|
80
69
|
}
|
|
81
70
|
|
package/src/charts.tsx
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from "recharts";
|
|
22
22
|
import type { ReactNode } from "react";
|
|
23
23
|
|
|
24
|
-
import type { AiUsageBreakdownItem, CommitRecord, ContributorSummary, RepoReportData, ScanProjectReport, ScanReportData } from "@git-snitch/core";
|
|
24
|
+
import type { AiUsageBreakdownItem, CommitRecord, ContributorSummary, RepoReportData, ReportAiUsageProjectSummary, ScanProjectReport, ScanReportData } from "@git-snitch/core";
|
|
25
25
|
|
|
26
26
|
import { EmptyState } from "./empty-state.js";
|
|
27
27
|
|
|
@@ -43,6 +43,10 @@ export type CodeOwnershipPoint = { readonly owner: string; readonly additions: n
|
|
|
43
43
|
export type ProjectComparisonPoint = { readonly project: string; readonly commits: number; readonly contributors: number; readonly filesChanged: number };
|
|
44
44
|
export type ActivityHeatmapCell = { readonly day: string; readonly hour: string; readonly commits: number };
|
|
45
45
|
export type AiUsageBreakdownPoint = { readonly name: string; readonly messages: number; readonly tokens: number; readonly cost: number };
|
|
46
|
+
export type ScanCommitSlice = { readonly name: string; readonly commits: number };
|
|
47
|
+
export type ScanChurnSlice = { readonly name: string; readonly additions: number; readonly deletions: number; readonly churn: number };
|
|
48
|
+
export type ScanAiProjectSlice = { readonly name: string; readonly messages: number; readonly tokens: number };
|
|
49
|
+
export type ScanAiModelSlice = { readonly name: string; readonly messages: number; readonly tokens: number };
|
|
46
50
|
|
|
47
51
|
type ChartPanelProps = {
|
|
48
52
|
readonly title: string;
|
|
@@ -494,6 +498,167 @@ export function ActivityHeatmap({ data }: { readonly data: readonly ActivityHeat
|
|
|
494
498
|
);
|
|
495
499
|
}
|
|
496
500
|
|
|
501
|
+
export function ScanCommitBarChart({ data }: { readonly data: readonly ScanCommitSlice[] }) {
|
|
502
|
+
return (
|
|
503
|
+
<ChartPanel
|
|
504
|
+
title="Commits per project"
|
|
505
|
+
description="Commit count across scanned repositories."
|
|
506
|
+
isEmpty={!hasPositiveValue(data, (item) => [item.commits])}
|
|
507
|
+
emptyTitle="No commits to chart"
|
|
508
|
+
emptyDescription="Commit comparison needs at least one scanned project with commits."
|
|
509
|
+
>
|
|
510
|
+
<ChartContainer config={{ commits: { label: "Commits", color: chartPalette[1] } }} className="h-64 w-full">
|
|
511
|
+
<BarChart data={data} layout="vertical" margin={{ left: 8, right: 16, top: 8, bottom: 0 }}>
|
|
512
|
+
<CartesianGrid horizontal={false} />
|
|
513
|
+
<XAxis type="number" hide />
|
|
514
|
+
<YAxis dataKey="name" type="category" width={112} tickLine={false} axisLine={false} />
|
|
515
|
+
<ChartTooltip content={<ChartTooltipContent />} />
|
|
516
|
+
<Bar dataKey="commits" fill="var(--color-commits)" radius={[0, 3, 3, 0]} {...staticChartProps} />
|
|
517
|
+
</BarChart>
|
|
518
|
+
</ChartContainer>
|
|
519
|
+
</ChartPanel>
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function ScanChurnPieChart({ data }: { readonly data: readonly ScanChurnSlice[] }) {
|
|
524
|
+
return (
|
|
525
|
+
<ChartPanel
|
|
526
|
+
title="Churn per project"
|
|
527
|
+
description="Lines changed (additions + deletions) by project."
|
|
528
|
+
isEmpty={!hasPositiveValue(data, (item) => [item.churn])}
|
|
529
|
+
emptyTitle="No churn to chart"
|
|
530
|
+
emptyDescription="Churn data appears after file-level additions or deletions exist."
|
|
531
|
+
>
|
|
532
|
+
<ChartContainer config={{ churn: { label: "Churn" } }} className="h-64 w-full">
|
|
533
|
+
<PieChart>
|
|
534
|
+
<ChartTooltip content={<ChartTooltipContent nameKey="name" />} />
|
|
535
|
+
<Pie data={data} dataKey="churn" nameKey="name" innerRadius={56} outerRadius={88} paddingAngle={2} {...staticChartProps}>
|
|
536
|
+
{data.map((slice, index) => (
|
|
537
|
+
<Cell key={slice.name} fill={chartPalette[index % chartPalette.length]} />
|
|
538
|
+
))}
|
|
539
|
+
</Pie>
|
|
540
|
+
</PieChart>
|
|
541
|
+
</ChartContainer>
|
|
542
|
+
</ChartPanel>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function ScanAiMessagesPieChart({ data }: { readonly data: readonly ScanAiProjectSlice[] }) {
|
|
547
|
+
return (
|
|
548
|
+
<ChartPanel
|
|
549
|
+
title="AI messages per project"
|
|
550
|
+
description="Message share across scanned repositories."
|
|
551
|
+
isEmpty={!hasPositiveValue(data, (item) => [item.messages])}
|
|
552
|
+
emptyTitle="No AI messages to chart"
|
|
553
|
+
emptyDescription="AI message data appears after local assistant records are matched to projects."
|
|
554
|
+
>
|
|
555
|
+
<ChartContainer config={{ messages: { label: "Messages" } }} className="h-64 w-full">
|
|
556
|
+
<PieChart>
|
|
557
|
+
<ChartTooltip content={<ChartTooltipContent nameKey="name" />} />
|
|
558
|
+
<Pie data={data} dataKey="messages" nameKey="name" innerRadius={56} outerRadius={88} paddingAngle={2} {...staticChartProps}>
|
|
559
|
+
{data.map((slice, index) => (
|
|
560
|
+
<Cell key={slice.name} fill={chartPalette[index % chartPalette.length]} />
|
|
561
|
+
))}
|
|
562
|
+
</Pie>
|
|
563
|
+
</PieChart>
|
|
564
|
+
</ChartContainer>
|
|
565
|
+
</ChartPanel>
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function ScanAiTokensBarChart({ data }: { readonly data: readonly ScanAiProjectSlice[] }) {
|
|
570
|
+
return (
|
|
571
|
+
<ChartPanel
|
|
572
|
+
title="AI tokens per project"
|
|
573
|
+
description="Token usage across scanned repositories."
|
|
574
|
+
isEmpty={!hasPositiveValue(data, (item) => [item.tokens])}
|
|
575
|
+
emptyTitle="No AI tokens to chart"
|
|
576
|
+
emptyDescription="AI token data appears after local assistant records are matched to projects."
|
|
577
|
+
>
|
|
578
|
+
<ChartContainer config={{ tokens: { label: "Tokens", color: chartPalette[2] } }} className="h-64 w-full">
|
|
579
|
+
<BarChart data={data} layout="vertical" margin={{ left: 8, right: 16, top: 8, bottom: 0 }}>
|
|
580
|
+
<CartesianGrid horizontal={false} />
|
|
581
|
+
<XAxis type="number" hide />
|
|
582
|
+
<YAxis dataKey="name" type="category" width={112} tickLine={false} axisLine={false} />
|
|
583
|
+
<ChartTooltip content={<ChartTooltipContent />} />
|
|
584
|
+
<Bar dataKey="tokens" fill="var(--color-tokens)" radius={[0, 3, 3, 0]} {...staticChartProps} />
|
|
585
|
+
</BarChart>
|
|
586
|
+
</ChartContainer>
|
|
587
|
+
</ChartPanel>
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export function ScanAiModelsPieChart({ data }: { readonly data: readonly ScanAiModelSlice[] }) {
|
|
592
|
+
return (
|
|
593
|
+
<ChartPanel
|
|
594
|
+
title="AI usage by model"
|
|
595
|
+
description="Message share across all models used in the scan."
|
|
596
|
+
isEmpty={!hasPositiveValue(data, (item) => [item.messages])}
|
|
597
|
+
emptyTitle="No AI model data to chart"
|
|
598
|
+
emptyDescription="AI model data appears after local assistant records with model metadata are matched."
|
|
599
|
+
>
|
|
600
|
+
<ChartContainer config={{ messages: { label: "Messages" } }} className="h-64 w-full">
|
|
601
|
+
<PieChart>
|
|
602
|
+
<ChartTooltip content={<ChartTooltipContent nameKey="name" />} />
|
|
603
|
+
<Pie data={data} dataKey="messages" nameKey="name" innerRadius={56} outerRadius={88} paddingAngle={2} {...staticChartProps}>
|
|
604
|
+
{data.map((slice, index) => (
|
|
605
|
+
<Cell key={slice.name} fill={chartPalette[index % chartPalette.length]} />
|
|
606
|
+
))}
|
|
607
|
+
</Pie>
|
|
608
|
+
</PieChart>
|
|
609
|
+
</ChartContainer>
|
|
610
|
+
</ChartPanel>
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export function deriveScanCommitData(projects: readonly ScanProjectReport[]): readonly ScanCommitSlice[] {
|
|
615
|
+
return projects
|
|
616
|
+
.map((project) => ({
|
|
617
|
+
name: project.repository.name,
|
|
618
|
+
commits: project.report.commits.length,
|
|
619
|
+
}))
|
|
620
|
+
.sort((left, right) => right.commits - left.commits || left.name.localeCompare(right.name));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export function deriveScanChurnData(projects: readonly ScanProjectReport[]): readonly ScanChurnSlice[] {
|
|
624
|
+
return projects
|
|
625
|
+
.map((project) => {
|
|
626
|
+
let additions = 0;
|
|
627
|
+
let deletions = 0;
|
|
628
|
+
for (const commit of project.report.commits) {
|
|
629
|
+
for (const file of commit.files) {
|
|
630
|
+
additions += file.additions;
|
|
631
|
+
deletions += file.deletions;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return { name: project.repository.name, additions, deletions, churn: additions + deletions };
|
|
635
|
+
})
|
|
636
|
+
.filter((item) => item.churn > 0)
|
|
637
|
+
.sort((left, right) => right.churn - left.churn || left.name.localeCompare(right.name));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export function deriveScanAiPerProjectData(projects: readonly ScanProjectReport[]): readonly ScanAiProjectSlice[] {
|
|
641
|
+
return projects
|
|
642
|
+
.filter((project) => project.report.aiUsage !== undefined && project.report.aiUsage.records > 0)
|
|
643
|
+
.map((project) => ({
|
|
644
|
+
name: project.repository.name,
|
|
645
|
+
messages: project.report.aiUsage!.records,
|
|
646
|
+
tokens: project.report.aiUsage!.tokens.total,
|
|
647
|
+
}))
|
|
648
|
+
.sort((left, right) => right.messages - left.messages || left.name.localeCompare(right.name));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export function deriveScanAiModelsData(aiUsage: ReportAiUsageProjectSummary): readonly ScanAiModelSlice[] {
|
|
652
|
+
return aiUsage.breakdowns.byModel
|
|
653
|
+
.filter((item) => item.records > 0 || item.tokens.total > 0)
|
|
654
|
+
.map((item) => ({
|
|
655
|
+
name: item.key,
|
|
656
|
+
messages: item.records,
|
|
657
|
+
tokens: item.tokens.total,
|
|
658
|
+
}))
|
|
659
|
+
.sort((left, right) => right.messages - left.messages || left.name.localeCompare(right.name));
|
|
660
|
+
}
|
|
661
|
+
|
|
497
662
|
function heatClass(commits: number) {
|
|
498
663
|
if (commits >= 8) {
|
|
499
664
|
return "border-chart-4/40 bg-chart-4";
|
package/src/layout.tsx
CHANGED
|
@@ -53,7 +53,7 @@ export function Header({ title, titleHref, eyebrow, description, actions }: Head
|
|
|
53
53
|
|
|
54
54
|
return (
|
|
55
55
|
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/75">
|
|
56
|
-
<div className="mx-auto flex w-full
|
|
56
|
+
<div className="mx-auto flex w-full flex-col gap-6 px-6 py-8 sm:px-10 lg:flex-row lg:items-end lg:justify-between">
|
|
57
57
|
<div className="max-w-4xl">
|
|
58
58
|
{eyebrow ? <p className="text-xs font-medium uppercase tracking-[0.24em] text-muted-foreground">{eyebrow}</p> : null}
|
|
59
59
|
{titleHref ? (
|
|
@@ -81,7 +81,7 @@ export function Navigation({ items, label = "Report sections" }: NavigationProps
|
|
|
81
81
|
|
|
82
82
|
return (
|
|
83
83
|
<nav aria-label={label} className="border-b bg-background/80">
|
|
84
|
-
<div className="mx-auto flex w-full
|
|
84
|
+
<div className="mx-auto flex w-full gap-2 overflow-x-auto px-6 py-3 sm:px-10">
|
|
85
85
|
{items.map((item) =>
|
|
86
86
|
item.disabled ? (
|
|
87
87
|
<span
|
|
@@ -146,7 +146,7 @@ export function AppShell({
|
|
|
146
146
|
<main className="min-h-screen w-full max-w-full overflow-x-hidden bg-background text-foreground transition-colors">
|
|
147
147
|
<Header title={title} titleHref={titleHref} eyebrow={eyebrow} description={description} actions={headerActions} />
|
|
148
148
|
<Navigation items={navigationItems} />
|
|
149
|
-
<div className={cn("mx-auto flex w-full
|
|
149
|
+
<div className={cn("mx-auto flex w-full flex-col gap-8 px-6 py-8 sm:px-10 sm:py-10")}>{children}</div>
|
|
150
150
|
</main>
|
|
151
151
|
);
|
|
152
152
|
}
|
package/src/scan-routes.tsx
CHANGED
|
@@ -5,6 +5,19 @@ import { cn } from "@git-snitch/ui/lib/utils";
|
|
|
5
5
|
|
|
6
6
|
import { AiUsagePanel, formatAiUsageCost, formatAiUsageTokens } from "./ai-usage.js";
|
|
7
7
|
import { ChartsRoute } from "./charts-route.js";
|
|
8
|
+
import {
|
|
9
|
+
LanguageDistributionChart,
|
|
10
|
+
ScanAiMessagesPieChart,
|
|
11
|
+
ScanAiModelsPieChart,
|
|
12
|
+
ScanAiTokensBarChart,
|
|
13
|
+
ScanChurnPieChart,
|
|
14
|
+
ScanCommitBarChart,
|
|
15
|
+
deriveLanguageDistributionData,
|
|
16
|
+
deriveScanAiModelsData,
|
|
17
|
+
deriveScanAiPerProjectData,
|
|
18
|
+
deriveScanChurnData,
|
|
19
|
+
deriveScanCommitData,
|
|
20
|
+
} from "./charts.js";
|
|
8
21
|
import { EmptyState } from "./empty-state.js";
|
|
9
22
|
import { StatsGrid } from "./layout.js";
|
|
10
23
|
import { RepoOverview } from "./overview.js";
|
|
@@ -318,6 +331,28 @@ function CrossProjectContributors({ report }: { readonly report: ScanReportData
|
|
|
318
331
|
);
|
|
319
332
|
}
|
|
320
333
|
|
|
334
|
+
function ScanCharts({ report }: { readonly report: ScanReportData }) {
|
|
335
|
+
const commitData = deriveScanCommitData(report.projects);
|
|
336
|
+
const churnData = deriveScanChurnData(report.projects);
|
|
337
|
+
const languageData = deriveLanguageDistributionData(report);
|
|
338
|
+
const aiPerProject = deriveScanAiPerProjectData(report.projects);
|
|
339
|
+
const aiModels = report.analysis.aiUsage !== undefined ? deriveScanAiModelsData(report.analysis.aiUsage) : [];
|
|
340
|
+
|
|
341
|
+
return (
|
|
342
|
+
<section aria-label="Visual comparison">
|
|
343
|
+
<h2 className="mb-4 text-lg font-semibold tracking-tight text-foreground">Visual comparison</h2>
|
|
344
|
+
<div className="grid gap-6 lg:grid-cols-3">
|
|
345
|
+
<ScanCommitBarChart data={commitData} />
|
|
346
|
+
<ScanChurnPieChart data={churnData} />
|
|
347
|
+
<LanguageDistributionChart data={languageData} />
|
|
348
|
+
{aiPerProject.length > 0 ? <ScanAiMessagesPieChart data={aiPerProject} /> : null}
|
|
349
|
+
{aiPerProject.length > 0 ? <ScanAiTokensBarChart data={aiPerProject} /> : null}
|
|
350
|
+
{aiModels.length > 0 ? <ScanAiModelsPieChart data={aiModels} /> : null}
|
|
351
|
+
</div>
|
|
352
|
+
</section>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
321
356
|
export function ScanOverview({ report }: ScanRouteProps) {
|
|
322
357
|
if (report.kind !== "scan") {
|
|
323
358
|
return scanDataMismatch("Scan overview is unavailable for repository reports");
|
|
@@ -335,6 +370,7 @@ export function ScanOverview({ report }: ScanRouteProps) {
|
|
|
335
370
|
/>
|
|
336
371
|
) : null}
|
|
337
372
|
<ProjectComparison report={report} />
|
|
373
|
+
<ScanCharts report={report} />
|
|
338
374
|
<CrossProjectContributors report={report} />
|
|
339
375
|
</div>
|
|
340
376
|
);
|