mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-26 01:34:51 +08:00
fix(usage): keep pricing reachable when there is no usage yet
The empty state replaced the whole dashboard, including the tabs, so a fresh install had no way into model pricing / models.dev sync until some usage existed. Add a "configure pricing" entry to the empty state that opens the tabs with pricing selected, without drawing the all-zero overview.
This commit is contained in:
1 parent
0523ec9de7
commit
7422c24bcd
6 files changed
+172
-118
No files matched your search
@@ -169,6 +169,8 @@ export function UsageDashboard({
|
||||
const [model, setModel] = useState<string | undefined>(undefined);
|
||||
const [statusCode, setStatusCode] = useState<number | undefined>(undefined);
|
||||
const [tab, setTab] = useState<UsageTab>("logs");
|
||||
// 没有用量时也能进定价页:新装的人往往先配好 models.dev 同步再开始用
|
||||
const [pricingWhileEmpty, setPricingWhileEmpty] = useState(false);
|
||||
const [refreshIntervalMs, setRefreshIntervalMs] = useState(() =>
|
||||
normalizeRefreshInterval(savedRefreshIntervalMs),
|
||||
);
|
||||
@@ -668,123 +670,149 @@ export function UsageDashboard({
|
||||
|
||||
const scopedAppType = appType === "all" ? undefined : appType;
|
||||
|
||||
const body = isEmpty ? (
|
||||
<div className="flex min-h-0 flex-1 flex-col items-center overflow-y-auto px-6 pb-6">
|
||||
<div className="mt-16 flex max-w-[440px] flex-col items-center gap-3 text-center">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="flex h-11 w-11 items-center justify-center rounded-full bg-subtle text-fg-2"
|
||||
>
|
||||
<ChartColumn className="h-5 w-5" strokeWidth={1.5} />
|
||||
</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="m-0 text-section text-fg-1">{t("usage.empty.title")}</p>
|
||||
<p className="m-0 text-body text-fg-2">{t("usage.empty.body")}</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="neutral"
|
||||
size="regular"
|
||||
disabled={syncingSession}
|
||||
onClick={() => void runManualSessionSync()}
|
||||
>
|
||||
{syncingSession && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
|
||||
{t("usage.sessionSync.syncNow")}
|
||||
</Button>
|
||||
const tabsSection = (
|
||||
<section aria-label={t("usage.tabs.label")} className="flex flex-col">
|
||||
<PageTabs<UsageTab>
|
||||
aria-label={t("usage.tabs.label")}
|
||||
items={TABS.map((id) => ({ value: id, label: tabLabel[id] }))}
|
||||
value={tab}
|
||||
onValueChange={setTab}
|
||||
idPrefix="usage-tab"
|
||||
controls="usage-tabpanel"
|
||||
trailing={tabTrailing}
|
||||
/>
|
||||
|
||||
<div
|
||||
role="tabpanel"
|
||||
id="usage-tabpanel"
|
||||
aria-labelledby={`usage-tab-${tab}`}
|
||||
>
|
||||
{tab === "logs" && (
|
||||
<RequestLogTable
|
||||
range={range}
|
||||
appType={appType}
|
||||
providerName={providerName}
|
||||
model={model}
|
||||
statusCode={statusCode}
|
||||
refreshIntervalMs={refreshIntervalMs}
|
||||
onOpenDetail={setDetailRequestId}
|
||||
/>
|
||||
)}
|
||||
{tab === "providers" && (
|
||||
<ProviderStatsTable
|
||||
range={range}
|
||||
appType={appType}
|
||||
providerName={providerName}
|
||||
model={model}
|
||||
refreshIntervalMs={refreshIntervalMs}
|
||||
/>
|
||||
)}
|
||||
{tab === "models" && (
|
||||
<ModelStatsTable
|
||||
range={range}
|
||||
appType={appType}
|
||||
providerName={providerName}
|
||||
model={model}
|
||||
refreshIntervalMs={refreshIntervalMs}
|
||||
/>
|
||||
)}
|
||||
{tab === "pricing" && <PricingConfigPanel />}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
id="main-content"
|
||||
className="flex min-h-0 flex-1 flex-col gap-3.5 overflow-y-auto px-6 pb-6 pt-1"
|
||||
>
|
||||
<UsageHero
|
||||
range={range}
|
||||
appType={scopedAppType}
|
||||
providerName={providerName}
|
||||
model={model}
|
||||
refreshIntervalMs={refreshIntervalMs}
|
||||
compact={compact}
|
||||
/>
|
||||
|
||||
{/* 「全部」看长期分布用热力图;24 小时到 30 天这类短范围用柱状图 */}
|
||||
{range.preset === "all" ? (
|
||||
<UsageHeatmap
|
||||
appType={appType}
|
||||
providerName={providerName}
|
||||
model={model}
|
||||
refreshIntervalMs={refreshIntervalMs}
|
||||
/>
|
||||
) : (
|
||||
<UsageTrendChart
|
||||
range={range}
|
||||
rangeLabel={rangeLabel}
|
||||
appType={appType}
|
||||
providerName={providerName}
|
||||
model={model}
|
||||
refreshIntervalMs={refreshIntervalMs}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 今天 / 本周 / 本月 / 近 7 天 / 连续天数:不随时间范围变,两种图下面都显示 */}
|
||||
<UsageDayTiles
|
||||
appType={appType}
|
||||
providerName={providerName}
|
||||
model={model}
|
||||
refreshIntervalMs={refreshIntervalMs}
|
||||
/>
|
||||
|
||||
<section aria-label={t("usage.tabs.label")} className="flex flex-col">
|
||||
<PageTabs<UsageTab>
|
||||
aria-label={t("usage.tabs.label")}
|
||||
items={TABS.map((id) => ({ value: id, label: tabLabel[id] }))}
|
||||
value={tab}
|
||||
onValueChange={setTab}
|
||||
idPrefix="usage-tab"
|
||||
controls="usage-tabpanel"
|
||||
trailing={tabTrailing}
|
||||
/>
|
||||
|
||||
<div
|
||||
role="tabpanel"
|
||||
id="usage-tabpanel"
|
||||
aria-labelledby={`usage-tab-${tab}`}
|
||||
>
|
||||
{tab === "logs" && (
|
||||
<RequestLogTable
|
||||
range={range}
|
||||
appType={appType}
|
||||
providerName={providerName}
|
||||
model={model}
|
||||
statusCode={statusCode}
|
||||
refreshIntervalMs={refreshIntervalMs}
|
||||
onOpenDetail={setDetailRequestId}
|
||||
/>
|
||||
)}
|
||||
{tab === "providers" && (
|
||||
<ProviderStatsTable
|
||||
range={range}
|
||||
appType={appType}
|
||||
providerName={providerName}
|
||||
model={model}
|
||||
refreshIntervalMs={refreshIntervalMs}
|
||||
/>
|
||||
)}
|
||||
{tab === "models" && (
|
||||
<ModelStatsTable
|
||||
range={range}
|
||||
appType={appType}
|
||||
providerName={providerName}
|
||||
model={model}
|
||||
refreshIntervalMs={refreshIntervalMs}
|
||||
/>
|
||||
)}
|
||||
{tab === "pricing" && <PricingConfigPanel />}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
const body =
|
||||
isEmpty && pricingWhileEmpty ? (
|
||||
// 空库里只有定价可配:不画全是 0 的概览和趋势,直接给页签
|
||||
<div
|
||||
id="main-content"
|
||||
className="flex min-h-0 flex-1 flex-col gap-3.5 overflow-y-auto px-6 pb-6 pt-1"
|
||||
>
|
||||
{tabsSection}
|
||||
</div>
|
||||
) : isEmpty ? (
|
||||
<div className="flex min-h-0 flex-1 flex-col items-center overflow-y-auto px-6 pb-6">
|
||||
<div className="mt-16 flex max-w-[440px] flex-col items-center gap-3 text-center">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="flex h-11 w-11 items-center justify-center rounded-full bg-subtle text-fg-2"
|
||||
>
|
||||
<ChartColumn className="h-5 w-5" strokeWidth={1.5} />
|
||||
</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="m-0 text-section text-fg-1">
|
||||
{t("usage.empty.title")}
|
||||
</p>
|
||||
<p className="m-0 text-body text-fg-2">{t("usage.empty.body")}</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="neutral"
|
||||
size="regular"
|
||||
disabled={syncingSession}
|
||||
onClick={() => void runManualSessionSync()}
|
||||
>
|
||||
{syncingSession && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
|
||||
{t("usage.sessionSync.syncNow")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="quiet"
|
||||
size="regular"
|
||||
onClick={() => {
|
||||
setTab("pricing");
|
||||
setPricingWhileEmpty(true);
|
||||
}}
|
||||
>
|
||||
{t("usage.empty.configurePricing")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
id="main-content"
|
||||
className="flex min-h-0 flex-1 flex-col gap-3.5 overflow-y-auto px-6 pb-6 pt-1"
|
||||
>
|
||||
<UsageHero
|
||||
range={range}
|
||||
appType={scopedAppType}
|
||||
providerName={providerName}
|
||||
model={model}
|
||||
refreshIntervalMs={refreshIntervalMs}
|
||||
compact={compact}
|
||||
/>
|
||||
|
||||
{/* 「全部」看长期分布用热力图;24 小时到 30 天这类短范围用柱状图 */}
|
||||
{range.preset === "all" ? (
|
||||
<UsageHeatmap
|
||||
appType={appType}
|
||||
providerName={providerName}
|
||||
model={model}
|
||||
refreshIntervalMs={refreshIntervalMs}
|
||||
/>
|
||||
) : (
|
||||
<UsageTrendChart
|
||||
range={range}
|
||||
rangeLabel={rangeLabel}
|
||||
appType={appType}
|
||||
providerName={providerName}
|
||||
model={model}
|
||||
refreshIntervalMs={refreshIntervalMs}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 今天 / 本周 / 本月 / 近 7 天 / 连续天数:不随时间范围变,两种图下面都显示 */}
|
||||
<UsageDayTiles
|
||||
appType={appType}
|
||||
providerName={providerName}
|
||||
model={model}
|
||||
refreshIntervalMs={refreshIntervalMs}
|
||||
/>
|
||||
|
||||
{tabsSection}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<AppPageHeader
|
||||
|
||||
@@ -2177,7 +2177,8 @@
|
||||
},
|
||||
"empty": {
|
||||
"title": "No usage data yet",
|
||||
"body": "Usage from session logs and from requests through the routing service shows up here."
|
||||
"body": "Usage from session logs and from requests through the routing service shows up here.",
|
||||
"configurePricing": "Configure model pricing"
|
||||
},
|
||||
"pricing": {
|
||||
"modelsDevTitle": "models.dev sync · {{status}}",
|
||||
|
||||
@@ -2177,7 +2177,8 @@
|
||||
},
|
||||
"empty": {
|
||||
"title": "使用量データはまだありません",
|
||||
"body": "セッションログとルーティングサービス経由のリクエストの使用量がここに記録されます。"
|
||||
"body": "セッションログとルーティングサービス経由のリクエストの使用量がここに記録されます。",
|
||||
"configurePricing": "モデル料金を設定"
|
||||
},
|
||||
"pricing": {
|
||||
"modelsDevTitle": "models.dev 同期 · {{status}}",
|
||||
|
||||
@@ -2178,7 +2178,8 @@
|
||||
},
|
||||
"empty": {
|
||||
"title": "還沒有用量資料",
|
||||
"body": "工作階段日誌和經路由服務的請求都會記在這裡。"
|
||||
"body": "工作階段日誌和經路由服務的請求都會記在這裡。",
|
||||
"configurePricing": "設定模型定價"
|
||||
},
|
||||
"pricing": {
|
||||
"modelsDevTitle": "models.dev 同步 · {{status}}",
|
||||
|
||||
@@ -2177,7 +2177,8 @@
|
||||
},
|
||||
"empty": {
|
||||
"title": "还没有用量数据",
|
||||
"body": "会话日志和经路由服务的请求都会记在这里。"
|
||||
"body": "会话日志和经路由服务的请求都会记在这里。",
|
||||
"configurePricing": "配置模型定价"
|
||||
},
|
||||
"pricing": {
|
||||
"modelsDevTitle": "models.dev 同步 · {{status}}",
|
||||
|
||||
@@ -471,4 +471,26 @@ describe("UsageDashboard", () => {
|
||||
expect(await screen.findByText("usage.empty.title")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("usage-hero")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("still lets the pricing tab be opened from the empty state", async () => {
|
||||
usageApiMock.getUsageSummary.mockResolvedValue({ totalRequests: 0 });
|
||||
const user = userEvent.setup();
|
||||
renderDashboard();
|
||||
|
||||
await screen.findByText("usage.empty.title");
|
||||
expect(
|
||||
screen.queryByTestId("pricing-config-panel"),
|
||||
).not.toBeInTheDocument();
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: "usage.empty.configurePricing" }),
|
||||
);
|
||||
expect(
|
||||
await screen.findByTestId("pricing-config-panel"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("tab", { name: "usage.tabs.pricing" }),
|
||||
).toHaveAttribute("aria-selected", "true");
|
||||
// 空库里没有可看的概览,不画全是 0 的卡
|
||||
expect(screen.queryByTestId("usage-hero")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user