FN-5929: track hidden usage windows by instance
Store hidden usage windows with per-instance identities while keeping legacy labels restorable. - persist hidden usage windows with an index-qualified identity instead of label-only keys - treat legacy label-only entries as matches for restore/show-hidden behavior - guard hidden-window counts when provider windows are empty or undefined - add regression coverage for duplicate labels, multiple hidden windows, and legacy persistence Files changed: packages/dashboard/app/components/UsageIndicator.tsx | 71 +++++++++---- packages/dashboard/app/components/__tests__/UsageIndicator.test.tsx | 113 ++++++++++++++++++++- 2 files changed, 159 insertions(+), 25 deletions(-) Fusion-Task-Id: FN-5929 Fusion-Task-Lineage: eb48988e-80b8-4d6c-90b9-0d324564f1aa
This commit is contained in:
@@ -162,39 +162,62 @@ function setProviderOrder(names: string[], projectId: string | undefined): void
|
|||||||
setScopedItem(PROVIDER_ORDER_KEY, JSON.stringify(names), projectId);
|
setScopedItem(PROVIDER_ORDER_KEY, JSON.stringify(names), projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const WINDOW_IDENTITY_DELIMITER = "::";
|
||||||
|
|
||||||
|
function getWindowIdentity(windowLabel: string, windowIndex: number): string {
|
||||||
|
return `${windowIndex}${WINDOW_IDENTITY_DELIMITER}${windowLabel}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesHiddenWindowEntry(
|
||||||
|
persistedEntry: string,
|
||||||
|
windowLabel: string,
|
||||||
|
windowIndex: number
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
persistedEntry === getWindowIdentity(windowLabel, windowIndex) ||
|
||||||
|
persistedEntry === windowLabel
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isWindowHidden(
|
function isWindowHidden(
|
||||||
providerName: string,
|
providerName: string,
|
||||||
windowLabel: string,
|
windowLabel: string,
|
||||||
|
windowIndex: number,
|
||||||
hidden: Record<string, string[]>
|
hidden: Record<string, string[]>
|
||||||
): boolean {
|
): boolean {
|
||||||
return hidden[providerName]?.includes(windowLabel) ?? false;
|
return (hidden[providerName] ?? []).some((persistedEntry) =>
|
||||||
|
matchesHiddenWindowEntry(persistedEntry, windowLabel, windowIndex)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRenderedHiddenWindowCount(
|
function getRenderedHiddenWindowCount(
|
||||||
providerName: string,
|
providerName: string,
|
||||||
windows: UsageWindow[],
|
windows: UsageWindow[] | undefined,
|
||||||
hidden: Record<string, string[]>
|
hidden: Record<string, string[]>
|
||||||
): number {
|
): number {
|
||||||
return windows.reduce((count, window) => {
|
return (windows ?? []).reduce((count, window, windowIndex) => {
|
||||||
return count + (isWindowHidden(providerName, window.label, hidden) ? 1 : 0);
|
return count + (isWindowHidden(providerName, window.label, windowIndex, hidden) ? 1 : 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRestorableHiddenWindowCount(
|
function getRestorableHiddenWindowCount(
|
||||||
providerName: string,
|
providerName: string,
|
||||||
windows: UsageWindow[],
|
windows: UsageWindow[] | undefined,
|
||||||
hidden: Record<string, string[]>
|
hidden: Record<string, string[]>
|
||||||
): number {
|
): number {
|
||||||
const renderedHiddenCount = getRenderedHiddenWindowCount(providerName, windows, hidden);
|
const liveWindows = windows ?? [];
|
||||||
const persistedHiddenLabels = hidden[providerName] ?? [];
|
const renderedHiddenCount = getRenderedHiddenWindowCount(providerName, liveWindows, hidden);
|
||||||
|
const persistedHiddenEntries = hidden[providerName] ?? [];
|
||||||
|
|
||||||
if (persistedHiddenLabels.length === 0) {
|
if (persistedHiddenEntries.length === 0) {
|
||||||
return renderedHiddenCount;
|
return renderedHiddenCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
const liveWindowLabels = new Set(windows.map((window) => window.label));
|
const orphanedHiddenCount = persistedHiddenEntries.reduce((count, persistedEntry) => {
|
||||||
const orphanedHiddenCount = persistedHiddenLabels.reduce((count, label) => {
|
const matchesLiveWindow = liveWindows.some((window, windowIndex) =>
|
||||||
return count + (liveWindowLabels.has(label) ? 0 : 1);
|
matchesHiddenWindowEntry(persistedEntry, window.label, windowIndex)
|
||||||
|
);
|
||||||
|
return count + (matchesLiveWindow ? 0 : 1);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
return renderedHiddenCount + orphanedHiddenCount;
|
return renderedHiddenCount + orphanedHiddenCount;
|
||||||
@@ -349,7 +372,7 @@ interface ProviderCardProps {
|
|||||||
provider: ProviderUsage;
|
provider: ProviderUsage;
|
||||||
viewMode: 'used' | 'remaining';
|
viewMode: 'used' | 'remaining';
|
||||||
hiddenWindows: Record<string, string[]>;
|
hiddenWindows: Record<string, string[]>;
|
||||||
onToggleWindow: (providerName: string, windowLabel: string) => void;
|
onToggleWindow: (providerName: string, windowLabel: string, windowIndex: number) => void;
|
||||||
onShowAllHidden: (providerName: string) => void;
|
onShowAllHidden: (providerName: string) => void;
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
isDragOver: boolean;
|
isDragOver: boolean;
|
||||||
@@ -434,7 +457,8 @@ function ProviderCard({
|
|||||||
onMoveUp,
|
onMoveUp,
|
||||||
onMoveDown,
|
onMoveDown,
|
||||||
}: ProviderCardProps) {
|
}: ProviderCardProps) {
|
||||||
const hiddenCount = getRestorableHiddenWindowCount(provider.name, provider.windows, hiddenWindows);
|
const providerWindows = provider.windows ?? [];
|
||||||
|
const hiddenCount = getRestorableHiddenWindowCount(provider.name, providerWindows, hiddenWindows);
|
||||||
const getStatusBadge = () => {
|
const getStatusBadge = () => {
|
||||||
switch (provider.status) {
|
switch (provider.status) {
|
||||||
case "ok":
|
case "ok":
|
||||||
@@ -526,10 +550,10 @@ function ProviderCard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{provider.windows.length > 0 ? (
|
{providerWindows.length > 0 ? (
|
||||||
<div className="usage-provider-windows">
|
<div className="usage-provider-windows">
|
||||||
{provider.windows.map((window, index) => {
|
{providerWindows.map((window, index) => {
|
||||||
const hidden = isWindowHidden(provider.name, window.label, hiddenWindows);
|
const hidden = isWindowHidden(provider.name, window.label, index, hiddenWindows);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UsageWindowRow
|
<UsageWindowRow
|
||||||
@@ -537,7 +561,7 @@ function ProviderCard({
|
|||||||
window={window}
|
window={window}
|
||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
isHidden={hidden}
|
isHidden={hidden}
|
||||||
onToggleHidden={() => onToggleWindow(provider.name, window.label)}
|
onToggleHidden={() => onToggleWindow(provider.name, window.label, index)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -694,10 +718,15 @@ export function UsageIndicator({ isOpen, onClose, projectId, anchorRect }: Usage
|
|||||||
setHiddenWindows(hiddenWindows, projectId);
|
setHiddenWindows(hiddenWindows, projectId);
|
||||||
}, [hiddenWindows, projectId]);
|
}, [hiddenWindows, projectId]);
|
||||||
|
|
||||||
const handleToggleWindow = useCallback((providerName: string, windowLabel: string) => {
|
const handleToggleWindow = useCallback((providerName: string, windowLabel: string, windowIndex: number) => {
|
||||||
setHiddenWindowsState((previous) => {
|
setHiddenWindowsState((previous) => {
|
||||||
if (isWindowHidden(providerName, windowLabel, previous)) {
|
const windowIdentity = getWindowIdentity(windowLabel, windowIndex);
|
||||||
const remaining = (previous[providerName] ?? []).filter((label) => label !== windowLabel);
|
const providerHiddenWindows = previous[providerName] ?? [];
|
||||||
|
|
||||||
|
if (isWindowHidden(providerName, windowLabel, windowIndex, previous)) {
|
||||||
|
const remaining = providerHiddenWindows.filter(
|
||||||
|
(persistedEntry) => !matchesHiddenWindowEntry(persistedEntry, windowLabel, windowIndex)
|
||||||
|
);
|
||||||
if (remaining.length === 0) {
|
if (remaining.length === 0) {
|
||||||
const { [providerName]: _removed, ...rest } = previous;
|
const { [providerName]: _removed, ...rest } = previous;
|
||||||
return rest;
|
return rest;
|
||||||
@@ -711,7 +740,7 @@ export function UsageIndicator({ isOpen, onClose, projectId, anchorRect }: Usage
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...previous,
|
...previous,
|
||||||
[providerName]: [...(previous[providerName] ?? []), windowLabel],
|
[providerName]: [...providerHiddenWindows, windowIdentity],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ const USAGE_VIEW_MODE_KEY = scopedKey("kb-usage-view-mode", TEST_PROJECT_ID);
|
|||||||
const USAGE_HIDDEN_WINDOWS_KEY = scopedKey("kb-usage-hidden-windows", TEST_PROJECT_ID);
|
const USAGE_HIDDEN_WINDOWS_KEY = scopedKey("kb-usage-hidden-windows", TEST_PROJECT_ID);
|
||||||
const USAGE_PROVIDER_ORDER_KEY = scopedKey("kb-usage-provider-order", TEST_PROJECT_ID);
|
const USAGE_PROVIDER_ORDER_KEY = scopedKey("kb-usage-provider-order", TEST_PROJECT_ID);
|
||||||
|
|
||||||
|
function getWindowIdentity(label: string, index: number): string {
|
||||||
|
return `${index}::${label}`;
|
||||||
|
}
|
||||||
|
|
||||||
describe("UsageIndicator", () => {
|
describe("UsageIndicator", () => {
|
||||||
const mockOnClose = vi.fn();
|
const mockOnClose = vi.fn();
|
||||||
const mockRefresh = vi.fn();
|
const mockRefresh = vi.fn();
|
||||||
@@ -913,11 +917,11 @@ describe("UsageIndicator", () => {
|
|||||||
fireEvent.click(screen.getByRole("button", { name: "Hide Session (5h)" }));
|
fireEvent.click(screen.getByRole("button", { name: "Hide Session (5h)" }));
|
||||||
|
|
||||||
expect(localStorage.getItem(USAGE_HIDDEN_WINDOWS_KEY)).toBe(
|
expect(localStorage.getItem(USAGE_HIDDEN_WINDOWS_KEY)).toBe(
|
||||||
JSON.stringify({ Anthropic: ["Session (5h)"] })
|
JSON.stringify({ Anthropic: [getWindowIdentity("Session (5h)", 0)] })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("restores hidden windows from localStorage on mount", () => {
|
it("restores hidden windows from legacy label-only localStorage on mount", () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
USAGE_HIDDEN_WINDOWS_KEY,
|
USAGE_HIDDEN_WINDOWS_KEY,
|
||||||
JSON.stringify({ Anthropic: ["Session (5h)"] })
|
JSON.stringify({ Anthropic: ["Session (5h)"] })
|
||||||
@@ -1028,7 +1032,56 @@ describe("UsageIndicator", () => {
|
|||||||
expect(screen.getByTestId("usage-show-hidden-btn")).toHaveTextContent("Show hidden (2)");
|
expect(screen.getByTestId("usage-show-hidden-btn")).toHaveTextContent("Show hidden (2)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("counts currently rendered hidden rows when persisted labels match duplicate live windows", () => {
|
it("hiding one duplicate-labeled MiniMax window only hides that instance", () => {
|
||||||
|
mockUseUsageData.mockReturnValue(createUsageDataState({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
name: "MiniMax",
|
||||||
|
icon: "🧠",
|
||||||
|
status: "ok",
|
||||||
|
windows: [
|
||||||
|
{
|
||||||
|
label: "Daily",
|
||||||
|
percentUsed: 20,
|
||||||
|
percentLeft: 80,
|
||||||
|
resetText: "resets in 1h",
|
||||||
|
resetMs: 3600000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Daily",
|
||||||
|
percentUsed: 60,
|
||||||
|
percentLeft: 40,
|
||||||
|
resetText: "resets in 4h",
|
||||||
|
resetMs: 14400000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: new Date(),
|
||||||
|
refresh: mockRefresh,
|
||||||
|
}));
|
||||||
|
|
||||||
|
render(<UsageIndicator isOpen={true} onClose={mockOnClose} projectId={TEST_PROJECT_ID} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getAllByRole("button", { name: "Hide Daily" })[1]);
|
||||||
|
|
||||||
|
const hiddenRows = Array.from(document.querySelectorAll(".usage-window--hidden"));
|
||||||
|
expect(hiddenRows).toHaveLength(1);
|
||||||
|
expect(hiddenRows[0]).toHaveTextContent("Daily");
|
||||||
|
expect(localStorage.getItem(USAGE_HIDDEN_WINDOWS_KEY)).toBe(
|
||||||
|
JSON.stringify({ MiniMax: [getWindowIdentity("Daily", 1)] })
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId("usage-show-hidden-btn")).toHaveTextContent("Show hidden (1)");
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId("usage-show-hidden-btn"));
|
||||||
|
|
||||||
|
expect(document.querySelectorAll(".usage-window--hidden")).toHaveLength(0);
|
||||||
|
expect(screen.queryByTestId("usage-show-hidden-btn")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("counts currently rendered hidden rows when legacy persisted labels match duplicate live windows", () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
USAGE_HIDDEN_WINDOWS_KEY,
|
USAGE_HIDDEN_WINDOWS_KEY,
|
||||||
JSON.stringify({ Anthropic: ["Daily"] })
|
JSON.stringify({ Anthropic: ["Daily"] })
|
||||||
@@ -1132,7 +1185,7 @@ describe("UsageIndicator", () => {
|
|||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Hide Session (5h)" }));
|
fireEvent.click(screen.getByRole("button", { name: "Hide Session (5h)" }));
|
||||||
expect(localStorage.getItem(USAGE_HIDDEN_WINDOWS_KEY)).toBe(
|
expect(localStorage.getItem(USAGE_HIDDEN_WINDOWS_KEY)).toBe(
|
||||||
JSON.stringify({ Anthropic: ["Session (5h)"] })
|
JSON.stringify({ Anthropic: [getWindowIdentity("Session (5h)", 0)] })
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("usage-show-hidden-btn"));
|
fireEvent.click(screen.getByTestId("usage-show-hidden-btn"));
|
||||||
@@ -1151,6 +1204,32 @@ describe("UsageIndicator", () => {
|
|||||||
expect(screen.getByText("Session (5h)").closest(".usage-window")).not.toHaveClass("usage-window--hidden");
|
expect(screen.getByText("Session (5h)").closest(".usage-window")).not.toHaveClass("usage-window--hidden");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("tracks multiple distinct hidden Anthropic windows per instance", () => {
|
||||||
|
mockUseUsageData.mockReturnValue(createUsageDataState({
|
||||||
|
providers: mockProviders,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: new Date(),
|
||||||
|
refresh: mockRefresh,
|
||||||
|
}));
|
||||||
|
|
||||||
|
render(<UsageIndicator isOpen={true} onClose={mockOnClose} projectId={TEST_PROJECT_ID} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Hide Session (5h)" }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Hide Weekly" }));
|
||||||
|
|
||||||
|
expect(document.querySelectorAll(".usage-window--hidden")).toHaveLength(2);
|
||||||
|
expect(screen.getByTestId("usage-show-hidden-btn")).toHaveTextContent("Show hidden (2)");
|
||||||
|
expect(localStorage.getItem(USAGE_HIDDEN_WINDOWS_KEY)).toBe(
|
||||||
|
JSON.stringify({
|
||||||
|
Anthropic: [
|
||||||
|
getWindowIdentity("Session (5h)", 0),
|
||||||
|
getWindowIdentity("Weekly", 1),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("does not show provider-level show hidden button when no windows are hidden", () => {
|
it("does not show provider-level show hidden button when no windows are hidden", () => {
|
||||||
mockUseUsageData.mockReturnValue(createUsageDataState({
|
mockUseUsageData.mockReturnValue(createUsageDataState({
|
||||||
providers: mockProviders,
|
providers: mockProviders,
|
||||||
@@ -1165,6 +1244,32 @@ describe("UsageIndicator", () => {
|
|||||||
expect(screen.queryByTestId("usage-show-hidden-btn")).not.toBeInTheDocument();
|
expect(screen.queryByTestId("usage-show-hidden-btn")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not crash or show hidden controls when provider windows are empty or undefined", () => {
|
||||||
|
mockUseUsageData.mockReturnValue(createUsageDataState({
|
||||||
|
providers: [
|
||||||
|
{ name: "Anthropic", icon: "🅰️", status: "ok", windows: [] },
|
||||||
|
{ name: "MiniMax", icon: "🧠", status: "ok", windows: undefined as unknown as ProviderUsage["windows"] },
|
||||||
|
],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: new Date(),
|
||||||
|
refresh: mockRefresh,
|
||||||
|
}));
|
||||||
|
|
||||||
|
render(
|
||||||
|
<UsageIndicator
|
||||||
|
isOpen={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
projectId={TEST_PROJECT_ID}
|
||||||
|
anchorRect={createAnchorRect()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Anthropic")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("MiniMax")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("usage-show-hidden-btn")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// ProviderIcon integration tests
|
// ProviderIcon integration tests
|
||||||
it("renders SVG provider icons instead of emoji", () => {
|
it("renders SVG provider icons instead of emoji", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user