feat: added storage drilldown in analytics
Signed-off-by: Varun Raj <varun@skcript.com>
This commit is contained in:
1 parent
4dd64aa1ff
commit
9139e82107
8 files changed
+66
-18
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user