diff --git a/src/frontend/components/DashboardCharts.tsx b/src/frontend/components/DashboardCharts.tsx index b1a6604..ea688d8 100644 --- a/src/frontend/components/DashboardCharts.tsx +++ b/src/frontend/components/DashboardCharts.tsx @@ -1,7 +1,8 @@ -import { BarChart, LineChart } from '@mantine/charts' +import { AreaChart, BarChart } from '@mantine/charts' import { Badge, Box, + Button, Group, Paper, Stack, @@ -11,14 +12,29 @@ import { } from '@mantine/core' import { TbArrowUpRight, TbChartBar, TbTimeline } from 'react-icons/tb' +type DailyRange = 7 | 30 | 90 + interface ChartProps { data?: any[] isLoading?: boolean } -export function VillageActivityLineChart({ data = [], isLoading }: ChartProps) { +interface ActivityChartProps extends ChartProps { + range?: DailyRange + onRangeChange?: (range: DailyRange) => void +} + +const RANGE_OPTIONS: { value: DailyRange; label: string }[] = [ + { value: 7, label: '7D' }, + { value: 30, label: '30D' }, + { value: 90, label: '3M' }, +] + +export function VillageActivityLineChart({ data = [], isLoading, range = 7, onRangeChange }: ActivityChartProps) { const theme = useMantineTheme() + const rangeLabel = range === 7 ? 'last 7 days' : range === 30 ? 'last 30 days' : 'last 3 months' + return ( @@ -29,21 +45,28 @@ export function VillageActivityLineChart({ data = [], isLoading }: ChartProps) { DAILY ACTIVITY - ALL VILLAGES - Trend over the last 7 days + Trend over the {rangeLabel} - { - isLoading && ( - }> - ... - - ) - } - + + {RANGE_OPTIONS.map((opt) => ( + + ))} + - { + if (!active || !payload?.length) return null + return ( +
+
+ {label} +
+
+ Activity: {payload[0].value} +
+
+ ) + }, }} styles={{ root: { - '.recharts-line-curve': { + '.recharts-area-curve': { strokeWidth: 3, filter: 'drop-shadow(0 4px 8px rgba(37, 99, 235, 0.3))' } @@ -71,9 +115,11 @@ export function VillageActivityLineChart({ data = [], isLoading }: ChartProps) { ) } -export function VillageComparisonBarChart({ data = [], isLoading }: ChartProps) { +export function VillageComparisonBarChart({ data = [], isLoading, range = 7, onRangeChange }: ActivityChartProps) { const theme = useMantineTheme() + const rangeLabel = range === 7 ? 'last 7 days' : range === 30 ? 'last 30 days' : 'last 3 months' + return ( @@ -84,9 +130,24 @@ export function VillageComparisonBarChart({ data = [], isLoading }: ChartProps) USAGE COMPARISON BETWEEN VILLAGES - Most active village deployments + Most active village deployments — {rangeLabel} + + {RANGE_OPTIONS.map((opt) => ( + + ))} + diff --git a/src/frontend/config/api.ts b/src/frontend/config/api.ts index 434ae57..2264dea 100644 --- a/src/frontend/config/api.ts +++ b/src/frontend/config/api.ts @@ -27,8 +27,8 @@ export const API_URLS = { return `${DESA_PLUS_PROXY}/api/monitoring/log-all-villages?${params}` }, getGridOverview: () => `${DESA_PLUS_PROXY}/api/monitoring/grid-overview`, - getDailyActivity: () => `${DESA_PLUS_PROXY}/api/monitoring/daily-activity`, - getComparisonActivity: () => `${DESA_PLUS_PROXY}/api/monitoring/comparison-activity`, + getDailyActivity: (range: 7 | 30 | 90 = 7) => `${DESA_PLUS_PROXY}/api/monitoring/daily-activity?range=${range}`, + getComparisonActivity: (range: 7 | 30 | 90 = 7) => `${DESA_PLUS_PROXY}/api/monitoring/comparison-activity?range=${range}`, postVersionUpdate: () => `${DESA_PLUS_PROXY}/api/monitoring/version-update`, createVillages: () => `${DESA_PLUS_PROXY}/api/monitoring/create-villages`, createUser: () => `${DESA_PLUS_PROXY}/api/monitoring/create-user`, diff --git a/src/frontend/routes/apps.$appId.index.tsx b/src/frontend/routes/apps.$appId.index.tsx index 0a45859..e5b3b40 100644 --- a/src/frontend/routes/apps.$appId.index.tsx +++ b/src/frontend/routes/apps.$appId.index.tsx @@ -52,9 +52,12 @@ function AppOverviewPage() { const [maintenance, setMaintenance] = useState(false) const [isSaving, setIsSaving] = useState(false) + const [dailyRange, setDailyRange] = useState<7 | 30 | 90>(7) + const [comparisonRange, setComparisonRange] = useState<7 | 30 | 90>(7) + const { data: gridRes, isLoading: gridLoading, mutate: mutateGrid } = useSWR(isDesaPlus ? API_URLS.getGridOverview() : null, fetcher) - const { data: dailyRes, isLoading: dailyLoading, mutate: mutateDaily } = useSWR(isDesaPlus ? API_URLS.getDailyActivity() : null, fetcher) - const { data: comparisonRes, isLoading: comparisonLoading, mutate: mutateComparison } = useSWR(isDesaPlus ? API_URLS.getComparisonActivity() : null, fetcher) + const { data: dailyRes, isLoading: dailyLoading, mutate: mutateDaily } = useSWR(isDesaPlus ? API_URLS.getDailyActivity(dailyRange) : null, fetcher) + const { data: comparisonRes, isLoading: comparisonLoading, mutate: mutateComparison } = useSWR(isDesaPlus ? API_URLS.getComparisonActivity(comparisonRange) : null, fetcher) const { data: appData, isLoading: appLoading } = useSWR(`/api/apps/${appId}`, fetcher) @@ -253,8 +256,8 @@ function AppOverviewPage() { - - + + diff --git a/src/frontend/routes/apps.$appId.villages.$villageId.tsx b/src/frontend/routes/apps.$appId.villages.$villageId.tsx index a34abc4..c7cdb20 100644 --- a/src/frontend/routes/apps.$appId.villages.$villageId.tsx +++ b/src/frontend/routes/apps.$appId.villages.$villageId.tsx @@ -123,16 +123,44 @@ function ActivityChart({ villageId }: { villageId: string }) { dataKey="label" series={[{ name: 'activity', color: '#2563EB' }]} curveType="monotone" - withTooltip={true} - withDots={true} + withTooltip + withDots withPointLabels={false} + tickLine="none" + gridAxis="x" + fillOpacity={0.4} tooltipAnimationDuration={150} tooltipProps={{ - allowEscapeViewBox: { x: true, y: false }, + content: ({ active, payload, label }: any) => { + if (!active || !payload?.length) return null + return ( +
+
+ {label} +
+
+ Activity: {payload[0].value} +
+
+ ) + }, }} - activeDotProps={{ - r: 6, - strokeWidth: 2, + activeDotProps={{ r: 6, strokeWidth: 2 }} + styles={{ + root: { + '.recharts-area-curve': { + strokeWidth: 3, + filter: 'drop-shadow(0 4px 8px rgba(37, 99, 235, 0.3))', + }, + }, }} /> )}