chore(explore): make metric/column search input clearable (#16320)

* chore(explore): make metric/column search input clearable

* Fix typo

* Fix test
This commit is contained in:
Kamil Gabryjelski 2021-08-18 12:23:23 +02:00 committed by GitHub
parent 5f060a2227
commit 2c595b09ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 211 additions and 170 deletions

View File

@ -116,7 +116,7 @@ test('should render 0 search results', async () => {
const searchInput = screen.getByPlaceholderText('Search Metrics & Columns');
search('nothing', searchInput);
expect(await screen.findByText('Showing 0 of 0')).toBeInTheDocument();
expect(await screen.findAllByText('Showing 0 of 0')).toHaveLength(2);
});
test('should search and render matching columns', async () => {

View File

@ -16,12 +16,19 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { styled, t } from '@superset-ui/core';
import Collapse from 'src/components/Collapse';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { ControlConfig, DatasourceMeta } from '@superset-ui/chart-controls';
import { debounce } from 'lodash';
import { matchSorter, rankings } from 'match-sorter';
import { css, styled, t } from '@superset-ui/core';
import Collapse from 'src/components/Collapse';
import { Input } from 'src/common/components';
import { FAST_DEBOUNCE } from 'src/constants';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { ExploreActions } from 'src/explore/actions/exploreActions';
@ -55,36 +62,39 @@ const ButtonContainer = styled.div`
`;
const DatasourceContainer = styled.div`
background-color: ${({ theme }) => theme.colors.grayscale.light4};
position: relative;
height: 100%;
display: flex;
flex-direction: column;
max-height: 100%;
.ant-collapse {
height: auto;
}
.field-selections {
padding: ${({ theme }) => `0 0 ${4 * theme.gridUnit}px`};
overflow: auto;
}
.field-length {
margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
font-size: ${({ theme }) => theme.typography.sizes.s}px;
color: ${({ theme }) => theme.colors.grayscale.light1};
}
.form-control.input-md {
width: calc(100% - ${({ theme }) => theme.gridUnit * 4}px);
margin: ${({ theme }) => theme.gridUnit * 2}px auto;
}
.type-label {
font-weight: ${({ theme }) => theme.typography.weights.light};
font-size: ${({ theme }) => theme.typography.sizes.s}px;
color: ${({ theme }) => theme.colors.grayscale.base};
}
.Control {
padding-bottom: 0;
}
${({ theme }) => css`
background-color: ${theme.colors.grayscale.light4};
position: relative;
height: 100%;
display: flex;
flex-direction: column;
max-height: 100%;
.ant-collapse {
height: auto;
}
.field-selections {
padding: 0 0 ${4 * theme.gridUnit}px;
overflow: auto;
}
.field-length {
margin-bottom: ${theme.gridUnit * 2}px;
font-size: ${theme.typography.sizes.s}px;
color: ${theme.colors.grayscale.light1};
}
.form-control.input-md {
width: calc(100% - ${theme.gridUnit * 4}px);
height: ${theme.gridUnit * 8}px;
margin: ${theme.gridUnit * 2}px auto;
}
.type-label {
font-weight: ${theme.typography.weights.light};
font-size: ${theme.typography.sizes.s}px;
color: ${theme.colors.grayscale.base};
}
.Control {
padding-bottom: 0;
}
`};
`;
const LabelWrapper = styled.div`
@ -183,59 +193,62 @@ export default function DataSourcePanel({
const DEFAULT_MAX_COLUMNS_LENGTH = 50;
const DEFAULT_MAX_METRICS_LENGTH = 50;
const search = 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) - Number(a.item.is_certified) ||
String(a.rankedValue).localeCompare(b.rankedValue),
}),
});
}, FAST_DEBOUNCE);
const search = useCallback(
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) - Number(a.item.is_certified) ||
String(a.rankedValue).localeCompare(b.rankedValue),
}),
});
}, FAST_DEBOUNCE),
[columns, metrics],
);
useEffect(() => {
setList({
@ -245,93 +258,121 @@ export default function DataSourcePanel({
setInputValue('');
}, [columns, datasource, metrics]);
const metricSlice = showAllMetrics
? lists.metrics
: lists.metrics.slice(0, DEFAULT_MAX_COLUMNS_LENGTH);
const columnSlice = showAllColumns
? lists.columns
: lists.columns.slice(0, DEFAULT_MAX_METRICS_LENGTH);
const metricSlice = useMemo(
() =>
showAllMetrics
? lists.metrics
: lists.metrics.slice(0, DEFAULT_MAX_METRICS_LENGTH),
[lists.metrics, showAllMetrics],
);
const columnSlice = useMemo(
() =>
showAllColumns
? lists.columns
: lists.columns.slice(0, DEFAULT_MAX_COLUMNS_LENGTH),
[lists.columns, showAllColumns],
);
const mainBody = (
<>
<input
type="text"
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">
<Collapse
bordered
defaultActiveKey={['metrics', 'column']}
expandIconPosition="right"
ghost
>
<Collapse.Panel
header={<span className="header">{t('Metrics')}</span>}
key="metrics"
const mainBody = useMemo(
() => (
<>
<Input
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">
<Collapse
bordered
defaultActiveKey={['metrics', 'column']}
expandIconPosition="right"
ghost
>
<div className="field-length">
{t(`Showing %s of %s`, metricSlice.length, lists.metrics.length)}
</div>
{metricSlice.map(m => (
<LabelContainer key={m.metric_name} className="column">
{enableExploreDnd ? (
<DatasourcePanelDragOption
value={m}
type={DndItemType.Metric}
/>
) : (
<StyledMetricOption metric={m} showType />
<Collapse.Panel
header={<span className="header">{t('Metrics')}</span>}
key="metrics"
>
<div className="field-length">
{t(
`Showing %s of %s`,
metricSlice.length,
lists.metrics.length,
)}
</LabelContainer>
))}
{lists.metrics.length > DEFAULT_MAX_METRICS_LENGTH ? (
<ButtonContainer>
<Button onClick={() => setShowAllMetrics(!showAllMetrics)}>
{showAllMetrics ? t('Show less...') : t('Show all...')}
</Button>
</ButtonContainer>
) : (
<></>
)}
</Collapse.Panel>
<Collapse.Panel
header={<span className="header">{t('Columns')}</span>}
key="column"
>
<div className="field-length">
{t(`Showing %s of %s`, columnSlice.length, lists.columns.length)}
</div>
{columnSlice.map(col => (
<LabelContainer key={col.column_name} className="column">
{enableExploreDnd ? (
<DatasourcePanelDragOption
value={col}
type={DndItemType.Column}
/>
) : (
<StyledColumnOption column={col} showType />
</div>
{metricSlice.map(m => (
<LabelContainer key={m.metric_name} className="column">
{enableExploreDnd ? (
<DatasourcePanelDragOption
value={m}
type={DndItemType.Metric}
/>
) : (
<StyledMetricOption metric={m} showType />
)}
</LabelContainer>
))}
{lists.metrics.length > DEFAULT_MAX_METRICS_LENGTH ? (
<ButtonContainer>
<Button onClick={() => setShowAllMetrics(!showAllMetrics)}>
{showAllMetrics ? t('Show less...') : t('Show all...')}
</Button>
</ButtonContainer>
) : (
<></>
)}
</Collapse.Panel>
<Collapse.Panel
header={<span className="header">{t('Columns')}</span>}
key="column"
>
<div className="field-length">
{t(
`Showing %s of %s`,
columnSlice.length,
lists.columns.length,
)}
</LabelContainer>
))}
{lists.columns.length > DEFAULT_MAX_COLUMNS_LENGTH ? (
<ButtonContainer>
<Button onClick={() => setShowAllColumns(!showAllColumns)}>
{showAllColumns ? t('Show Less...') : t('Show all...')}
</Button>
</ButtonContainer>
) : (
<></>
)}
</Collapse.Panel>
</Collapse>
</div>
</>
</div>
{columnSlice.map(col => (
<LabelContainer key={col.column_name} className="column">
{enableExploreDnd ? (
<DatasourcePanelDragOption
value={col}
type={DndItemType.Column}
/>
) : (
<StyledColumnOption column={col} showType />
)}
</LabelContainer>
))}
{lists.columns.length > DEFAULT_MAX_COLUMNS_LENGTH ? (
<ButtonContainer>
<Button onClick={() => setShowAllColumns(!showAllColumns)}>
{showAllColumns ? t('Show Less...') : t('Show all...')}
</Button>
</ButtonContainer>
) : (
<></>
)}
</Collapse.Panel>
</Collapse>
</div>
</>
),
[
columnSlice,
inputValue,
lists.columns.length,
lists.metrics.length,
metricSlice,
search,
showAllColumns,
showAllMetrics,
],
);
return (