Skip to content

Commit

Permalink
chore(explore): Hide ineligible metric and column list
Browse files Browse the repository at this point in the history
  • Loading branch information
justinpark committed Mar 27, 2024
1 parent 38eecfc commit 92ea7fe
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/
import React from 'react';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import { render, screen, waitFor, within } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import DatasourcePanel, {
IDatasource,
Expand All @@ -29,6 +29,8 @@ import {
} from 'src/explore/components/DatasourcePanel/fixtures';
import { DatasourceType } from '@superset-ui/core';
import DatasourceControl from 'src/explore/components/controls/DatasourceControl';
import ExploreContainer from '../ExploreContainer';
import { DndColumnSelect } from '../controls/DndColumnSelectControl';

jest.mock(
'react-virtualized-auto-sizer',
Expand Down Expand Up @@ -211,3 +213,48 @@ test('should not render a save dataset modal when datasource is not query or dat

expect(screen.queryByText(/create a dataset/i)).toBe(null);
});

test('should render only eligible metrics and columns when disallow_adhoc_metrics is set', async () => {
const newProps = {
...props,
datasource: {
...datasource,
extra: JSON.stringify({ disallow_adhoc_metrics: true }),
},
};
const column1FilterProps = {
type: 'DndColumnSelect' as const,
name: 'Filter',
onChange: jest.fn(),
options: [{ column_name: columns[1].column_name }],
actions: { setControlValue: jest.fn() },
};
const column2FilterProps = {
type: 'DndColumnSelect' as const,
name: 'Filter',
onChange: jest.fn(),
options: [
{ column_name: columns[1].column_name },
{ column_name: columns[2].column_name },
],
actions: { setControlValue: jest.fn() },
};
const { getByTestId } = render(
<ExploreContainer>
<DatasourcePanel {...newProps} />
<DndColumnSelect {...column1FilterProps} />
<DndColumnSelect {...column2FilterProps} />
</ExploreContainer>,
{ useRedux: true, useDnd: true },
);
const selections = getByTestId('fieldSelections');
expect(
within(selections).queryByText(columns[0].column_name),
).not.toBeInTheDocument();
expect(
within(selections).queryByText(columns[1].column_name),
).toBeInTheDocument();
expect(
within(selections).queryByText(columns[2].column_name),
).toBeInTheDocument();
});
205 changes: 117 additions & 88 deletions superset-frontend/src/explore/components/DatasourcePanel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useMemo, useState } from 'react';
import React, { useContext, useMemo, useState } from 'react';
import {
css,
DatasourceType,
Expand All @@ -30,7 +30,7 @@ import { ControlConfig } from '@superset-ui/chart-controls';
import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList as List } from 'react-window';

import { debounce, isArray } from 'lodash';
import { isArray } from 'lodash';
import { matchSorter, rankings } from 'match-sorter';
import Alert from 'src/components/Alert';
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
Expand All @@ -39,12 +39,16 @@ import { Input } from 'src/components/Input';
import { FAST_DEBOUNCE } from 'src/constants';
import { ExploreActions } from 'src/explore/actions/exploreActions';
import Control from 'src/explore/components/Control';
import { useDebounceValue } from 'src/hooks/useDebounceValue';
import DatasourcePanelItem, {
ITEM_HEIGHT,
DataSourcePanelColumn,
DEFAULT_MAX_COLUMNS_LENGTH,
DEFAULT_MAX_METRICS_LENGTH,
} from './DatasourcePanelItem';
import { DndItemType } from '../DndItemType';
import { DndItemValue } from './types';
import { DropzoneContext } from '../ExploreContainer';

