From 9d02b51fa0367f2de2ec997dd241b58135437c2c Mon Sep 17 00:00:00 2001 From: PeterYurkovich Date: Tue, 13 Jan 2026 14:47:51 -0500 Subject: [PATCH] fix: remove top level dispatch; refactor to avoid late useEffect a --- web/src/components/MetricsPage.tsx | 281 ++++++++++++++++------------- 1 file changed, 158 insertions(+), 123 deletions(-) diff --git a/web/src/components/MetricsPage.tsx b/web/src/components/MetricsPage.tsx index b412c4f4..397dea5e 100644 --- a/web/src/components/MetricsPage.tsx +++ b/web/src/components/MetricsPage.tsx @@ -710,135 +710,170 @@ export const QueryTable: FC = ({ index, namespace, customDataso setSortBy({}); }, [namespace, query]); - if (!isEnabled || !isExpanded || !query) { - return null; - } - - if (error) { - return ; - } - - if (!data) { - return ; - } - - // Add any data series from `series` (those displayed in the graph) that are not in `data.result`. - // This happens for queries that exclude a series currently, but included that same series at some - // point during the graph's range. - const expiredSeries = _.differenceWith(series, data.result, (s, r) => _.isEqual(s, r.metric)); - const result = expiredSeries.length - ? [...data.result, ...expiredSeries.map((metric) => ({ metric }))] - : data.result; - - if (!result || result.length === 0) { - return ( -
- {t('No datapoints found.')} -
- ); - } + const isUnused = !isEnabled || !isExpanded || !query; + const isError = !!error; + const isLoading = !data; + const result = useMemo(() => { + if (isUnused || isError || isLoading) { + return []; + } + // Add any data series from `series` (those displayed in the graph) that are not + // in `data.result`.This happens for queries that exclude a series currently, but + // included that same series at some point during the graph's range. + const expiredSeries = _.differenceWith(series, data.result, (s, r) => _.isEqual(s, r.metric)); + return expiredSeries.length + ? [...data.result, ...expiredSeries.map((metric) => ({ metric }))] + : data.result; + }, [data?.result, series, isUnused, isError, isLoading]); + const isEmptyGraph = !result || result.length === 0; + + const tableData = useMemo(() => { + if (isUnused || isError || isLoading || isEmptyGraph) { + return {}; + } + const transforms: ITransform[] = [sortable, wrappable]; - const transforms: ITransform[] = [sortable, wrappable]; - - const buttonCell = (labels) => ({ title: }); - - let columns, rows; - if (data.resultType === 'scalar') { - columns = [ - '', - { - title: t('Value'), - transforms, - cellTransforms: [ - (data: IFormatterValueType) => { - const val = data?.title ? data.title : data; - return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; - }, - ], - }, - ]; - rows = [[buttonCell({}), _.get(result, '[1]')]]; - } else if (data.resultType === 'string') { - columns = [ - { - title: t('Value'), - transforms, - cellTransforms: [ - (data: IFormatterValueType) => { - const val = data?.title ? data.title : data; - return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; - }, - ], - }, - ]; - rows = [[result?.[1]]]; - } else { - const allLabelKeys = _.uniq(_.flatMap(result, ({ metric }) => Object.keys(metric))).sort(); - - columns = [ - '', - ...allLabelKeys.map((k) => ({ - title: {k === '__name__' ? t('Name') : k}, - transforms, - })), - { - title: t('Value'), - transforms, - cellTransforms: [ - (data: IFormatterValueType) => { - const val = data?.title ? data.title : data; - return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; - }, - ], - }, - ]; + const buttonCell = (labels) => ({ title: }); - let rowMapper; - if (data.resultType === 'matrix') { - rowMapper = ({ metric, values }) => [ + let columns, rows; + if (data.resultType === 'scalar') { + columns = [ '', - ..._.map(allLabelKeys, (k) => metric[k]), { - title: ( - <> - {_.map(values, ([time, v]) => ( -
- {v} @{time} -
- ))} - - ), + title: t('Value'), + transforms, + cellTransforms: [ + (data: IFormatterValueType) => { + const val = data?.title ? data.title : data; + return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; + }, + ], + }, + ]; + rows = [[buttonCell({}), _.get(result, '[1]')]]; + } else if (data.resultType === 'string') { + columns = [ + { + title: t('Value'), + transforms, + cellTransforms: [ + (data: IFormatterValueType) => { + const val = data?.title ? data.title : data; + return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; + }, + ], }, ]; + rows = [[result?.[1]]]; } else { - rowMapper = ({ metric, value }) => [ - buttonCell(metric), - ..._.map(allLabelKeys, (k) => metric[k]), - _.get(value, '[1]', { title: {t('None')} }), + const allLabelKeys = _.uniq(_.flatMap(result, ({ metric }) => Object.keys(metric))).sort(); + + columns = [ + '', + ...allLabelKeys.map((k) => ({ + title: {k === '__name__' ? t('Name') : k}, + transforms, + })), + { + title: t('Value'), + transforms, + cellTransforms: [ + (data: IFormatterValueType) => { + const val = data?.title ? data.title : data; + return !Number.isNaN(Number(val)) ? valueFormat(Number(val)) : val; + }, + ], + }, ]; - } - rows = _.map(result, rowMapper); - if (sortBy) { - // Sort Values column numerically and sort all the other columns alphabetically - const valuesColIndex = allLabelKeys.length + 1; - const sort = - sortBy.index === valuesColIndex - ? (cells) => { - const v = Number(cells[valuesColIndex]); - return Number.isNaN(v) ? 0 : v; - } - : `${sortBy.index}`; - rows = _.orderBy(rows, [sort], [sortBy.direction]); + let rowMapper; + if (data.resultType === 'matrix') { + rowMapper = ({ metric, values }) => [ + '', + ..._.map(allLabelKeys, (k) => metric[k]), + { + title: ( + <> + {_.map(values, ([time, v]) => ( +
+ {v} @{time} +
+ ))} + + ), + }, + ]; + } else { + rowMapper = ({ metric, value }) => [ + buttonCell(metric), + ..._.map(allLabelKeys, (k) => metric[k]), + _.get(value, '[1]', { title: {t('None')} }), + ]; + } + + rows = _.map(result, rowMapper); + if (sortBy) { + // Sort Values column numerically and sort all the other columns alphabetically + const valuesColIndex = allLabelKeys.length + 1; + const sort = + sortBy.index === valuesColIndex + ? (cells) => { + const v = Number(cells[valuesColIndex]); + return Number.isNaN(v) ? 0 : v; + } + : `${sortBy.index}`; + rows = _.orderBy(rows, [sort], [sortBy.direction]); + } } - } - // Dispatch query table result so QueryKebab can access it for data export - dispatch(queryBrowserPatchQuery(index, { queryTableData: { columns, rows } })); + const onSort = (e, i, direction) => setSortBy({ index: i, direction }); - const onSort = (e, i, direction) => setSortBy({ index: i, direction }); + const tableRows = rows.slice((page - 1) * perPage, page * perPage).map((cells) => ({ cells })); - const tableRows = rows.slice((page - 1) * perPage, page * perPage).map((cells) => ({ cells })); + return { + onSort, + tableRows, + columns, + rows, + }; + }, [ + data?.resultType, + isEmptyGraph, + index, + isUnused, + isError, + isLoading, + page, + perPage, + result, + sortBy, + t, + valueFormat, + ]); + + useEffect(() => { + if (tableData.columns && tableData.rows) { + dispatch( + queryBrowserPatchQuery(index, { + queryTableData: { columns: tableData.columns, rows: tableData.rows }, + }), + ); + } + }, [dispatch, index, tableData?.columns, tableData?.rows]); + + if (isUnused) { + return null; + } else if (isError) { + return ; + } else if (isLoading) { + return ; + } else if (isEmptyGraph) { + return ( +
+ {t('No datapoints found.')} +
+ ); + } return ( <> @@ -854,19 +889,19 @@ export const QueryTable: FC = ({ index, namespace, customDataso - {columns.map((col, columnIndex) => { + {tableData?.columns.map((col, columnIndex) => { const sortParams = columnIndex !== 0 ? { sort: { sortBy, - onSort, + onSort: tableData?.onSort, columnIndex, }, } @@ -880,15 +915,15 @@ export const QueryTable: FC = ({ index, namespace, customDataso - {tableRows.map((row, rowIndex) => ( + {tableData?.tableRows.map((row, rowIndex) => ( {row.cells?.map((cell, cellIndex) => (
- {columns[cellIndex].cellTransforms - ? columns[cellIndex].cellTransforms[0]( + {tableData?.columns[cellIndex].cellTransforms + ? tableData?.columns[cellIndex].cellTransforms[0]( typeof cell === 'string' ? cell : cell?.title, ) : typeof cell === 'string' @@ -902,7 +937,7 @@ export const QueryTable: FC = ({ index, namespace, customDataso