You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by vi...@apache.org on 2021/05/24 12:23:03 UTC
[superset] branch master updated: chore: Improves the native
filters UI/UX - iteration 2 (#14753)
This is an automated email from the ASF dual-hosted git repository.
villebro pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new bee6f3b chore: Improves the native filters UI/UX - iteration 2 (#14753)
bee6f3b is described below
commit bee6f3ba8a3dd13ddd624723d30ca40e905af6d7
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Mon May 24 09:22:05 2021 -0300
chore: Improves the native filters UI/UX - iteration 2 (#14753)
---
.../nativeFilters/FilterBar/FilterBar.test.tsx | 8 +-
.../FiltersConfigModal/FilterTabs.tsx | 71 +++--
.../FiltersConfigForm/FiltersConfigForm.tsx | 348 ++++++++++++---------
.../FiltersConfigModal/FiltersConfigModal.tsx | 13 +-
4 files changed, 271 insertions(+), 169 deletions(-)
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx
index 4507e38..3cfb259 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx
@@ -90,6 +90,7 @@ const addFilterSetFlow = async () => {
// check description
expect(screen.getByText('Filters (1)')).toBeInTheDocument();
expect(screen.getByText(FILTER_NAME)).toBeInTheDocument();
+
expect(screen.getAllByText('Last week').length).toBe(2);
// apply filters
@@ -304,7 +305,7 @@ describe('FilterBar', () => {
await addFilterFlow();
- expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled();
+ expect(screen.getByTestId(getTestId('apply-button'))).toBeEnabled();
});
it('add and apply filter set', async () => {
@@ -317,6 +318,8 @@ describe('FilterBar', () => {
await addFilterFlow();
+ userEvent.click(screen.getByTestId(getTestId('apply-button')));
+
await addFilterSetFlow();
// change filter
@@ -340,6 +343,7 @@ describe('FilterBar', () => {
screen.getByTestId(getTestId('filter-set-wrapper')),
).not.toHaveAttribute('data-selected', 'true');
userEvent.click(screen.getByTestId(getTestId('filter-set-wrapper')));
+ userEvent.click(screen.getAllByText('Filters (1)')[1]);
expect(await screen.findByText('Last week')).toBeInTheDocument();
userEvent.click(screen.getByTestId(getTestId('apply-button')));
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled();
@@ -355,6 +359,8 @@ describe('FilterBar', () => {
await addFilterFlow();
+ userEvent.click(screen.getByTestId(getTestId('apply-button')));
+
await addFilterSetFlow();
userEvent.click(screen.getByTestId(getTestId('filter-set-menu-button')));
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTabs.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTabs.tsx
index 6f4ae3d..bab37da 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTabs.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTabs.tsx
@@ -84,31 +84,58 @@ export const FilterTabTitle = styled.span`
`;
const FilterTabsContainer = styled(LineEditableTabs)`
- // extra selector specificity:
- &.ant-tabs-card > .ant-tabs-nav .ant-tabs-tab {
- min-width: ${FILTER_WIDTH}px;
- margin: 0 ${({ theme }) => theme.gridUnit * 2}px 0 0;
- padding: ${({ theme }) => theme.gridUnit}px
- ${({ theme }) => theme.gridUnit * 2}px;
-
- &:hover,
- &-active {
- color: ${({ theme }) => theme.colors.grayscale.dark1};
- border-radius: ${({ theme }) => theme.borderRadius}px;
- background-color: ${({ theme }) => theme.colors.secondary.light4};
-
- .ant-tabs-tab-remove > svg {
- color: ${({ theme }) => theme.colors.grayscale.base};
- transition: all 0.3s;
+ ${({ theme }) => `
+ height: 100%;
+
+ & > .ant-tabs-content-holder {
+ border-left: 1px solid ${theme.colors.grayscale.light2};
+ margin-right: ${theme.gridUnit * 4}px;
+ }
+ & > .ant-tabs-content-holder ~ .ant-tabs-content-holder {
+ border: none;
+ }
+
+ &.ant-tabs-left
+ > .ant-tabs-content-holder
+ > .ant-tabs-content
+ > .ant-tabs-tabpane {
+ padding-left: ${theme.gridUnit * 4}px;
+ margin-top: ${theme.gridUnit * 4}px;
+ }
+
+ .ant-tabs-nav-list {
+ padding-top: ${theme.gridUnit * 4}px;
+ padding-right: ${theme.gridUnit * 2}px;
+ padding-bottom: ${theme.gridUnit * 4}px;
+ padding-left: ${theme.gridUnit * 3}px;
+ }
+
+ // extra selector specificity:
+ &.ant-tabs-card > .ant-tabs-nav .ant-tabs-tab {
+ min-width: ${FILTER_WIDTH}px;
+ margin: 0 ${theme.gridUnit * 2}px 0 0;
+ padding: ${theme.gridUnit}px
+ ${theme.gridUnit * 2}px;
+
+ &:hover,
+ &-active {
+ color: ${theme.colors.grayscale.dark1};
+ border-radius: ${theme.borderRadius}px;
+ background-color: ${theme.colors.secondary.light4};
+
+ .ant-tabs-tab-remove > svg {
+ color: ${theme.colors.grayscale.base};
+ transition: all 0.3s;
+ }
}
}
- }
- .ant-tabs-tab-btn {
- text-align: left;
- justify-content: space-between;
- text-transform: unset;
- }
+ .ant-tabs-tab-btn {
+ text-align: left;
+ justify-content: space-between;
+ text-transform: unset;
+ }
+ `}
`;
type FilterTabsProps = {
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
index 47a0637..e0fbf8b 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
@@ -44,6 +44,7 @@ import DateFilterControl from 'src/explore/components/controls/DateFilterControl
import { addDangerToast } from 'src/messageToasts/actions';
import { ClientErrorObject } from 'src/utils/getClientErrorObject';
import SelectControl from 'src/explore/components/controls/SelectControl';
+import Collapse from 'src/components/Collapse';
import Button from 'src/components/Button';
import { getChartDataRequest } from 'src/chart/chartAction';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
@@ -76,6 +77,12 @@ const StyledContainer = styled.div`
justify-content: space-between;
`;
+const StyledDatasetContainer = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+`;
+
export const StyledFormItem = styled(Form.Item)`
width: 49%;
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
@@ -95,6 +102,36 @@ const CleanFormItem = styled(Form.Item)`
margin-bottom: 0;
`;
+const StyledCollapse = styled(Collapse)`
+ margin-left: ${({ theme }) => theme.gridUnit * -4 - 1}px;
+ margin-right: ${({ theme }) => theme.gridUnit * -4}px;
+ border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
+ border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
+ border-radius: 0px;
+
+ .ant-collapse-header {
+ border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
+ border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
+ margin-top: -1px;
+ border-radius: 0px;
+ }
+
+ .ant-collapse-content {
+ border: 0px;
+ }
+
+ &.ant-collapse > .ant-collapse-item {
+ border: 0px;
+ border-radius: 0px;
+ }
+`;
+
+const StyledTabs = styled(Tabs)`
+ .ant-tabs-nav-list {
+ padding: 0px;
+ }
+`;
+
const FilterTabs = {
configuration: {
key: 'configuration',
@@ -106,6 +143,17 @@ const FilterTabs = {
},
};
+const FilterPanels = {
+ basic: {
+ key: 'basic',
+ name: t('Basic'),
+ },
+ advanced: {
+ key: 'advanced',
+ name: t('Advanced'),
+ },
+};
+
export interface FiltersConfigFormProps {
filterId: string;
filterToEdit?: Filter;
@@ -278,7 +326,7 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
return (
<>
- <Tabs defaultActiveKey={FilterTabs.configuration.key} centered>
+ <StyledTabs defaultActiveKey={FilterTabs.configuration.key} centered>
<TabPane
tab={FilterTabs.configuration.name}
key={FilterTabs.configuration.key}
@@ -317,7 +365,7 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
</StyledFormItem>
</StyledContainer>
{hasDataset && (
- <>
+ <StyledDatasetContainer>
<StyledFormItem
name={['filters', filterId, 'dataset']}
initialValue={{ value: initialDatasetId }}
@@ -375,156 +423,170 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
/>
</StyledFormItem>
)}
- {hasAdditionalFilters && (
- <>
- <StyledFormItem
- name={['filters', filterId, 'adhoc_filters']}
- initialValue={filterToEdit?.adhoc_filters}
- >
- <AdhocFilterControl
- columns={
- datasetDetails?.columns?.filter(
- (c: ColumnMeta) => c.filterable,
- ) || []
- }
- savedMetrics={datasetDetails?.metrics || []}
- datasource={datasetDetails}
- onChange={(filters: AdhocFilter[]) => {
+ </StyledDatasetContainer>
+ )}
+ <StyledCollapse>
+ <Collapse.Panel
+ header={FilterPanels.basic.name}
+ key={FilterPanels.basic.key}
+ >
+ {hasFilledDataset && (
+ <CleanFormItem
+ name={['filters', filterId, 'defaultValueFormData']}
+ hidden
+ initialValue={newFormData}
+ />
+ )}
+ <CleanFormItem
+ name={['filters', filterId, 'defaultValueQueriesData']}
+ hidden
+ initialValue={null}
+ />
+ {isCascadingFilter && (
+ <StyledFormItem
+ name={['filters', filterId, 'parentFilter']}
+ label={<StyledLabel>{t('Parent filter')}</StyledLabel>}
+ initialValue={parentFilterOptions.find(
+ ({ value }) => value === filterToEdit?.cascadeParentIds[0],
+ )}
+ data-test="parent-filter-input"
+ >
+ <Select
+ placeholder={t('None')}
+ options={parentFilterOptions}
+ isClearable
+ />
+ </StyledFormItem>
+ )}
+ <StyledContainer>
+ <StyledFormItem className="bottom" label={<StyledLabel />}>
+ {hasDataset && hasFilledDataset && (
+ <Button onClick={refreshHandler}>
+ {isDataDirty ? t('Populate') : t('Refresh')}
+ </Button>
+ )}
+ </StyledFormItem>
+ <StyledFormItem
+ name={['filters', filterId, 'defaultDataMask']}
+ initialValue={filterToEdit?.defaultDataMask}
+ data-test="default-input"
+ label={<StyledLabel>{t('Default Value')}</StyledLabel>}
+ >
+ {showDefaultValue ? (
+ <DefaultValue
+ setDataMask={dataMask => {
setNativeFilterFieldValues(form, filterId, {
- adhoc_filters: filters,
+ defaultDataMask: dataMask,
});
forceUpdate();
}}
- label={<StyledLabel>{t('Adhoc filters')}</StyledLabel>}
+ filterId={filterId}
+ hasDataset={hasDataset}
+ form={form}
+ formData={newFormData}
/>
- </StyledFormItem>
+ ) : hasFilledDataset ? (
+ t('Click "Populate" to get "Default Value" ->')
+ ) : (
+ t('Fill all required fields to enable "Default Value"')
+ )}
+ </StyledFormItem>
+ </StyledContainer>
+ <StyledCheckboxFormItem
+ name={['filters', filterId, 'isInstant']}
+ initialValue={filterToEdit?.isInstant || false}
+ valuePropName="checked"
+ colon={false}
+ >
+ <Checkbox data-test="apply-changes-instantly-checkbox">
+ {t('Apply changes instantly')}
+ </Checkbox>
+ </StyledCheckboxFormItem>
+ <ControlItems
+ disabled={!showDefaultValue}
+ filterToEdit={filterToEdit}
+ formFilter={formFilter}
+ filterId={filterId}
+ form={form}
+ forceUpdate={forceUpdate}
+ />
+ </Collapse.Panel>
+ {((hasDataset && hasAdditionalFilters) || hasMetrics) && (
+ <Collapse.Panel
+ header={FilterPanels.advanced.name}
+ key={FilterPanels.advanced.key}
+ >
+ {hasDataset && hasAdditionalFilters && (
+ <>
+ <StyledFormItem
+ name={['filters', filterId, 'adhoc_filters']}
+ initialValue={filterToEdit?.adhoc_filters}
+ >
+ <AdhocFilterControl
+ columns={
+ datasetDetails?.columns?.filter(
+ (c: ColumnMeta) => c.filterable,
+ ) || []
+ }
+ savedMetrics={datasetDetails?.metrics || []}
+ datasource={datasetDetails}
+ onChange={(filters: AdhocFilter[]) => {
+ setNativeFilterFieldValues(form, filterId, {
+ adhoc_filters: filters,
+ });
+ forceUpdate();
+ }}
+ label={<StyledLabel>{t('Adhoc filters')}</StyledLabel>}
+ />
+ </StyledFormItem>
+ <StyledFormItem
+ name={['filters', filterId, 'time_range']}
+ label={<StyledLabel>{t('Time range')}</StyledLabel>}
+ initialValue={filterToEdit?.time_range || 'No filter'}
+ >
+ <DateFilterControl
+ name="time_range"
+ onChange={timeRange => {
+ setNativeFilterFieldValues(form, filterId, {
+ time_range: timeRange,
+ });
+ forceUpdate();
+ }}
+ />
+ </StyledFormItem>
+ </>
+ )}
+ {hasMetrics && (
<StyledFormItem
- name={['filters', filterId, 'time_range']}
- label={<StyledLabel>{t('Time range')}</StyledLabel>}
- initialValue={filterToEdit?.time_range || 'No filter'}
+ // don't show the column select unless we have a dataset
+ // style={{ display: datasetId == null ? undefined : 'none' }}
+ name={['filters', filterId, 'sortMetric']}
+ initialValue={filterToEdit?.sortMetric}
+ label={<StyledLabel>{t('Sort Metric')}</StyledLabel>}
+ data-test="field-input"
>
- <DateFilterControl
- name="time_range"
- onChange={timeRange => {
- setNativeFilterFieldValues(form, filterId, {
- time_range: timeRange,
- });
- forceUpdate();
+ <SelectControl
+ form={form}
+ filterId={filterId}
+ name="sortMetric"
+ options={metrics.map((metric: Metric) => ({
+ value: metric.metric_name,
+ label: metric.verbose_name ?? metric.metric_name,
+ }))}
+ onChange={(value: string | null): void => {
+ if (value !== undefined) {
+ setNativeFilterFieldValues(form, filterId, {
+ sortMetric: value,
+ });
+ forceUpdate();
+ }
}}
/>
</StyledFormItem>
- </>
- )}
- </>
- )}
- {hasFilledDataset && (
- <CleanFormItem
- name={['filters', filterId, 'defaultValueFormData']}
- hidden
- initialValue={newFormData}
- />
- )}
- <CleanFormItem
- name={['filters', filterId, 'defaultValueQueriesData']}
- hidden
- initialValue={null}
- />
- {isCascadingFilter && (
- <StyledFormItem
- name={['filters', filterId, 'parentFilter']}
- label={<StyledLabel>{t('Parent filter')}</StyledLabel>}
- initialValue={parentFilterOptions.find(
- ({ value }) => value === filterToEdit?.cascadeParentIds[0],
- )}
- data-test="parent-filter-input"
- >
- <Select
- placeholder={t('None')}
- options={parentFilterOptions}
- isClearable
- />
- </StyledFormItem>
- )}
- <StyledContainer>
- <StyledFormItem className="bottom" label={<StyledLabel />}>
- {hasDataset && hasFilledDataset && (
- <Button onClick={refreshHandler}>
- {isDataDirty ? t('Populate') : t('Refresh')}
- </Button>
- )}
- </StyledFormItem>
- <StyledFormItem
- name={['filters', filterId, 'defaultDataMask']}
- initialValue={filterToEdit?.defaultDataMask}
- data-test="default-input"
- label={<StyledLabel>{t('Default Value')}</StyledLabel>}
- >
- {showDefaultValue ? (
- <DefaultValue
- setDataMask={dataMask => {
- setNativeFilterFieldValues(form, filterId, {
- defaultDataMask: dataMask,
- });
- forceUpdate();
- }}
- filterId={filterId}
- hasDataset={hasDataset}
- form={form}
- formData={newFormData}
- />
- ) : hasFilledDataset ? (
- t('Click "Populate" to get "Default Value" ->')
- ) : (
- t('Fill all required fields to enable "Default Value"')
- )}
- </StyledFormItem>
- </StyledContainer>
- <StyledCheckboxFormItem
- name={['filters', filterId, 'isInstant']}
- initialValue={filterToEdit?.isInstant || false}
- valuePropName="checked"
- colon={false}
- >
- <Checkbox data-test="apply-changes-instantly-checkbox">
- {t('Apply changes instantly')}
- </Checkbox>
- </StyledCheckboxFormItem>
- <ControlItems
- disabled={!showDefaultValue}
- filterToEdit={filterToEdit}
- formFilter={formFilter}
- filterId={filterId}
- form={form}
- forceUpdate={forceUpdate}
- />
- {hasMetrics && (
- <StyledFormItem
- // don't show the column select unless we have a dataset
- // style={{ display: datasetId == null ? undefined : 'none' }}
- name={['filters', filterId, 'sortMetric']}
- initialValue={filterToEdit?.sortMetric}
- label={<StyledLabel>{t('Sort Metric')}</StyledLabel>}
- data-test="field-input"
- >
- <SelectControl
- form={form}
- filterId={filterId}
- name="sortMetric"
- options={metrics.map((metric: Metric) => ({
- value: metric.metric_name,
- label: metric.verbose_name ?? metric.metric_name,
- }))}
- onChange={(value: string | null): void => {
- if (value !== undefined) {
- setNativeFilterFieldValues(form, filterId, {
- sortMetric: value,
- });
- forceUpdate();
- }
- }}
- />
- </StyledFormItem>
- )}
+ )}
+ </Collapse.Panel>
+ )}
+ </StyledCollapse>
</TabPane>
<TabPane
tab={FilterTabs.scoping.name}
@@ -542,7 +604,7 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
formScoping={formFilter?.scoping}
/>
</TabPane>
- </Tabs>
+ </StyledTabs>
</>
);
};
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx
index 4b25bd5..c1017b6 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx
@@ -37,6 +37,13 @@ import FilterTabs from './FilterTabs';
import FiltersConfigForm from './FiltersConfigForm/FiltersConfigForm';
import { useOpenModal, useRemoveCurrentFilter } from './state';
+const StyledModalWrapper = styled(StyledModal)`
+ min-width: 700px;
+ .ant-modal-body {
+ padding: 0px;
+ }
+`;
+
export const StyledModalBody = styled.div`
display: flex;
height: 500px;
@@ -205,11 +212,11 @@ export function FiltersConfigModal({
};
return (
- <StyledModal
+ <StyledModalWrapper
visible={isOpen}
maskClosable={false}
title={t('Filters configuration and scoping')}
- width="55%"
+ width="50%"
destroyOnClose
onCancel={handleCancel}
onOk={handleSave}
@@ -269,6 +276,6 @@ export function FiltersConfigModal({
</StyledForm>
</StyledModalBody>
</ErrorBoundary>
- </StyledModal>
+ </StyledModalWrapper>
);
}