diff --git a/table/src/components/TablePanel.test.tsx b/table/src/components/TablePanel.test.tsx index 96b731dfa..654cff1da 100644 --- a/table/src/components/TablePanel.test.tsx +++ b/table/src/components/TablePanel.test.tsx @@ -26,6 +26,7 @@ import { VirtuosoMockContext } from 'react-virtuoso'; import { TimeSeriesData } from '@perses-dev/spec'; import { TableOptions, TimeSeriesTableProps } from '../models'; import { + MOCK_MULTI_QUERY_DATA_EMPTY, MOCK_MULTI_QUERY_DATA_Q1, MOCK_MULTI_QUERY_DATA_Q2, MOCK_MULTI_QUERY_DATA_WITH_ZERO, @@ -460,5 +461,44 @@ describe('TablePanel', () => { }, TEST_TIMEOUT ); + + it( + 'should show N/A for columns defined in columnSettings but with no data in any query', + async () => { + // Simulates Request Hard column: defined in columnSettings as value #3, + // but the third query returns no data at all (e.g. no ResourceQuotas on cluster) + const options: TableOptions = { + columnSettings: [ + { name: 'timestamp', hide: true }, + { name: 'namespace', header: 'Namespace' }, + { name: 'value #1', header: 'Value 1' }, + { name: 'value #2', header: 'Value 2' }, + { name: 'value #3', header: 'Value 3 (empty query)' }, + ], + cellSettings: [{ condition: { kind: 'Misc', spec: { value: 'null' } }, text: 'N/A' }], + transforms: [ + { kind: 'MergeSeries', spec: {} }, + { kind: 'JoinByColumnValue', spec: { columns: ['namespace'] } }, + ], + enableFiltering: true, + }; + + // Q1 has data, Q2 has partial data, Q3 is completely empty + renderMultiQueryPanel( + [MOCK_MULTI_QUERY_DATA_Q1, MOCK_MULTI_QUERY_DATA_Q2, MOCK_MULTI_QUERY_DATA_EMPTY], + options + ); + + // Both namespaces should be present + expect(await screen.findByRole('cell', { name: 'ns-a' })).toBeInTheDocument(); + expect(await screen.findByRole('cell', { name: 'ns-b' })).toBeInTheDocument(); + + // Value 3 column should show N/A for all rows since no query produced data for it + const naCells = await screen.findAllByRole('cell', { name: 'N/A' }); + // At minimum: ns-b missing value #2 (1) + both rows missing value #3 (2) = 3 N/A cells + expect(naCells.length).toBeGreaterThanOrEqual(3); + }, + TEST_TIMEOUT + ); }); }); diff --git a/table/src/components/TablePanel.tsx b/table/src/components/TablePanel.tsx index b86f46159..75106f32f 100644 --- a/table/src/components/TablePanel.tsx +++ b/table/src/components/TablePanel.tsx @@ -452,8 +452,16 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps } } + // Include column names from columnSettings so that columns with no data + // are still extended with undefined for cellSettings evaluation (e.g. N/A for null) + for (const col of spec.columnSettings ?? []) { + if (!result.includes(col.name)) { + result.push(col.name); + } + } + return result; - }, [data]); + }, [data, spec.columnSettings]); const columnsFormat = useMemo(() => { const columnsFormat: Record = {}; diff --git a/table/src/test/mock-query-results.ts b/table/src/test/mock-query-results.ts index f07f88e00..433511f3c 100644 --- a/table/src/test/mock-query-results.ts +++ b/table/src/test/mock-query-results.ts @@ -326,6 +326,14 @@ export const MOCK_MULTI_QUERY_DATA_WITH_ZERO: TimeSeriesData = { ], }; +// Query that returns no data at all — simulates a metric like cpu_request_hard +// when no ResourceQuotas exist on the cluster. +export const MOCK_MULTI_QUERY_DATA_EMPTY: TimeSeriesData = { + timeRange: { start: new Date(1666625535000), end: new Date(1666625535000) }, + stepMs: 24379, + series: [], +}; + export const MOCK_TIME_SERIES_QUERY_DEFINITION = { kind: 'TimeSeriesQuery', spec: {