fix(web): render and filter usage as environments respond#9860
2 unresolved threads · Jump to file
apps/web/src/components/usage/UsagePage.tsx · Earlier revision c5365fd1 · right 683
Original diff
@@ -592,66 +601,145 @@ function UsageCoverageNotice({
);
}
-/**
- * Per-device progress while the page waits for every environment to answer.
- * Only rendered with two or more devices; a lone device has nothing to
- * enumerate.
- */
-function UsageDeviceStrip({
+/** Environment selection and scan progress share a permanent header control. */
+function UsageEnvironmentFilter({
environments,
+ selectedEnvironments,
+ selectedEnvironmentIds,
+ onSelectionChange,
+ showUsageStatus,
+ isPartial,
+ duplicateSources,
+ staleEnvironments,
}: {
readonly environments: readonly EnvironmentUsageStatus[];
+ readonly selectedEnvironments: readonly EnvironmentUsageStatus[];
+ readonly selectedEnvironmentIds: ReadonlySet<EnvironmentId> | null;
+ readonly onSelectionChange: (ids: ReadonlySet<EnvironmentId> | null) => void;
+ readonly showUsageStatus: boolean;
+ readonly isPartial: boolean;
+ readonly duplicateSources: readonly string[];
+ readonly staleEnvironments: readonly string[];
}) {
- const scanning = environments.filter(
- (environment) => environment.summary === null && environment.error === null,
- );
+ const allSelected = selectedEnvironmentIds === null;
+ const label = allSelected
+ ? "All environments"
+ : selectedEnvironments.length === 1
+ ? selectedEnvironments[0]!.label
+ : `${selectedEnvironments.length} environments`;
+ const pendingCount = selectedEnvironments.filter(
+ (environment) =>
+ environment.error === null && (environment.isPending || environment.summary === null),
+ ).length;
+ const hasIssue =
+ selectedEnvironments.some((environment) => environment.error !== null) ||
+ staleEnvironments.length > 0;
+
return (
- <div className="flex flex-wrap items-baseline gap-x-4 gap-y-1 border border-border px-3 py-2 text-xs">
- {environments.map((environment) => {
- if (environment.summary !== null) {
- return (
- <span
- key={environment.environmentId}
- className="flex items-center gap-1 text-foreground"
- >
- <CheckIcon className="size-3 text-emerald-600 dark:text-emerald-300/90" aria-hidden />
- {environment.label}
- </span>
- );
- }
- if (environment.error !== null) {
- return (
- <span
- key={environment.environmentId}
- className="flex items-center gap-1 text-destructive"
- >
- <XIcon className="size-3" aria-hidden />
- {environment.label}
- </span>
- );
- }
- return (
- <span
- key={environment.environmentId}
- className="animate-status-pulse text-muted-foreground"
- >
- {environment.label}…
+ <>
+ <Menu>
+ <MenuTrigger className="group/usage-environment inline-flex min-w-0 max-w-full cursor-pointer items-center gap-1 rounded-sm text-left focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring">
+ <span className="min-w-0 truncate">{label}</span>
+ <span className="flex size-3.5 shrink-0 items-center justify-center text-muted-foreground">
+ {showUsageStatus && pendingCount > 0 ? (
+ <>
+ <CircleDashedIcon className="size-3.5" aria-hidden />
+ <span className="sr-only">
+ {pendingCount} {pendingCount === 1 ? "environment" : "environments"} still
+ scanning
+ {isPartial ? "; totals are partial" : ""}
+ </span>
+ </>
+ ) : showUsageStatus && hasIssue ? (
+ <CircleAlertIcon
+ className="size-3.5 text-amber-600 dark:text-amber-400"
+ aria-label="Some environments could not report usage"
+ />
+ ) : (
+ <ChevronDownIcon
+ className="size-3.5 opacity-0 transition-opacity group-hover/usage-environment:opacity-100 group-focus-visible/usage-environment:opacity-100 group-data-popup-open/usage-environment:opacity-100"
+ aria-hidden
+ />
+ )}
</span>
- );
- })}
- <span className="ms-auto text-muted-foreground">
- {scanning.length === 1
- ? "1 device still scanning"
- : `${scanning.length} devices still scanning`}
- </span>
- </div>
+ </MenuTrigger>
+ <MenuPopup align="start" className="w-80 max-w-[calc(100vw-2rem)]">
+ <MenuCheckboxItem
+ checked={allSelected}
+ closeOnClick={false}
+ onCheckedChange={(checked) => onSelectionChange(checked ? null : new Set())}
+ >
+ All environments
+ </MenuCheckboxItem>
+ <MenuSeparator />
+ {environments.map((environment) => {
+ const checked =
+ selectedEnvironmentIds === null ||
+ selectedEnvironmentIds.has(environment.environmentId);
+ const status =
+ environment.error !== null
+ ? "Unavailable"
+ : staleEnvironments.includes(environment.environmentId)
+ ? "Update required"Stale status ignores unselected environments
Low Severity
The dropdown lists every environment but reads Update required from merged.staleEnvironments, which only includes the current selection. An unselected incompatible environment shows as Ready, and deselecting a stale one flips its status the same way.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit c5365fd1c732bbaacdf2f878d0d4532fc95cc4d6. Configure here.
Fixed in ed79de694. Each menu row now checks its own summary against the same compatibility range used by the merger, independently of selection. Verified in the browser that deselecting an incompatible environment keeps Update required. Existing version-range tests, Usage state/page tests, web typecheck, and scoped lint pass.
apps/web/src/components/usage/UsageLimits.tsx · Earlier revision c65b7709 · right 440
Original diff
@@ -426,10 +426,18 @@ function SourceLimits({ source, now }: { readonly source: LimitsSource; readonly
* Countdowns anchor to render time rather than ticking: a live clock would
* repaint the page every minute for no decision-changing gain.
*/
-export function UsageLimitsSection() {
+export function UsageLimitsSection({
+ selectedEnvironmentIds,
+}: {
+ readonly selectedEnvironmentIds: ReadonlySet<EnvironmentId> | null;
+}) {
const presentations = useAtomValue(environmentPresentations.presentationsAtom);
- const groups = collectLimitsGroups(presentations);
- const sources = collectLimitSources(presentations);
+ const selected =
+ selectedEnvironmentIds === null
+ ? presentations
+ : new Map([...presentations].filter(([id]) => selectedEnvironmentIds.has(id)));
+ const groups = collectLimitsGroups(selected);
+ const sources = collectLimitSources(selected);Limits empty copy ignores filter
Low Severity
The new environment filter can hide every limits-reporting environment, but the empty state still says no connected environment reports limits. On the Limits tab, a missing or cleared selection also says to connect or select an environment to see usage.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit c65b7709e1b305db60fc551d8635eef42319c094. Configure here.
Fixed in 3c48e353a. The limits empty state now refers to selected environments, and clearing the filter on the Limits tab says to select an environment to see limits. Scoped lint and the usage/page tests pass.