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/06/04 13:23:32 UTC

[superset] branch master updated: feat(native-filters): add markers and number formatter + simple tests (#14981)

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 355223d  feat(native-filters): add markers and number formatter + simple tests (#14981)
355223d is described below

commit 355223d3fe4b022dd350dde02937251c13c19020
Author: Ville Brofeldt <33...@users.noreply.github.com>
AuthorDate: Fri Jun 4 16:22:54 2021 +0300

    feat(native-filters): add markers and number formatter + simple tests (#14981)
---
 .../components/Range/RangeFilterPlugin.stories.tsx |  78 +++++++++++++
 .../components/Range/RangeFilterPlugin.test.tsx    | 121 +++++++++++++++++++++
 .../filters/components/Range/RangeFilterPlugin.tsx |  60 ++++++++--
 3 files changed, 248 insertions(+), 11 deletions(-)

diff --git a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.stories.tsx b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.stories.tsx
new file mode 100644
index 0000000..fb442e8
--- /dev/null
+++ b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.stories.tsx
@@ -0,0 +1,78 @@
+/**
+ * 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 React from 'react';
+import { action } from '@storybook/addon-actions';
+import {
+  SuperChart,
+  getChartTransformPropsRegistry,
+  GenericDataType,
+} from '@superset-ui/core';
+import RangeFilterPlugin from './index';
+import transformProps from './transformProps';
+
+new RangeFilterPlugin().configure({ key: 'filter_range' }).register();
+
+getChartTransformPropsRegistry().registerValue('filter_range', transformProps);
+
+export default {
+  title: 'Filter Plugins',
+};
+
+export const range = ({ width, height }: { width: number; height: number }) => (
+  <SuperChart
+    chartType="filter_range"
+    width={width}
+    height={height}
+    queriesData={[{ data: [{ min: 10, max: 100 }] }]}
+    filterState={{ value: [10, 70] }}
+    formData={{
+      groupby: ['SP_POP_TOTL'],
+      adhoc_filters: [],
+      extra_filters: [],
+      viz_type: 'filter_range',
+      metrics: [
+        {
+          aggregate: 'MIN',
+          column: {
+            column_name: 'SP_POP_TOTL',
+            id: 1,
+            type_generic: GenericDataType.NUMERIC,
+          },
+          expressionType: 'SIMPLE',
+          hasCustomLabel: true,
+          label: 'min',
+        },
+        {
+          aggregate: 'MAX',
+          column: {
+            column_name: 'SP_POP_TOTL',
+            id: 2,
+            type_generic: GenericDataType.NUMERIC,
+          },
+          expressionType: 'SIMPLE',
+          hasCustomLabel: true,
+          label: 'max',
+        },
+      ],
+    }}
+    hooks={{
+      setDataMask: action('setDataMask'),
+    }}
+  />
+);
diff --git a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx
new file mode 100644
index 0000000..cf9420e
--- /dev/null
+++ b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx
@@ -0,0 +1,121 @@
+/**
+ * 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 { AppSection, GenericDataType } from '@superset-ui/core';
+import React from 'react';
+import { render } from 'spec/helpers/testing-library';
+import RangeFilterPlugin from './RangeFilterPlugin';
+import transformProps from './transformProps';
+
+const rangeProps = {
+  formData: {
+    datasource: '3__table',
+    groupby: ['SP_POP_TOTL'],
+    adhocFilters: [],
+    extraFilters: [],
+    extraFormData: {},
+    granularitySqla: 'ds',
+    metrics: [
+      {
+        aggregate: 'MIN',
+        column: {
+          column_name: 'SP_POP_TOTL',
+          id: 1,
+          type_generic: GenericDataType.NUMERIC,
+        },
+        expressionType: 'SIMPLE',
+        hasCustomLabel: true,
+        label: 'min',
+      },
+      {
+        aggregate: 'MAX',
+        column: {
+          column_name: 'SP_POP_TOTL',
+          id: 2,
+          type_generic: GenericDataType.NUMERIC,
+        },
+        expressionType: 'SIMPLE',
+        hasCustomLabel: true,
+        label: 'max',
+      },
+    ],
+    rowLimit: 1000,
+    showSearch: true,
+    defaultValue: [10, 70],
+    timeRangeEndpoints: ['inclusive', 'exclusive'],
+    urlParams: {},
+    vizType: 'filter_range',
+    inputRef: { current: null },
+  },
+  height: 20,
+  hooks: {},
+  filterState: { value: [10, 70] },
+  queriesData: [
+    {
+      rowcount: 1,
+      colnames: ['min', 'max'],
+      coltypes: [GenericDataType.NUMERIC, GenericDataType.NUMERIC],
+      data: [{ min: 10, max: 100 }],
+      applied_filters: [],
+      rejected_filters: [],
+    },
+  ],
+  width: 220,
+  behaviors: ['NATIVE_FILTER'],
+  isRefreshing: false,
+  appSection: AppSection.DASHBOARD,
+};
+
+describe('RangeFilterPlugin', () => {
+  const setDataMask = jest.fn();
+  const getWrapper = (props = {}) =>
+    render(
+      // @ts-ignore
+      <RangeFilterPlugin
+        // @ts-ignore
+        {...transformProps({
+          ...rangeProps,
+          formData: { ...rangeProps.formData, ...props },
+        })}
+        setDataMask={setDataMask}
+      />,
+    );
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should call setDataMask with correct filter', () => {
+    getWrapper();
+    expect(setDataMask).toHaveBeenCalledWith({
+      extraFormData: {
+        filters: [
+          {
+            col: 'SP_POP_TOTL',
+            op: '<=',
+            val: 70,
+          },
+        ],
+      },
+      filterState: {
+        label: 'x ≤ 70',
+        value: [10, 70],
+      },
+    });
+  });
+});
diff --git a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx
index 083c41e..8d88e51 100644
--- a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx
+++ b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { t } from '@superset-ui/core';
+import { getNumberFormatter, NumberFormats, t } from '@superset-ui/core';
 import React, { useEffect, useState } from 'react';
 import { Slider } from 'src/common/components';
 import { PluginFilterRangeProps } from './types';
@@ -35,6 +35,8 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
     inputRef,
     filterState,
   } = props;
+  const numberFormatter = getNumberFormatter(NumberFormats.SMART_NUMBER);
+
   const [row] = data;
   // @ts-ignore
   const { min, max }: { min: number; max: number } = row;
@@ -43,15 +45,55 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
   const [value, setValue] = useState<[number, number]>(
     defaultValue ?? [min, max],
   );
+  const [marks, setMarks] = useState<{ [key: number]: string }>({});
+
+  const getBounds = (
+    value: [number, number],
+  ): { lower: number | null; upper: number | null } => {
+    const [lowerRaw, upperRaw] = value;
+    return {
+      lower: lowerRaw > min ? lowerRaw : null,
+      upper: upperRaw < max ? upperRaw : null,
+    };
+  };
+
+  const getLabel = (lower: number | null, upper: number | null): string => {
+    if (lower !== null && upper !== null) {
+      return `${numberFormatter(lower)} ≤ x ≤ ${numberFormatter(upper)}`;
+    }
+    if (lower !== null) {
+      return `x ≥ ${numberFormatter(lower)}`;
+    }
+    if (upper !== null) {
+      return `x ≤ ${numberFormatter(upper)}`;
+    }
+    return '';
+  };
 
-  const handleAfterChange = (value: [number, number]) => {
-    const [lower, upper] = value;
+  const getMarks = (
+    lower: number | null,
+    upper: number | null,
+  ): { [key: number]: string } => {
+    const newMarks: { [key: number]: string } = {};
+    if (lower !== null) {
+      newMarks[lower] = numberFormatter(lower);
+    }
+    if (upper !== null) {
+      newMarks[upper] = numberFormatter(upper);
+    }
+    return newMarks;
+  };
+
+  const handleAfterChange = (value: [number, number]): void => {
     setValue(value);
+    const { lower, upper } = getBounds(value);
+    setMarks(getMarks(lower, upper));
 
     setDataMask({
       extraFormData: getRangeExtraFormData(col, lower, upper),
       filterState: {
-        value,
+        value: lower !== null || upper !== null ? value : null,
+        label: getLabel(lower, upper),
       },
     });
   };
@@ -64,12 +106,6 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
     handleAfterChange(filterState.value ?? [min, max]);
   }, [JSON.stringify(filterState.value)]);
 
-  useEffect(() => {
-    handleAfterChange(defaultValue ?? [min, max]);
-    // I think after Config Modal update some filter it re-creates default value for all other filters
-    // so we can process it like this `JSON.stringify` or start to use `Immer`
-  }, [JSON.stringify(defaultValue)]);
-
   return (
     <Styles height={height} width={width}>
       {Number.isNaN(Number(min)) || Number.isNaN(Number(max)) ? (
@@ -80,10 +116,12 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
             range
             min={min}
             max={max}
-            value={value}
+            value={value ?? [min, max]}
             onAfterChange={handleAfterChange}
             onChange={handleChange}
+            tipFormatter={value => numberFormatter(value)}
             ref={inputRef}
+            marks={marks}
           />
         </div>
       )}