"Fix" grid positioning, style minirealtimeanalytics
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user