mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
working on statistics page.
This commit is contained in:
@@ -1,15 +0,0 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col gap-4 p-4">
|
||||
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<div className="aspect-video rounded-xl bg-muted/50"/>
|
||||
<div className="aspect-video rounded-xl bg-muted/50"/>
|
||||
<div className="aspect-video rounded-xl bg-muted/50"/>
|
||||
</div>
|
||||
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min"/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {prisma} from "@/prisma";
|
||||
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {EvolutionLineChart} from "@/components/wrappers/charts/evolution-line-chart";
|
||||
import {PercentageLineChart} from "@/components/wrappers/charts/percentage-line-chart";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
|
||||
const projectsCount = await prisma.project.count({
|
||||
where: {
|
||||
organization: {
|
||||
slug: "default"
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const backupsEvolution = await prisma.backup.findMany({
|
||||
select: {
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
const backupsRate = await prisma.backup.groupBy({
|
||||
by: ['createdAt', 'status'],
|
||||
where: {
|
||||
status: {
|
||||
in: ['success', 'failed'],
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>
|
||||
Statistics
|
||||
</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent className="flex flex-col gap-y-4">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Projects</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{projectsCount}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>KPI 2</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>KPI 3</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Evolution of the number of backups</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<EvolutionLineChart data={backupsEvolution}/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Success rate of backups</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PercentageLineChart data={backupsRate}/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</PageContent>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
+2
-2
@@ -55,7 +55,7 @@
|
||||
"dockerode": "^4.0.2",
|
||||
"embla-carousel-react": "^8.3.1",
|
||||
"input-otp": "^1.4.0",
|
||||
"lucide-react": "^0.454.0",
|
||||
"lucide-react": "^0.468.0",
|
||||
"minio": "^8.0.2",
|
||||
"next": "15.0.3",
|
||||
"next-auth": "^5.0.0-beta.25",
|
||||
@@ -70,7 +70,7 @@
|
||||
"react-hook-form": "^7.53.1",
|
||||
"react-resizable-panels": "^2.1.6",
|
||||
"react-twc": "^1.4.2",
|
||||
"recharts": "^2.13.3",
|
||||
"recharts": "^2.15.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"sonner": "^1.6.1",
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
|
||||
import * as React from "react"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
import {
|
||||
NameType,
|
||||
Payload,
|
||||
ValueType,
|
||||
} from "recharts/types/component/DefaultTooltipContent"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ export const SidebarMenuCustom = (props: SidebarMenuCustomProps) => {
|
||||
icon: ShieldHalf,
|
||||
},
|
||||
{
|
||||
title: "Statistic",
|
||||
url: "kpi",
|
||||
title: "Statistics",
|
||||
url: "statistics",
|
||||
icon: ChartArea,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
// "use client"
|
||||
//
|
||||
// import {CartesianGrid, Line, LineChart, Tooltip, XAxis, YAxis} from "recharts"
|
||||
// import {ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent,} from "@/components/ui/chart"
|
||||
// import {humanReadableDate} from "@/utils/date-formatting";
|
||||
//
|
||||
//
|
||||
// export type lineChartProps = {
|
||||
// data: any
|
||||
// }
|
||||
//
|
||||
// export function EvolutionLineChart(props: lineChartProps) {
|
||||
//
|
||||
// const {data} = props
|
||||
//
|
||||
// // Process data to calculate cumulative count
|
||||
// const cumulativeData = data.reduce((acc, backup) => {
|
||||
// const date = backup.createdAt.toISOString().split("T")[0]; // Format as YYYY-MM-DD
|
||||
//
|
||||
// // Increment count for the current date or initialize it
|
||||
// if (acc.length && acc[acc.length - 1].date === date) {
|
||||
// acc[acc.length - 1].count += 1;
|
||||
// } else {
|
||||
// const lastCount = acc.length ? acc[acc.length - 1].count : 0;
|
||||
// acc.push({date, count: lastCount + 1});
|
||||
// }
|
||||
//
|
||||
// return acc;
|
||||
// }, [] as { date: string; count: number }[]);
|
||||
//
|
||||
//
|
||||
// const chartConfig = {
|
||||
// date: {
|
||||
// label: "Date",
|
||||
// color: "#2563eb",
|
||||
// },
|
||||
// count: {
|
||||
// label: "Number of backups",
|
||||
// color: "#60a5fa",
|
||||
// },
|
||||
// } satisfies ChartConfig
|
||||
//
|
||||
//
|
||||
// return (
|
||||
// <ChartContainer config={chartConfig}>
|
||||
// <LineChart
|
||||
// accessibilityLayer
|
||||
// data={data}
|
||||
// margin={{
|
||||
// left: 12,
|
||||
// right: 12,
|
||||
// }}
|
||||
// >
|
||||
// <CartesianGrid vertical={false}/>
|
||||
// <XAxis
|
||||
// dataKey="date"
|
||||
// tickLine={false}
|
||||
// axisLine={false}
|
||||
// tickMargin={8}
|
||||
// tickFormatter={(value) => humanReadableDate(Date(value)).split(' ')[0]}
|
||||
// />
|
||||
// <YAxis/>
|
||||
// <ChartTooltip
|
||||
// cursor={false}
|
||||
// content={<ChartTooltipContent hideLabel/>}
|
||||
// />
|
||||
// <Line
|
||||
// dataKey="count"
|
||||
// type="linear"
|
||||
// stroke="var(--color-desktop)"
|
||||
// strokeWidth={2}
|
||||
// dot={false}
|
||||
// />
|
||||
// </LineChart>
|
||||
// </ChartContainer>
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// export function PercentageLineChart(props: lineChartProps) {
|
||||
//
|
||||
// const {data} = props
|
||||
//
|
||||
// const chartConfig = {
|
||||
// date: {
|
||||
// label: "Date",
|
||||
// color: "#2563eb",
|
||||
// },
|
||||
// successRate: {
|
||||
// label: "Success Rate",
|
||||
// color: "#60a5fa",
|
||||
// },
|
||||
// } satisfies ChartConfig
|
||||
//
|
||||
// const dailyStats = data.reduce((acc, backup) => {
|
||||
// const date = backup.createdAt.toISOString().split("T")[0]; // Format YYYY-MM-DD
|
||||
// const status = backup.status;
|
||||
//
|
||||
// if (!acc[date]) {
|
||||
// acc[date] = {success: 0, failed: 0, total: 0};
|
||||
// }
|
||||
//
|
||||
// acc[date][status === "success" ? "success" : "failed"] += backup._count.id;
|
||||
// acc[date].total += backup._count.id;
|
||||
//
|
||||
// return acc;
|
||||
// }, {} as Record<string, { success: number; failed: number; total: number }>);
|
||||
//
|
||||
// // Format data for the chart
|
||||
// const formattedData = Object.entries(dailyStats).map(([date, stats]) => ({
|
||||
// date,
|
||||
// successRate: (stats.success / stats.total) * 100,
|
||||
// }));
|
||||
//
|
||||
// return (
|
||||
// <ChartContainer config={chartConfig}>
|
||||
// <LineChart data={formattedData}>
|
||||
// <CartesianGrid strokeDasharray="3 3"/>
|
||||
// <XAxis dataKey="date"/>
|
||||
// <YAxis domain={[0, 100]} tickFormatter={(tick) => `${tick}%`}/>
|
||||
// {/*<Tooltip formatter={(value: number) => `${value.toFixed(2)}%`}/>*/}
|
||||
// <ChartTooltip
|
||||
// content={<ChartTooltipContent/>}
|
||||
// cursor={false}
|
||||
// defaultIndex={1}
|
||||
// formatter={(value, name) => (
|
||||
// <div className="flex min-w-[130px] items-center text-xs text-muted-foreground">
|
||||
// {chartConfig[name as keyof typeof chartConfig]?.label ||
|
||||
// name}
|
||||
// <div className="ml-auto flex items-baseline gap-0.5 font-mono font-medium tabular-nums text-foreground">
|
||||
// {value}
|
||||
// <span className="font-normal text-muted-foreground">
|
||||
// %
|
||||
// </span>
|
||||
// </div>
|
||||
// </div>
|
||||
// )}
|
||||
// />
|
||||
// <Line type="step" dataKey="successRate" stroke="#8884d8" strokeWidth={2}/>
|
||||
// </LineChart>
|
||||
// </ChartContainer>
|
||||
// )
|
||||
//
|
||||
// }
|
||||
//
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client"
|
||||
|
||||
import {ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent} from "@/components/ui/chart";
|
||||
import {CartesianGrid, Line, LineChart, XAxis, YAxis} from "recharts";
|
||||
import {humanReadableDate} from "@/utils/date-formatting";
|
||||
|
||||
const data = [
|
||||
{date: "2024-12-01", count: 1},
|
||||
{date: "2024-12-02", count: 2},
|
||||
{date: "2024-12-03", count: 4},
|
||||
{date: "2024-12-04", count: 8},
|
||||
{date: "2024-12-05", count: 9},
|
||||
{date: "2024-12-06", count: 9},
|
||||
{date: "2024-12-07", count: 10},
|
||||
{date: "2024-12-08", count: 13},
|
||||
{date: "2024-12-09", count: 15},
|
||||
{date: "2024-12-10", count: 18},
|
||||
|
||||
]
|
||||
|
||||
|
||||
type Data = {
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
|
||||
export type evolutionLineChartProps = {
|
||||
data: Data[]
|
||||
}
|
||||
|
||||
export function EvolutionLineChart(props: evolutionLineChartProps) {
|
||||
|
||||
const {data} = props
|
||||
console.log("aaa data", data)
|
||||
|
||||
|
||||
// Process data to calculate cumulative count
|
||||
const cumulativeData = data.reduce((acc, backup) => {
|
||||
const date = backup.createdAt.toISOString().split("T")[0]; // Format as YYYY-MM-DD
|
||||
|
||||
// Increment count for the current date or initialize it
|
||||
if (acc.length && acc[acc.length - 1].date === date) {
|
||||
acc[acc.length - 1].count += 1;
|
||||
} else {
|
||||
const lastCount = acc.length ? acc[acc.length - 1].count : 0;
|
||||
acc.push({date, count: lastCount + 1});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as { date: string; count: number }[]);
|
||||
|
||||
|
||||
const chartConfig = {
|
||||
date: {
|
||||
label: "Date",
|
||||
color: "#2563eb",
|
||||
},
|
||||
count: {
|
||||
label: "Number of backups",
|
||||
color: "#60a5fa",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={data}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false}/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => humanReadableDate(Date(value)).split(' ')[0]}
|
||||
/>
|
||||
<YAxis/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel/>}
|
||||
/>
|
||||
<Line
|
||||
dataKey="count"
|
||||
type="linear"
|
||||
stroke="var(--color-desktop)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client"
|
||||
|
||||
import {ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent} from "@/components/ui/chart";
|
||||
import {CartesianGrid, Line, LineChart, XAxis, YAxis} from "recharts";
|
||||
|
||||
type Data = {
|
||||
createdAt: Date;
|
||||
status: "success" | "failed";
|
||||
_count: { id: number };
|
||||
}
|
||||
|
||||
export type percentageLineChartProps = {
|
||||
data: Data[]
|
||||
}
|
||||
|
||||
|
||||
export function PercentageLineChart(props: percentageLineChartProps) {
|
||||
|
||||
const {data} = props
|
||||
|
||||
const chartConfig = {
|
||||
date: {
|
||||
label: "Date",
|
||||
color: "#2563eb",
|
||||
},
|
||||
successRate: {
|
||||
label: "Success Rate",
|
||||
color: "#60a5fa",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
const dailyStats = data.reduce((acc, backup) => {
|
||||
const date = backup.createdAt.toISOString().split("T")[0]; // Format YYYY-MM-DD
|
||||
const status = backup.status;
|
||||
|
||||
if (!acc[date]) {
|
||||
acc[date] = {success: 0, failed: 0, total: 0};
|
||||
}
|
||||
|
||||
acc[date][status === "success" ? "success" : "failed"] += backup._count.id;
|
||||
acc[date].total += backup._count.id;
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, { success: number; failed: number; total: number }>);
|
||||
|
||||
// Format data for the chart
|
||||
const formattedData = Object.entries(dailyStats).map(([date, stats]) => ({
|
||||
date,
|
||||
successRate: (stats.success / stats.total) * 100,
|
||||
}));
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={formattedData}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3"/>
|
||||
<XAxis dataKey="date"/>
|
||||
<YAxis domain={[0, 100]} tickFormatter={(tick) => `${tick}%`}/>
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent/>}
|
||||
cursor={false}
|
||||
defaultIndex={1}
|
||||
formatter={(value, name) => (
|
||||
<div className="flex min-w-[130px] items-center text-xs text-muted-foreground">
|
||||
{chartConfig[name as keyof typeof chartConfig]?.label ||
|
||||
name}
|
||||
<div
|
||||
className="ml-auto flex items-baseline gap-0.5 font-mono font-medium tabular-nums text-foreground">
|
||||
{value}
|
||||
<span className="font-normal text-muted-foreground">
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Line type="step" dataKey="successRate" stroke="#8884d8" strokeWidth={2}/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -10,12 +10,11 @@ import {
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Download, MoreHorizontal, Trash2} from "lucide-react";
|
||||
import {MoreHorizontal} from "lucide-react";
|
||||
import {ReloadIcon} from "@radix-ui/react-icons";
|
||||
import {Restoration} from "@prisma/client";
|
||||
|
||||
|
||||
|
||||
export const restoreColumns: ColumnDef<Restoration>[] = [
|
||||
{
|
||||
accessorKey: "id",
|
||||
|
||||
@@ -4046,10 +4046,10 @@ lru-cache@^5.1.1:
|
||||
dependencies:
|
||||
yallist "^3.0.2"
|
||||
|
||||
lucide-react@^0.454.0:
|
||||
version "0.454.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.454.0.tgz#a81b9c482018720f07ead0503ae502d94d528444"
|
||||
integrity sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==
|
||||
lucide-react@^0.468.0:
|
||||
version "0.468.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.468.0.tgz#830c1bfd905575ddd23b832baa420c87db166910"
|
||||
integrity sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==
|
||||
|
||||
make-dir@^3.1.0:
|
||||
version "3.1.0"
|
||||
@@ -4862,10 +4862,10 @@ recharts-scale@^0.4.4:
|
||||
dependencies:
|
||||
decimal.js-light "^2.4.1"
|
||||
|
||||
recharts@^2.13.3:
|
||||
version "2.13.3"
|
||||
resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.13.3.tgz#a5ce61e493dff921a14a14a8f42a9f2d2bbefd5a"
|
||||
integrity sha512-YDZ9dOfK9t3ycwxgKbrnDlRC4BHdjlY73fet3a0C1+qGMjXVZe6+VXmpOIIhzkje5MMEL8AN4hLIe4AMskBzlA==
|
||||
recharts@^2.15.0:
|
||||
version "2.15.0"
|
||||
resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.15.0.tgz#0b77bff57a43885df9769ae649a14cb1a7fe19aa"
|
||||
integrity sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==
|
||||
dependencies:
|
||||
clsx "^2.0.0"
|
||||
eventemitter3 "^4.0.1"
|
||||
|
||||
Reference in New Issue
Block a user