mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Refactoring charts in statistics in organization.
This commit is contained in:
@@ -78,9 +78,6 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||
);
|
||||
|
||||
const Placeholder = ({text}: { text: string }) => (
|
||||
<div className="flex h-48 items-center justify-center text-sm text-muted-foreground">{text}</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
@@ -125,31 +122,12 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
</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>
|
||||
{sortedBackupsEvolution.length > 0 ? (
|
||||
<EvolutionLineChart data={sortedBackupsEvolution}/>
|
||||
) : (
|
||||
<Placeholder text="No backup data available"/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<EvolutionLineChart
|
||||
data={sortedBackupsEvolution}
|
||||
/>
|
||||
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Success rate of backups</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{backupsRate.length > 0 ? (
|
||||
<PercentageLineChart data={backupsRate}/>
|
||||
) : (
|
||||
<Placeholder text="No backup rate data available"/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<PercentageLineChart
|
||||
data={backupsRate}/>
|
||||
</div>
|
||||
</PageContent>
|
||||
</Page>
|
||||
|
||||
@@ -1,47 +1,45 @@
|
||||
"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";
|
||||
import {ChartConfig, ChartTooltip} from "@/components/ui/chart";
|
||||
import {CartesianGrid, TooltipProps, XAxis, YAxis} from "recharts";
|
||||
import {formatDayOnly} from "@/utils/date-formatting";
|
||||
import {LineChartCustom} from "@/components/wrappers/dashboard/statistics/charts/line-chart";
|
||||
import {generateFakeEvolutionData} from "@/components/wrappers/dashboard/statistics/charts/fake-data";
|
||||
|
||||
type Data = {
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
export type evolutionLineChartProps = {
|
||||
data: Data[];
|
||||
type Payload = {
|
||||
date: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export function EvolutionLineChart(props: evolutionLineChartProps) {
|
||||
const { data } = props;
|
||||
type LineChartDatum = {
|
||||
date: string
|
||||
count: number
|
||||
}
|
||||
|
||||
// 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 dailyData = data
|
||||
export type EvolutionLineChartProps = {
|
||||
data: Data[];
|
||||
|
||||
};
|
||||
|
||||
export function EvolutionLineChart(props: EvolutionLineChartProps) {
|
||||
const {data} = props;
|
||||
|
||||
const fakeData = generateFakeEvolutionData(14, 2, 10)
|
||||
|
||||
const dailyData = fakeData
|
||||
.reduce((acc, backup) => {
|
||||
const date = backup.createdAt.toISOString().split("T")[0]; // Format: YYYY-MM-DD
|
||||
const date = backup.createdAt.toISOString().split("T")[0];
|
||||
|
||||
// Find if the date already exists in the accumulator
|
||||
const existing = acc.find(item => item.date === date);
|
||||
|
||||
if (existing) {
|
||||
existing.count += 1;
|
||||
} else {
|
||||
acc.push({ date, count: 1 });
|
||||
acc.push({date, count: 1});
|
||||
}
|
||||
|
||||
return acc;
|
||||
@@ -51,40 +49,66 @@ export function EvolutionLineChart(props: evolutionLineChartProps) {
|
||||
const chartConfig = {
|
||||
date: {
|
||||
label: "Date",
|
||||
color: "#2563eb",
|
||||
},
|
||||
count: {
|
||||
label: "Number of backups",
|
||||
color: "#60a5fa",
|
||||
label: "Number of backups ",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={dailyData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => {
|
||||
return humanReadableDate(new Date(value)).split(" ")[0]
|
||||
}}
|
||||
/>
|
||||
<YAxis />
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Line dataKey="count" type="linear" stroke="#60a5fa" strokeWidth={2} dot={false} />
|
||||
|
||||
{/*<Line dataKey="count" type="linear" stroke="var(--color-desktop)" strokeWidth={2} dot={false} />*/}
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
<LineChartCustom<LineChartDatum>
|
||||
config={chartConfig}
|
||||
data={dailyData}
|
||||
title="Evolution of the number of backups"
|
||||
dataKey="count"
|
||||
>
|
||||
<CartesianGrid vertical={false}/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) =>
|
||||
formatDayOnly(new Date(value))
|
||||
}
|
||||
/>
|
||||
|
||||
<YAxis tickLine={false}/>
|
||||
|
||||
<ChartTooltip
|
||||
cursor={{strokeDasharray: "3 3"}}
|
||||
content={<EvolutionTooltip/>}
|
||||
/>
|
||||
|
||||
</LineChartCustom>
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function EvolutionTooltip({
|
||||
active,
|
||||
payload,
|
||||
}: TooltipProps<number, string>) {
|
||||
if (!active || !payload || payload.length === 0) return null;
|
||||
|
||||
const data = payload[0].payload as Payload;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-background px-3 py-2 shadow-md">
|
||||
<p className="text-sm font-medium">
|
||||
{formatDayOnly(new Date(data.date))}
|
||||
</p>
|
||||
|
||||
<div className="mt-1 flex items-center gap-2 text-sm">
|
||||
<span className="h-2 w-2 rounded-full bg-[#fc6504]"/>
|
||||
<span className="text-muted-foreground">Backups :</span>
|
||||
<span className="ml-auto font-semibold">
|
||||
{data.count}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
type Data = {
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate fake backup events over a time range.
|
||||
*
|
||||
* @param days Number of days to generate
|
||||
* @param minPerDay Minimum events per day
|
||||
* @param maxPerDay Maximum events per day
|
||||
*/
|
||||
export function generateFakeEvolutionData(
|
||||
days: number = 30,
|
||||
minPerDay: number = 1,
|
||||
maxPerDay: number = 8
|
||||
): Data[] {
|
||||
const result: Data[] = [];
|
||||
const now = new Date();
|
||||
|
||||
for (let d = 0; d < days; d++) {
|
||||
const day = new Date(now);
|
||||
day.setDate(now.getDate() - d);
|
||||
|
||||
const events =
|
||||
Math.floor(Math.random() * (maxPerDay - minPerDay + 1)) + minPerDay;
|
||||
|
||||
for (let i = 0; i < events; i++) {
|
||||
const createdAt = new Date(day);
|
||||
createdAt.setHours(
|
||||
Math.floor(Math.random() * 24),
|
||||
Math.floor(Math.random() * 60),
|
||||
Math.floor(Math.random() * 60)
|
||||
);
|
||||
|
||||
result.push({ createdAt });
|
||||
}
|
||||
}
|
||||
|
||||
return result.sort(
|
||||
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import {ChartConfig, ChartContainer} from "@/components/ui/chart";
|
||||
import {Line, LineChart} from "recharts";
|
||||
import {ReactNode, useState} from "react";
|
||||
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {PlaceholderChart} from "@/components/wrappers/dashboard/statistics/charts/utils/placeholder";
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||
|
||||
type LineChartCustomProps<T> = {
|
||||
config: ChartConfig,
|
||||
data: T[],
|
||||
children: ReactNode,
|
||||
title: string,
|
||||
dataKey: string,
|
||||
margin?: { left: number; right: number }
|
||||
}
|
||||
|
||||
|
||||
export const LineChartCustom = <T extends { date: string }>({
|
||||
config,
|
||||
title,
|
||||
data,
|
||||
children,
|
||||
dataKey,
|
||||
margin
|
||||
}: LineChartCustomProps<T>) => {
|
||||
|
||||
|
||||
const [timeRange, setTimeRange] = useState("7d")
|
||||
const filteredData = data.filter((item) => {
|
||||
const date = new Date(item.date)
|
||||
const referenceDate = new Date()
|
||||
let daysToSubtract = 90
|
||||
if (timeRange === "30d") {
|
||||
daysToSubtract = 30
|
||||
} else if (timeRange === "7d") {
|
||||
daysToSubtract = 7
|
||||
}
|
||||
const startDate = new Date(referenceDate)
|
||||
startDate.setDate(startDate.getDate() - daysToSubtract)
|
||||
return date >= startDate
|
||||
})
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<>
|
||||
{data.length > 0 ? (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex items-center gap-4 space-y-0 border-b py-0 sm:flex-row">
|
||||
<CardTitle className="text-sm md:text-lg">{title}</CardTitle>
|
||||
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||
<SelectTrigger
|
||||
className=" rounded-lg sm:ml-auto sm:flex"
|
||||
aria-label="Select a value"
|
||||
>
|
||||
<SelectValue placeholder="Last 3 months"/>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl">
|
||||
<SelectItem value="7d" className="rounded-lg">
|
||||
Last 7 days
|
||||
</SelectItem>
|
||||
<SelectItem value="30d" className="rounded-lg">
|
||||
Last 30 days
|
||||
</SelectItem>
|
||||
<SelectItem value="90d" className="rounded-lg">
|
||||
Last 3 months
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
</CardHeader>
|
||||
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6 md:pt-0 md:px-6 ">
|
||||
<ChartContainer config={config}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={filteredData}
|
||||
margin={margin ? margin : {
|
||||
left: -35,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
<Line
|
||||
dataKey={dataKey}
|
||||
type="linear"
|
||||
strokeWidth={2}
|
||||
stroke="#fc6504"
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
) : (
|
||||
<PlaceholderChart text="No backup data available"/>
|
||||
)}
|
||||
|
||||
|
||||
</>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
|
||||
import { EStatusSchema } from "@/db/schema/types";
|
||||
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
|
||||
import {ChartConfig, ChartTooltip} from "@/components/ui/chart";
|
||||
import {EStatusSchema} from "@/db/schema/types";
|
||||
import {LineChartCustom} from "@/components/wrappers/dashboard/statistics/charts/line-chart";
|
||||
import {CartesianGrid, TooltipProps, XAxis, YAxis} from "recharts";
|
||||
import {formatDayOnly} from "@/utils/date-formatting";
|
||||
|
||||
type Data = {
|
||||
createdAt: Date;
|
||||
@@ -10,31 +12,26 @@ type Data = {
|
||||
_count: number;
|
||||
};
|
||||
|
||||
type Payload = {
|
||||
date: string
|
||||
successRate: number
|
||||
}
|
||||
|
||||
export type percentageLineChartProps = {
|
||||
data: Data[];
|
||||
};
|
||||
|
||||
export function PercentageLineChart(props: percentageLineChartProps) {
|
||||
const { data } = props;
|
||||
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 date = backup.createdAt.toISOString().split("T")[0];
|
||||
const status = backup.status;
|
||||
|
||||
if (!acc[date]) {
|
||||
acc[date] = { success: 0, failed: 0, total: 0 };
|
||||
acc[date] = {success: 0, failed: 0, total: 0};
|
||||
}
|
||||
|
||||
acc[date][status === "success" ? "success" : "failed"] += backup._count;
|
||||
@@ -45,34 +42,81 @@ export function PercentageLineChart(props: percentageLineChartProps) {
|
||||
{} 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,
|
||||
}));
|
||||
|
||||
|
||||
const chartConfig = {
|
||||
date: {
|
||||
label: "Date",
|
||||
},
|
||||
successRate: {
|
||||
label: "Success Rate",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
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>
|
||||
|
||||
<LineChartCustom<Payload>
|
||||
title="Success rate of backups"
|
||||
config={chartConfig}
|
||||
data={formattedData}
|
||||
dataKey="successRate"
|
||||
margin={{
|
||||
left: -15,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
{/*<CartesianGrid strokeDasharray="3 3" />*/}
|
||||
<CartesianGrid vertical={false}/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) =>
|
||||
formatDayOnly(new Date(value))
|
||||
}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
domain={[0, 100]}
|
||||
tickFormatter={(tick) => `${tick}%`}
|
||||
/>
|
||||
<ChartTooltip
|
||||
defaultIndex={1}
|
||||
cursor={{strokeDasharray: "3 3"}}
|
||||
content={<PourcentTooltip/>}
|
||||
/>
|
||||
</LineChartCustom>
|
||||
);
|
||||
}
|
||||
|
||||
function PourcentTooltip({
|
||||
active,
|
||||
payload,
|
||||
}: TooltipProps<number, string>) {
|
||||
if (!active || !payload || payload.length === 0) return null;
|
||||
|
||||
const data = payload[0].payload as Payload;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-background px-3 py-2 shadow-md">
|
||||
<p className="text-sm font-medium">
|
||||
{formatDayOnly(new Date(data.date))}
|
||||
</p>
|
||||
|
||||
<div className="mt-1 flex items-center gap-2 text-sm">
|
||||
<span className="h-2 w-2 rounded-full bg-[#fc6504]"/>
|
||||
<span className="text-muted-foreground">Success Rate :</span>
|
||||
<span className="ml-auto font-semibold">
|
||||
{data.successRate} %
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
export const PlaceholderChart = ({text}: { text: string }) => (
|
||||
<div className="flex h-48 items-center justify-center text-sm text-muted-foreground">{text}</div>
|
||||
);
|
||||
@@ -38,3 +38,12 @@ export function formatDateLastContact(lastContact: string | number | Date | null
|
||||
: "Never connected.";
|
||||
}
|
||||
|
||||
export function formatDayOnly(date: Date) {
|
||||
return new Intl.DateTimeFormat(LOCALE, {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
timeZone: TIMEZONE,
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user