"Fix" grid positioning, style minirealtimeanalytics

This commit is contained in:
2025-01-01 22:23:50 -05:00
parent 11ef78e27b
commit 925eda8677
3 changed files with 182 additions and 229 deletions

View File

@@ -65,7 +65,7 @@ const PinProtectedLayout = ({ children }) => {
const SmallLayout = () => { const SmallLayout = () => {
const DATETIME_SCALE = 2; const DATETIME_SCALE = 2;
const STATS_SCALE = 1.65; const STATS_SCALE = 1.65;
const ANALYTICS_SCALE = 1.5; const ANALYTICS_SCALE = 1.65;
return ( return (
<div className="min-h-screen w-screen"> <div className="min-h-screen w-screen">
@@ -74,46 +74,43 @@ const SmallLayout = () => {
</span> </span>
<div className="p-4 grid grid-cols-12 gap-4"> <div className="p-4 grid grid-cols-12 gap-4">
<div className="col-span-3 relative"> {/* DateTime */}
<div <div className="col-span-3">
className="origin-top-left" <div style={{
style={{
transform: `scale(${DATETIME_SCALE})`, transform: `scale(${DATETIME_SCALE})`,
width: `${100/DATETIME_SCALE}%`, transformOrigin: 'top left',
}} width: `${100/DATETIME_SCALE}%`
> }}>
<DateTimeWeatherDisplay scaleFactor={DATETIME_SCALE} /> <DateTimeWeatherDisplay />
</div> </div>
</div> </div>
<div className="col-span-9 relative"> {/* Stats and Analytics */}
<div <div className="col-span-9">
className="origin-top-left" <div>
style={{ <div style={{
transform: `scale(${STATS_SCALE})`, transform: `scale(${STATS_SCALE})`,
width: `${100/STATS_SCALE}%`, transformOrigin: 'top left',
}} width: `${100/STATS_SCALE}%`
> }}>
<MiniStatCards <MiniStatCards
title="Live Stats" title="Live Stats"
timeRange="today" timeRange="today"
/> />
</div> </div>
</div> </div>
<div className="w-1/2 ml-auto mt-28 pl-1">
<div className="col-span-12 relative"> <div style={{
<div
className="origin-top-left"
style={{
transform: `scale(${ANALYTICS_SCALE})`, transform: `scale(${ANALYTICS_SCALE})`,
width: `${100/ANALYTICS_SCALE}%`, transformOrigin: 'top left',
}} width: `${100/ANALYTICS_SCALE}%`
> }}>
<MiniRealtimeAnalytics /> <MiniRealtimeAnalytics />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@@ -100,19 +100,9 @@ const MiniRealtimeAnalytics = () => {
} }
return ( return (
<Card className="w-full bg-gradient-to-br from-purple-900 to-purple-800 backdrop-blur-sm h-full">
<CardHeader className="p-4 pb-2">
<div className="flex justify-between items-center">
<CardTitle className="text-lg font-bold text-purple-100">
Real-Time Analytics
</CardTitle>
<div className="text-xs text-purple-200">
{format(new Date(basicData.lastUpdated), "h:mm a")}
</div>
</div>
</CardHeader>
<CardContent className="p-4 pt-0">
<CardContent className="p-0">
{error && ( {error && (
<Alert variant="destructive" className="mb-4"> <Alert variant="destructive" className="mb-4">
<AlertTriangle className="h-4 w-4" /> <AlertTriangle className="h-4 w-4" />
@@ -126,10 +116,10 @@ const MiniRealtimeAnalytics = () => {
"Active users", "Active users",
basicData.last30MinUsers, basicData.last30MinUsers,
{ {
colorClass: "text-purple-200", colorClass: "text-sky-200",
titleClass: "text-purple-100 font-bold text-md", titleClass: "text-sky-100 font-bold text-md",
descriptionClass: "text-purple-200 text-md font-semibold", descriptionClass: "text-sky-200 text-md font-semibold",
background: "bg-gradient-to-br from-purple-800 to-purple-700" background: "bg-gradient-to-br from-sky-800 to-sky-700"
} }
)} )}
{summaryCard( {summaryCard(
@@ -137,15 +127,15 @@ const MiniRealtimeAnalytics = () => {
"Active users", "Active users",
basicData.last5MinUsers, basicData.last5MinUsers,
{ {
colorClass: "text-purple-200", colorClass: "text-sky-200",
titleClass: "text-purple-100 font-bold text-md", titleClass: "text-sky-100 font-bold text-md",
descriptionClass: "text-purple-200 text-md font-semibold", descriptionClass: "text-sky-200 text-md font-semibold",
background: "bg-gradient-to-br from-purple-800 to-purple-700" background: "bg-gradient-to-br from-sky-800 to-sky-700"
} }
)} )}
</div> </div>
<div className="h-[235px] bg-gradient-to-br from-purple-800 to-purple-700 rounded-lg p-2"> <div className="bg-gradient-to-br from-sky-800 to-sky-700 rounded-lg p-2">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<BarChart <BarChart
data={basicData.byMinute} data={basicData.byMinute}
@@ -155,11 +145,11 @@ const MiniRealtimeAnalytics = () => {
dataKey="minute" dataKey="minute"
tickFormatter={(value) => value + "m"} tickFormatter={(value) => value + "m"}
className="text-xs" className="text-xs"
tick={{ fill: "#E9D5FF" }} tick={{ fill: "#BAE6FD" }}
/> />
<YAxis <YAxis
className="text-xs" className="text-xs"
tick={{ fill: "#E9D5FF" }} tick={{ fill: "#BAE6FD" }}
/> />
<Tooltip <Tooltip
content={({ active, payload }) => { content={({ active, payload }) => {
@@ -168,16 +158,16 @@ const MiniRealtimeAnalytics = () => {
Date.now() + payload[0].payload.minute * 60000 Date.now() + payload[0].payload.minute * 60000
); );
return ( return (
<Card className="p-2 shadow-lg bg-purple-800 border-none"> <Card className="p-2 shadow-lg bg-sky-800 border-none">
<CardContent className="p-0 space-y-1"> <CardContent className="p-0 space-y-1">
<p className="font-medium text-sm text-purple-100 border-b border-purple-700 pb-1 mb-1"> <p className="font-medium text-sm text-sky-100 border-b border-sky-700 pb-1 mb-1">
{format(timestamp, "h:mm a")} {format(timestamp, "h:mm a")}
</p> </p>
<div className="flex justify-between items-center text-sm"> <div className="flex justify-between items-center text-sm">
<span className="text-purple-200"> <span className="text-sky-200">
Active Users: Active Users:
</span> </span>
<span className="font-medium ml-4 text-purple-100"> <span className="font-medium ml-4 text-sky-100">
{payload[0].value.toLocaleString()} {payload[0].value.toLocaleString()}
</span> </span>
</div> </div>
@@ -188,12 +178,11 @@ const MiniRealtimeAnalytics = () => {
return null; return null;
}} }}
/> />
<Bar dataKey="users" fill="#A855F7" /> <Bar dataKey="users" fill="#0EA5E9" />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
</CardContent> </CardContent>
</Card>
); );
}; };

View File

@@ -32,7 +32,7 @@ import {
import { format } from "date-fns"; import { format } from "date-fns";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
const METRIC_COLORS = { export const METRIC_COLORS = {
activeUsers: { activeUsers: {
color: "#8b5cf6", color: "#8b5cf6",
className: "text-purple-600 dark:text-purple-400", className: "text-purple-600 dark:text-purple-400",
@@ -47,50 +47,154 @@ const METRIC_COLORS = {
}, },
}; };
const formatNumber = (value, decimalPlaces = 0) => { export const summaryCard = (label, sublabel, value, options = {}) => {
return new Intl.NumberFormat("en-US", {
minimumFractionDigits: decimalPlaces,
maximumFractionDigits: decimalPlaces,
}).format(value || 0);
};
const formatPercent = (value, decimalPlaces = 1) =>
`${(value || 0).toFixed(decimalPlaces)}%`;
const summaryCard = (label, sublabel, value, options = {}) => {
const { const {
isMonetary = false, colorClass = "text-gray-900 dark:text-gray-100",
isPercentage = false, titleClass = "text-sm font-medium text-gray-600 dark:text-gray-300",
decimalPlaces = 0, descriptionClass = "text-sm text-muted-foreground",
colorClass = METRIC_COLORS.activeUsers.className, background = "bg-white dark:bg-gray-900/60",
} = options; } = options;
let displayValue;
if (isMonetary) {
displayValue = formatCurrency(value, decimalPlaces);
} else if (isPercentage) {
displayValue = formatPercent(value, decimalPlaces);
} else {
displayValue = formatNumber(value, decimalPlaces);
}
return ( return (
<Card> <Card className={`${background} backdrop-blur-sm`}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 px-4 py-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 px-4 py-2">
<span className="text-md">{label}</span> <span className={titleClass}>{label}</span>
</CardHeader> </CardHeader>
<CardContent className="px-4 pt-0 pb-2"> <CardContent className="px-4 pt-0 pb-2">
<div className={`text-2xl font-bold mb-1 ${colorClass}`}> <div className={`text-2xl font-bold mb-1 ${colorClass}`}>
{displayValue} {value.toLocaleString()}
</div> </div>
<div className="text-sm text-muted-foreground">{sublabel}</div> <div className={descriptionClass}>{sublabel}</div>
</CardContent> </CardContent>
</Card> </Card>
); );
}; };
const QuotaInfo = ({ tokenQuota }) => { export const SkeletonSummaryCard = () => (
// Add early return if tokenQuota is null or undefined <Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader className="flex flex-row items-center justify-between space-y-0 px-4 py-2">
<Skeleton className="h-4 w-24 bg-muted" />
</CardHeader>
<CardContent className="px-4 pt-0 pb-2">
<Skeleton className="h-8 w-20 mb-1 bg-muted" />
<Skeleton className="h-4 w-32 bg-muted" />
</CardContent>
</Card>
);
export const SkeletonBarChart = () => (
<div className="h-[235px] bg-white dark:bg-gray-900/60 backdrop-blur-sm rounded-lg p-4">
<div className="h-full relative">
{/* Grid lines */}
{[...Array(5)].map((_, i) => (
<div
key={i}
className="absolute w-full h-px bg-muted"
style={{ top: `${(i + 1) * 20}%` }}
/>
))}
{/* Y-axis labels */}
<div className="absolute left-0 top-0 bottom-0 w-8 flex flex-col justify-between py-4">
{[...Array(5)].map((_, i) => (
<Skeleton key={i} className="h-3 w-6 bg-muted" />
))}
</div>
{/* X-axis labels */}
<div className="absolute left-8 right-4 bottom-0 flex justify-between">
{[...Array(6)].map((_, i) => (
<Skeleton key={i} className="h-3 w-8 bg-muted" />
))}
</div>
{/* Bars */}
<div className="absolute inset-x-8 bottom-6 top-4 flex items-end justify-between">
{[...Array(30)].map((_, i) => (
<div
key={i}
className="w-1.5 bg-muted"
style={{
height: `${Math.random() * 80 + 10}%`,
}}
/>
))}
</div>
</div>
</div>
);
export const SkeletonTable = () => (
<div className="space-y-2 h-[230px] overflow-y-auto">
<Table>
<TableHeader>
<TableRow className="dark:border-gray-800">
<TableHead>
<Skeleton className="h-4 w-32 bg-muted" />
</TableHead>
<TableHead className="text-right">
<Skeleton className="h-4 w-24 ml-auto bg-muted" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{[...Array(8)].map((_, i) => (
<TableRow key={i} className="dark:border-gray-800">
<TableCell>
<Skeleton className="h-4 w-48 bg-muted" />
</TableCell>
<TableCell className="text-right">
<Skeleton className="h-4 w-12 ml-auto bg-muted" />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
export const processBasicData = (data) => {
const last30MinUsers = parseInt(
data.userResponse?.rows?.[0]?.metricValues?.[0]?.value || 0
);
const last5MinUsers = parseInt(
data.fiveMinResponse?.rows?.[0]?.metricValues?.[0]?.value || 0
);
const byMinute = Array.from({ length: 30 }, (_, i) => {
const matchingRow = data.timeSeriesResponse?.rows?.find(
(row) => parseInt(row.dimensionValues[0].value) === i
);
const users = matchingRow
? parseInt(matchingRow.metricValues[0].value)
: 0;
const timestamp = new Date(Date.now() - i * 60000);
return {
minute: -i,
users,
timestamp: timestamp.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
}),
};
}).reverse();
const tokenQuota = data.quotaInfo
? {
projectHourly: data.quotaInfo.projectHourly || {},
daily: data.quotaInfo.daily || {},
serverErrors: data.quotaInfo.serverErrors || {},
thresholdedRequests: data.quotaInfo.thresholdedRequests || {},
}
: null;
return {
last30MinUsers,
last5MinUsers,
byMinute,
tokenQuota,
lastUpdated: new Date().toISOString(),
};
};
export const QuotaInfo = ({ tokenQuota }) => {
if (!tokenQuota || typeof tokenQuota !== "object") return null; if (!tokenQuota || typeof tokenQuota !== "object") return null;
const { const {
@@ -100,7 +204,6 @@ const QuotaInfo = ({ tokenQuota }) => {
thresholdedRequests = {}, thresholdedRequests = {},
} = tokenQuota; } = tokenQuota;
// Add null checks and default values for all properties
const { const {
remaining: projectHourlyRemaining = 0, remaining: projectHourlyRemaining = 0,
consumed: projectHourlyConsumed = 0, consumed: projectHourlyConsumed = 0,
@@ -116,13 +219,11 @@ const QuotaInfo = ({ tokenQuota }) => {
consumed: thresholdConsumed = 0, consumed: thresholdConsumed = 0,
} = thresholdedRequests; } = thresholdedRequests;
// Calculate percentages with safe math
const hourlyPercentage = ((projectHourlyRemaining / 14000) * 100).toFixed(1); const hourlyPercentage = ((projectHourlyRemaining / 14000) * 100).toFixed(1);
const dailyPercentage = ((dailyRemaining / 200000) * 100).toFixed(1); const dailyPercentage = ((dailyRemaining / 200000) * 100).toFixed(1);
const errorPercentage = ((errorsRemaining / 10) * 100).toFixed(1); const errorPercentage = ((errorsRemaining / 10) * 100).toFixed(1);
const thresholdPercentage = ((thresholdRemaining / 120) * 100).toFixed(1); const thresholdPercentage = ((thresholdRemaining / 120) * 100).toFixed(1);
// Determine color based on remaining percentage
const getStatusColor = (percentage) => { const getStatusColor = (percentage) => {
const numericPercentage = parseFloat(percentage); const numericPercentage = parseFloat(percentage);
if (isNaN(numericPercentage) || numericPercentage < 20) if (isNaN(numericPercentage) || numericPercentage < 20)
@@ -180,86 +281,6 @@ const QuotaInfo = ({ tokenQuota }) => {
); );
}; };
const SkeletonSummaryCard = () => (
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader className="flex flex-row items-center justify-between space-y-0 px-4 py-2">
<Skeleton className="h-4 w-24 bg-muted" />
</CardHeader>
<CardContent className="px-4 pt-0 pb-2">
<Skeleton className="h-8 w-20 mb-1 bg-muted" />
<Skeleton className="h-4 w-32 bg-muted" />
</CardContent>
</Card>
);
const SkeletonBarChart = () => (
<div className="h-[235px] bg-card rounded-lg p-4">
<div className="h-full relative">
{/* Grid lines */}
{[...Array(5)].map((_, i) => (
<div
key={i}
className="absolute w-full h-px bg-muted"
style={{ top: `${(i + 1) * 20}%` }}
/>
))}
{/* Y-axis labels */}
<div className="absolute left-0 top-0 bottom-0 w-8 flex flex-col justify-between py-4">
{[...Array(5)].map((_, i) => (
<Skeleton key={i} className="h-3 w-6 bg-muted" />
))}
</div>
{/* X-axis labels */}
<div className="absolute left-8 right-4 bottom-0 flex justify-between">
{[...Array(6)].map((_, i) => (
<Skeleton key={i} className="h-3 w-8 bg-muted" />
))}
</div>
{/* Bars */}
<div className="absolute inset-x-8 bottom-6 top-4 flex items-end justify-between">
{[...Array(30)].map((_, i) => (
<div
key={i}
className="w-1.5 bg-muted"
style={{
height: `${Math.random() * 80 + 10}%`,
}}
/>
))}
</div>
</div>
</div>
);
const SkeletonTable = () => (
<div className="space-y-2 h-[230px] overflow-y-auto">
<Table>
<TableHeader>
<TableRow className="dark:border-gray-800">
<TableHead>
<Skeleton className="h-4 w-32 bg-muted" />
</TableHead>
<TableHead className="text-right">
<Skeleton className="h-4 w-24 ml-auto bg-muted" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{[...Array(8)].map((_, i) => (
<TableRow key={i} className="dark:border-gray-800">
<TableCell className="py-2.5">
<Skeleton className="h-4 w-48 bg-muted" />
</TableCell>
<TableCell className="text-right py-2.5">
<Skeleton className="h-4 w-12 ml-auto bg-muted" />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
export const RealtimeAnalytics = () => { export const RealtimeAnalytics = () => {
const [basicData, setBasicData] = useState({ const [basicData, setBasicData] = useState({
last30MinUsers: 0, last30MinUsers: 0,
@@ -279,50 +300,6 @@ export const RealtimeAnalytics = () => {
const [isPaused, setIsPaused] = useState(false); const [isPaused, setIsPaused] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const processBasicData = (data) => {
const last30MinUsers = parseInt(
data.userResponse?.rows?.[0]?.metricValues?.[0]?.value || 0
);
const last5MinUsers = parseInt(
data.fiveMinResponse?.rows?.[0]?.metricValues?.[0]?.value || 0
);
const byMinute = Array.from({ length: 30 }, (_, i) => {
const matchingRow = data.timeSeriesResponse?.rows?.find(
(row) => parseInt(row.dimensionValues[0].value) === i
);
const users = matchingRow
? parseInt(matchingRow.metricValues[0].value)
: 0;
const timestamp = new Date(Date.now() - i * 60000);
return {
minute: -i,
users,
timestamp: timestamp.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
}),
};
}).reverse();
const tokenQuota = data.quotaInfo
? {
projectHourly: data.quotaInfo.projectHourly || {},
daily: data.quotaInfo.daily || {},
serverErrors: data.quotaInfo.serverErrors || {},
thresholdedRequests: data.quotaInfo.thresholdedRequests || {},
}
: null;
return {
last30MinUsers,
last5MinUsers,
byMinute,
tokenQuota,
lastUpdated: new Date().toISOString(),
};
};
const processDetailedData = (data) => { const processDetailedData = (data) => {
return { return {
currentPages: currentPages:
@@ -636,14 +613,4 @@ export const RealtimeAnalytics = () => {
); );
}; };
export {
summaryCard,
SkeletonSummaryCard,
SkeletonBarChart,
SkeletonTable,
processBasicData,
METRIC_COLORS,
QuotaInfo
};
export default RealtimeAnalytics; export default RealtimeAnalytics;