feat: added storage drilldown in analytics

Signed-off-by: Varun Raj <varun@skcript.com>
This commit is contained in:
Varun Raj committed 2024-11-30 17:58:27 +05:30
1 parent 4dd64aa1ff
commit 9139e82107
8 files changed
+66 -18

No files matched your search

+5 -3
View File
@@ -85,14 +85,16 @@ bun run dev
- [x] Manage People
- [x] Smart Merge
- [ ] Manage Albums
- [x] Manage Albums
- [ ] Bulk Delete
- [ ] Bulk Edit
- [ ] Filters
- [x] Potential Albums
- [ ] Statistics
- [x] People in Album
- [x] Missing Location
- [x] Statistics
- [x] EXIF Data
- [ ] Assets Overtime Chart
- [x] Assets Overtime Chart
**Tech Related**
@@ -2,15 +2,17 @@ import { Card } from "@/components/ui/card";
import PieChart, { IPieChartData } from "@/components/ui/pie-chart";
import { getExifDistribution, ISupportedEXIFColumns } from "@/handlers/api/analytics.handler";
import React, { useEffect, useState } from "react";
import { ValueType } from "recharts/types/component/DefaultTooltipContent";
export interface IEXIFDistributionProps {
column: ISupportedEXIFColumns;
title: string;
description: string;
tooltipValueFormatter?: (value?: number | string | undefined | ValueType) => string;
}
export default function EXIFDistribution(
{ column, title, description }: IEXIFDistributionProps
{ column, title, description, tooltipValueFormatter }: IEXIFDistributionProps
) {
const [chartData, setChartData] = useState<IPieChartData[]>([]);
const [loading, setLoading] = useState(false);
@@ -26,13 +28,12 @@ export default function EXIFDistribution(
fetchData();
}, [column]);
return (
<Card
title={title}
description={description}
>
<PieChart data={chartData} loading={loading} errorMessage={errorMessage} />
<PieChart data={chartData} loading={loading} errorMessage={errorMessage} tooltipValueFormatter={tooltipValueFormatter} />
</Card>
);
}
+3 -1
View File
@@ -114,6 +114,7 @@ const ChartTooltipContent = React.forwardRef<
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
valueFormatter?: (value: number | string | undefined | ValueType) => string
}
>(
(
@@ -126,6 +127,7 @@ const ChartTooltipContent = React.forwardRef<
hideIndicator = false,
label,
labelFormatter,
valueFormatter = (value) => value?.toLocaleString(),
labelClassName,
formatter,
color,
@@ -243,7 +245,7 @@ const ChartTooltipContent = React.forwardRef<
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
{valueFormatter(item.value)}
</span>
)}
</div>
+11 -5
View File
@@ -2,17 +2,18 @@ import { CHART_COLORS, PIE_CONFIG } from "@/config/constants/chart.constant";
import React, { useMemo } from "react";
import { Label, Pie, PieChart as PieChartRoot } from "recharts";
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "./chart";
import { int } from "drizzle-orm/mysql-core";
import { ValueType } from "recharts/types/component/DefaultTooltipContent";
export interface IPieChartData { label: string; value: number }
export interface IPieChartData { label: string; value: number; valueLabel?: string }
interface ChartProps {
data: IPieChartData[];
topLabel?: string;
loading?: boolean;
errorMessage?: string | null;
tooltipValueFormatter?: (value: number | string | undefined | ValueType) => string;
}
export default function PieChart({ data: _data, topLabel, loading, errorMessage }: ChartProps) {
export default function PieChart({ data: _data, topLabel, loading, errorMessage, tooltipValueFormatter }: ChartProps) {
const topItem = _data[0];
const data = useMemo(() => _data.map((item, index) => ({
@@ -45,7 +46,12 @@ export default function PieChart({ data: _data, topLabel, loading, errorMessage
<PieChartRoot accessibilityLayer>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
content={<ChartTooltipContent
hideLabel
nameKey="value"
labelKey="valueLabel"
valueFormatter={tooltipValueFormatter}
/>}
/>
<Pie
dataKey="value"
@@ -71,7 +77,7 @@ export default function PieChart({ data: _data, topLabel, loading, errorMessage
y={viewBox.cy}
className="fill-foreground text-3xl font-bold"
>
{topItem.value.toLocaleString()}
{(topItem.valueLabel || topItem.value).toLocaleString()}
</tspan>
<tspan
x={viewBox.cx}
+1 -1
View File
@@ -2,7 +2,7 @@ import { ASSET_STATISTICS, EXIF_DISTRIBUTION_PATH, HEATMAP_DATA, LIVE_PHOTO_STAT
import API from "@/lib/api";
export type ISupportedEXIFColumns =
"make" | "model" | "focal-length" | "city" | "state" | "country" | "iso" | "exposureTime" | 'lensModel' | "projectionType";
"make" | "model" | "focal-length" | "city" | "state" | "country" | "iso" | "exposureTime" | 'lensModel' | "projectionType" | "storage";
export const getExifDistribution = async (column: ISupportedEXIFColumns) => {
return API.get(EXIF_DISTRIBUTION_PATH(column));
+8 -3
View File
@@ -7,18 +7,22 @@ import EXIFDistribution, {
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import AssetHeatMap from "@/components/analytics/exif/AssetHeatMap";
import { useEffect, useState } from "react";
import { getAssetStatistics, getLivePhotoStatistics } from "@/handlers/api/analytics.handler";
import { humanizeBytes } from "@/helpers/string.helper";
const inter = Inter({ subsets: ["latin"] });
const exifCharts: IEXIFDistributionProps[] = [
{
column: "storage",
title: "Storage",
description: "Distribution of storage",
tooltipValueFormatter: (value) => humanizeBytes(value as number * 1000000),
},
{
column: "make",
title: "Make",
@@ -132,6 +136,7 @@ export default function ExifDataAnalytics() {
column={chart.column}
title={chart.title}
description={chart.description}
tooltipValueFormatter={chart.tooltipValueFormatter}
/>
))}
+1 -2
View File
@@ -1,5 +1,4 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { CHART_COLORS } from "@/config/constants/chart.constant";
import { db } from "@/config/db";
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
import { assets, exif } from "@/schema";
@@ -16,7 +15,7 @@ const columnMap = {
iso: exif.iso,
exposureTime: exif.exposureTime,
lensModel: exif.lensModel,
projectionType: exif.projectionType
projectionType: exif.projectionType,
}
export default async function handler(
+33
View File
@@ -0,0 +1,33 @@
import { db } from "@/config/db";
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
import { humanizeBytes } from "@/helpers/string.helper";
import { assets, exif, users } from "@/schema";
import { Value } from "@radix-ui/react-select";
import { desc, eq } from "drizzle-orm";
import { sum } from "drizzle-orm";
import { NextApiResponse } from "next";
import { NextApiRequest } from "next";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const currentUser = await getCurrentUser(req);
if (!currentUser) {
return res.status(401).json({ error: "Unauthorized" });
}
const dbStorage = await db.select({
label: users.name,
value: sum(exif.fileSizeInByte)
})
.from(assets)
.leftJoin(exif, eq(assets.id, exif.assetId))
.leftJoin(users, eq(assets.ownerId, users.id))
.orderBy(desc(sum(exif.fileSizeInByte)))
.groupBy(assets.ownerId, users.name)
const cleanedData = dbStorage.map((item) => ({
label: item.label,
value: Math.round((parseInt(item.value ?? "0") / 1000000)),
valueLabel: humanizeBytes(parseInt(item.value ?? "0")),
}));
res.status(200).json(cleanedData);
}