- }
+ }
loading={anyLoading}
- title="Cost"
- value={formatCost(totalCost)}
+ title="Tasks"
+ value={flowStats ? formatNumber(flowStats.totalTasksCount) : '0'}
/>
- }
- loading={anyLoading}
- title="Tokens"
- value={formatTokenCount(totalTokens)}
- />
- }
loading={anyLoading}
title="Tool Calls"
value={toolcalls ? formatNumber(toolcalls.totalCount) : '0'}
/>
- }
+ }
loading={anyLoading}
- title="Tasks"
- value={flowStats ? formatNumber(flowStats.totalTasksCount) : '0'}
+ title="Tokens"
+ value={formatTokenCount(totalTokens)}
+ />
+ }
+ loading={anyLoading}
+ title="Cost"
+ value={formatCost(totalCost)}
/>
@@ -219,15 +195,9 @@ export const FlowDashboardOverview = ({ flowId }: { flowId: string }) => {
{item.functionName}
-
+
{item.isAgent ? 'Agent' : 'Tool'}
-
+
{formatNumber(item.totalCount)}
diff --git a/frontend/src/lib/storage-keys.ts b/frontend/src/lib/storage-keys.ts
index 61ee323..b6936c1 100644
--- a/frontend/src/lib/storage-keys.ts
+++ b/frontend/src/lib/storage-keys.ts
@@ -1,6 +1,6 @@
const STORAGE_KEY_SEPARATOR = '_4_';
-export type LocalStorageKeyType = 'column' | 'page' | 'sorting';
+export type LocalStorageKeyType = 'column' | 'page' | 'period' | 'sorting';
export function getColumnStorageKey(urlPath?: string): string {
return getStorageKey('column', urlPath);
@@ -10,6 +10,10 @@ export function getPageStorageKey(urlPath?: string): string {
return getStorageKey('page', urlPath);
}
+export function getPeriodStorageKey(urlPath?: string): string {
+ return getStorageKey('period', urlPath);
+}
+
export function getSortingStorageKey(urlPath?: string): string {
return getStorageKey('sorting', urlPath);
}
diff --git a/frontend/src/lib/utils/format.ts b/frontend/src/lib/utils/format.ts
index 2d26c97..6d08840 100644
--- a/frontend/src/lib/utils/format.ts
+++ b/frontend/src/lib/utils/format.ts
@@ -18,3 +18,59 @@ export const formatDate = (date: Date) => {
return format(date, 'HH:mm, d MMM yyyy', { locale: enUS });
};
+
+export const formatNumber = (value: number): string => new Intl.NumberFormat('en-US').format(value);
+
+export const formatTokenCount = (count: number): string => {
+ if (count >= 1_000_000_000) {
+ return `${(count / 1_000_000_000).toFixed(1)}B`;
+ }
+
+ if (count >= 1_000_000) {
+ return `${(count / 1_000_000).toFixed(1)}M`;
+ }
+
+ if (count >= 1_000) {
+ return `${(count / 1_000).toFixed(1)}K`;
+ }
+
+ return count.toString();
+};
+
+export const formatCost = (cost: number): string => {
+ if (!cost) {
+ return '$0';
+ }
+
+ if (cost >= 1) {
+ return `$${cost.toFixed(2)}`;
+ }
+
+ if (cost >= 0.01) {
+ return `$${cost.toFixed(3)}`;
+ }
+
+ return `$${cost.toFixed(4)}`;
+};
+
+export const formatDuration = (seconds: number): string => {
+ if (seconds >= 3600) {
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+
+ return `${hours}h ${minutes}m`;
+ }
+
+ if (seconds >= 60) {
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = Math.floor(seconds % 60);
+
+ return `${minutes}m ${remainingSeconds}s`;
+ }
+
+ if (seconds >= 1) {
+ return `${seconds.toFixed(1)}s`;
+ }
+
+ return `${(seconds * 1000).toFixed(0)}ms`;
+};
diff --git a/frontend/src/pages/dashboard/dashboard-analytics.tsx b/frontend/src/pages/dashboard/dashboard-analytics.tsx
index ead664b..78194a4 100644
--- a/frontend/src/pages/dashboard/dashboard-analytics.tsx
+++ b/frontend/src/pages/dashboard/dashboard-analytics.tsx
@@ -1,19 +1,23 @@
import { format } from 'date-fns';
import { ChevronRight, Clock, Loader2, Wrench } from 'lucide-react';
-import { useState } from 'react';
-import { Area, AreaChart, Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
+import { useMemo, useState } from 'react';
+import { Area, AreaChart, Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts';
-import type { UsageStatsPeriod } from '@/graphql/types';
+import type { FlowFragmentFragment, UsageStatsPeriod } from '@/graphql/types';
+import { ChartCard, ChartTooltip } from '@/components/dashboard';
+import { FlowStatusBadge } from '@/components/icons/flow-status-badge';
+import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import {
useFlowsExecutionStatsByPeriodQuery,
+ useFlowsQuery,
useFlowsStatsByPeriodQuery,
useToolcallsStatsByPeriodQuery,
useUsageStatsByPeriodQuery,
} from '@/graphql/types';
-import { formatCost, formatDuration, formatNumber, formatTokenCount } from '@/pages/dashboard/format-utils';
+import { formatCost, formatDuration, formatNumber, formatTokenCount } from '@/lib/utils/format';
const CHART_COLORS = {
area1: 'var(--color-chart-1)',
@@ -31,48 +35,7 @@ const formatDateLabel = (dateString: string): string => {
}
};
-const ChartLoading = () => (
-
-
-
-);
-
-const CustomTooltip = ({
- active,
- formatter,
- label,
- payload,
-}: {
- active?: boolean;
- formatter?: (value: number, name: string) => string;
- label?: string;
- payload?: Array<{ color: string; name: string; value: number }>;
-}) => {
- if (!active || !payload?.length) {
- return null;
- }
-
- return (
-
-
{label ? formatDateLabel(label) : ''}
- {payload.map((entry) => (
-
-
- {entry.name}:
-
- {formatter ? formatter(entry.value, entry.name) : formatNumber(entry.value)}
-
-
- ))}
-
- );
-};
+const axisTickStyle = { fill: 'var(--color-muted-foreground)', fontSize: 12 };
export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) => {
const { data: usageByPeriodData, loading: usageByPeriodLoading } = useUsageStatsByPeriodQuery({
@@ -87,12 +50,22 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) =>
const { data: executionStatsData, loading: executionStatsLoading } = useFlowsExecutionStatsByPeriodQuery({
variables: { period },
});
+ const { data: flowsData } = useFlowsQuery();
+
+ const flowsById = useMemo(() => {
+ const map = new Map();
+ (flowsData?.flows ?? []).forEach((flow) => {
+ map.set(flow.id, flow);
+ });
+
+ return map;
+ }, [flowsData?.flows]);
const usageChartData = [...(usageByPeriodData?.usageStatsByPeriod ?? [])].reverse().map((item) => ({
cacheIn: item.stats.totalUsageCacheIn,
costIn: item.stats.totalUsageCostIn,
costOut: item.stats.totalUsageCostOut,
- date: formatDateLabel(item.date),
+ date: item.date,
tokensIn: item.stats.totalUsageIn,
tokensOut: item.stats.totalUsageOut,
totalCost: item.stats.totalUsageCostIn + item.stats.totalUsageCostOut,
@@ -100,13 +73,13 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) =>
const toolcallsChartData = [...(toolcallsByPeriodData?.toolcallsStatsByPeriod ?? [])].reverse().map((item) => ({
count: item.stats.totalCount,
- date: formatDateLabel(item.date),
+ date: item.date,
duration: item.stats.totalDurationSeconds,
}));
const flowsChartData = [...(flowsByPeriodData?.flowsStatsByPeriod ?? [])].reverse().map((item) => ({
assistants: item.stats.totalAssistantsCount,
- date: formatDateLabel(item.date),
+ date: item.date,
flows: item.stats.totalFlowsCount,
subtasks: item.stats.totalSubtasksCount,
tasks: item.stats.totalTasksCount,
@@ -116,210 +89,184 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) =>
return (
+
+
+
+
+
+ }
+ cursor={{ fill: 'var(--color-muted-foreground)', fillOpacity: 0.1 }}
+ />
+
+
+
+
+
+
-
-
- Token Usage Over Time
- Input and output tokens processed daily
-
-
- {usageByPeriodLoading ? (
-
- ) : (
-
-
-
-
-
- formatTokenCount(value)} />}
- />
-
-
-
-
- )}
-
-
+
+
+
+
+
+ }
+ cursor={{ fill: 'var(--color-muted-foreground)', fillOpacity: 0.1 }}
+ />
+
+
+
-
-
- Cost Over Time
- LLM spending per day
-
-
- {usageByPeriodLoading ? (
-
- ) : (
-
-
-
-
- formatCost(value)}
- tickMargin={8}
- />
- formatCost(value)} />} />
-
-
-
-
- )}
-
-
-
-
-
- Tool Calls Over Time
- Number of tool executions per day
-
-
- {toolcallsByPeriodLoading ? (
-
- ) : (
-
-
-
-
-
- }
- cursor={{ fill: 'var(--color-muted-foreground)', fillOpacity: 0.1 }}
- />
-
-
-
- )}
-
-
-
-
-
- Flows Activity Over Time
- Flows, tasks, and subtasks created per day
-
-
- {flowsByPeriodLoading ? (
-
- ) : (
-
-
-
-
-
- }
- cursor={{ fill: 'var(--color-muted-foreground)', fillOpacity: 0.1 }}
- />
-
-
-
-
-
- )}
-
-
+
+
+
+
+
+ formatTokenCount(value)}
+ labelFormatter={formatDateLabel}
+ />
+ }
+ />
+
+
+
+
+
+
+
+
+ formatCost(value)}
+ tickMargin={8}
+ />
+ formatCost(value)}
+ labelFormatter={formatDateLabel}
+ />
+ }
+ />
+
+
+
+
+
Flow Execution Details
@@ -339,6 +286,7 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) =>
{executionStats.map((flow) => (
))}
@@ -370,25 +318,39 @@ type FlowExecution = {
totalToolcallsCount: number;
};
-const FlowExecutionItem = ({ flow }: { flow: FlowExecution }) => {
+const FlowExecutionItem = ({ flow, flowMeta }: { flow: FlowExecution; flowMeta?: FlowFragmentFragment }) => {
const [isOpen, setIsOpen] = useState(false);
+ const taskCount = flow.tasks.length;
+ const subtaskCount = flow.tasks.reduce((sum, task) => sum + task.subtasks.length, 0);
return (
-
-
- {flow.flowTitle || `Flow #${flow.flowId}`}
-
+
+
+
+
+ {flow.flowTitle || `Flow #${flow.flowId}`}
+ {flowMeta?.status && }
+ {flowMeta?.provider?.name && {flowMeta.provider.name}}
+
+
+ {taskCount} {taskCount === 1 ? 'task' : 'tasks'}
+ {subtaskCount > 0 && ` · ${subtaskCount} ${subtaskCount === 1 ? 'subtask' : 'subtasks'}`}
+ {flow.totalAssistantsCount > 0 &&
+ ` · ${flow.totalAssistantsCount} ${flow.totalAssistantsCount === 1 ? 'assistant' : 'assistants'}`}
+
+
+
{formatDuration(flow.totalDurationSeconds)}
- {flow.totalToolcallsCount}
+ {formatNumber(flow.totalToolcallsCount)}
@@ -432,7 +394,7 @@ const TaskExecutionItem = ({ task }: { task: FlowExecution['tasks'][number] }) =
- {task.totalToolcallsCount}
+ {formatNumber(task.totalToolcallsCount)}
@@ -454,7 +416,7 @@ const TaskExecutionItem = ({ task }: { task: FlowExecution['tasks'][number] }) =
- {subtask.totalToolcallsCount}
+ {formatNumber(subtask.totalToolcallsCount)}
diff --git a/frontend/src/pages/dashboard/dashboard-overview.tsx b/frontend/src/pages/dashboard/dashboard-overview.tsx
index 67ba6af..5901a5a 100644
--- a/frontend/src/pages/dashboard/dashboard-overview.tsx
+++ b/frontend/src/pages/dashboard/dashboard-overview.tsx
@@ -2,8 +2,9 @@ import { Activity, CircleDollarSign, Cpu, GitFork, Loader2 } from 'lucide-react'
import type { UsageStatsFragmentFragment } from '@/graphql/types';
+import { MetricCard } from '@/components/dashboard';
+import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
-import { Skeleton } from '@/components/ui/skeleton';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import {
useFlowsStatsTotalQuery,
@@ -14,32 +15,7 @@ import {
useUsageStatsByProviderQuery,
useUsageStatsTotalQuery,
} from '@/graphql/types';
-import { formatCost, formatDuration, formatNumber, formatTokenCount } from '@/pages/dashboard/format-utils';
-
-const StatCard = ({
- description,
- icon,
- loading,
- title,
- value,
-}: {
- description: string;
- icon: React.ReactNode;
- loading: boolean;
- title: string;
- value: string;
-}) => (
-