fix(ui): stamp the tag list with the range it answers

Changing the date range left the previous range's tags in state until the
new request landed, so the filter either offered tags that range no longer
has or, when the old range was empty, stated "no tags" about a range nobody
had measured yet.

Stamp the fetched list with its range key and select it during render, the
same way the request tiles above already guard against a superseded range.
Clearing in the effect would be a render too late.
This commit is contained in:
Yuneng Jiang 2026-08-27 15:43:39 -07:00
parent 13b5c80c90
commit beed32eb60
No known key found for this signature in database
2 changed files with 48 additions and 5 deletions

View file

@ -610,6 +610,42 @@ describe("UsagePage", () => {
expect(screen.getByTestId("entity-usage")).toHaveAttribute("data-entity-list", "[]");
});
it("should drop the previous range's tags as soon as the range changes", async () => {
mockTagListCall.mockResolvedValue({ "old-range-tag": { name: "old-range-tag" } } as never);
renderWithProviders(<UsagePage {...defaultProps} />);
act(() => {
fireEvent.change(screen.getByTestId("usage-view-select"), { target: { value: "tag" } });
});
await waitFor(() => {
expect(screen.getByTestId("entity-usage")).toHaveAttribute(
"data-entity-list",
JSON.stringify([{ label: "old-range-tag", value: "old-range-tag" }]),
);
});
let resolveNewRange: (tags: Record<string, unknown>) => void = () => {};
mockTagListCall.mockReturnValue(
new Promise((resolve) => {
resolveNewRange = resolve;
}) as ReturnType<typeof networking.tagListCall>,
);
act(() => {
fireEvent.click(screen.getByTestId("pick-a-different-range"));
});
expect(screen.getByTestId("entity-usage")).toHaveAttribute("data-entity-list", "null");
await act(async () => {
resolveNewRange({});
});
expect(screen.getByTestId("entity-usage")).toHaveAttribute("data-entity-list", "[]");
});
it("should show tag usage selector option for internal users", async () => {
mockUseAuthorized.mockReturnValue({
isLoading: false,

View file

@ -96,7 +96,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
to: initialToDate,
});
const [allTags, setAllTags] = useState<EntityList[] | null>(null);
const [fetchedTags, setFetchedTags] = useState<FetchedForRange<EntityList[]> | null>(null);
const { data: customers = [] } = useCustomers();
const { data: agentsResponse } = useAgents();
const { data: currentUser } = useCurrentUser();
@ -138,6 +138,12 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
const startTime = useMemo(() => (dateValue.from ? new Date(dateValue.from) : null), [dateValue.from]);
const endTime = useMemo(() => (dateValue.to ? new Date(dateValue.to) : null), [dateValue.to]);
// Stamped and selected during render like the request tiles below: the tag
// filter reads "no tags" from an empty list, so a list left over from the
// previous range would state that about a range nobody has measured yet.
const currentTagRangeKey = fetchedRangeKey(startTime, endTime);
const allTags = selectForRange(fetchedTags, currentTagRangeKey);
useEffect(() => {
if (!accessToken) return;
let cancelled = false;
@ -145,12 +151,13 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
try {
const tags = await tagListCall(accessToken, startTime, endTime);
if (cancelled) return;
setAllTags(
Object.values(tags).map((tag: Tag) => ({
setFetchedTags({
rangeKey: currentTagRangeKey,
value: Object.values(tags).map((tag: Tag) => ({
label: tag.name,
value: tag.name,
})),
);
});
} catch (e) {
if (!cancelled) {
console.error("Failed to fetch tag list", e);
@ -160,7 +167,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
return () => {
cancelled = true;
};
}, [accessToken, startTime, endTime]);
}, [accessToken, startTime, endTime, currentTagRangeKey]);
// Everything the request tiles read is stamped with the range it answers and
// selected during render, rather than cleared in an effect. An effect runs