FN-7076: show Command Center concurrency utilization
Surface Command Center concurrency utilization beside the editable caps. - Add loaded-only running-agent readouts for global and current-project concurrency. - Render clamped current-use markers on the global and project max-concurrent sliders without intercepting drags. - Cover loaded, zero, unavailable, over-subscribed, and persistence behavior in Command Center tests. - Document the utilization indicators and add a release changeset. Files changed: .../fn-7076-command-center-concurrency-counts.md | 7 ++ docs/dashboard-guide.md | 3 +- .../command-center/CommandCenterControls.css | 22 ++++ .../command-center/CommandCenterControls.tsx | 103 ++++++++++++++----- .../__tests__/CommandCenterControls.test.tsx | 111 +++++++++++++++++++++ 5 files changed, 219 insertions(+), 27 deletions(-) Fusion-Task-Id: FN-7076 Fusion-Task-Lineage: 540a57aa-a31b-4a57-aff6-47b373713ab3
This commit is contained in:
7
.changeset/fn-7076-command-center-concurrency-counts.md
Normal file
7
.changeset/fn-7076-command-center-concurrency-counts.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Command Center Concurrency now shows running agents and current-use markers on global/project sliders.
|
||||
category: feature
|
||||
dev: CommandCenterControls reuses useGlobalConcurrency's currentlyActive/projectsActive (FN-7071) to render count readouts and clamped slider-track dots; no new backend routes.
|
||||
@@ -839,7 +839,8 @@ Features:
|
||||
- Global date-range picker in the header scopes the analytics tabs; **Last 24h**, **Last 7 days**, **Last 30 days**, **All time**, and custom/open-ended ranges each request their selected analytics window. **Mission Control** remains live rather than historical.
|
||||
<!-- FNXC:CommandCenter 2026-06-25-19:47: FN-7019 restored the user-facing picker contract: preset and custom date-range selections must change every historical analytics tab, while Mission Control stays live and intentionally ignores historical range filters. -->
|
||||
<!-- FNXC:CommandCenter 2026-06-19-23:54: FN-6755 moved team-specific operations out of Overview: org hierarchy and heartbeat pause/resume live in Team, while Overview keeps global AI engine, concurrency, and theme controls. -->
|
||||
- **Overview controls dashboard** sits at the top of the Overview landing surface on desktop and mobile. It includes AI engine stop/start backed by `globalPause`, live scheduler status from executor stats, range sliders for `maxConcurrent`, `maxTriageConcurrent`, and `maxWorktrees` that persist through `/api/settings`, and a compact theme dropdown with the same color-chip swatches and Shadcn variant list as Settings → Appearance. These controls reuse existing APIs and App-level theme setters; they do not add a new backend route or second theme owner.
|
||||
<!-- FNXC:GlobalConcurrencyControls 2026-06-26-00:00: The Command Center Concurrency card mirrors the footer concurrency popover by showing read-only running-agent counts and current-use markers for the shared global cap and current-project max-concurrent slider. -->
|
||||
- **Overview controls dashboard** sits at the top of the Overview landing surface on desktop and mobile. It includes AI engine stop/start backed by `globalPause`, live scheduler status from executor stats, the shared Global Max Concurrent slider backed by `/api/global-concurrency`, range sliders for `maxConcurrent`, `maxTriageConcurrent`, and `maxWorktrees` that persist through `/api/settings`, and a compact theme dropdown with the same color-chip swatches and Shadcn variant list as Settings → Appearance. The global and current-project max-concurrent sliders show running-agent counts plus a current-use dot on the track once utilization data loads; triage and worktree sliders remain cap-only. These controls reuse existing APIs and App-level theme setters; they do not add a new backend route or second theme owner.
|
||||
- **Overview** summarizes token usage/cost, autonomy, active nodes, sessions, agent runs, tasks done, model breadth, and real open signals, and includes the SDLC throughput funnel for the selected range at the bottom of the Overview content in loading, error, empty, and populated states. Its token total and Live activity snapshot token metric refresh on a bounded live cadence and animate number changes while preserving reduced-motion preferences. The sessions card uses the selected-range `ActivityAnalytics.sessions` value already loaded for the overview. The Live activity snapshot also shows the current board-state count for tasks in progress, independent of the selected analytics date range. Overview includes a graph-rich software-factory snapshot with the existing tokens-by-model bar, tool-category bar, real recharts token-share pie, and the daily activity multi-series line chart placed before the daily activity sparkline/trend so the richer line graph sits higher in the chart grid. These reuse the already-loaded tokens, tools, activity, and signals analytics; the signals count comes from `/api/command-center/signals` and renders unavailable (`—`) while the incidents-backed response is loading or unavailable. The chart reveal/glow accents are decorative and disabled when reduced-motion preferences are active. The SDLC completion rate is shown as a radial gauge and is calculated as cohort conversion from in-range triage entrants, so the rate is capped at 100% even when older tasks finish during the range.
|
||||
<!-- FNXC:CommandCenter 2026-06-21-00:00: Command Center cost must read as an estimated, derived value from recorded token counts and the hand-maintained model pricing map; it is never persisted, and the UI must surface prices-as-of, stale low-confidence, and unavailable unknown-model states instead of implying billing truth. -->
|
||||
<!-- FNXC:CommandCenter 2026-06-22-00:00: FN-6876 requires user-maintained/LiteLLM-fetched pricing overrides to feed Tokens and Team estimates immediately without implying provider billing reconciliation. -->
|
||||
|
||||
@@ -114,6 +114,24 @@
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.cc-controls-range-wrap {
|
||||
--cc-controls-range-thumb-size: var(--space-md);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* FNXC:GlobalConcurrencyControls 2026-06-26-00:00: The Command Center current-use marker mirrors the footer marker, reuses the status-dot convention, uses logical inline positioning for RTL, and never intercepts range drags. */
|
||||
.cc-controls-use-marker {
|
||||
--use-pct: 0%;
|
||||
--use-offset: 0%;
|
||||
position: absolute;
|
||||
inset-block-start: 50%;
|
||||
inset-inline-start: var(--use-offset, var(--use-pct));
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:CommandCenter 2026-06-20-00:25:
|
||||
Mobile users must be able to drag horizontal concurrency sliders without the page scroll gesture stealing the thumb movement. Scope touch-action to these range inputs and preserve desktop accent/full-width behavior while increasing the hit area with design tokens.
|
||||
@@ -143,6 +161,10 @@ Mobile users must be able to drag horizontal concurrency sliders without the pag
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.cc-controls-range-wrap {
|
||||
--cc-controls-range-thumb-size: var(--space-xl);
|
||||
}
|
||||
|
||||
.cc-controls-slider input[type="range"],
|
||||
.cc-controls-touch-slider {
|
||||
min-block-size: var(--space-2xl);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, type CSSProperties } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Power } from "lucide-react";
|
||||
import { DEFAULT_PROJECT_SETTINGS, type ColorTheme, type ThemeMode } from "@fusion/core";
|
||||
@@ -62,6 +62,18 @@ function getConcurrencySliderMax(key: keyof ConcurrencyValues, value: number) {
|
||||
return Math.max(CONCURRENCY_SLIDER_LIMITS[key].max, value);
|
||||
}
|
||||
|
||||
function getUseMarkerRatio(current: number, min: number, max: number) {
|
||||
if (max <= min) return 0;
|
||||
return clamp((current - min) / (max - min), 0, 1);
|
||||
}
|
||||
|
||||
function getUseMarkerStyle(ratio: number): CSSProperties {
|
||||
return {
|
||||
"--use-pct": `${ratio * 100}%`,
|
||||
"--use-offset": `calc((var(--cc-controls-range-thumb-size) / 2) + ((100% - var(--cc-controls-range-thumb-size)) * ${ratio}))`,
|
||||
} as CSSProperties;
|
||||
}
|
||||
|
||||
function StatusPill({ paused, label }: { paused: boolean; label: string }) {
|
||||
return (
|
||||
<span className="cc-controls-status-pill">
|
||||
@@ -149,6 +161,11 @@ export function CommandCenterControls({ projectId, colorTheme, themeMode, shadcn
|
||||
|
||||
const effectiveGlobalPaused = globalPaused;
|
||||
const concurrencyValues = concurrencyState.data ?? DEFAULT_CONCURRENCY_VALUES;
|
||||
const globalCountsLoaded = gc.status === "loaded";
|
||||
const projectActive = gc.projectActiveCount(projectId);
|
||||
const maxConcurrentSliderMax = getConcurrencySliderMax("maxConcurrent", concurrencyValues.maxConcurrent);
|
||||
const globalUseMarkerRatio = getUseMarkerRatio(gc.currentlyActive, gc.min, gc.sliderMax);
|
||||
const projectUseMarkerRatio = getUseMarkerRatio(projectActive, CONCURRENCY_SLIDER_LIMITS.maxConcurrent.min, maxConcurrentSliderMax);
|
||||
// FNXC:GlobalConcurrencyControls 2026-06-25-22:45: Mirror the per-project slider save-state labels for the shared global cap.
|
||||
// FNXC:GlobalConcurrencyControls 2026-06-26-06:05: Explicit load-error branch — a failed initial load leaves saveState "idle", so the label otherwise fell through to "Ready" while the slider was disabled and an error alert shown.
|
||||
const globalSaveLabel = gc.status === "loading" || gc.status === "idle"
|
||||
@@ -260,22 +277,41 @@ export function CommandCenterControls({ projectId, colorTheme, themeMode, shadcn
|
||||
FNXC:GlobalConcurrencyControls 2026-06-25-14:10:
|
||||
Operators need to adjust the global cross-project concurrency cap from the footer engine menu and the dashboard Concurrency card, not just the Settings modal; global cap is distinct from per-project maxConcurrent and persists via the central /api/global-concurrency endpoint.
|
||||
*/}
|
||||
{/**
|
||||
FNXC:GlobalConcurrencyControls 2026-06-26-00:00:
|
||||
The Command Center Concurrency card mirrors the footer's read-only utilization readouts from the shared global-concurrency hook. These counts are display-only capacity context and must never write running-agent totals back to settings.
|
||||
*/}
|
||||
<label className="cc-controls-slider cc-controls-slider--global" htmlFor="cc-global-max-concurrent">
|
||||
<span className="cc-controls-slider-label">
|
||||
{t("settings.scheduling.globalMaxConcurrent", "Global Max Concurrent")}
|
||||
<strong>{gc.value}</strong>
|
||||
</span>
|
||||
<small className="cc-controls-slider-caption">{t("settings.scheduling.maximumConcurrentAgentsAcrossAllProjects", "Maximum concurrent agents across all projects")}</small>
|
||||
<input
|
||||
id="cc-global-max-concurrent"
|
||||
className="cc-controls-touch-slider"
|
||||
type="range"
|
||||
min={gc.min}
|
||||
max={gc.sliderMax}
|
||||
value={gc.value}
|
||||
disabled={!gc.interactive}
|
||||
onChange={(event) => gc.setValue(event.target.value)}
|
||||
/>
|
||||
{globalCountsLoaded ? (
|
||||
<small className="cc-controls-slider-caption" data-testid="cc-global-running">
|
||||
{t("commandCenter.controls.concurrency.runningGlobal", "{{count}} running (all projects)", { count: gc.currentlyActive })}
|
||||
</small>
|
||||
) : null}
|
||||
<span className="cc-controls-range-wrap">
|
||||
<input
|
||||
id="cc-global-max-concurrent"
|
||||
className="cc-controls-touch-slider"
|
||||
type="range"
|
||||
min={gc.min}
|
||||
max={gc.sliderMax}
|
||||
value={gc.value}
|
||||
disabled={!gc.interactive}
|
||||
onChange={(event) => gc.setValue(event.target.value)}
|
||||
/>
|
||||
{globalCountsLoaded ? (
|
||||
<span
|
||||
className="status-dot status-dot--online cc-controls-use-marker"
|
||||
style={getUseMarkerStyle(globalUseMarkerRatio)}
|
||||
data-testid="cc-global-use-marker"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
{/* FNXC:GlobalConcurrencyControls 2026-06-25-22:45: Surface the shared cap's save-state (and a fetch-error message that the card previously lacked) so operators see Saving…/Saved/Save failed and know when the slider is non-interactive due to a load failure. */}
|
||||
<span className={`cc-controls-save-state cc-controls-save-state--${gc.saveState}`} aria-live="polite">
|
||||
{globalSaveLabel}
|
||||
@@ -287,21 +323,36 @@ export function CommandCenterControls({ projectId, colorTheme, themeMode, shadcn
|
||||
{t("commandCenter.controls.concurrency.maxConcurrent", "Max concurrent tasks")}
|
||||
<strong>{concurrencyValues.maxConcurrent}</strong>
|
||||
</span>
|
||||
<input
|
||||
id="cc-max-concurrent"
|
||||
className="cc-controls-touch-slider"
|
||||
type="range"
|
||||
min={CONCURRENCY_SLIDER_LIMITS.maxConcurrent.min}
|
||||
max={getConcurrencySliderMax("maxConcurrent", concurrencyValues.maxConcurrent)}
|
||||
value={concurrencyValues.maxConcurrent}
|
||||
disabled={concurrencyState.status === "loading"}
|
||||
onChange={(event) => updateConcurrencyValue(
|
||||
"maxConcurrent",
|
||||
event.target.value,
|
||||
CONCURRENCY_SLIDER_LIMITS.maxConcurrent.min,
|
||||
getConcurrencySliderMax("maxConcurrent", concurrencyValues.maxConcurrent),
|
||||
)}
|
||||
/>
|
||||
{globalCountsLoaded ? (
|
||||
<small className="cc-controls-slider-caption" data-testid="cc-project-running">
|
||||
{t("commandCenter.controls.concurrency.runningProject", "{{count}} running (this project)", { count: projectActive })}
|
||||
</small>
|
||||
) : null}
|
||||
<span className="cc-controls-range-wrap">
|
||||
<input
|
||||
id="cc-max-concurrent"
|
||||
className="cc-controls-touch-slider"
|
||||
type="range"
|
||||
min={CONCURRENCY_SLIDER_LIMITS.maxConcurrent.min}
|
||||
max={maxConcurrentSliderMax}
|
||||
value={concurrencyValues.maxConcurrent}
|
||||
disabled={concurrencyState.status === "loading"}
|
||||
onChange={(event) => updateConcurrencyValue(
|
||||
"maxConcurrent",
|
||||
event.target.value,
|
||||
CONCURRENCY_SLIDER_LIMITS.maxConcurrent.min,
|
||||
maxConcurrentSliderMax,
|
||||
)}
|
||||
/>
|
||||
{globalCountsLoaded ? (
|
||||
<span
|
||||
className="status-dot status-dot--online cc-controls-use-marker"
|
||||
style={getUseMarkerStyle(projectUseMarkerRatio)}
|
||||
data-testid="cc-project-use-marker"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
</label>
|
||||
<label className="cc-controls-slider" htmlFor="cc-max-triage-concurrent">
|
||||
<span className="cc-controls-slider-label">
|
||||
|
||||
@@ -13,7 +13,9 @@ const commandCenterControlsCss = readFileSync(
|
||||
const mocks = vi.hoisted(() => ({
|
||||
fetchSettings: vi.fn(),
|
||||
fetchConfig: vi.fn(),
|
||||
fetchGlobalConcurrency: vi.fn(),
|
||||
updateSettings: vi.fn(),
|
||||
updateGlobalConcurrency: vi.fn(),
|
||||
toggleGlobalPause: vi.fn(),
|
||||
toggleEnginePause: vi.fn(),
|
||||
refresh: vi.fn(),
|
||||
@@ -26,7 +28,9 @@ const mocks = vi.hoisted(() => ({
|
||||
vi.mock("../../../api/legacy", () => ({
|
||||
fetchSettings: mocks.fetchSettings,
|
||||
fetchConfig: mocks.fetchConfig,
|
||||
fetchGlobalConcurrency: mocks.fetchGlobalConcurrency,
|
||||
updateSettings: mocks.updateSettings,
|
||||
updateGlobalConcurrency: mocks.updateGlobalConcurrency,
|
||||
}));
|
||||
|
||||
vi.mock("../../../hooks/useAppSettings", () => ({
|
||||
@@ -64,7 +68,14 @@ beforeEach(() => {
|
||||
mocks.appSettings.enginePaused = false;
|
||||
mocks.fetchSettings.mockResolvedValue({ maxConcurrent: 2, maxTriageConcurrent: 2, maxWorktrees: 4 });
|
||||
mocks.fetchConfig.mockResolvedValue({ maxConcurrent: 2, rootDir: "/repo" });
|
||||
mocks.fetchGlobalConcurrency.mockResolvedValue({
|
||||
globalMaxConcurrent: 8,
|
||||
currentlyActive: 3,
|
||||
queuedCount: 0,
|
||||
projectsActive: { "project-a": 2 },
|
||||
});
|
||||
mocks.updateSettings.mockResolvedValue({});
|
||||
mocks.updateGlobalConcurrency.mockResolvedValue({});
|
||||
mocks.refresh.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
@@ -94,6 +105,103 @@ describe("CommandCenterControls", () => {
|
||||
expect(mocks.toggleEnginePause).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows loaded global and current-project running counts and use markers", async () => {
|
||||
renderControls("project-a");
|
||||
|
||||
await flushPromises();
|
||||
const section = screen.getByTestId("cc-controls-concurrency");
|
||||
|
||||
expect(within(section).getByTestId("cc-global-running")).toHaveTextContent("3 running (all projects)");
|
||||
expect(within(section).getByTestId("cc-project-running")).toHaveTextContent("2 running (this project)");
|
||||
expect(within(section).getByTestId("cc-global-use-marker").style.getPropertyValue("--use-pct")).toBe(`${((3 - 1) / (32 - 1)) * 100}%`);
|
||||
expect(within(section).getByTestId("cc-project-use-marker").style.getPropertyValue("--use-pct")).toBe(`${((2 - 1) / (50 - 1)) * 100}%`);
|
||||
expect(within(section).queryAllByTestId(/cc-.*-use-marker/)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("shows truthful zero or missing project running counts only after utilization loads", async () => {
|
||||
mocks.fetchGlobalConcurrency.mockResolvedValueOnce({
|
||||
globalMaxConcurrent: 8,
|
||||
currentlyActive: 0,
|
||||
queuedCount: 0,
|
||||
projectsActive: {},
|
||||
});
|
||||
renderControls(undefined);
|
||||
|
||||
await flushPromises();
|
||||
const section = screen.getByTestId("cc-controls-concurrency");
|
||||
|
||||
expect(within(section).getByTestId("cc-global-running")).toHaveTextContent("0 running (all projects)");
|
||||
expect(within(section).getByTestId("cc-project-running")).toHaveTextContent("0 running (this project)");
|
||||
expect(within(section).getByTestId("cc-global-use-marker").style.getPropertyValue("--use-pct")).toBe("0%");
|
||||
expect(within(section).getByTestId("cc-project-use-marker").style.getPropertyValue("--use-pct")).toBe("0%");
|
||||
});
|
||||
|
||||
it("clamps over-subscribed current-use markers while keeping truthful counts", async () => {
|
||||
mocks.fetchSettings.mockResolvedValueOnce({ maxConcurrent: 4, maxTriageConcurrent: 2, maxWorktrees: 4 });
|
||||
mocks.fetchGlobalConcurrency.mockResolvedValueOnce({
|
||||
globalMaxConcurrent: 8,
|
||||
currentlyActive: 60,
|
||||
queuedCount: 0,
|
||||
projectsActive: { "project-a": 60 },
|
||||
});
|
||||
renderControls("project-a");
|
||||
|
||||
await flushPromises();
|
||||
const section = screen.getByTestId("cc-controls-concurrency");
|
||||
|
||||
expect(within(section).getByTestId("cc-global-running")).toHaveTextContent("60 running (all projects)");
|
||||
expect(within(section).getByTestId("cc-project-running")).toHaveTextContent("60 running (this project)");
|
||||
expect(within(section).getByTestId("cc-global-use-marker").style.getPropertyValue("--use-pct")).toBe("100%");
|
||||
expect(within(section).getByTestId("cc-project-use-marker").style.getPropertyValue("--use-pct")).toBe("100%");
|
||||
});
|
||||
|
||||
it("suppresses running counts before global utilization finishes loading", async () => {
|
||||
let resolveGlobalConcurrency!: (value: { globalMaxConcurrent: number; currentlyActive: number; queuedCount: number; projectsActive: Record<string, number> }) => void;
|
||||
mocks.fetchGlobalConcurrency.mockReturnValueOnce(new Promise((resolve) => {
|
||||
resolveGlobalConcurrency = resolve;
|
||||
}));
|
||||
renderControls("project-a");
|
||||
const section = screen.getByTestId("cc-controls-concurrency");
|
||||
|
||||
expect(within(section).queryByTestId("cc-global-running")).toBeNull();
|
||||
expect(within(section).queryByTestId("cc-project-running")).toBeNull();
|
||||
expect(within(section).queryByTestId("cc-global-use-marker")).toBeNull();
|
||||
expect(within(section).queryByTestId("cc-project-use-marker")).toBeNull();
|
||||
|
||||
resolveGlobalConcurrency({ globalMaxConcurrent: 8, currentlyActive: 1, queuedCount: 0, projectsActive: {} });
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("suppresses running counts while global utilization is unavailable", async () => {
|
||||
mocks.fetchGlobalConcurrency.mockRejectedValueOnce(new Error("global unavailable"));
|
||||
renderControls("project-a");
|
||||
|
||||
await flushPromises();
|
||||
const section = screen.getByTestId("cc-controls-concurrency");
|
||||
|
||||
expect(within(section).queryByTestId("cc-global-running")).toBeNull();
|
||||
expect(within(section).queryByTestId("cc-project-running")).toBeNull();
|
||||
expect(within(section).queryByTestId("cc-global-use-marker")).toBeNull();
|
||||
expect(within(section).queryByTestId("cc-project-use-marker")).toBeNull();
|
||||
});
|
||||
|
||||
it("persists shared global cap slider changes without mutating project settings", async () => {
|
||||
renderControls("project-a");
|
||||
|
||||
await flushPromises();
|
||||
const section = screen.getByTestId("cc-controls-concurrency");
|
||||
const slider = within(section).getByLabelText(/global max concurrent/i);
|
||||
fireEvent.change(slider, { target: { value: "10" } });
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(500);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(mocks.updateGlobalConcurrency).toHaveBeenCalledWith({ globalMaxConcurrent: 10 });
|
||||
expect(mocks.updateSettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("persists bounded concurrency slider changes and refreshes settings", async () => {
|
||||
renderControls("project-a");
|
||||
|
||||
@@ -241,8 +349,11 @@ describe("CommandCenterControls", () => {
|
||||
}
|
||||
// jsdom cannot simulate whether a touch drag is captured by page scrolling, so this verifies the CSS contract that enables horizontal thumb drags on mobile.
|
||||
expect(commandCenterControlsCss).toContain("touch-action: pan-y");
|
||||
expect(commandCenterControlsCss).toContain("pointer-events: none");
|
||||
expect(commandCenterControlsCss).toContain("inset-inline-start: var(--use-offset, var(--use-pct))");
|
||||
expect(commandCenterControlsCss).toContain("@media (max-width: 768px)");
|
||||
expect(commandCenterControlsCss).toContain("min-block-size: var(--space-2xl)");
|
||||
expect(commandCenterControlsCss).toContain("--cc-controls-range-thumb-size: var(--space-xl)");
|
||||
});
|
||||
|
||||
it("shows save error indicator when concurrency update fails", async () => {
|
||||
|
||||
Reference in New Issue
Block a user