"Fix" grid positioning, style minirealtimeanalytics
This commit is contained in:
@@ -65,7 +65,7 @@ const PinProtectedLayout = ({ children }) => {
|
||||
const SmallLayout = () => {
|
||||
const DATETIME_SCALE = 2;
|
||||
const STATS_SCALE = 1.65;
|
||||
const ANALYTICS_SCALE = 1.5;
|
||||
const ANALYTICS_SCALE = 1.65;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-screen">
|
||||
@@ -74,46 +74,43 @@ const SmallLayout = () => {
|
||||
</span>
|
||||
|
||||
<div className="p-4 grid grid-cols-12 gap-4">
|
||||
<div className="col-span-3 relative">
|
||||
<div
|
||||
className="origin-top-left"
|
||||
style={{
|
||||
{/* DateTime */}
|
||||
<div className="col-span-3">
|
||||
<div style={{
|
||||
transform: `scale(${DATETIME_SCALE})`,
|
||||
width: `${100/DATETIME_SCALE}%`,
|
||||
}}
|
||||
>
|
||||
<DateTimeWeatherDisplay scaleFactor={DATETIME_SCALE} />
|
||||
transformOrigin: 'top left',
|
||||
width: `${100/DATETIME_SCALE}%`
|
||||
}}>
|
||||
<DateTimeWeatherDisplay />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-9 relative">
|
||||
<div
|
||||
className="origin-top-left"
|
||||
style={{
|
||||
{/* Stats and Analytics */}
|
||||
<div className="col-span-9">
|
||||
<div>
|
||||
<div style={{
|
||||
transform: `scale(${STATS_SCALE})`,
|
||||
width: `${100/STATS_SCALE}%`,
|
||||
}}
|
||||
>
|
||||
transformOrigin: 'top left',
|
||||
width: `${100/STATS_SCALE}%`
|
||||
}}>
|
||||
<MiniStatCards
|
||||
title="Live Stats"
|
||||
timeRange="today"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 relative">
|
||||
<div
|
||||
className="origin-top-left"
|
||||
style={{
|
||||
<div className="w-1/2 ml-auto mt-28 pl-1">
|
||||
<div style={{
|
||||
transform: `scale(${ANALYTICS_SCALE})`,
|
||||
width: `${100/ANALYTICS_SCALE}%`,
|
||||
}}
|
||||
>
|
||||
transformOrigin: 'top left',
|
||||
width: `${100/ANALYTICS_SCALE}%`
|
||||
}}>
|
||||
<MiniRealtimeAnalytics />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -100,19 +100,9 @@ const MiniRealtimeAnalytics = () => {
|
||||
}
|
||||
|
||||
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 && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
@@ -126,10 +116,10 @@ const MiniRealtimeAnalytics = () => {
|
||||
"Active users",
|
||||
basicData.last30MinUsers,
|
||||
{
|
||||
colorClass: "text-purple-200",
|
||||
titleClass: "text-purple-100 font-bold text-md",
|
||||
descriptionClass: "text-purple-200 text-md font-semibold",
|
||||
background: "bg-gradient-to-br from-purple-800 to-purple-700"
|
||||
colorClass: "text-sky-200",
|
||||
titleClass: "text-sky-100 font-bold text-md",
|
||||
descriptionClass: "text-sky-200 text-md font-semibold",
|
||||
background: "bg-gradient-to-br from-sky-800 to-sky-700"
|
||||
}
|
||||
)}
|
||||
{summaryCard(
|
||||
@@ -137,15 +127,15 @@ const MiniRealtimeAnalytics = () => {
|
||||
"Active users",
|
||||
basicData.last5MinUsers,
|
||||
{
|
||||
colorClass: "text-purple-200",
|
||||
titleClass: "text-purple-100 font-bold text-md",
|
||||
descriptionClass: "text-purple-200 text-md font-semibold",
|
||||
background: "bg-gradient-to-br from-purple-800 to-purple-700"
|
||||
colorClass: "text-sky-200",
|
||||
titleClass: "text-sky-100 font-bold text-md",
|
||||
descriptionClass: "text-sky-200 text-md font-semibold",
|
||||
background: "bg-gradient-to-br from-sky-800 to-sky-700"
|
||||
}
|
||||
)}
|
||||
</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%">
|
||||
<BarChart
|
||||
data={basicData.byMinute}
|
||||
@@ -155,11 +145,11 @@ const MiniRealtimeAnalytics = () => {
|
||||
dataKey="minute"
|
||||
tickFormatter={(value) => value + "m"}
|
||||
className="text-xs"
|
||||
tick={{ fill: "#E9D5FF" }}
|
||||
tick={{ fill: "#BAE6FD" }}
|
||||
/>
|
||||
<YAxis
|
||||
className="text-xs"
|
||||
tick={{ fill: "#E9D5FF" }}
|
||||
tick={{ fill: "#BAE6FD" }}
|
||||
/>
|
||||
<Tooltip
|
||||
content={({ active, payload }) => {
|
||||
@@ -168,16 +158,16 @@ const MiniRealtimeAnalytics = () => {
|
||||
Date.now() + payload[0].payload.minute * 60000
|
||||
);
|
||||
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">
|
||||
<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")}
|
||||
</p>
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-purple-200">
|
||||
<span className="text-sky-200">
|
||||
Active Users:
|
||||
</span>
|
||||
<span className="font-medium ml-4 text-purple-100">
|
||||
<span className="font-medium ml-4 text-sky-100">
|
||||
{payload[0].value.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
@@ -188,12 +178,11 @@ const MiniRealtimeAnalytics = () => {
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey="users" fill="#A855F7" />
|
||||
<Bar dataKey="users" fill="#0EA5E9" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
import { format } from "date-fns";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const METRIC_COLORS = {
|
||||
export const METRIC_COLORS = {
|
||||
activeUsers: {
|
||||
color: "#8b5cf6",
|
||||
className: "text-purple-600 dark:text-purple-400",
|
||||
@@ -47,50 +47,154 @@ const METRIC_COLORS = {
|
||||
},
|
||||
};
|
||||
|
||||
const formatNumber = (value, decimalPlaces = 0) => {
|
||||
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 = {}) => {
|
||||
export const summaryCard = (label, sublabel, value, options = {}) => {
|
||||
const {
|
||||
isMonetary = false,
|
||||
isPercentage = false,
|
||||
decimalPlaces = 0,
|
||||
colorClass = METRIC_COLORS.activeUsers.className,
|
||||
colorClass = "text-gray-900 dark:text-gray-100",
|
||||
titleClass = "text-sm font-medium text-gray-600 dark:text-gray-300",
|
||||
descriptionClass = "text-sm text-muted-foreground",
|
||||
background = "bg-white dark:bg-gray-900/60",
|
||||
} = options;
|
||||
|
||||
let displayValue;
|
||||
if (isMonetary) {
|
||||
displayValue = formatCurrency(value, decimalPlaces);
|
||||
} else if (isPercentage) {
|
||||
displayValue = formatPercent(value, decimalPlaces);
|
||||
} else {
|
||||
displayValue = formatNumber(value, decimalPlaces);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card className={`${background} backdrop-blur-sm`}>
|
||||
<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>
|
||||
<CardContent className="px-4 pt-0 pb-2">
|
||||
<div className={`text-2xl font-bold mb-1 ${colorClass}`}>
|
||||
{displayValue}
|
||||
{value.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{sublabel}</div>
|
||||
<div className={descriptionClass}>{sublabel}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const QuotaInfo = ({ tokenQuota }) => {
|
||||
// Add early return if tokenQuota is null or undefined
|
||||
export 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>
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
const {
|
||||
@@ -100,7 +204,6 @@ const QuotaInfo = ({ tokenQuota }) => {
|
||||
thresholdedRequests = {},
|
||||
} = tokenQuota;
|
||||
|
||||
// Add null checks and default values for all properties
|
||||
const {
|
||||
remaining: projectHourlyRemaining = 0,
|
||||
consumed: projectHourlyConsumed = 0,
|
||||
@@ -116,13 +219,11 @@ const QuotaInfo = ({ tokenQuota }) => {
|
||||
consumed: thresholdConsumed = 0,
|
||||
} = thresholdedRequests;
|
||||
|
||||
// Calculate percentages with safe math
|
||||
const hourlyPercentage = ((projectHourlyRemaining / 14000) * 100).toFixed(1);
|
||||
const dailyPercentage = ((dailyRemaining / 200000) * 100).toFixed(1);
|
||||
const errorPercentage = ((errorsRemaining / 10) * 100).toFixed(1);
|
||||
const thresholdPercentage = ((thresholdRemaining / 120) * 100).toFixed(1);
|
||||
|
||||
// Determine color based on remaining percentage
|
||||
const getStatusColor = (percentage) => {
|
||||
const numericPercentage = parseFloat(percentage);
|
||||
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 = () => {
|
||||
const [basicData, setBasicData] = useState({
|
||||
last30MinUsers: 0,
|
||||
@@ -279,50 +300,6 @@ export const RealtimeAnalytics = () => {
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
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) => {
|
||||
return {
|
||||
currentPages:
|
||||
@@ -636,14 +613,4 @@ export const RealtimeAnalytics = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
summaryCard,
|
||||
SkeletonSummaryCard,
|
||||
SkeletonBarChart,
|
||||
SkeletonTable,
|
||||
processBasicData,
|
||||
METRIC_COLORS,
|
||||
QuotaInfo
|
||||
};
|
||||
|
||||
export default RealtimeAnalytics;
|
||||
|
||||
Reference in New Issue
Block a user