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>
   );
 }