interface DatasourceControl extends ControlConfig {
datasource?: IDatasource;
Expand All @@ -61,6 +65,7 @@ export interface IDatasource {
datasource_name?: string | null;
name?: string | null;
schema?: string | null;
extra?: string | null;
}

export interface Props {
Expand Down Expand Up @@ -122,18 +127,48 @@ const StyledInfoboxWrapper = styled.div`

const BORDER_WIDTH = 2;

const sortCertifiedFirst = (slice: DataSourcePanelColumn[]) =>
slice.sort((a, b) => (b?.is_certified ?? 0) - (a?.is_certified ?? 0));

export default function DataSourcePanel({
datasource,
formData,
controls: { datasource: datasourceControl },
actions,
width,
}: Props) {
const [dropzones] = useContext(DropzoneContext);
const { columns: _columns, metrics } = datasource;
const extra = useMemo<{ disallow_adhoc_metrics?: boolean }>(() => {
let extra = {};
if (datasource?.extra) {
try {
extra = JSON.parse(datasource.extra);
} catch {} // eslint-disable-line no-empty
}
return extra;
}, [datasource.extra]);
const allowlistOnly = extra.disallow_adhoc_metrics;

const allowedColumns = useMemo(() => {
const validators = Object.values(dropzones);
if (!isArray(_columns)) return [];
return allowlistOnly
? _columns.filter(column =>
validators.some(validator =>
validator({
value: column as DndItemValue,
type: DndItemType.Column,
}),
),
)
: _columns;
}, [dropzones, _columns, allowlistOnly]);

// display temporal column first
const columns = useMemo(
() =>
[...(isArray(_columns) ? _columns : [])].sort((col1, col2) => {
[...allowedColumns].sort((col1, col2) => {
if (col1?.is_dttm && !col2?.is_dttm) {
return -1;
}
Expand All @@ -142,106 +177,102 @@ export default function DataSourcePanel({
}
return 0;
}),
[_columns],
[allowedColumns],
);

const allowedMetrics = useMemo(() => {
const validators = Object.values(dropzones);
return allowlistOnly
? metrics.filter(metric =>
validators.some(validator =>
validator({ value: metric, type: DndItemType.Metric }),
),
)
: metrics;
}, [dropzones, metrics, allowlistOnly]);

const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false);
const [inputValue, setInputValue] = useState('');
const [lists, setList] = useState({
columns,
metrics,
});
const [showAllMetrics, setShowAllMetrics] = useState(false);
const [showAllColumns, setShowAllColumns] = useState(false);
const [collapseMetrics, setCollapseMetrics] = useState(false);
const [collapseColumns, setCollapseColumns] = useState(false);
const searchKeyword = useDebounceValue(inputValue, FAST_DEBOUNCE);

const search = useMemo(
() =>
debounce((value: string) => {
if (value === '') {
setList({ columns, metrics });
return;
}
setList({
columns: matchSorter(columns, value, {
keys: [
{
key: 'verbose_name',
threshold: rankings.CONTAINS,
},
{
key: 'column_name',
threshold: rankings.CONTAINS,
},
{
key: item =>
[item?.description ?? '', item?.expression ?? ''].map(
x => x?.replace(/[_\n\s]+/g, ' ') || '',
),
threshold: rankings.CONTAINS,
maxRanking: rankings.CONTAINS,
},
],
keepDiacritics: true,
}),
metrics: matchSorter(metrics, value, {
keys: [
{
key: 'verbose_name',
threshold: rankings.CONTAINS,
},
{
key: 'metric_name',
threshold: rankings.CONTAINS,
},
{
key: item =>
[item?.description ?? '', item?.expression ?? ''].map(
x => x?.replace(/[_\n\s]+/g, ' ') || '',
),
threshold: rankings.CONTAINS,
maxRanking: rankings.CONTAINS,
},
],
keepDiacritics: true,
baseSort: (a, b) =>
Number(b?.item?.is_certified ?? 0) -
Number(a?.item?.is_certified ?? 0) ||
String(a?.rankedValue ?? '').localeCompare(b?.rankedValue ?? ''),
}),
});
}, FAST_DEBOUNCE),
[columns, metrics],
);

useEffect(() => {
setList({
columns,
metrics,
const filteredColumns = useMemo(() => {
if (!searchKeyword) {
return columns ?? [];
}
return matchSorter(columns, searchKeyword, {
keys: [
{
key: 'verbose_name',
threshold: rankings.CONTAINS,
},
{
key: 'column_name',
threshold: rankings.CONTAINS,
},
{
key: item =>
[item?.description ?? '', item?.expression ?? ''].map(
x => x?.replace(/[_\n\s]+/g, ' ') || '',
),
threshold: rankings.CONTAINS,
maxRanking: rankings.CONTAINS,
},
],
keepDiacritics: true,
});
setInputValue('');
}, [columns, datasource, metrics]);
}, [columns, searchKeyword]);

const sortCertifiedFirst = (slice: DataSourcePanelColumn[]) =>
slice.sort((a, b) => (b?.is_certified ?? 0) - (a?.is_certified ?? 0));
const filteredMetrics = useMemo(() => {
if (!searchKeyword) {
return allowedMetrics ?? [];
}
return matchSorter(allowedMetrics, searchKeyword, {
keys: [
{
key: 'verbose_name',
threshold: rankings.CONTAINS,
},
{
key: 'metric_name',
threshold: rankings.CONTAINS,
},
{
key: item =>
[item?.description ?? '', item?.expression ?? ''].map(
x => x?.replace(/[_\n\s]+/g, ' ') || '',
),
threshold: rankings.CONTAINS,
maxRanking: rankings.CONTAINS,
},
],
keepDiacritics: true,
baseSort: (a, b) =>
Number(b?.item?.is_certified ?? 0) -
Number(a?.item?.is_certified ?? 0) ||
String(a?.rankedValue ?? '').localeCompare(b?.rankedValue ?? ''),
});
}, [allowedMetrics, searchKeyword]);

const metricSlice = useMemo(
() =>
showAllMetrics
? lists?.metrics
: lists?.metrics?.slice?.(0, DEFAULT_MAX_METRICS_LENGTH),
[lists?.metrics, showAllMetrics],
? filteredMetrics
: filteredMetrics?.slice?.(0, DEFAULT_MAX_METRICS_LENGTH),
[filteredMetrics, showAllMetrics],
);

const columnSlice = useMemo(
() =>
showAllColumns
? sortCertifiedFirst(lists?.columns)
? sortCertifiedFirst(filteredColumns)
: sortCertifiedFirst(
lists?.columns?.slice?.(0, DEFAULT_MAX_COLUMNS_LENGTH),
filteredColumns?.slice?.(0, DEFAULT_MAX_COLUMNS_LENGTH),
),
[lists.columns, showAllColumns],
[filteredColumns, showAllColumns],
);

const showInfoboxCheck = () => {
Expand All @@ -268,13 +299,12 @@ export default function DataSourcePanel({
allowClear
onChange={evt => {
setInputValue(evt.target.value);
search(evt.target.value);
}}
value={inputValue}
className="form-control input-md"
placeholder={t('Search Metrics & Columns')}
/>
<div className="field-selections">
<div className="field-selections" data-test="fieldSelections">
{datasourceIsSaveable && showInfoboxCheck() && (
<StyledInfoboxWrapper>
<Alert
Expand Down Expand Up @@ -321,8 +351,8 @@ export default function DataSourcePanel({
metricSlice,
columnSlice,
width,
totalMetrics: lists?.metrics.length,
totalColumns: lists?.columns.length,
totalMetrics: filteredMetrics.length,
totalColumns: filteredColumns.length,
showAllMetrics,
onShowAllMetricsChange: setShowAllMetrics,
showAllColumns,
Expand All @@ -345,10 +375,9 @@ export default function DataSourcePanel({
[
columnSlice,
inputValue,
lists.columns.length,
lists?.metrics?.length,
filteredColumns.length,
filteredMetrics.length,
metricSlice,
search,
showAllColumns,
showAllMetrics,
collapseMetrics,
Expand Down
Loading

0 comments on commit 92ea7fe

Please sign in to comment.