Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/app/cypress/component/date-range-picker.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { type DateRange, DateRangePicker } from '@/components/ui/date-range-pick
function DateRangePickerHarness({
initialRange = { startDate: '', endDate: '' },
availableDates,
disabled,
}: {
initialRange?: DateRange;
availableDates?: string[];
disabled?: boolean;
}) {
const [range, setRange] = useState<DateRange>(initialRange);
return (
Expand All @@ -17,6 +19,7 @@ function DateRangePickerHarness({
onChange={setRange}
availableDates={availableDates}
placeholder="Select date range"
disabled={disabled}
/>
<div data-testid="date-range-output">
{range.startDate && range.endDate ? `${range.startDate} to ${range.endDate}` : 'no range'}
Expand All @@ -31,6 +34,13 @@ describe('DateRangePicker', () => {
cy.get('[data-testid="date-range-wrapper"]').should('contain', 'Select date range');
});

it('does not open dialog when disabled', () => {
cy.mount(<DateRangePickerHarness disabled />);
cy.contains('Select date range').should('be.disabled');
cy.contains('Select date range').click({ force: true });
cy.get('[role="dialog"]').should('not.exist');
});

it('opens dialog when clicked', () => {
cy.mount(<DateRangePickerHarness />);
cy.contains('Select date range').click();
Expand Down Expand Up @@ -64,7 +74,7 @@ describe('DateRangePicker', () => {
const dates = ['2025-12-01', '2025-12-15', '2026-01-01', '2026-02-01', '2026-03-01'];
cy.mount(<DateRangePickerHarness availableDates={dates} />);
cy.contains('Select date range').click();
cy.contains('button', 'Max Range').should('be.visible');
cy.contains('button', 'All Time').should('be.visible');
});

it('shows overlay when no available dates', () => {
Expand Down
132 changes: 113 additions & 19 deletions packages/app/cypress/component/inference-chart-controls.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import InferenceChartControls from '@/components/inference/ui/ChartControls';
import GpuComparisonCard from '@/components/inference/ui/GpuComparisonCard';
import { mountWithProviders } from '../support/test-utils';

describe('Inference ChartControls', () => {
Expand Down Expand Up @@ -41,39 +42,132 @@ describe('Inference ChartControls', () => {
cy.get('@setSelectedYAxisMetric').should('have.been.calledOnce');
});

it('hides the GPU comparison section when no GPUs are selected', () => {
// Default mock: selectedGPUs = [] — GPU date range pickers should not render
it('does not render GPU comparison date controls (moved to GpuComparisonCard)', () => {
cy.contains('Comparison Date Range').should('not.exist');
cy.contains('Intermediary Dates').should('not.exist');
cy.contains('GPU Comparison').should('not.exist');
});
});

describe('GpuComparisonCard', () => {
const gpuOptions = [
{ value: 'h100_sglang', label: 'H100 SGLang' },
{ value: 'b200_sglang', label: 'B200 SGLang' },
{ value: 'mi300x_sglang', label: 'MI300X SGLang' },
{ value: 'h200_sglang', label: 'H200 SGLang' },
];

it('renders the GPU config multi-select', () => {
// The GPU Config label should be present (hideGpuComparison defaults to false)
cy.contains('GPU Config').should('be.visible');
it('renders GPU multiselect; date range disabled until a GPU is selected', () => {
mountWithProviders(<GpuComparisonCard />, {
inference: {
selectedGPUs: [],
availableGPUs: gpuOptions,
},
});

cy.get('[data-testid="gpu-comparison-card"]').should('be.visible');
cy.contains('GPU Comparison').should('be.visible');
cy.contains('Select one or more GPUs for date range comparison.').should('not.exist');
cy.get('[data-testid="gpu-comparison-expand-toggle"]').should(
'have.attr',
'aria-expanded',
'false',
);
cy.get('[data-testid="gpu-comparison-expand-toggle"]').click();
cy.contains('Select one or more GPUs for date range comparison.').should('be.visible');
cy.get('[data-testid="gpu-multiselect"]').should('be.visible');
cy.get('[data-testid="gpu-multiselect-trigger"]').should('be.visible');
cy.contains('Comparison Date Range').should('be.visible');
cy.get('#gpu-comparison-date-picker').should('be.disabled');
cy.get('[data-testid="date-range-shortcuts"]').should('be.visible');
cy.get('[data-testid="date-shortcut-all-time"]').should('be.disabled');
cy.get('[data-testid="date-shortcut-last-90-days"]').should('be.disabled');
cy.get('[data-testid="date-shortcut-last-30-days"]').should('be.disabled');
});
});

describe('Inference ChartControls with GPUs selected', () => {
it('shows the date range picker when GPUs are selected', () => {
mountWithProviders(<InferenceChartControls />, {
it('enables date range picker and shortcuts when one GPU is selected', () => {
mountWithProviders(<GpuComparisonCard />, {
inference: {
selectedGPUs: ['h100'],
selectedGPUs: ['h100_sglang'],
selectedDateRange: { startDate: '', endDate: '' },
availableGPUs: gpuOptions,
dateRangeAvailableDates: ['2025-10-05', '2025-11-01', '2025-12-01'],
},
});

cy.contains('Comparison Date Range').should('be.visible');
cy.get('#gpu-comparison-date-picker').should('not.be.disabled');
cy.get('[data-testid="date-shortcut-all-time"]').should('not.be.disabled');
});

it('shows enabled shortcut buttons that call setSelectedDateRange when two GPUs are selected', () => {
mountWithProviders(<GpuComparisonCard />, {
inference: {
selectedGPUs: ['h100_sglang', 'b200_sglang'],
selectedDateRange: { startDate: '', endDate: '' },
availableGPUs: gpuOptions,
dateRangeAvailableDates: [
'2025-10-05',
'2025-11-01',
'2025-12-01',
'2026-01-01',
'2026-02-01',
'2026-03-01',
],
},
});

cy.get('[data-testid="date-shortcut-all-time"]').should('not.be.disabled');
cy.get('[data-testid="date-shortcut-all-time"]').click();
cy.get('@setSelectedDateRange').should('have.been.calledOnce');
});
});

describe('Inference ChartControls with hideGpuComparison', () => {
it('hides GPU config selector when hideGpuComparison is true', () => {
mountWithProviders(<InferenceChartControls hideGpuComparison />, {
inference: {},
it('selecting a third GPU from the multiselect calls setSelectedGPUs with three GPUs', () => {
mountWithProviders(<GpuComparisonCard />, {
inference: {
selectedGPUs: ['h100_sglang', 'b200_sglang'],
selectedDateRange: { startDate: '', endDate: '' },
availableGPUs: gpuOptions,
},
});

cy.get('#gpu-comparison-date-picker').should('not.be.disabled');
cy.get('[data-testid="gpu-multiselect-trigger"]').click();
cy.contains('[role="option"]', 'MI300X SGLang').click();
cy.get('@setSelectedGPUs').should(
'have.been.calledWith',
Cypress.sinon.match((v: string[]) => v.length === 3 && v.includes('mi300x_sglang')),
);
});

it('shows max-selection summary when four GPUs are selected', () => {
mountWithProviders(<GpuComparisonCard />, {
inference: {
selectedGPUs: ['h100_sglang', 'b200_sglang', 'mi300x_sglang', 'h200_sglang'],
selectedDateRange: { startDate: '', endDate: '' },
availableGPUs: gpuOptions,
},
});

// Wait for card expansion, then open the dropdown.
// With 4 chips the center of the trigger may land on a chip's remove
// button (which stopPropagates), so target the chevron icon instead.
cy.get('[data-testid="gpu-multiselect-trigger"]').should('be.visible');
cy.get('[data-testid="gpu-multiselect-trigger"] svg').last().click();
cy.contains('4 / 4 selected').should('be.visible');
});

it('calls setSelectedGPUs without the removed GPU when a chip remove control is clicked', () => {
mountWithProviders(<GpuComparisonCard />, {
inference: {
selectedGPUs: ['h100_sglang', 'b200_sglang', 'mi300x_sglang'],
selectedDateRange: { startDate: '', endDate: '' },
availableGPUs: gpuOptions,
},
});

cy.contains('GPU Config').should('not.exist');
cy.get('[data-testid="gpu-multiselect"]').should('not.exist');
cy.get('[aria-label="Remove MI300X SGLang"]').click();
cy.get('@setSelectedGPUs').should(
'have.been.calledWith',
Cypress.sinon.match((v: string[]) => v.length === 2 && !v.includes('mi300x_sglang')),
);
});
});
4 changes: 2 additions & 2 deletions packages/app/cypress/e2e/historical-trends.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ describe('Historical Trends — Content & Interactions', () => {
cy.get('#historical-log-scale').should('have.attr', 'data-state', 'unchecked');
});

it('GPU Config multi-select is hidden (Historical Trends uses hideGpuComparison)', () => {
cy.get('[data-testid="gpu-multiselect"]').should('not.exist');
it('GPU Comparison card is not shown on Historical Trends (inference-only)', () => {
cy.get('[data-testid="gpu-comparison-card"]').should('not.exist');
});

it('Y-axis metric selector is present and can be changed', () => {
Expand Down
86 changes: 85 additions & 1 deletion packages/app/cypress/e2e/inference-chart.cy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/** Opens dropdown without hitting chip remove controls (they stopPropagation). */
function openGpuMultiselect() {
cy.get('[data-testid="gpu-multiselect-trigger"]').find('svg').last().click();
}

describe('Inference Chart', () => {
before(() => {
cy.window().then((win) => {
Expand Down Expand Up @@ -47,6 +52,85 @@ describe('Inference Chart', () => {
});

it('shows the sidebar legend for GPU types', () => {
cy.get('.sidebar-legend').should('be.visible');
// GpuComparisonCard sits above charts; first chart legend may be below the fold.
cy.get('.sidebar-legend').first().scrollIntoView();
cy.get('.sidebar-legend').first().should('be.visible');
});
});

describe('GPU Comparison Card', () => {
beforeEach(() => {
cy.visit('/inference', {
onBeforeLoad(win) {
win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now()));
},
});
cy.get('[data-testid="gpu-comparison-card"]').should('be.visible');
});

it('starts collapsed; expanding shows GPU comparison controls', () => {
cy.contains('Select one or more GPUs for date range comparison.').should('not.exist');
cy.get('[data-testid="gpu-comparison-expand-toggle"]').should(
'have.attr',
'aria-expanded',
'false',
);
cy.get('[data-testid="gpu-comparison-expand-toggle"]').click();
cy.get('[data-testid="gpu-comparison-expand-toggle"]').should(
'have.attr',
'aria-expanded',
'true',
);
cy.contains('Select one or more GPUs for date range comparison.').should('be.visible');
cy.get('[data-testid="gpu-multiselect-trigger"]').should('be.visible');
});

describe('when expanded', () => {
beforeEach(() => {
cy.get('[data-testid="gpu-comparison-expand-toggle"]').click();
});

it('renders the GPU comparison card with a single GPU multiselect', () => {
cy.get('[data-testid="gpu-multiselect"]').should('be.visible');
cy.get('[data-testid="gpu-multiselect-trigger"]').should('be.visible');
});

it('shows date range shortcuts that are disabled until a GPU is selected', () => {
cy.get('[data-testid="date-range-shortcuts"]').should('be.visible');
cy.get('[data-testid="date-shortcut-all-time"]').should('be.disabled');
});

it('selects one GPU and verifies date range controls unlock', () => {
openGpuMultiselect();
cy.get('[role="option"]').first().click();

// "All Time" stays disabled when the selected GPU has only one date in fixtures.
cy.get('#gpu-comparison-date-picker').should('not.be.disabled');
});

it('selects two GPUs and verifies date range auto-defaults', () => {
openGpuMultiselect();
cy.get('[role="option"]').first().click();

// Pick eq(2) so the second GPU is a different family (e.g. b300) with a
// distinct date — eq(1) can be the MTP pair of eq(0) sharing one date.
openGpuMultiselect();
cy.get('[role="option"]').eq(2).click();

cy.get('[data-testid="date-shortcut-all-time"]').should('not.be.disabled');
cy.get('#gpu-comparison-date-picker').should('not.be.disabled');
});

it('selecting a third GPU adds a third removable chip to the multiselect', () => {
openGpuMultiselect();
cy.get('[role="option"]').first().click();
openGpuMultiselect();
cy.get('[role="option"]').eq(1).click();
openGpuMultiselect();
cy.get('[role="option"]').eq(2).click();
cy.get('[data-testid="gpu-multiselect"]')
.find('[aria-label^="Remove "]')
.should('have.length', 3);
});
});
});
2 changes: 1 addition & 1 deletion packages/app/src/components/dashboard-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function DashboardShell({ children }: { children: React.ReactNode }) {
<NudgeEngine scope="dashboard" />
<UnofficialRunProvider>
<main className="relative">
<div className="container mx-auto px-4 lg:px-8 flex flex-col gap-4">
<div className="container mx-auto px-4 lg:px-8 flex flex-col gap-2 md:gap-3">
<TabNav />
<GlobalFilterProvider>{children}</GlobalFilterProvider>
</div>
Expand Down
27 changes: 25 additions & 2 deletions packages/app/src/components/inference/InferenceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { clearAllMtpFamilies, resolveMtpToggle } from '@/lib/mtp-exclusion';
import { filterRunsByModel, getDisplayLabel } from '@/lib/utils';

import { useChartData } from './hooks/useChartData';
import { normalizeComparisonGpuList } from './utils/normalize-comparison-gpus';

/** @internal Exported for test provider wrapping only. */
export const InferenceContext = createContext<InferenceChartContextType | undefined>(undefined);
Expand Down Expand Up @@ -108,7 +109,8 @@ export function InferenceProvider({
// ── Inference-specific filter state ─────────────────────────────────────────
const [selectedGPUs, setSelectedGPUs] = useState<string[]>(() => {
const urlGpus = getUrlParam('i_gpus');
return urlGpus ? urlGpus.split(',').filter(Boolean) : [];
const parts = urlGpus ? urlGpus.split(',').filter(Boolean) : [];
return normalizeComparisonGpuList(parts);
});
const [selectedYAxisMetric, setSelectedYAxisMetric] = useState<string>(
() => getUrlParam('i_metric') || 'y_tpPerGpu',
Expand Down Expand Up @@ -636,6 +638,27 @@ export function InferenceProvider({
}
}, [dateRangeAvailableDates]);

// Auto-default to max date range once when GPU comparison first becomes ready.
// Uses a ref to fire only on the transition from 0 GPUs to >=1, avoiding a loop
// when the user intentionally clears the date range.
const prevGpuCountRef = useRef(selectedGPUs.length);
useEffect(() => {
const wasBelow = prevGpuCountRef.current < 1;
prevGpuCountRef.current = selectedGPUs.length;
if (!wasBelow) return;
if (selectedGPUs.length === 0) return;
if (selectedDateRange.startDate && selectedDateRange.endDate) return;
if (dateRangeAvailableDates.length < 2) return;
const startDate = dateRangeAvailableDates[0];
const endDate = dateRangeAvailableDates.at(-1)!;
setSelectedDateRange({ startDate, endDate });
track('inference_date_range_auto_defaulted', {
startDate,
endDate,
gpuCount: selectedGPUs.length,
});
}, [selectedGPUs, selectedDateRange, dateRangeAvailableDates]);

useEffect(() => {
setActiveDates(allDateIds);
}, [allDateIds, setActiveDates]);
Expand Down Expand Up @@ -814,7 +837,7 @@ export function InferenceProvider({
setActivePresetId(preset.id);
setHighContrast(true);
if (config.gpus && config.gpus.length > 0) {
setSelectedGPUs(config.gpus);
setSelectedGPUs(normalizeComparisonGpuList(config.gpus));
if (config.useDateRange) {
setSelectedDateRange({ startDate: '', endDate: '' });
setSelectedDates([]);
Expand Down
Loading
Loading