fix(ui): keep the usage filter visible when the caller's scope is empty

The tag usage filter was removed from the DOM whenever the tag list came
back empty, so an internal user whose traffic all runs through team keys
saw a blank Tag Usage panel with no filter and no explanation. Their scope
is legitimately empty, but the page gave them no way to tell that apart
from a broken or gated feature.

Render the filter whenever the entity list has resolved, disabling it and
swapping in an entity-specific empty message when there are no options.
A still-unresolved list keeps hiding the control as before.
This commit is contained in:
Yuneng Jiang 2026-08-27 14:51:40 -07:00
parent 3746ba58d7
commit cbac167a4a
No known key found for this signature in database
4 changed files with 76 additions and 4 deletions

View file

@ -65,10 +65,19 @@ vi.mock("@/components/EntityUsageExport/EntityUsageExportModal", () => ({
}));
vi.mock("@/components/EntityUsageExport", () => ({
UsageExportHeader: ({ filterLabel, filterSlot }: { filterLabel?: string; filterSlot?: ReactNode }) => (
UsageExportHeader: ({
filterLabel,
filterSlot,
showFilters,
}: {
filterLabel?: string;
filterSlot?: ReactNode;
showFilters?: boolean;
}) => (
<div>
<span>Usage Export Header</span>
<span>{filterLabel}</span>
<span>{`show-filters:${showFilters === true}`}</span>
{filterSlot}
</div>
),
@ -739,6 +748,26 @@ describe("EntityUsage", () => {
});
});
it("should still request the filter when the caller's tag scope is empty", async () => {
render(<EntityUsage {...defaultProps} entityList={[]} />);
await waitFor(() => {
expect(mockTagDailyActivityCall).toHaveBeenCalled();
});
expect(screen.getByText("show-filters:true")).toBeInTheDocument();
});
it("should not request the filter while the entity list is still unresolved", async () => {
render(<EntityUsage {...defaultProps} entityList={null} />);
await waitFor(() => {
expect(mockTagDailyActivityCall).toHaveBeenCalled();
});
expect(screen.getByText("show-filters:false")).toBeInTheDocument();
});
it("should display Agent Activity tab for team entity type", async () => {
render(<EntityUsage {...defaultProps} entityType="team" />);

View file

@ -661,7 +661,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
dateValue={dateValue}
entityType={entityType}
spendData={spendData}
showFilters={filterSlot === undefined && entityList !== null && entityList.length > 0}
showFilters={filterSlot === undefined && entityList !== null}
filterSlot={filterSlot}
filterLabel={getFilterLabel(entityType)}
filterPlaceholder={getFilterPlaceholder(entityType)}

View file

@ -84,4 +84,41 @@ describe("UsageExportHeader", () => {
expect(screen.getByTestId("custom-filter")).toBeInTheDocument();
expect(screen.queryByRole("combobox")).not.toBeInTheDocument();
});
it("should keep the filter visible and disabled with an explanation when the caller has no options", () => {
renderWithProviders(
<UsageExportHeader
{...defaultProps}
entityType="tag"
showFilters
filterLabel="Filter by tag"
filterPlaceholder="Select tag to filter..."
filterOptions={[]}
onFiltersChange={vi.fn()}
/>,
);
expect(screen.getByText("Filter by tag")).toBeInTheDocument();
const input = screen.getByPlaceholderText("No tags with usage in this range");
expect(input).toBeDisabled();
expect(screen.queryByPlaceholderText("Select tag to filter...")).not.toBeInTheDocument();
});
it("should leave the filter enabled with its normal placeholder when options exist", () => {
renderWithProviders(
<UsageExportHeader
{...defaultProps}
entityType="tag"
showFilters
filterLabel="Filter by tag"
filterPlaceholder="Select tag to filter..."
filterOptions={[{ label: "prod", value: "prod" }]}
onFiltersChange={vi.fn()}
/>,
);
const input = screen.getByPlaceholderText("Select tag to filter...");
expect(input).toBeEnabled();
expect(screen.queryByPlaceholderText("No tags with usage in this range")).not.toBeInTheDocument();
});
});

View file

@ -54,9 +54,11 @@ const UsageExportHeader: React.FC<UsageExportHeaderProps> = ({
const anchor = useComboboxAnchor();
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
const hasFilters = filterSlot != null || (showFilters && filterOptions.length > 0);
const hasFilters = filterSlot != null || showFilters;
const optionValues = filterOptions.map((option) => option.value);
const labelOf = (value: string) => filterOptions.find((option) => option.value === value)?.label ?? value;
const hasNoOptions = filterOptions.length === 0;
const emptyPlaceholder = `No ${entityType}s with usage in this range`;
const filterList = (
<ComboboxContent anchor={anchor}>
@ -74,6 +76,7 @@ const UsageExportHeader: React.FC<UsageExportHeaderProps> = ({
const builtInFilter = (
<Combobox
multiple
disabled={hasNoOptions}
items={optionValues}
value={selectedFilters}
onValueChange={(next: string[]) => onFiltersChange?.(next)}
@ -88,7 +91,10 @@ const UsageExportHeader: React.FC<UsageExportHeaderProps> = ({
))
}
</ComboboxValue>
<ComboboxChipsInput placeholder={filterPlaceholder} aria-label={filterPlaceholder} />
<ComboboxChipsInput
placeholder={hasNoOptions ? emptyPlaceholder : filterPlaceholder}
aria-label={hasNoOptions ? emptyPlaceholder : filterPlaceholder}
/>
{selectedFilters.length > 0 && <ComboboxClear aria-label={`Clear ${filterLabel ?? "filters"}`} />}
</ComboboxChips>
{filterList}