mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-31 01:35:16 +08:00
Add overview provider and model filters
This commit is contained in:
@@ -276,6 +276,8 @@ function App() {
|
||||
const [agentAnalysisLoading, setAgentAnalysisLoading] = useState(false);
|
||||
const [agentAnalysisRange, setAgentAnalysisRange] = useState<UsageStatsRange>("7d");
|
||||
const [agentAnalysisSession, setAgentAnalysisSession] = useState<AgentAnalysisSessionSelection>();
|
||||
const [usageModelFilter, setUsageModelFilter] = useState("");
|
||||
const [usageProviderFilter, setUsageProviderFilter] = useState("");
|
||||
const [usageRange, setUsageRange] = useState<UsageStatsRange>("7d");
|
||||
const [usageStats, setUsageStats] = useState<UsageStatsSnapshot>(fallbackUsageStats);
|
||||
const [providerAccountSnapshots, setProviderAccountSnapshots] = useState<ProviderAccountSnapshot[]>([]);
|
||||
@@ -454,7 +456,11 @@ function App() {
|
||||
|
||||
let cancelled = false;
|
||||
const refreshUsageStats = () => {
|
||||
const filter: UsageStatsFilter | undefined = usageRange === "today" ? { includeProxy: true } : undefined;
|
||||
const filter: UsageStatsFilter = {
|
||||
...(usageRange === "today" ? { includeProxy: true } : {}),
|
||||
...(usageProviderFilter ? { provider: usageProviderFilter } : {}),
|
||||
...(usageModelFilter ? { model: usageModelFilter } : {})
|
||||
};
|
||||
void window.ccr?.getUsageStats(usageRange, filter).then((snapshot) => {
|
||||
if (!cancelled) {
|
||||
setUsageStats(snapshot);
|
||||
@@ -466,7 +472,29 @@ function App() {
|
||||
cancelled = true;
|
||||
stopPolling();
|
||||
};
|
||||
}, [usageRange]);
|
||||
}, [usageModelFilter, usageProviderFilter, usageRange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!usageProviderFilter) {
|
||||
return;
|
||||
}
|
||||
if (!draftConfig.Providers.some((provider) => provider.name === usageProviderFilter)) {
|
||||
setUsageProviderFilter("");
|
||||
}
|
||||
}, [draftConfig.Providers, usageProviderFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!usageModelFilter) {
|
||||
return;
|
||||
}
|
||||
const modelAvailable = draftConfig.Providers.some((provider) =>
|
||||
(!usageProviderFilter || provider.name === usageProviderFilter) &&
|
||||
provider.models.some((model) => model.trim() === usageModelFilter)
|
||||
);
|
||||
if (!modelAvailable) {
|
||||
setUsageModelFilter("");
|
||||
}
|
||||
}, [draftConfig.Providers, usageModelFilter, usageProviderFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.ccr) {
|
||||
@@ -2900,6 +2928,13 @@ function App() {
|
||||
snapshot: agentAnalysis
|
||||
},
|
||||
overview: {
|
||||
usageFilters: {
|
||||
modelFilter: usageModelFilter,
|
||||
providerFilter: usageProviderFilter,
|
||||
providers: draftConfig.Providers,
|
||||
setModelFilter: setUsageModelFilter,
|
||||
setProviderFilter: setUsageProviderFilter
|
||||
},
|
||||
onWidgetsChange: changeOverviewWidgets,
|
||||
overviewWidgets: normalizeOverviewWidgets(draftConfig.overviewWidgets),
|
||||
providerAccounts: providerAccountSnapshots,
|
||||
|
||||
@@ -18,13 +18,24 @@ import {
|
||||
ProviderAccountMeter, ProviderAccountSnapshot, ReactNode, ReactPointerEvent, rectSortingStrategy, RefreshCw, Select,
|
||||
SelectControl, SortableContext, sortableKeyboardCoordinates, systemStatusIconClass, systemStatusPointTooltip, systemStatusSegmentClass,
|
||||
systemStatusTooltipPositionClass, Tooltip, translateOptions, Trash2, UsageComparisonRow, usageRangeOptions,
|
||||
UsageSeriesPoint, UsageStatsRange, UsageStatsSnapshot, usageStatusTone, UsageTotals, useAppText,
|
||||
GatewayProviderConfig, UsageSeriesPoint, UsageStatsRange, UsageStatsSnapshot, usageStatusTone, UsageTotals, useAppText,
|
||||
useEffect, useMemo, useRef, useSensor, useSensors, useSortable,
|
||||
useState, X, XAxis, YAxis
|
||||
} from "../shared/index";
|
||||
import { buildTokenActivity, type TokenActivityCell } from "@/lib/usage-activity";
|
||||
import { ShareCardWidget } from "./share-cards";
|
||||
import { Cloud, Rocket } from "lucide-react";
|
||||
|
||||
type OverviewUsageFilters = {
|
||||
modelFilter: string;
|
||||
providerFilter: string;
|
||||
providers: GatewayProviderConfig[];
|
||||
setModelFilter: (model: string) => void;
|
||||
setProviderFilter: (provider: string) => void;
|
||||
};
|
||||
|
||||
const emptyOverviewProviders: GatewayProviderConfig[] = [];
|
||||
|
||||
export function OverviewView({
|
||||
onWidgetsChange,
|
||||
overviewWidgets,
|
||||
@@ -32,6 +43,7 @@ export function OverviewView({
|
||||
providerAccountRefreshing = false,
|
||||
refreshProviderAccounts,
|
||||
setUsageRange,
|
||||
usageFilters,
|
||||
usageRange,
|
||||
usageStats
|
||||
}: {
|
||||
@@ -41,6 +53,7 @@ export function OverviewView({
|
||||
providerAccountRefreshing?: boolean;
|
||||
refreshProviderAccounts?: () => void | Promise<void>;
|
||||
setUsageRange: (range: UsageStatsRange) => void;
|
||||
usageFilters?: OverviewUsageFilters;
|
||||
usageRange: UsageStatsRange;
|
||||
usageStats: UsageStatsSnapshot;
|
||||
}) {
|
||||
@@ -67,6 +80,11 @@ export function OverviewView({
|
||||
const visibleWidgets = displayWidgets.filter((widget) => widget.enabled);
|
||||
const activeWidget = visibleWidgets.find((widget) => widget.id === activeWidgetId);
|
||||
const selectedWidget = widgets.find((widget) => widget.id === selectedWidgetId);
|
||||
const filterProviders = usageFilters?.providers ?? emptyOverviewProviders;
|
||||
const providerFilter = usageFilters?.providerFilter ?? "";
|
||||
const modelFilter = usageFilters?.modelFilter ?? "";
|
||||
const providerOptions = useMemo(() => overviewProviderFilterOptions(filterProviders, t), [filterProviders, t]);
|
||||
const modelOptions = useMemo(() => overviewModelFilterOptions(filterProviders, providerFilter, t), [filterProviders, providerFilter, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editing) {
|
||||
@@ -153,6 +171,17 @@ export function OverviewView({
|
||||
setSelectedWidgetId((current) => current === id ? undefined : current);
|
||||
}
|
||||
|
||||
function changeProviderFilter(provider: string) {
|
||||
usageFilters?.setProviderFilter(provider);
|
||||
if (modelFilter && provider && !overviewProviderHasModel(filterProviders, provider, modelFilter)) {
|
||||
usageFilters?.setModelFilter("");
|
||||
}
|
||||
}
|
||||
|
||||
function changeModelFilter(model: string) {
|
||||
usageFilters?.setModelFilter(model);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!editing || !selectedWidgetId || activeWidgetId) {
|
||||
return;
|
||||
@@ -308,8 +337,21 @@ export function OverviewView({
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<h2 className="truncate text-[18px] font-semibold tracking-tight">{t("Overview")}</h2>
|
||||
<OverviewUsageRangeSelector range={usageRange} setRange={setUsageRange} />
|
||||
<Select
|
||||
aria-label={t("Provider")}
|
||||
className="h-8 w-[168px] bg-[length:14px] px-2 pr-7 text-[12px]"
|
||||
onValueChange={changeProviderFilter}
|
||||
options={providerOptions}
|
||||
value={providerFilter}
|
||||
/>
|
||||
<Select
|
||||
aria-label={t("Model")}
|
||||
className="h-8 w-[220px] bg-[length:14px] px-2 pr-7 text-[12px]"
|
||||
onValueChange={changeModelFilter}
|
||||
options={modelOptions}
|
||||
value={modelFilter}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{editing ? (
|
||||
@@ -403,6 +445,50 @@ function OverviewUsageRangeSelector({
|
||||
);
|
||||
}
|
||||
|
||||
function overviewProviderFilterOptions(providers: GatewayProviderConfig[], translate: (value: string) => string): Array<{ label: string; value: string }> {
|
||||
const providerNames = new Set<string>();
|
||||
for (const provider of providers) {
|
||||
const name = provider.name.trim();
|
||||
if (name) {
|
||||
providerNames.add(name);
|
||||
}
|
||||
}
|
||||
return [
|
||||
{ label: translate("All providers"), value: "" },
|
||||
...Array.from(providerNames).map((provider) => ({ label: provider, value: provider }))
|
||||
];
|
||||
}
|
||||
|
||||
function overviewModelFilterOptions(
|
||||
providers: GatewayProviderConfig[],
|
||||
providerFilter: string,
|
||||
translate: (value: string) => string
|
||||
): Array<{ label: string; value: string }> {
|
||||
const models = new Set<string>();
|
||||
for (const provider of providers) {
|
||||
if (providerFilter && provider.name !== providerFilter) {
|
||||
continue;
|
||||
}
|
||||
for (const rawModel of provider.models) {
|
||||
const model = rawModel.trim();
|
||||
if (model) {
|
||||
models.add(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{ label: translate("All models"), value: "" },
|
||||
...Array.from(models).map((model) => ({ label: model, value: model }))
|
||||
];
|
||||
}
|
||||
|
||||
function overviewProviderHasModel(providers: GatewayProviderConfig[], providerName: string, modelName: string): boolean {
|
||||
return providers.some((provider) =>
|
||||
provider.name === providerName &&
|
||||
provider.models.some((model) => model.trim() === modelName)
|
||||
);
|
||||
}
|
||||
|
||||
function isEditableKeyboardTarget(target: Element | undefined): boolean {
|
||||
return Boolean(target?.closest("input, textarea, select, [contenteditable='true'], [contenteditable='plaintext-only'], [role='textbox']"));
|
||||
}
|
||||
|
||||
@@ -596,6 +596,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
|
||||
"Agent Mix": "Agent 分布",
|
||||
"Agent profiles": "Agent 配置档案",
|
||||
"All agents": "全部 Agent",
|
||||
"All models": "全部模型",
|
||||
"All providers": "全部供应商",
|
||||
"API key": "API 密钥",
|
||||
"API key created": "API 密钥已创建",
|
||||
|
||||
@@ -42,7 +42,9 @@ test("OverviewView renders every overview widget type", () => {
|
||||
/>
|
||||
);
|
||||
|
||||
assert.match(html, /<h2 class="[^"]*">Overview<\/h2>/);
|
||||
assert.doesNotMatch(html, /<h2 class="[^"]*">Overview<\/h2>/);
|
||||
assert.match(html, /All providers/);
|
||||
assert.match(html, /All models/);
|
||||
assert.match(html, /aria-label="Edit widgets"/);
|
||||
assert.match(html, /System status/);
|
||||
assert.match(html, /API Service/);
|
||||
@@ -79,7 +81,9 @@ test("OverviewView renders the empty widget layout state", () => {
|
||||
/>
|
||||
);
|
||||
|
||||
assert.match(html, /Overview/);
|
||||
assert.doesNotMatch(html, /<h2 class="[^"]*">Overview<\/h2>/);
|
||||
assert.match(html, /All providers/);
|
||||
assert.match(html, /All models/);
|
||||
assert.match(html, /No widgets configured/);
|
||||
assert.match(html, /aria-label="Edit widgets"/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user