import React, { useState } from 'react';
import { useTelemetryQuery } from '@/hooks/use-query/useTelemetryQuery';
import { Header } from '@/components/layout/Header';
import { Sidebar } from '@/components/layout/Sidebar';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend, Area, AreaChart } from 'recharts';
import { useAppsQuery } from '@/hooks/use-query/useAppsQuery';
import { useRolloutVersions, useRolloutAdoption } from '@/hooks/use-query/useRolloutQuery';
import { useChannelQuery } from '@/hooks/use-query/useChannelQuery';
import { usePlatformQuery } from '@/hooks/use-query/usePlatformQuery';
import { useArchitectureQuery } from '@/hooks/use-query/useArchitectureQuery';
import { useTheme } from '@/providers/themeProvider';
import { AppListItem } from '@/hooks/use-query/useAppsQuery';
import { Channel } from '@/hooks/use-query/useChannelQuery';
import { Platform } from '@/hooks/use-query/usePlatformQuery';
import { Architecture } from '@/hooks/use-query/useArchitectureQuery';
import DatePicker from 'react-datepicker';
import "react-datepicker/dist/react-datepicker.css";
import { Dropdown } from '@/components/common/Dropdown';
import { FIELD_LABEL, STATUS_BADGE } from '@/components/common/ui';
import { getPlatformIcon } from '@/utils/platformIcon';
const PANEL_CLASS = 'bg-theme-card rounded-lg border border-theme-card-hover shadow-md backdrop-blur-lg';
const CHART_GRID_STROKE = 'rgba(148, 163, 184, 0.25)';
const CHART_AXIS_STROKE = 'rgba(148, 163, 184, 0.7)';
const TOOLTIP_STYLE = {
backgroundColor: 'var(--theme-card)',
border: '1px solid var(--theme-card-hover)',
color: 'var(--theme-primary)',
borderRadius: '12px',
boxShadow: '0 10px 30px rgba(15, 23, 42, 0.25)',
};
const DATE_PICKER_POPUP_STYLE = {
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
boxShadow: '0 16px 40px rgba(15, 23, 42, 0.35)',
};
const DARK_CHART_COLORS = ['#5AA9FF', '#34D399', '#F59E0B', '#F97316', '#A78BFA', '#2DD4BF', '#FB7185', '#22D3EE'];
const LIGHT_CHART_COLORS = ['#2563EB', '#059669', '#D97706', '#EA580C', '#7C3AED', '#0D9488', '#E11D48', '#0891B2'];
type Filters = {
apps: string[];
channels: string[];
platforms: string[];
architectures: string[];
range?: 'today' | 'week' | 'month';
date?: string;
};
const StatCard = ({ title, value, icon }: { title: string; value: number; icon: React.ReactNode }) => (
);
const RolloutHealthRow = ({
appName,
version,
channel,
configuredPercent,
observedPercent,
observedClients,
eligibleClients,
barColor,
}: {
appName: string;
version: string;
channel: string;
configuredPercent: number;
observedPercent: number | null;
observedClients: number | null;
eligibleClients: number | null;
barColor: string;
}) => {
const clampedObserved = observedPercent === null ? 0 : Math.min(Math.max(observedPercent, 0), 100);
const clampedTarget = Math.min(Math.max(configuredPercent, 0), 100);
return (
{appName}
v{version}
{channel}
target {configuredPercent}%
observed{' '}
{observedPercent === null ? '—' : `${observedPercent.toFixed(1)}%`}
{observedClients === null || eligibleClients === null
? 'No telemetry for this version in the selected period.'
: `${formatNumber(observedClients)} of ${formatNumber(eligibleClients)} eligible clients on this version or newer.`}
);
};
const numberFormatter = new Intl.NumberFormat('en-US');
const formatNumber = (value: number | string) => {
const normalizedValue = typeof value === 'number' ? value : Number(value);
if (Number.isNaN(normalizedValue)) {
return String(value);
}
return numberFormatter.format(normalizedValue);
};
const formatChartDate = (value: string) => {
const parsedDate = new Date(value);
if (Number.isNaN(parsedDate.getTime())) {
return value;
}
return parsedDate.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
};
const formatChartDateLong = (value: string) => {
const parsedDate = new Date(value);
if (Number.isNaN(parsedDate.getTime())) {
return value;
}
return parsedDate.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
};
const compareVersions = (a: string, b: string) => {
const partsA = a.replace(/^v/i, '').split('.').map(Number);
const partsB = b.replace(/^v/i, '').split('.').map(Number);
const len = Math.max(partsA.length, partsB.length);
for (let i = 0; i < len; i++) {
const numA = partsA[i] ?? 0;
const numB = partsB[i] ?? 0;
if (Number.isNaN(numA) || Number.isNaN(numB)) {
const cmp = a.localeCompare(b);
if (cmp !== 0) {
return cmp;
}
continue;
}
if (numA !== numB) {
return numA - numB;
}
}
return 0;
};
const SinglePointTrendView = ({
value,
color,
dateLabel,
}: {
value: number;
color: string;
dateLabel: string;
}) => (
Single Day Snapshot
{formatNumber(value)}
{dateLabel}
);
const MultiFilter = ({
label,
placeholder,
values,
options,
onToggle,
panelClassName,
}: {
label: string;
placeholder: string;
values: string[];
options: { value: string; label: string; icon?: string }[];
onToggle: (value: string) => void;
panelClassName: string;
}) => (
{label}
{values.length > 0 && (
{values.map((value) => (
{value}
onToggle(value)}
className="ml-1 rounded p-0.5 text-white/70 transition-colors hover:bg-white/15 hover:text-red-300"
aria-label={`Remove ${value}`}
>
))}
)}
);
export const StatisticsPage = () => {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const { theme } = useTheme();
const [filters, setFilters] = useState({
apps: [],
channels: [],
platforms: [],
architectures: [],
range: 'today'
});
const [selectedDate, setSelectedDate] = useState(null);
const [showDatePicker, setShowDatePicker] = useState(false);
const tooltipTextColor = theme === 'dark' ? '#FFFFFF' : '#000000';
const tooltipContentStyle = React.useMemo(
() => ({ ...TOOLTIP_STYLE, color: tooltipTextColor }),
[tooltipTextColor]
);
const tooltipLabelStyle = React.useMemo(
() => ({
color: tooltipTextColor,
fontWeight: 'bold',
marginBottom: '4px',
}),
[tooltipTextColor]
);
const tooltipItemStyle = React.useMemo(
() => ({
color: tooltipTextColor,
padding: '4px 0',
}),
[tooltipTextColor]
);
const chartColors = React.useMemo(
() => (theme === 'dark' ? DARK_CHART_COLORS : LIGHT_CHART_COLORS),
[theme]
);
const lineChartConfig = React.useMemo(
() => ({
requests: chartColors[0],
uniqueClients: chartColors[1],
latestUsers: chartColors[2],
outdatedClients: chartColors[3],
}),
[chartColors]
);
const sharedTooltipProps = React.useMemo(
() => ({
contentStyle: tooltipContentStyle,
labelStyle: tooltipLabelStyle,
itemStyle: tooltipItemStyle,
formatter: (value: number | string) => formatNumber(value),
}),
[tooltipContentStyle, tooltipLabelStyle, tooltipItemStyle]
);
const { apps = [] } = useAppsQuery();
const { channels = [] } = useChannelQuery();
const { platforms = [] } = usePlatformQuery();
const { architectures = [] } = useArchitectureQuery();
const { data, isLoading, refetch } = useTelemetryQuery(filters);
const rolloutAppNames = React.useMemo(
() => (filters.apps.length ? filters.apps : (apps as AppListItem[]).map((a) => a.AppName)),
[filters.apps, apps]
);
const rolloutScope = React.useMemo(
() => ({ channels: filters.channels, platforms: filters.platforms, architectures: filters.architectures }),
[filters.channels, filters.platforms, filters.architectures]
);
const { rolloutVersions } = useRolloutVersions(rolloutAppNames, rolloutScope);
const rolloutAppList = React.useMemo(
() => [...new Set(rolloutVersions.map((r) => r.appName))],
[rolloutVersions]
);
const adoptionFilters = React.useMemo(
() => ({
channels: filters.channels,
platforms: filters.platforms,
architectures: filters.architectures,
range: filters.range,
date: filters.date,
}),
[filters.channels, filters.platforms, filters.architectures, filters.range, filters.date]
);
const { byApp: adoptionByApp } = useRolloutAdoption(rolloutAppList, adoptionFilters);
const rolloutRows = React.useMemo(() => {
return rolloutVersions
.map((r) => {
const app = adoptionByApp[r.appName];
// Hide rollouts for apps with no telemetry in view (idle/other apps),
// but keep apps that have traffic even if this version is at 0% adoption.
if (!app || app.uniqueClients <= 0) {
return null;
}
const usageList = app.usage;
const hasUsage = usageList.some((u) => u.version === r.version);
const geCount = (version: string) =>
usageList.reduce(
(acc, u) => acc + (compareVersions(u.version, version) >= 0 ? u.client_count : 0),
0
);
const predecessor = [...usageList]
.map((u) => u.version)
.sort(compareVersions)
.reverse()
.find((v) => compareVersions(v, r.version) < 0);
const observedClients = hasUsage ? geCount(r.version) : null;
const eligibleClients = predecessor ? geCount(predecessor) : app.uniqueClients;
const observedPercent =
hasUsage && eligibleClients > 0 ? ((observedClients as number) / eligibleClients) * 100 : null;
return { r, observedClients, eligibleClients, observedPercent };
})
.filter((row): row is NonNullable => row !== null);
}, [rolloutVersions, adoptionByApp]);
const dailyStats = React.useMemo(() => (Array.isArray(data?.daily_stats) ? data.daily_stats : []), [data?.daily_stats]);
const hasTrendData = dailyStats.length > 0;
const showSinglePointTrend = dailyStats.length === 1;
const singlePointDateLabel = showSinglePointTrend ? formatChartDateLong(String(dailyStats[0].date)) : '';
const handleOptionClick = (dropdownName: keyof Filters, value: string) => {
setFilters(prev => {
const currentValues = prev[dropdownName] as string[];
const newValues = currentValues.includes(value)
? currentValues.filter(v => v !== value)
: [...currentValues, value];
return { ...prev, [dropdownName]: newValues };
});
};
const handleTimeRangeChange = (range: 'today' | 'week' | 'month' | 'custom') => {
if (range === 'custom') {
setShowDatePicker(true);
setFilters(prev => ({ ...prev, range: undefined, date: undefined }));
} else {
setShowDatePicker(false);
setSelectedDate(null);
setFilters(prev => ({ ...prev, range, date: undefined }));
}
};
const handleDateChange = (date: Date | null) => {
setSelectedDate(date);
if (date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const formattedDate = `${year}-${month}-${day}`;
setFilters(prev => ({ ...prev, date: formattedDate, range: undefined }));
setShowDatePicker(false);
}
};
React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (!target.closest('.react-datepicker') && !target.closest('.date-picker-container')) {
setShowDatePicker(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
if (isLoading) {
return (
setIsSidebarOpen(false)} />
setIsSidebarOpen(true)}
onCreateClick={() => {}}
createButtonText=""
hideSearch={true}
/>
);
}
if (!data) {
return (
setIsSidebarOpen(false)} />
setIsSidebarOpen(true)}
onCreateClick={() => {}}
createButtonText=""
hideSearch={true}
/>
No Data Available
There are no statistics available for the selected period.
);
}
const NoDataMessage = () => (
No data available for this section
);
return (
setIsSidebarOpen(false)} />
setIsSidebarOpen(true)}
onCreateClick={() => {}}
createButtonText=""
hideSearch={true}
/>
Filters & Time Range
{data.summary?.total_requests || 0} requests in view
{/* Time Range Selector */}
{ e.currentTarget.blur(); refetch(); }}
className={`header-action-btn flex items-center px-2 sm:px-3 py-1.5 text-base sm:text-lg font-semibold ${isLoading ? 'opacity-70 cursor-not-allowed' : ''}`}
disabled={isLoading}
>
{isLoading ? 'Updating...' : 'Update Data'}
handleTimeRangeChange('today')}
className={`header-additional-btn px-2 sm:px-4 py-2 text-sm sm:text-base ${filters.range === 'today' && !filters.date ? 'header-action-btn' : ''}`}
>
Today
handleTimeRangeChange('week')}
className={`header-additional-btn px-2 sm:px-4 py-2 text-sm sm:text-base ${filters.range === 'week' ? 'header-action-btn' : ''}`}
>
Last Week
handleTimeRangeChange('month')}
className={`header-additional-btn px-2 sm:px-4 py-2 text-sm sm:text-base ${filters.range === 'month' ? 'header-action-btn' : ''}`}
>
Last Month
handleTimeRangeChange('custom')}
className={`header-additional-btn w-full sm:w-auto px-2 sm:px-4 py-2 text-sm sm:text-base ${filters.date ? 'header-action-btn' : ''}`}
>
{filters.date ? new Date(filters.date).toLocaleDateString() : 'Custom Date'}
{showDatePicker && (
)}
{/* Filters */}
handleOptionClick('apps', value)}
options={(Array.isArray(apps) ? (apps as AppListItem[]) : []).map((app) => ({
value: app.AppName,
label: app.AppName,
}))}
/>
handleOptionClick('channels', value)}
options={(Array.isArray(channels) ? (channels as Channel[]) : []).map((channel) => ({
value: channel.ChannelName,
label: channel.ChannelName,
}))}
/>
handleOptionClick('platforms', value)}
options={(Array.isArray(platforms) ? (platforms as Platform[]) : []).map((platform) => ({
value: platform.PlatformName,
label: platform.PlatformName,
icon: getPlatformIcon(platform.PlatformName),
}))}
/>
handleOptionClick('architectures', value)}
options={(Array.isArray(architectures) ? (architectures as Architecture[]) : []).map((arch) => ({
value: arch.ArchID,
label: arch.ArchID,
}))}
/>
{/* Summary Cards */}
{/* Rollout Health */}
{rolloutRows.length > 0 && (
Rollout Health
{rolloutRows.length} staged {rolloutRows.length === 1 ? 'rollout' : 'rollouts'}
{rolloutRows.map(({ r, observedClients, eligibleClients, observedPercent }, index) => (
))}
Observed is adoption within the eligible pool: clients on the rollout version or newer, over clients on
the previous version or newer — the same base the rollout percent is defined against, so it lines up with
the target. It trails and converges as eligible clients check in, with a ~±2% bucketing margin. Filter by
the rollout's channel/platform to scope it.
)}
{/* Trend Graphs */}
{/* Total Requests Trend */}
Total Requests Trend
{hasTrendData ? (
showSinglePointTrend ? (
) : (
formatNumber(value)} />
formatChartDateLong(String(value))} />
)
) : (
)}
{/* Unique Clients Trend */}
Unique Clients Trend
{hasTrendData ? (
showSinglePointTrend ? (
) : (
formatNumber(value)} />
formatChartDateLong(String(value))} />
)
) : (
)}
{/* Latest Version Users Trend */}
Latest Version Users Trend
{hasTrendData ? (
showSinglePointTrend ? (
) : (
formatNumber(value)} />
formatChartDateLong(String(value))} />
)
) : (
)}
{/* Outdated Clients Trend */}
Outdated Clients Trend
{hasTrendData ? (
showSinglePointTrend ? (
) : (
formatNumber(value)} />
formatChartDateLong(String(value))} />
)
) : (
)}
{/* Charts */}
{/* Version Usage Chart */}
Version Usage
{data.versions?.usage ? (
formatNumber(value)} />
) : (
)}
{/* Platform Distribution Chart */}
Platform Distribution
{data.platforms ? (
{data.platforms.map((_, index) => (
|
))}
{value} }
/>
) : (
)}
{/* Architecture Distribution Chart */}
Architecture Distribution
{data.architectures ? (
{data.architectures.map((_, index) => (
|
))}
{value} }
/>
) : (
)}
{/* Channel Distribution Chart */}
Channel Distribution
{data.channels ? (
{data.channels.map((_, index) => (
|
))}
{value} }
/>
) : (
)}
{/* Version Table */}
Version Details
{data.versions?.usage ? (
Version
Client Count
% of Total
{data.versions.usage.map((version) => (
{version.version}
{version.client_count}
{((version.client_count / (data.summary?.unique_clients || 1)) * 100).toFixed(1)}%
))}
) : (
)}
);
};