fix: row limits & row count labels are confusing (#27700)

This commit is contained in:
Maxime Beauchemin 2024-04-02 13:58:35 -07:00 committed by GitHub
parent 9fea3154fa
commit 12fe2929a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 50 additions and 53 deletions

View File

@ -67,6 +67,7 @@ export interface ChartDataResponseResult {
is_cached: boolean; is_cached: boolean;
query: string; query: string;
rowcount: number; rowcount: number;
sql_rowcount: number;
stacktrace: string | null; stacktrace: string | null;
status: status:
| 'stopped' | 'stopped'

View File

@ -80,6 +80,7 @@ const basicQueryResult: ChartDataResponseResult = {
is_cached: false, is_cached: false,
query: 'SELECT ...', query: 'SELECT ...',
rowcount: 100, rowcount: 100,
sql_rowcount: 100,
stacktrace: null, stacktrace: null,
status: 'success', status: 'success',
from_dttm: null, from_dttm: null,

View File

@ -44,6 +44,7 @@ export const useResultsTableView = (
<SingleQueryResultPane <SingleQueryResultPane
colnames={chartDataResult[0].colnames} colnames={chartDataResult[0].colnames}
coltypes={chartDataResult[0].coltypes} coltypes={chartDataResult[0].coltypes}
rowcount={chartDataResult[0].sql_rowcount}
data={chartDataResult[0].data} data={chartDataResult[0].data}
dataSize={DATA_SIZE} dataSize={DATA_SIZE}
datasourceId={datasourceId} datasourceId={datasourceId}
@ -61,6 +62,7 @@ export const useResultsTableView = (
colnames={res.colnames} colnames={res.colnames}
coltypes={res.coltypes} coltypes={res.coltypes}
data={res.data} data={res.data}
rowcount={res.sql_rowcount}
dataSize={DATA_SIZE} dataSize={DATA_SIZE}
datasourceId={datasourceId} datasourceId={datasourceId}
isVisible isVisible

View File

@ -66,7 +66,7 @@ export const ChartPills = forwardRef(
> >
{!isLoading && firstQueryResponse && ( {!isLoading && firstQueryResponse && (
<RowCountLabel <RowCountLabel
rowcount={Number(firstQueryResponse.rowcount) || 0} rowcount={Number(firstQueryResponse.sql_rowcount) || 0}
limit={Number(rowLimit) || 0} limit={Number(rowLimit) || 0}
/> />
)} )}

View File

@ -1,36 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import { RowCount } from '.';
test('Render a RowCount with a single row', () => {
render(<RowCount data={[{}]} loading={false} />);
expect(screen.getByText('1 row')).toBeInTheDocument();
});
test('Render a RowCount with multiple rows', () => {
render(<RowCount data={[{}, {}, {}]} loading={false} />);
expect(screen.getByText('3 rows')).toBeInTheDocument();
});
test('Render a RowCount on loading', () => {
render(<RowCount data={[{}, {}, {}]} loading />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});

View File

@ -44,7 +44,6 @@ import Button from 'src/components/Button';
import Popover from 'src/components/Popover'; import Popover from 'src/components/Popover';
import { prepareCopyToClipboardTabularData } from 'src/utils/common'; import { prepareCopyToClipboardTabularData } from 'src/utils/common';
import CopyToClipboard from 'src/components/CopyToClipboard'; import CopyToClipboard from 'src/components/CopyToClipboard';
import RowCountLabel from 'src/explore/components/RowCountLabel';
import { getTimeColumns, setTimeColumns } from './utils'; import { getTimeColumns, setTimeColumns } from './utils';
export const CellNull = styled('span')` export const CellNull = styled('span')`
@ -118,14 +117,6 @@ export const FilterInput = ({
); );
}; };
export const RowCount = ({
data,
loading,
}: {
data?: Record<string, any>[];
loading: boolean;
}) => <RowCountLabel rowcount={data?.length ?? 0} loading={loading} />;
enum FormatPickerValue { enum FormatPickerValue {
Formatted = 'formatted', Formatted = 'formatted',
Original = 'original', Original = 'original',

View File

@ -22,10 +22,10 @@ import { css, GenericDataType, styled } from '@superset-ui/core';
import { import {
CopyToClipboardButton, CopyToClipboardButton,
FilterInput, FilterInput,
RowCount,
} from 'src/explore/components/DataTableControl'; } from 'src/explore/components/DataTableControl';
import { applyFormattingToTabularData } from 'src/utils/common'; import { applyFormattingToTabularData } from 'src/utils/common';
import { getTimeColumns } from 'src/explore/components/DataTableControl/utils'; import { getTimeColumns } from 'src/explore/components/DataTableControl/utils';
import RowCountLabel from 'src/explore/components/RowCountLabel';
import { TableControlsProps } from '../types'; import { TableControlsProps } from '../types';
export const TableControlsWrapper = styled.div` export const TableControlsWrapper = styled.div`
@ -47,6 +47,7 @@ export const TableControls = ({
onInputChange, onInputChange,
columnNames, columnNames,
columnTypes, columnTypes,
rowcount,
isLoading, isLoading,
}: TableControlsProps) => { }: TableControlsProps) => {
const originalTimeColumns = getTimeColumns(datasourceId); const originalTimeColumns = getTimeColumns(datasourceId);
@ -74,7 +75,7 @@ export const TableControls = ({
align-items: center; align-items: center;
`} `}
> >
<RowCount data={data} loading={isLoading} /> <RowCountLabel rowcount={rowcount} loading={isLoading} />
<CopyToClipboardButton data={formattedData} columns={columnNames} /> <CopyToClipboardButton data={formattedData} columns={columnNames} />
</div> </div>
</TableControlsWrapper> </TableControlsWrapper>

View File

@ -48,6 +48,7 @@ export const SamplesPane = ({
const [colnames, setColnames] = useState<string[]>([]); const [colnames, setColnames] = useState<string[]>([]);
const [coltypes, setColtypes] = useState<GenericDataType[]>([]); const [coltypes, setColtypes] = useState<GenericDataType[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [rowcount, setRowCount] = useState<number>(0);
const [responseError, setResponseError] = useState<string>(''); const [responseError, setResponseError] = useState<string>('');
const datasourceId = useMemo( const datasourceId = useMemo(
() => `${datasource.id}__${datasource.type}`, () => `${datasource.id}__${datasource.type}`,
@ -66,6 +67,7 @@ export const SamplesPane = ({
setData(ensureIsArray(response.data)); setData(ensureIsArray(response.data));
setColnames(ensureIsArray(response.colnames)); setColnames(ensureIsArray(response.colnames));
setColtypes(ensureIsArray(response.coltypes)); setColtypes(ensureIsArray(response.coltypes));
setRowCount(response.rowcount);
setResponseError(''); setResponseError('');
cache.add(datasource); cache.add(datasource);
if (queryForce && actions) { if (queryForce && actions) {
@ -108,6 +110,7 @@ export const SamplesPane = ({
data={filteredData} data={filteredData}
columnNames={colnames} columnNames={colnames}
columnTypes={coltypes} columnTypes={coltypes}
rowcount={rowcount}
datasourceId={datasourceId} datasourceId={datasourceId}
onInputChange={input => setFilterText(input)} onInputChange={input => setFilterText(input)}
isLoading={isLoading} isLoading={isLoading}
@ -128,6 +131,7 @@ export const SamplesPane = ({
data={filteredData} data={filteredData}
columnNames={colnames} columnNames={colnames}
columnTypes={coltypes} columnTypes={coltypes}
rowcount={rowcount}
datasourceId={datasourceId} datasourceId={datasourceId}
onInputChange={input => setFilterText(input)} onInputChange={input => setFilterText(input)}
isLoading={isLoading} isLoading={isLoading}

View File

@ -30,6 +30,7 @@ export const SingleQueryResultPane = ({
data, data,
colnames, colnames,
coltypes, coltypes,
rowcount,
datasourceId, datasourceId,
dataSize = 50, dataSize = 50,
isVisible, isVisible,
@ -55,6 +56,7 @@ export const SingleQueryResultPane = ({
data={filteredData} data={filteredData}
columnNames={colnames} columnNames={colnames}
columnTypes={coltypes} columnTypes={coltypes}
rowcount={rowcount}
datasourceId={datasourceId} datasourceId={datasourceId}
onInputChange={input => setFilterText(input)} onInputChange={input => setFilterText(input)}
isLoading={false} isLoading={false}

View File

@ -119,6 +119,7 @@ export const useResultsPane = ({
data={[]} data={[]}
columnNames={[]} columnNames={[]}
columnTypes={[]} columnTypes={[]}
rowcount={0}
datasourceId={queryFormData.datasource} datasourceId={queryFormData.datasource}
onInputChange={() => {}} onInputChange={() => {}}
isLoading={false} isLoading={false}
@ -135,7 +136,6 @@ export const useResultsPane = ({
<EmptyStateMedium image="document.svg" title={title} />, <EmptyStateMedium image="document.svg" title={title} />,
); );
} }
return resultResp return resultResp
.slice(0, queryCount) .slice(0, queryCount)
.map((result, idx) => ( .map((result, idx) => (
@ -143,6 +143,7 @@ export const useResultsPane = ({
data={result.data} data={result.data}
colnames={result.colnames} colnames={result.colnames}
coltypes={result.coltypes} coltypes={result.coltypes}
rowcount={result.rowcount}
dataSize={dataSize} dataSize={dataSize}
datasourceId={queryFormData.datasource} datasourceId={queryFormData.datasource}
key={idx} key={idx}

View File

@ -93,6 +93,8 @@ describe('DataTablesPane', () => {
data: [{ __timestamp: 1230768000000, genre: 'Action' }], data: [{ __timestamp: 1230768000000, genre: 'Action' }],
colnames: ['__timestamp', 'genre'], colnames: ['__timestamp', 'genre'],
coltypes: [2, 1], coltypes: [2, 1],
rowcount: 1,
sql_rowcount: 1,
}, },
], ],
}, },
@ -125,6 +127,8 @@ describe('DataTablesPane', () => {
], ],
colnames: ['__timestamp', 'genre'], colnames: ['__timestamp', 'genre'],
coltypes: [2, 1], coltypes: [2, 1],
rowcount: 2,
sql_rowcount: 2,
}, },
], ],
}, },
@ -135,6 +139,7 @@ describe('DataTablesPane', () => {
}); });
userEvent.click(screen.getByText('Results')); userEvent.click(screen.getByText('Results'));
expect(await screen.findByText('2 rows')).toBeVisible(); expect(await screen.findByText('2 rows')).toBeVisible();
expect(screen.getByText('Action')).toBeVisible(); expect(screen.getByText('Action')).toBeVisible();
expect(screen.getByText('Horror')).toBeVisible(); expect(screen.getByText('Horror')).toBeVisible();

View File

@ -50,6 +50,8 @@ describe('ResultsPaneOnDashboard', () => {
], ],
colnames: ['__timestamp', 'genre'], colnames: ['__timestamp', 'genre'],
coltypes: [2, 1], coltypes: [2, 1],
rowcount: 2,
sql_rowcount: 2,
}, },
], ],
}, },
@ -78,6 +80,8 @@ describe('ResultsPaneOnDashboard', () => {
data: [{ genre: 'Action' }, { genre: 'Horror' }], data: [{ genre: 'Action' }, { genre: 'Horror' }],
colnames: ['genre'], colnames: ['genre'],
coltypes: [1], coltypes: [1],
rowcount: 2,
sql_rowcount: 2,
}, },
], ],
}, },

View File

@ -50,6 +50,8 @@ describe('SamplesPane', () => {
], ],
colnames: ['__timestamp', 'genre'], colnames: ['__timestamp', 'genre'],
coltypes: [2, 1], coltypes: [2, 1],
rowcount: 2,
sql_rowcount: 2,
}, },
}, },
); );

View File

@ -71,11 +71,13 @@ export interface TableControlsProps {
columnNames: string[]; columnNames: string[];
columnTypes: GenericDataType[]; columnTypes: GenericDataType[];
isLoading: boolean; isLoading: boolean;
rowcount: number;
} }
export interface QueryResultInterface { export interface QueryResultInterface {
colnames: string[]; colnames: string[];
coltypes: GenericDataType[]; coltypes: GenericDataType[];
rowcount: number;
data: Record<string, any>[][]; data: Record<string, any>[][];
} }

View File

@ -52,7 +52,7 @@ test('RowCountLabel renders limit with danger and tooltip', async () => {
expect(screen.getByText(expectedText)).toBeInTheDocument(); expect(screen.getByText(expectedText)).toBeInTheDocument();
userEvent.hover(screen.getByText(expectedText)); userEvent.hover(screen.getByText(expectedText));
const tooltip = await screen.findByRole('tooltip'); const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toHaveTextContent('Limit reached'); expect(tooltip).toHaveTextContent('The row limit');
expect(tooltip).toHaveStyle('background: rgba(0, 0, 0, 0.902);'); expect(tooltip).toHaveStyle('background: rgba(0, 0, 0, 0.902);');
}); });

