mirror of
https://github.com/apache/superset.git
synced 2026-05-03 23:14:29 +00:00
Compare commits
13 Commits
fix/check-
...
fix-explor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
235d4ea516 | ||
|
|
860f8cbe0f | ||
|
|
2fad87569c | ||
|
|
c6f54471dc | ||
|
|
7539138702 | ||
|
|
e0b1b557d7 | ||
|
|
bc5a5c2ac5 | ||
|
|
3a562dbe29 | ||
|
|
73b780a28c | ||
|
|
caeb6a6b7c | ||
|
|
19072074c5 | ||
|
|
f2037fa332 | ||
|
|
6c71800436 |
@@ -29,9 +29,12 @@ import {
|
|||||||
} from 'spec/helpers/testing-library';
|
} from 'spec/helpers/testing-library';
|
||||||
import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
|
import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
|
||||||
import mockState from 'spec/fixtures/mockState';
|
import mockState from 'spec/fixtures/mockState';
|
||||||
|
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||||
import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
|
import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
|
||||||
import DrillByModal, { DrillByModalProps } from './DrillByModal';
|
import DrillByModal, { DrillByModalProps } from './DrillByModal';
|
||||||
|
|
||||||
|
setupAGGridModules();
|
||||||
|
|
||||||
// Mock the isEmbedded function
|
// Mock the isEmbedded function
|
||||||
jest.mock('src/dashboard/util/isEmbedded', () => ({
|
jest.mock('src/dashboard/util/isEmbedded', () => ({
|
||||||
isEmbedded: jest.fn(() => false),
|
isEmbedded: jest.fn(() => false),
|
||||||
@@ -406,16 +409,9 @@ describe('Table view with pagination', () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check that pagination is rendered (there's also a breadcrumb list)
|
|
||||||
const lists = screen.getAllByRole('list');
|
|
||||||
const paginationList = lists.find(list =>
|
|
||||||
list.className?.includes('pagination'),
|
|
||||||
);
|
|
||||||
expect(paginationList).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle pagination in table view', async () => {
|
test('should render data in table view', async () => {
|
||||||
await renderModal({
|
await renderModal({
|
||||||
column: { column_name: 'state', verbose_name: null },
|
column: { column_name: 'state', verbose_name: null },
|
||||||
drillByConfig: {
|
drillByConfig: {
|
||||||
@@ -432,19 +428,9 @@ describe('Table view with pagination', () => {
|
|||||||
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check that first page data is shown
|
// Check that data is rendered in the grid
|
||||||
expect(screen.getByText('State0')).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Check pagination controls exist
|
|
||||||
const nextPageButton = screen.getByTitle('Next Page');
|
|
||||||
expect(nextPageButton).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Click next page
|
|
||||||
userEvent.click(nextPageButton);
|
|
||||||
|
|
||||||
// Verify page changed (State0 should not be visible on page 2)
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByText('State0')).not.toBeInTheDocument();
|
expect(screen.getByText('State0')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -542,11 +528,12 @@ describe('Table view with pagination', () => {
|
|||||||
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should show empty state
|
// ag-grid shows its own empty overlay when there are no rows
|
||||||
expect(screen.getByText('No data')).toBeInTheDocument();
|
const tableContainer = screen.getByTestId('drill-by-results-table');
|
||||||
|
expect(tableContainer).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle sorting in table view', async () => {
|
test('should render grid in table view', async () => {
|
||||||
await renderModal({
|
await renderModal({
|
||||||
column: { column_name: 'state', verbose_name: null },
|
column: { column_name: 'state', verbose_name: null },
|
||||||
drillByConfig: {
|
drillByConfig: {
|
||||||
@@ -563,16 +550,7 @@ describe('Table view with pagination', () => {
|
|||||||
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find sortable column header
|
|
||||||
const sortableHeaders = screen.getAllByTestId('sort-header');
|
|
||||||
expect(sortableHeaders.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// Click to sort
|
|
||||||
userEvent.click(sortableHeaders[0]);
|
|
||||||
|
|
||||||
// Table should still be rendered without crashes
|
// Table should still be rendered without crashes
|
||||||
await waitFor(() => {
|
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,25 +25,12 @@ import {
|
|||||||
within,
|
within,
|
||||||
waitFor,
|
waitFor,
|
||||||
} from 'spec/helpers/testing-library';
|
} from 'spec/helpers/testing-library';
|
||||||
|
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||||
import { useResultsTableView } from './useResultsTableView';
|
import { useResultsTableView } from './useResultsTableView';
|
||||||
|
|
||||||
const capturedProps: any[] = [];
|
beforeAll(() => {
|
||||||
|
setupAGGridModules();
|
||||||
jest.mock(
|
});
|
||||||
'src/explore/components/DataTablesPane/components/SingleQueryResultPane',
|
|
||||||
() => {
|
|
||||||
const actual = jest.requireActual(
|
|
||||||
'src/explore/components/DataTablesPane/components/SingleQueryResultPane',
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
SingleQueryResultPane: (props: any) => {
|
|
||||||
capturedProps.push(props);
|
|
||||||
return actual.SingleQueryResultPane(props);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const MOCK_CHART_DATA_RESULT = [
|
const MOCK_CHART_DATA_RESULT = [
|
||||||
{
|
{
|
||||||
@@ -92,9 +79,9 @@ test('Displays results table for 1 query', () => {
|
|||||||
);
|
);
|
||||||
render(result.current, { useRedux: true });
|
render(result.current, { useRedux: true });
|
||||||
expect(screen.queryByRole('tablist')).not.toBeInTheDocument();
|
expect(screen.queryByRole('tablist')).not.toBeInTheDocument();
|
||||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
expect(screen.getByText('name')).toBeInTheDocument();
|
||||||
expect(screen.getAllByTestId('sort-header')).toHaveLength(2);
|
expect(screen.getByText('sum__num')).toBeInTheDocument();
|
||||||
expect(screen.getAllByTestId('table-row')).toHaveLength(4);
|
expect(screen.getByText('Michael')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Displays results for 2 queries', async () => {
|
test('Displays results for 2 queries', async () => {
|
||||||
@@ -102,60 +89,18 @@ test('Displays results for 2 queries', async () => {
|
|||||||
useResultsTableView(MOCK_CHART_DATA_RESULT, '1__table', true),
|
useResultsTableView(MOCK_CHART_DATA_RESULT, '1__table', true),
|
||||||
);
|
);
|
||||||
render(result.current, { useRedux: true });
|
render(result.current, { useRedux: true });
|
||||||
const getActiveTabElement = () =>
|
|
||||||
document.querySelector('.ant-tabs-tabpane-active') as HTMLElement;
|
|
||||||
|
|
||||||
const tablistElement = screen.getByRole('tablist');
|
const tablistElement = screen.getByRole('tablist');
|
||||||
expect(tablistElement).toBeInTheDocument();
|
expect(tablistElement).toBeInTheDocument();
|
||||||
expect(within(tablistElement).getByText('Results 1')).toBeInTheDocument();
|
expect(within(tablistElement).getByText('Results 1')).toBeInTheDocument();
|
||||||
expect(within(tablistElement).getByText('Results 2')).toBeInTheDocument();
|
expect(within(tablistElement).getByText('Results 2')).toBeInTheDocument();
|
||||||
|
|
||||||
expect(within(getActiveTabElement()).getByRole('table')).toBeInTheDocument();
|
expect(screen.getByText('Michael')).toBeInTheDocument();
|
||||||
expect(
|
|
||||||
within(getActiveTabElement()).getAllByTestId('sort-header'),
|
|
||||||
).toHaveLength(2);
|
|
||||||
expect(
|
|
||||||
within(getActiveTabElement()).getAllByTestId('table-row'),
|
|
||||||
).toHaveLength(4);
|
|
||||||
|
|
||||||
userEvent.click(screen.getByText('Results 2'));
|
userEvent.click(screen.getByText('Results 2'));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(
|
expect(screen.getByText('gender')).toBeInTheDocument();
|
||||||
within(getActiveTabElement()).getAllByTestId('sort-header'),
|
|
||||||
).toHaveLength(3);
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
within(getActiveTabElement()).getAllByTestId('table-row'),
|
|
||||||
).toHaveLength(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('passes isPaginationSticky={false} to SingleQueryResultPane for single query', () => {
|
|
||||||
capturedProps.length = 0;
|
|
||||||
const { result } = renderHook(() =>
|
|
||||||
useResultsTableView(MOCK_CHART_DATA_RESULT.slice(0, 1), '1__table', true),
|
|
||||||
);
|
|
||||||
render(result.current, { useRedux: true });
|
|
||||||
|
|
||||||
expect(capturedProps.length).toBeGreaterThan(0);
|
|
||||||
capturedProps.forEach(props => {
|
|
||||||
expect(props).toMatchObject({
|
|
||||||
isPaginationSticky: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('passes isPaginationSticky={false} to SingleQueryResultPane for multiple queries', () => {
|
|
||||||
capturedProps.length = 0;
|
|
||||||
const { result } = renderHook(() =>
|
|
||||||
useResultsTableView(MOCK_CHART_DATA_RESULT, '1__table', true),
|
|
||||||
);
|
|
||||||
render(result.current, { useRedux: true });
|
|
||||||
|
|
||||||
expect(capturedProps.length).toBeGreaterThanOrEqual(2);
|
|
||||||
capturedProps.forEach(props => {
|
|
||||||
expect(props).toMatchObject({
|
|
||||||
isPaginationSticky: false,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
expect(screen.getByText('boy')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,13 +22,12 @@ import { t } from '@apache-superset/core/translation';
|
|||||||
import { SingleQueryResultPane } from 'src/explore/components/DataTablesPane/components/SingleQueryResultPane';
|
import { SingleQueryResultPane } from 'src/explore/components/DataTablesPane/components/SingleQueryResultPane';
|
||||||
import Tabs from '@superset-ui/core/components/Tabs';
|
import Tabs from '@superset-ui/core/components/Tabs';
|
||||||
|
|
||||||
const DATA_SIZE = 15;
|
const ResultContainer = styled.div`
|
||||||
|
${() => css`
|
||||||
const PaginationContainer = styled.div`
|
display: flex;
|
||||||
${({ theme }) => css`
|
flex-direction: column;
|
||||||
& .pagination-container {
|
flex: 1;
|
||||||
bottom: ${-theme.sizeUnit * 4}px;
|
min-height: 0;
|
||||||
}
|
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -42,19 +41,17 @@ export const useResultsTableView = (
|
|||||||
}
|
}
|
||||||
if (chartDataResult.length === 1) {
|
if (chartDataResult.length === 1) {
|
||||||
return (
|
return (
|
||||||
<PaginationContainer data-test="drill-by-results-table">
|
<ResultContainer data-test="drill-by-results-table">
|
||||||
<SingleQueryResultPane
|
<SingleQueryResultPane
|
||||||
colnames={chartDataResult[0].colnames}
|
colnames={chartDataResult[0].colnames}
|
||||||
coltypes={chartDataResult[0].coltypes}
|
coltypes={chartDataResult[0].coltypes}
|
||||||
rowcount={chartDataResult[0].sql_rowcount}
|
rowcount={chartDataResult[0].sql_rowcount}
|
||||||
data={chartDataResult[0].data}
|
data={chartDataResult[0].data}
|
||||||
dataSize={DATA_SIZE}
|
|
||||||
datasourceId={datasourceId}
|
datasourceId={datasourceId}
|
||||||
isVisible
|
isVisible
|
||||||
canDownload={canDownload}
|
canDownload={canDownload}
|
||||||
isPaginationSticky={false}
|
|
||||||
/>
|
/>
|
||||||
</PaginationContainer>
|
</ResultContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -64,19 +61,17 @@ export const useResultsTableView = (
|
|||||||
key: `result-tab-${index}`,
|
key: `result-tab-${index}`,
|
||||||
label: t('Results %s', index + 1),
|
label: t('Results %s', index + 1),
|
||||||
children: (
|
children: (
|
||||||
<PaginationContainer>
|
<ResultContainer>
|
||||||
<SingleQueryResultPane
|
<SingleQueryResultPane
|
||||||
colnames={res.colnames}
|
colnames={res.colnames}
|
||||||
coltypes={res.coltypes}
|
coltypes={res.coltypes}
|
||||||
data={res.data}
|
data={res.data}
|
||||||
rowcount={res.sql_rowcount}
|
rowcount={res.sql_rowcount}
|
||||||
dataSize={DATA_SIZE}
|
|
||||||
datasourceId={datasourceId}
|
datasourceId={datasourceId}
|
||||||
isVisible
|
isVisible
|
||||||
canDownload={canDownload}
|
canDownload={canDownload}
|
||||||
isPaginationSticky={false}
|
|
||||||
/>
|
/>
|
||||||
</PaginationContainer>
|
</ResultContainer>
|
||||||
),
|
),
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ export const DataTablesPane = ({
|
|||||||
<StyledDiv>
|
<StyledDiv>
|
||||||
<SamplesPane
|
<SamplesPane
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
|
queryFormData={queryFormData}
|
||||||
queryForce={queryForce}
|
queryForce={queryForce}
|
||||||
isRequest={isRequest.samples}
|
isRequest={isRequest.samples}
|
||||||
setForceQuery={setForceQuery}
|
setForceQuery={setForceQuery}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { styled, css } from '@apache-superset/core/theme';
|
|||||||
import { GenericDataType } from '@apache-superset/core/common';
|
import { GenericDataType } from '@apache-superset/core/common';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { zip } from 'lodash';
|
import { zip } from 'lodash';
|
||||||
|
import { Select } from 'antd';
|
||||||
import {
|
import {
|
||||||
CopyToClipboardButton,
|
CopyToClipboardButton,
|
||||||
FilterInput,
|
FilterInput,
|
||||||
@@ -29,10 +30,19 @@ import { getTimeColumns } from 'src/explore/components/DataTableControl/utils';
|
|||||||
import RowCountLabel from 'src/components/RowCountLabel';
|
import RowCountLabel from 'src/components/RowCountLabel';
|
||||||
import { TableControlsProps } from '../types';
|
import { TableControlsProps } from '../types';
|
||||||
|
|
||||||
|
export const ROW_LIMIT_OPTIONS = [
|
||||||
|
{ value: 100, label: '100 rows' },
|
||||||
|
{ value: 500, label: '500 rows' },
|
||||||
|
{ value: 1000, label: '1k rows' },
|
||||||
|
{ value: 5000, label: '5k rows' },
|
||||||
|
{ value: 10000, label: '10k rows' },
|
||||||
|
];
|
||||||
|
|
||||||
export const TableControlsWrapper = styled.div`
|
export const TableControlsWrapper = styled.div`
|
||||||
${({ theme }) => `
|
${({ theme }) => `
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding-top: ${theme.sizeUnit * 2}px;
|
||||||
padding-bottom: ${theme.sizeUnit * 2}px;
|
padding-bottom: ${theme.sizeUnit * 2}px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
@@ -51,6 +61,9 @@ export const TableControls = ({
|
|||||||
rowcount,
|
rowcount,
|
||||||
isLoading,
|
isLoading,
|
||||||
canDownload,
|
canDownload,
|
||||||
|
rowLimit,
|
||||||
|
rowLimitOptions,
|
||||||
|
onRowLimitChange,
|
||||||
}: TableControlsProps) => {
|
}: TableControlsProps) => {
|
||||||
const originalTimeColumns = getTimeColumns(datasourceId);
|
const originalTimeColumns = getTimeColumns(datasourceId);
|
||||||
const formattedTimeColumns = zip<string, GenericDataType>(
|
const formattedTimeColumns = zip<string, GenericDataType>(
|
||||||
@@ -76,9 +89,23 @@ export const TableControls = ({
|
|||||||
css={css`
|
css={css`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<RowCountLabel rowcount={rowcount} loading={isLoading} />
|
{onRowLimitChange && (
|
||||||
|
<Select
|
||||||
|
value={rowLimit}
|
||||||
|
onChange={onRowLimitChange}
|
||||||
|
options={rowLimitOptions}
|
||||||
|
size="small"
|
||||||
|
css={css`
|
||||||
|
min-width: 110px;
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(!onRowLimitChange || rowcount < (rowLimit ?? Infinity)) && (
|
||||||
|
<RowCountLabel rowcount={rowcount} loading={isLoading} />
|
||||||
|
)}
|
||||||
{canDownload && (
|
{canDownload && (
|
||||||
<CopyToClipboardButton data={formattedData} columns={columnNames} />
|
<CopyToClipboardButton data={formattedData} columns={columnNames} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -20,64 +20,96 @@ import { useState, useEffect, useMemo, useCallback } from 'react';
|
|||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import { ensureIsArray } from '@superset-ui/core';
|
import { ensureIsArray } from '@superset-ui/core';
|
||||||
import { styled } from '@apache-superset/core/theme';
|
import { styled } from '@apache-superset/core/theme';
|
||||||
import {
|
import { EmptyState, Loading } from '@superset-ui/core/components';
|
||||||
TableView,
|
|
||||||
TableSize,
|
|
||||||
EmptyState,
|
|
||||||
Loading,
|
|
||||||
EmptyWrapperType,
|
|
||||||
} from '@superset-ui/core/components';
|
|
||||||
import { GenericDataType } from '@apache-superset/core/common';
|
import { GenericDataType } from '@apache-superset/core/common';
|
||||||
import {
|
import { GridTable } from 'src/components/GridTable';
|
||||||
useFilteredTableData,
|
import { GridSize } from 'src/components/GridTable/constants';
|
||||||
useTableColumns,
|
|
||||||
} from 'src/explore/components/DataTableControl';
|
|
||||||
import { getDatasourceSamples } from 'src/components/Chart/chartAction';
|
import { getDatasourceSamples } from 'src/components/Chart/chartAction';
|
||||||
import { TableControls } from './DataTableControls';
|
import { getDrillPayload } from 'src/components/Chart/DrillDetail/utils';
|
||||||
|
import {
|
||||||
|
useGridColumns,
|
||||||
|
useKeywordFilter,
|
||||||
|
useGridHeight,
|
||||||
|
} from './useGridResultTable';
|
||||||
|
import { TableControls, ROW_LIMIT_OPTIONS } from './DataTableControls';
|
||||||
import { SamplesPaneProps } from '../types';
|
import { SamplesPaneProps } from '../types';
|
||||||
|
|
||||||
const Error = styled.pre`
|
const Error = styled.pre`
|
||||||
margin-top: ${({ theme }) => `${theme.sizeUnit * 4}px`};
|
margin-top: ${({ theme }) => `${theme.sizeUnit * 4}px`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const cache = new WeakSet();
|
const GridContainer = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GridSizer = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const cache = new WeakMap();
|
||||||
|
|
||||||
|
const DEFAULT_ROW_LIMIT = 100;
|
||||||
|
|
||||||
export const SamplesPane = ({
|
export const SamplesPane = ({
|
||||||
isRequest,
|
isRequest,
|
||||||
datasource,
|
datasource,
|
||||||
|
queryFormData,
|
||||||
queryForce,
|
queryForce,
|
||||||
setForceQuery,
|
setForceQuery,
|
||||||
dataSize = 50,
|
|
||||||
isVisible,
|
isVisible,
|
||||||
canDownload,
|
canDownload,
|
||||||
}: SamplesPaneProps) => {
|
}: SamplesPaneProps) => {
|
||||||
const [filterText, setFilterText] = useState('');
|
const [filterText, setFilterText] = useState('');
|
||||||
|
const [rowLimit, setRowLimit] = useState(DEFAULT_ROW_LIMIT);
|
||||||
const [data, setData] = useState<Record<string, any>[][]>([]);
|
const [data, setData] = useState<Record<string, any>[][]>([]);
|
||||||
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 [rowcount, setRowCount] = useState<number>(0);
|
||||||
const [responseError, setResponseError] = useState<string>('');
|
const [responseError, setResponseError] = useState<string>('');
|
||||||
|
const { gridHeight, measuredRef } = useGridHeight();
|
||||||
const datasourceId = useMemo(
|
const datasourceId = useMemo(
|
||||||
() => `${datasource.id}__${datasource.type}`,
|
() => `${datasource.id}__${datasource.type}`,
|
||||||
[datasource],
|
[datasource],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleRowLimitChange = useCallback(
|
||||||
|
(limit: number) => {
|
||||||
|
setRowLimit(limit);
|
||||||
|
cache.delete(queryFormData);
|
||||||
|
},
|
||||||
|
[queryFormData],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isRequest && queryForce) {
|
if (isRequest && queryForce) {
|
||||||
cache.delete(datasource);
|
cache.delete(queryFormData);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRequest && !cache.has(datasource)) {
|
if (isRequest && !cache.has(queryFormData)) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
getDatasourceSamples(datasource.type, datasource.id, queryForce, {})
|
const payload =
|
||||||
|
getDrillPayload(
|
||||||
|
queryFormData as Parameters<typeof getDrillPayload>[0],
|
||||||
|
) ?? {};
|
||||||
|
getDatasourceSamples(
|
||||||
|
datasource.type,
|
||||||
|
datasource.id,
|
||||||
|
queryForce,
|
||||||
|
payload,
|
||||||
|
rowLimit,
|
||||||
|
1,
|
||||||
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
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);
|
setRowCount(response.rowcount);
|
||||||
setResponseError('');
|
setResponseError('');
|
||||||
cache.add(datasource);
|
cache.set(queryFormData, true);
|
||||||
if (queryForce) {
|
if (queryForce) {
|
||||||
setForceQuery?.(false);
|
setForceQuery?.(false);
|
||||||
}
|
}
|
||||||
@@ -92,20 +124,10 @@ export const SamplesPane = ({
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [datasource, isRequest, queryForce]);
|
}, [datasource, queryFormData, isRequest, queryForce, rowLimit]);
|
||||||
|
|
||||||
// this is to preserve the order of the columns, even if there are integer values,
|
const columns = useGridColumns(colnames, coltypes, data);
|
||||||
// while also only grabbing the first column's keys
|
const keywordFilter = useKeywordFilter(filterText);
|
||||||
const columns = useTableColumns(
|
|
||||||
colnames,
|
|
||||||
coltypes,
|
|
||||||
data,
|
|
||||||
datasourceId,
|
|
||||||
isVisible,
|
|
||||||
{}, // moreConfig
|
|
||||||
true, // allowHTML
|
|
||||||
);
|
|
||||||
const filteredData = useFilteredTableData(filterText, data);
|
|
||||||
|
|
||||||
const handleInputChange = useCallback(
|
const handleInputChange = useCallback(
|
||||||
(input: string) => setFilterText(input),
|
(input: string) => setFilterText(input),
|
||||||
@@ -120,7 +142,7 @@ export const SamplesPane = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableControls
|
<TableControls
|
||||||
data={filteredData}
|
data={data}
|
||||||
columnNames={colnames}
|
columnNames={colnames}
|
||||||
columnTypes={coltypes}
|
columnTypes={coltypes}
|
||||||
rowcount={rowcount}
|
rowcount={rowcount}
|
||||||
@@ -128,6 +150,9 @@ export const SamplesPane = ({
|
|||||||
onInputChange={handleInputChange}
|
onInputChange={handleInputChange}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
canDownload={canDownload}
|
canDownload={canDownload}
|
||||||
|
rowLimit={rowLimit}
|
||||||
|
rowLimitOptions={ROW_LIMIT_OPTIONS}
|
||||||
|
onRowLimitChange={handleRowLimitChange}
|
||||||
/>
|
/>
|
||||||
<Error>{responseError}</Error>
|
<Error>{responseError}</Error>
|
||||||
</>
|
</>
|
||||||
@@ -142,7 +167,7 @@ export const SamplesPane = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableControls
|
<TableControls
|
||||||
data={filteredData}
|
data={data}
|
||||||
columnNames={colnames}
|
columnNames={colnames}
|
||||||
columnTypes={coltypes}
|
columnTypes={coltypes}
|
||||||
rowcount={rowcount}
|
rowcount={rowcount}
|
||||||
@@ -150,19 +175,22 @@ export const SamplesPane = ({
|
|||||||
onInputChange={handleInputChange}
|
onInputChange={handleInputChange}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
canDownload={canDownload}
|
canDownload={canDownload}
|
||||||
|
rowLimit={rowLimit}
|
||||||
|
rowLimitOptions={ROW_LIMIT_OPTIONS}
|
||||||
|
onRowLimitChange={handleRowLimitChange}
|
||||||
/>
|
/>
|
||||||
<TableView
|
<GridContainer>
|
||||||
columns={columns}
|
<GridSizer ref={measuredRef}>
|
||||||
data={filteredData}
|
<GridTable
|
||||||
pageSize={dataSize}
|
data={data}
|
||||||
noDataText={t('No results')}
|
columns={columns}
|
||||||
emptyWrapperType={EmptyWrapperType.Small}
|
height={gridHeight}
|
||||||
className="table-condensed"
|
size={GridSize.Small}
|
||||||
isPaginationSticky
|
externalFilter={keywordFilter}
|
||||||
showRowCount={false}
|
showRowNumber
|
||||||
size={TableSize.Small}
|
/>
|
||||||
small
|
</GridSizer>
|
||||||
/>
|
</GridContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,46 +17,52 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { styled } from '@apache-superset/core/theme';
|
||||||
|
import { GridTable } from 'src/components/GridTable';
|
||||||
|
import { GridSize } from 'src/components/GridTable/constants';
|
||||||
import {
|
import {
|
||||||
TableView,
|
useGridColumns,
|
||||||
TableSize,
|
useKeywordFilter,
|
||||||
EmptyWrapperType,
|
useGridHeight,
|
||||||
} from '@superset-ui/core/components';
|
} from './useGridResultTable';
|
||||||
import {
|
|
||||||
useFilteredTableData,
|
|
||||||
useTableColumns,
|
|
||||||
} from 'src/explore/components/DataTableControl';
|
|
||||||
import { TableControls } from './DataTableControls';
|
import { TableControls } from './DataTableControls';
|
||||||
import { SingleQueryResultPaneProp } from '../types';
|
import { SingleQueryResultPaneProp } from '../types';
|
||||||
|
|
||||||
|
const ResultPaneContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GridContainer = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GridSizer = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
export const SingleQueryResultPane = ({
|
export const SingleQueryResultPane = ({
|
||||||
data,
|
data,
|
||||||
colnames,
|
colnames,
|
||||||
coltypes,
|
coltypes,
|
||||||
rowcount,
|
rowcount,
|
||||||
datasourceId,
|
datasourceId,
|
||||||
dataSize = 50,
|
|
||||||
isVisible,
|
|
||||||
canDownload,
|
canDownload,
|
||||||
columnDisplayNames,
|
columnDisplayNames,
|
||||||
isPaginationSticky = true,
|
rowLimit,
|
||||||
|
rowLimitOptions,
|
||||||
|
onRowLimitChange,
|
||||||
}: SingleQueryResultPaneProp) => {
|
}: SingleQueryResultPaneProp) => {
|
||||||
const [filterText, setFilterText] = useState('');
|
const [filterText, setFilterText] = useState('');
|
||||||
|
const { gridHeight, measuredRef } = useGridHeight();
|
||||||
|
|
||||||
// this is to preserve the order of the columns, even if there are integer values,
|
const columns = useGridColumns(colnames, coltypes, data, columnDisplayNames);
|
||||||
// while also only grabbing the first column's keys
|
const keywordFilter = useKeywordFilter(filterText);
|
||||||
const columns = useTableColumns(
|
|
||||||
colnames,
|
|
||||||
coltypes,
|
|
||||||
data,
|
|
||||||
datasourceId,
|
|
||||||
isVisible,
|
|
||||||
{}, // moreConfig
|
|
||||||
true, // allowHTML
|
|
||||||
columnDisplayNames,
|
|
||||||
);
|
|
||||||
const filteredData = useFilteredTableData(filterText, data);
|
|
||||||
|
|
||||||
const handleInputChange = useCallback(
|
const handleInputChange = useCallback(
|
||||||
(input: string) => setFilterText(input),
|
(input: string) => setFilterText(input),
|
||||||
@@ -64,9 +70,9 @@ export const SingleQueryResultPane = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ResultPaneContainer>
|
||||||
<TableControls
|
<TableControls
|
||||||
data={filteredData}
|
data={data}
|
||||||
columnNames={colnames}
|
columnNames={colnames}
|
||||||
columnTypes={coltypes}
|
columnTypes={coltypes}
|
||||||
rowcount={rowcount}
|
rowcount={rowcount}
|
||||||
@@ -74,19 +80,22 @@ export const SingleQueryResultPane = ({
|
|||||||
onInputChange={handleInputChange}
|
onInputChange={handleInputChange}
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
canDownload={canDownload}
|
canDownload={canDownload}
|
||||||
|
rowLimit={rowLimit}
|
||||||
|
rowLimitOptions={rowLimitOptions}
|
||||||
|
onRowLimitChange={onRowLimitChange}
|
||||||
/>
|
/>
|
||||||
<TableView
|
<GridContainer>
|
||||||
columns={columns}
|
<GridSizer ref={measuredRef}>
|
||||||
size={TableSize.Small}
|
<GridTable
|
||||||
data={filteredData}
|
data={data}
|
||||||
pageSize={dataSize}
|
columns={columns}
|
||||||
noDataText={t('No results')}
|
height={gridHeight}
|
||||||
emptyWrapperType={EmptyWrapperType.Small}
|
size={GridSize.Small}
|
||||||
className="table-condensed"
|
externalFilter={keywordFilter}
|
||||||
isPaginationSticky={isPaginationSticky}
|
showRowNumber
|
||||||
showRowCount={false}
|
/>
|
||||||
small
|
</GridSizer>
|
||||||
/>
|
</GridContainer>
|
||||||
</>
|
</ResultPaneContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* 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 { useMemo, useCallback, useRef, useState } from 'react';
|
||||||
|
import { getTimeFormatter, safeHtmlSpan, TimeFormats } from '@superset-ui/core';
|
||||||
|
import { Constants } from '@superset-ui/core/components';
|
||||||
|
import { GenericDataType } from '@apache-superset/core/common';
|
||||||
|
import type { IRowNode } from 'ag-grid-community';
|
||||||
|
|
||||||
|
const timeFormatter = getTimeFormatter(TimeFormats.DATABASE_DATETIME);
|
||||||
|
|
||||||
|
export function useGridColumns(
|
||||||
|
colnames: string[] | undefined,
|
||||||
|
coltypes: GenericDataType[] | undefined,
|
||||||
|
data: Record<string, any>[] | undefined,
|
||||||
|
columnDisplayNames?: Record<string, string>,
|
||||||
|
) {
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
colnames && data?.length
|
||||||
|
? colnames
|
||||||
|
.filter((column: string) => Object.keys(data[0]).includes(column))
|
||||||
|
.map((key, index) => {
|
||||||
|
const colType = coltypes?.[index];
|
||||||
|
const headerLabel = columnDisplayNames?.[key] ?? key;
|
||||||
|
return {
|
||||||
|
label: key,
|
||||||
|
headerName: headerLabel,
|
||||||
|
render: ({ value }: { value: unknown }) => {
|
||||||
|
if (value === true) {
|
||||||
|
return Constants.BOOL_TRUE_DISPLAY;
|
||||||
|
}
|
||||||
|
if (value === false) {
|
||||||
|
return Constants.BOOL_FALSE_DISPLAY;
|
||||||
|
}
|
||||||
|
if (value === null) {
|
||||||
|
return (
|
||||||
|
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>
|
||||||
|
{Constants.NULL_DISPLAY}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
colType === GenericDataType.Temporal &&
|
||||||
|
typeof value === 'number'
|
||||||
|
) {
|
||||||
|
return timeFormatter(value);
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return safeHtmlSpan(value);
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
[colnames, data, coltypes, columnDisplayNames],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useKeywordFilter(filterText: string) {
|
||||||
|
return useCallback(
|
||||||
|
(node: IRowNode) => {
|
||||||
|
if (filterText && node.data) {
|
||||||
|
const lowerFilter = filterText.toLowerCase();
|
||||||
|
return Object.values(node.data).some(
|
||||||
|
(value: unknown) =>
|
||||||
|
value != null && String(value).toLowerCase().includes(lowerFilter),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[filterText],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures the height of an absolutely-positioned inner element that fills
|
||||||
|
* its relative-positioned parent. Uses a callback ref so the ResizeObserver
|
||||||
|
* is created when the element mounts (which may be after initial render if
|
||||||
|
* the component conditionally renders a loading state first).
|
||||||
|
*/
|
||||||
|
export function useGridHeight(fallbackHeight = 400) {
|
||||||
|
const [gridHeight, setGridHeight] = useState(fallbackHeight);
|
||||||
|
const observerRef = useRef<ResizeObserver | null>(null);
|
||||||
|
|
||||||
|
const measuredRef = useCallback((el: HTMLDivElement | null) => {
|
||||||
|
if (observerRef.current) {
|
||||||
|
observerRef.current.disconnect();
|
||||||
|
observerRef.current = null;
|
||||||
|
}
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(entries => {
|
||||||
|
const entry = entries[0];
|
||||||
|
if (entry) {
|
||||||
|
const h = Math.floor(entry.contentRect.height);
|
||||||
|
if (h > 0) {
|
||||||
|
setGridHeight(prev => (prev !== h ? h : prev));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(el);
|
||||||
|
observerRef.current = observer;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { gridHeight, measuredRef };
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { useState, useEffect, ReactElement, useCallback } from 'react';
|
import { useState, useEffect, useMemo, ReactElement, useCallback } from 'react';
|
||||||
|
|
||||||
import { t } from '@apache-superset/core/translation';
|
import { t } from '@apache-superset/core/translation';
|
||||||
import {
|
import {
|
||||||
@@ -29,7 +29,7 @@ import { EmptyState, Loading } from '@superset-ui/core/components';
|
|||||||
import { getChartDataRequest } from 'src/components/Chart/chartAction';
|
import { getChartDataRequest } from 'src/components/Chart/chartAction';
|
||||||
import { ResultsPaneProps, QueryResultInterface } from '../types';
|
import { ResultsPaneProps, QueryResultInterface } from '../types';
|
||||||
import { SingleQueryResultPane } from './SingleQueryResultPane';
|
import { SingleQueryResultPane } from './SingleQueryResultPane';
|
||||||
import { TableControls } from './DataTableControls';
|
import { TableControls, ROW_LIMIT_OPTIONS } from './DataTableControls';
|
||||||
|
|
||||||
const Error = styled.pre`
|
const Error = styled.pre`
|
||||||
margin-top: ${({ theme }) => `${theme.sizeUnit * 4}px`};
|
margin-top: ${({ theme }) => `${theme.sizeUnit * 4}px`};
|
||||||
@@ -53,7 +53,6 @@ export const useResultsPane = ({
|
|||||||
errorMessage,
|
errorMessage,
|
||||||
setForceQuery,
|
setForceQuery,
|
||||||
isVisible,
|
isVisible,
|
||||||
dataSize = 50,
|
|
||||||
canDownload,
|
canDownload,
|
||||||
columnDisplayNames,
|
columnDisplayNames,
|
||||||
}: ResultsPaneProps): ReactElement[] => {
|
}: ResultsPaneProps): ReactElement[] => {
|
||||||
@@ -61,6 +60,8 @@ export const useResultsPane = ({
|
|||||||
queryFormData?.viz_type || queryFormData?.vizType,
|
queryFormData?.viz_type || queryFormData?.vizType,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const chartRowLimit = Number(queryFormData?.row_limit) || 10000;
|
||||||
|
const [rowLimit, setRowLimit] = useState(1000);
|
||||||
const [resultResp, setResultResp] = useState<QueryResultInterface[]>([]);
|
const [resultResp, setResultResp] = useState<QueryResultInterface[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [responseError, setResponseError] = useState<string>('');
|
const [responseError, setResponseError] = useState<string>('');
|
||||||
@@ -69,12 +70,28 @@ export const useResultsPane = ({
|
|||||||
|
|
||||||
const noOpInputChange = useCallback(() => {}, []);
|
const noOpInputChange = useCallback(() => {}, []);
|
||||||
|
|
||||||
|
// Never exceed the chart's own row_limit
|
||||||
|
const effectiveRowLimit = Math.min(rowLimit, chartRowLimit);
|
||||||
|
|
||||||
|
const cappedFormData = useMemo(
|
||||||
|
() => ({ ...queryFormData, row_limit: effectiveRowLimit }),
|
||||||
|
[queryFormData, effectiveRowLimit],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRowLimitChange = useCallback(
|
||||||
|
(limit: number) => {
|
||||||
|
setRowLimit(limit);
|
||||||
|
cache.delete(cappedFormData);
|
||||||
|
},
|
||||||
|
[cappedFormData],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// it's an invalid formData when gets a errorMessage
|
// it's an invalid formData when gets a errorMessage
|
||||||
if (errorMessage) return;
|
if (errorMessage) return;
|
||||||
if (isRequest && cache.has(queryFormData)) {
|
if (isRequest && cache.has(cappedFormData)) {
|
||||||
setResultResp(
|
setResultResp(
|
||||||
ensureIsArray(cache.get(queryFormData)) as QueryResultInterface[],
|
ensureIsArray(cache.get(cappedFormData)) as QueryResultInterface[],
|
||||||
);
|
);
|
||||||
setResponseError('');
|
setResponseError('');
|
||||||
if (queryForce) {
|
if (queryForce) {
|
||||||
@@ -82,10 +99,10 @@ export const useResultsPane = ({
|
|||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
if (isRequest && !cache.has(queryFormData)) {
|
if (isRequest && !cache.has(cappedFormData)) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
getChartDataRequest({
|
getChartDataRequest({
|
||||||
formData: queryFormData,
|
formData: cappedFormData,
|
||||||
force: queryForce,
|
force: queryForce,
|
||||||
resultFormat: 'json',
|
resultFormat: 'json',
|
||||||
resultType: 'results',
|
resultType: 'results',
|
||||||
@@ -94,7 +111,7 @@ export const useResultsPane = ({
|
|||||||
.then(({ json }) => {
|
.then(({ json }) => {
|
||||||
setResultResp(ensureIsArray(json.result) as QueryResultInterface[]);
|
setResultResp(ensureIsArray(json.result) as QueryResultInterface[]);
|
||||||
setResponseError('');
|
setResponseError('');
|
||||||
cache.set(queryFormData, json.result);
|
cache.set(cappedFormData, json.result);
|
||||||
if (queryForce) {
|
if (queryForce) {
|
||||||
setForceQuery?.(false);
|
setForceQuery?.(false);
|
||||||
}
|
}
|
||||||
@@ -108,7 +125,7 @@ export const useResultsPane = ({
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [queryFormData, isRequest]);
|
}, [cappedFormData, isRequest]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (errorMessage) {
|
if (errorMessage) {
|
||||||
@@ -163,11 +180,13 @@ export const useResultsPane = ({
|
|||||||
colnames={result.colnames}
|
colnames={result.colnames}
|
||||||
coltypes={result.coltypes}
|
coltypes={result.coltypes}
|
||||||
rowcount={result.rowcount}
|
rowcount={result.rowcount}
|
||||||
dataSize={dataSize}
|
|
||||||
datasourceId={queryFormData.datasource}
|
datasourceId={queryFormData.datasource}
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
canDownload={canDownload}
|
canDownload={canDownload}
|
||||||
columnDisplayNames={columnDisplayNames}
|
columnDisplayNames={columnDisplayNames}
|
||||||
|
rowLimit={rowLimit}
|
||||||
|
rowLimitOptions={ROW_LIMIT_OPTIONS}
|
||||||
|
onRowLimitChange={handleRowLimitChange}
|
||||||
/>
|
/>
|
||||||
</StyledDiv>
|
</StyledDiv>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -19,16 +19,16 @@
|
|||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { FeatureFlag } from '@superset-ui/core';
|
import { FeatureFlag } from '@superset-ui/core';
|
||||||
import * as copyUtils from 'src/utils/copy';
|
import * as copyUtils from 'src/utils/copy';
|
||||||
import {
|
import { render, screen, userEvent } from 'spec/helpers/testing-library';
|
||||||
render,
|
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||||
screen,
|
|
||||||
userEvent,
|
|
||||||
waitForElementToBeRemoved,
|
|
||||||
} from 'spec/helpers/testing-library';
|
|
||||||
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
|
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
|
||||||
import { DataTablesPane } from '..';
|
import { DataTablesPane } from '..';
|
||||||
import { createDataTablesPaneProps } from './fixture';
|
import { createDataTablesPaneProps } from './fixture';
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
setupAGGridModules();
|
||||||
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('DataTablesPane', () => {
|
describe('DataTablesPane', () => {
|
||||||
// Collapsed/expanded state depends on local storage
|
// Collapsed/expanded state depends on local storage
|
||||||
@@ -175,12 +175,6 @@ describe('DataTablesPane', () => {
|
|||||||
|
|
||||||
expect(screen.getByText('Action')).toBeVisible();
|
expect(screen.getByText('Action')).toBeVisible();
|
||||||
expect(screen.getByText('Horror')).toBeVisible();
|
expect(screen.getByText('Horror')).toBeVisible();
|
||||||
|
|
||||||
userEvent.type(screen.getByPlaceholderText('Search'), 'hor');
|
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() => screen.queryByText('Action'));
|
|
||||||
expect(screen.getByText('Horror')).toBeVisible();
|
|
||||||
expect(screen.queryByText('Action')).not.toBeInTheDocument();
|
|
||||||
fetchMock.clearHistory().removeRoutes();
|
fetchMock.clearHistory().removeRoutes();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,14 +20,18 @@ import fetchMock from 'fetch-mock';
|
|||||||
import {
|
import {
|
||||||
screen,
|
screen,
|
||||||
render,
|
render,
|
||||||
userEvent,
|
|
||||||
waitForElementToBeRemoved,
|
waitForElementToBeRemoved,
|
||||||
waitFor,
|
waitFor,
|
||||||
} from 'spec/helpers/testing-library';
|
} from 'spec/helpers/testing-library';
|
||||||
import { ChartMetadata, ChartPlugin, VizType } from '@superset-ui/core';
|
import { ChartMetadata, ChartPlugin, VizType } from '@superset-ui/core';
|
||||||
|
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||||
import { ResultsPaneOnDashboard } from '../components';
|
import { ResultsPaneOnDashboard } from '../components';
|
||||||
import { createResultsPaneOnDashboardProps } from './fixture';
|
import { createResultsPaneOnDashboardProps } from './fixture';
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
setupAGGridModules();
|
||||||
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('ResultsPaneOnDashboard', () => {
|
describe('ResultsPaneOnDashboard', () => {
|
||||||
// render and render errorMessage
|
// render and render errorMessage
|
||||||
@@ -126,12 +130,12 @@ describe('ResultsPaneOnDashboard', () => {
|
|||||||
expect(await findByText('Bad request')).toBeVisible();
|
expect(await findByText('Bad request')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('force query, render and search', async () => {
|
test('force query, render', async () => {
|
||||||
const props = createResultsPaneOnDashboardProps({
|
const props = createResultsPaneOnDashboardProps({
|
||||||
sliceId: 144,
|
sliceId: 144,
|
||||||
queryForce: true,
|
queryForce: true,
|
||||||
});
|
});
|
||||||
const { queryByText, getByPlaceholderText } = render(
|
const { queryByText } = render(
|
||||||
<ResultsPaneOnDashboard {...props} setForceQuery={setForceQuery} />,
|
<ResultsPaneOnDashboard {...props} setForceQuery={setForceQuery} />,
|
||||||
{
|
{
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
@@ -144,11 +148,6 @@ describe('ResultsPaneOnDashboard', () => {
|
|||||||
expect(queryByText('2 rows')).toBeVisible();
|
expect(queryByText('2 rows')).toBeVisible();
|
||||||
expect(queryByText('Action')).toBeVisible();
|
expect(queryByText('Action')).toBeVisible();
|
||||||
expect(queryByText('Horror')).toBeVisible();
|
expect(queryByText('Horror')).toBeVisible();
|
||||||
|
|
||||||
userEvent.type(getByPlaceholderText('Search'), 'hor');
|
|
||||||
await waitForElementToBeRemoved(() => queryByText('Action'));
|
|
||||||
expect(queryByText('Horror')).toBeVisible();
|
|
||||||
expect(queryByText('Action')).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('multiple results pane', async () => {
|
test('multiple results pane', async () => {
|
||||||
|
|||||||
@@ -17,19 +17,19 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import {
|
import { render, waitFor } from 'spec/helpers/testing-library';
|
||||||
render,
|
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||||
userEvent,
|
|
||||||
waitForElementToBeRemoved,
|
|
||||||
waitFor,
|
|
||||||
} from 'spec/helpers/testing-library';
|
|
||||||
import { SamplesPane } from '../components';
|
import { SamplesPane } from '../components';
|
||||||
import { createSamplesPaneProps } from './fixture';
|
import { createSamplesPaneProps } from './fixture';
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
setupAGGridModules();
|
||||||
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('SamplesPane', () => {
|
describe('SamplesPane', () => {
|
||||||
fetchMock.post(
|
fetchMock.post(
|
||||||
'end:/datasource/samples?force=false&datasource_type=table&datasource_id=34',
|
'end:/datasource/samples?force=false&datasource_type=table&datasource_id=34&per_page=100&page=1',
|
||||||
{
|
{
|
||||||
result: {
|
result: {
|
||||||
data: [],
|
data: [],
|
||||||
@@ -40,7 +40,7 @@ describe('SamplesPane', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
fetchMock.post(
|
fetchMock.post(
|
||||||
'end:/datasource/samples?force=true&datasource_type=table&datasource_id=35',
|
'end:/datasource/samples?force=true&datasource_type=table&datasource_id=35&per_page=100&page=1',
|
||||||
{
|
{
|
||||||
result: {
|
result: {
|
||||||
data: [
|
data: [
|
||||||
@@ -56,7 +56,7 @@ describe('SamplesPane', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
fetchMock.post(
|
fetchMock.post(
|
||||||
'end:/datasource/samples?force=false&datasource_type=table&datasource_id=36',
|
'end:/datasource/samples?force=false&datasource_type=table&datasource_id=36&per_page=100&page=1',
|
||||||
400,
|
400,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -91,12 +91,12 @@ describe('SamplesPane', () => {
|
|||||||
expect(await findByText('Error: Bad request')).toBeVisible();
|
expect(await findByText('Error: Bad request')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('force query, render and search', async () => {
|
test('force query, render', async () => {
|
||||||
const props = createSamplesPaneProps({
|
const props = createSamplesPaneProps({
|
||||||
datasourceId: 35,
|
datasourceId: 35,
|
||||||
queryForce: true,
|
queryForce: true,
|
||||||
});
|
});
|
||||||
const { queryByText, getByPlaceholderText } = render(
|
const { queryByText } = render(
|
||||||
<SamplesPane {...props} setForceQuery={setForceQuery} />,
|
<SamplesPane {...props} setForceQuery={setForceQuery} />,
|
||||||
{
|
{
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
@@ -109,10 +109,5 @@ describe('SamplesPane', () => {
|
|||||||
expect(queryByText('2 rows')).toBeVisible();
|
expect(queryByText('2 rows')).toBeVisible();
|
||||||
expect(queryByText('Action')).toBeVisible();
|
expect(queryByText('Action')).toBeVisible();
|
||||||
expect(queryByText('Horror')).toBeVisible();
|
expect(queryByText('Horror')).toBeVisible();
|
||||||
|
|
||||||
userEvent.type(getByPlaceholderText('Search'), 'hor');
|
|
||||||
await waitForElementToBeRemoved(() => queryByText('Action'));
|
|
||||||
expect(queryByText('Horror')).toBeVisible();
|
|
||||||
expect(queryByText('Action')).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -90,6 +90,10 @@ export const createSamplesPaneProps = ({
|
|||||||
({
|
({
|
||||||
isRequest,
|
isRequest,
|
||||||
datasource: { ...datasource, id: datasourceId },
|
datasource: { ...datasource, id: datasourceId },
|
||||||
|
queryFormData: {
|
||||||
|
...queryFormData,
|
||||||
|
datasource: `${datasourceId}__table`,
|
||||||
|
},
|
||||||
queryForce,
|
queryForce,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
setForceQuery: jest.fn(),
|
setForceQuery: jest.fn(),
|
||||||
|
|||||||
@@ -56,10 +56,9 @@ export interface ResultsPaneProps {
|
|||||||
export interface SamplesPaneProps {
|
export interface SamplesPaneProps {
|
||||||
isRequest: boolean;
|
isRequest: boolean;
|
||||||
datasource: Datasource;
|
datasource: Datasource;
|
||||||
|
queryFormData: LatestQueryFormData;
|
||||||
queryForce: boolean;
|
queryForce: boolean;
|
||||||
setForceQuery?: SetForceQueryAction;
|
setForceQuery?: SetForceQueryAction;
|
||||||
dataSize?: number;
|
|
||||||
// reload OriginalFormattedTimeColumns from localStorage when isVisible is true
|
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
canDownload: boolean;
|
canDownload: boolean;
|
||||||
}
|
}
|
||||||
@@ -74,6 +73,9 @@ export interface TableControlsProps {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
rowcount: number;
|
rowcount: number;
|
||||||
canDownload: boolean;
|
canDownload: boolean;
|
||||||
|
rowLimit?: number;
|
||||||
|
rowLimitOptions?: { value: number; label: string }[];
|
||||||
|
onRowLimitChange?: (limit: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryResultInterface {
|
export interface QueryResultInterface {
|
||||||
@@ -86,11 +88,11 @@ export interface QueryResultInterface {
|
|||||||
export interface SingleQueryResultPaneProp extends QueryResultInterface {
|
export interface SingleQueryResultPaneProp extends QueryResultInterface {
|
||||||
// {datasource.id}__{datasource.type}, eg: 1__table
|
// {datasource.id}__{datasource.type}, eg: 1__table
|
||||||
datasourceId?: string;
|
datasourceId?: string;
|
||||||
dataSize?: number;
|
|
||||||
// reload OriginalFormattedTimeColumns from localStorage when isVisible is true
|
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
canDownload: boolean;
|
canDownload: boolean;
|
||||||
// Optional map of column/metric name -> verbose label
|
// Optional map of column/metric name -> verbose label
|
||||||
columnDisplayNames?: Record<string, string>;
|
columnDisplayNames?: Record<string, string>;
|
||||||
isPaginationSticky?: boolean;
|
rowLimit?: number;
|
||||||
|
rowLimitOptions?: { value: number; label: string }[];
|
||||||
|
onRowLimitChange?: (limit: number) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class SamplesRequestSchema(Schema):
|
|||||||
force = fields.Boolean(load_default=False)
|
force = fields.Boolean(load_default=False)
|
||||||
page = fields.Integer(load_default=1)
|
page = fields.Integer(load_default=1)
|
||||||
per_page = fields.Integer(
|
per_page = fields.Integer(
|
||||||
validate=validate.Range(min=1, max=1000),
|
validate=validate.Range(min=1, max=10000),
|
||||||
load_default=None,
|
load_default=None,
|
||||||
)
|
)
|
||||||
dashboard_id = fields.Integer(required=False, allow_none=True, load_default=None)
|
dashboard_id = fields.Integer(required=False, allow_none=True, load_default=None)
|
||||||
|
|||||||
@@ -798,7 +798,7 @@ def test_get_samples_pagination(test_client, login_as_admin, virtual_dataset):
|
|||||||
assert rv.json["result"]["total_count"] == 10
|
assert rv.json["result"]["total_count"] == 10
|
||||||
|
|
||||||
# 2. incorrect per_page
|
# 2. incorrect per_page
|
||||||
per_pages = (current_app.config["SAMPLES_ROW_LIMIT"] + 1, 0, "xx")
|
per_pages = (10001, 0, "xx")
|
||||||
for per_page in per_pages:
|
for per_page in per_pages:
|
||||||
uri = f"/datasource/samples?datasource_id={virtual_dataset.id}&datasource_type=table&per_page={per_page}" # noqa: E501
|
uri = f"/datasource/samples?datasource_id={virtual_dataset.id}&datasource_type=table&per_page={per_page}" # noqa: E501
|
||||||
rv = test_client.post(uri, json={})
|
rv = test_client.post(uri, json={})
|
||||||
|
|||||||
Reference in New Issue
Block a user