View File

@ -28,9 +28,13 @@ type RowCountLabelProps = {
loading?: boolean; loading?: boolean;
}; };
const limitReachedMsg = t(
'The row limit set for the chart was reached. The chart may show partial data.',
);
export default function RowCountLabel(props: RowCountLabelProps) { export default function RowCountLabel(props: RowCountLabelProps) {
const { rowcount = 0, limit, loading } = props; const { rowcount = 0, limit = null, loading } = props;
const limitReached = rowcount === limit; const limitReached = limit && rowcount >= limit;
const type = const type =
limitReached || (rowcount === 0 && !loading) ? 'danger' : 'default'; limitReached || (rowcount === 0 && !loading) ? 'danger' : 'default';
const formattedRowCount = getNumberFormatter()(rowcount); const formattedRowCount = getNumberFormatter()(rowcount);
@ -46,7 +50,7 @@ export default function RowCountLabel(props: RowCountLabelProps) {
</Label> </Label>
); );
return limitReached ? ( return limitReached ? (
<Tooltip id="tt-rowcount-tooltip" title={<span>{t('Limit reached')}</span>}> <Tooltip id="tt-rowcount-tooltip" title={<span>{limitReachedMsg}</span>}>
{label} {label}
</Tooltip> </Tooltip>
) : ( ) : (

View File

@ -135,6 +135,8 @@ def _get_full(
"data": payload.get("data"), "data": payload.get("data"),
"colnames": payload.get("colnames"), "colnames": payload.get("colnames"),
"coltypes": payload.get("coltypes"), "coltypes": payload.get("coltypes"),
"rowcount": payload.get("rowcount"),
"sql_rowcount": payload.get("sql_rowcount"),
} }
return payload return payload

View File

@ -194,6 +194,7 @@ class QueryContextProcessor:
"status": cache.status, "status": cache.status,
"stacktrace": cache.stacktrace, "stacktrace": cache.stacktrace,
"rowcount": len(cache.df.index), "rowcount": len(cache.df.index),
"sql_rowcount": cache.sql_rowcount,
"from_dttm": query_obj.from_dttm, "from_dttm": query_obj.from_dttm,
"to_dttm": query_obj.to_dttm, "to_dttm": query_obj.to_dttm,
"label_map": label_map, "label_map": label_map,

View File

@ -64,6 +64,7 @@ class QueryCacheManager:
is_cached: bool | None = None, is_cached: bool | None = None,
cache_dttm: str | None = None, cache_dttm: str | None = None,
cache_value: dict[str, Any] | None = None, cache_value: dict[str, Any] | None = None,
sql_rowcount: int | None = None,
) -> None: ) -> None:
self.df = df self.df = df
self.query = query self.query = query
@ -79,6 +80,7 @@ class QueryCacheManager:
self.is_cached = is_cached self.is_cached = is_cached
self.cache_dttm = cache_dttm self.cache_dttm = cache_dttm
self.cache_value = cache_value self.cache_value = cache_value
self.sql_rowcount = sql_rowcount
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def set_query_result( def set_query_result(
@ -102,6 +104,7 @@ class QueryCacheManager:
self.rejected_filter_columns = query_result.rejected_filter_columns self.rejected_filter_columns = query_result.rejected_filter_columns
self.error_message = query_result.error_message self.error_message = query_result.error_message
self.df = query_result.df self.df = query_result.df
self.sql_rowcount = query_result.sql_rowcount
self.annotation_data = {} if annotation_data is None else annotation_data self.annotation_data = {} if annotation_data is None else annotation_data
if self.status != QueryStatus.FAILED: if self.status != QueryStatus.FAILED:
@ -117,6 +120,7 @@ class QueryCacheManager:
"applied_filter_columns": self.applied_filter_columns, "applied_filter_columns": self.applied_filter_columns,
"rejected_filter_columns": self.rejected_filter_columns, "rejected_filter_columns": self.rejected_filter_columns,
"annotation_data": self.annotation_data, "annotation_data": self.annotation_data,
"sql_rowcount": self.sql_rowcount,
} }
if self.is_loaded and key and self.status != QueryStatus.FAILED: if self.is_loaded and key and self.status != QueryStatus.FAILED:
self.set( self.set(
@ -167,6 +171,7 @@ class QueryCacheManager:
query_cache.status = QueryStatus.SUCCESS query_cache.status = QueryStatus.SUCCESS
query_cache.is_loaded = True query_cache.is_loaded = True
query_cache.is_cached = cache_value is not None query_cache.is_cached = cache_value is not None
query_cache.sql_rowcount = cache_value.get("sql_rowcount", None)
query_cache.cache_dttm = ( query_cache.cache_dttm = (
cache_value["dttm"] if cache_value is not None else None cache_value["dttm"] if cache_value is not None else None
) )

View File

@ -584,6 +584,7 @@ class QueryResult: # pylint: disable=too-few-public-methods
self.errors = errors or [] self.errors = errors or []
self.from_dttm = from_dttm self.from_dttm = from_dttm
self.to_dttm = to_dttm self.to_dttm = to_dttm
self.sql_rowcount = len(self.df.index) if not self.df.empty else 0
class ExtraJSONMixin: class ExtraJSONMixin:

View File

@ -167,6 +167,8 @@ class Superset(BaseSupersetView):
"data": payload["df"].to_dict("records"), "data": payload["df"].to_dict("records"),
"colnames": payload.get("colnames"), "colnames": payload.get("colnames"),
"coltypes": payload.get("coltypes"), "coltypes": payload.get("coltypes"),
"rowcount": payload.get("rowcount"),
"sql_rowcount": payload.get("sql_rowcount"),
}, },
) )

View File

@ -262,6 +262,8 @@ class BaseViz: # pylint: disable=too-many-public-methods
"data": payload["df"].to_dict(orient="records"), "data": payload["df"].to_dict(orient="records"),
"colnames": payload.get("colnames"), "colnames": payload.get("colnames"),
"coltypes": payload.get("coltypes"), "coltypes": payload.get("coltypes"),
"rowcount": payload.get("rowcount"),
"sql_rowcount": payload.get("sql_rowcount"),
} }
@deprecated(deprecated_in="3.0") @deprecated(deprecated_in="3.0")