diff --git a/superset-frontend/src/components/AlteredSliceTag/AlteredSliceTag.test.jsx b/superset-frontend/src/components/AlteredSliceTag/AlteredSliceTag.test.jsx
index 7501ce638..39b85a3fa 100644
--- a/superset-frontend/src/components/AlteredSliceTag/AlteredSliceTag.test.jsx
+++ b/superset-frontend/src/components/AlteredSliceTag/AlteredSliceTag.test.jsx
@@ -17,325 +17,265 @@
* under the License.
*/
import React from 'react';
-import { styledMount as mount } from 'spec/helpers/theming';
-import { getChartControlPanelRegistry } from '@superset-ui/core';
+import '@testing-library/jest-dom/extend-expect';
+import { render, screen } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import AlteredSliceTag, {
+ alterForComparison,
+ formatValueHandler,
+ isEqualish,
+} from 'src/components/AlteredSliceTag';
+import { defaultProps } from './AlteredSliceTagMocks';
-import AlteredSliceTag from 'src/components/AlteredSliceTag';
-import ModalTrigger from 'src/components/ModalTrigger';
-import { Tooltip } from 'src/components/Tooltip';
-import TableCollection from 'src/components/TableCollection';
-import TableView from 'src/components/TableView';
+const controlsMap = {
+ b: { type: 'BoundsControl', label: 'Bounds' },
+ column_collection: { type: 'CollectionControl', label: 'Collection' },
+ metrics: { type: 'MetricsControl', label: 'Metrics' },
+ adhoc_filters: { type: 'AdhocFilterControl', label: 'Adhoc' },
+ other_control: { type: 'OtherControl', label: 'Other' },
+};
-import {
- defaultProps,
- expectedDiffs,
- expectedRows,
- fakePluginControls,
-} from './AlteredSliceTagMocks';
+test('renders the "Altered" label', () => {
+ render(
+ ,
+ );
-const getTableWrapperFromModalBody = modalBody =>
- modalBody.find(TableView).find(TableCollection);
-
-describe('AlteredSliceTag', () => {
- let wrapper;
- let props;
- let controlsMap;
-
- beforeEach(() => {
- getChartControlPanelRegistry().registerValue(
- 'altered_slice_tag_spec',
- fakePluginControls,
- );
- props = { ...defaultProps };
- wrapper = mount();
- ({ controlsMap } = wrapper.instance().state);
- });
-
- it('correctly determines form data differences', () => {
- const diffs = wrapper.instance().getDiffs(props);
- expect(diffs).toEqual(expectedDiffs);
- expect(wrapper.instance().state.rows).toEqual(expectedRows);
- expect(wrapper.instance().state.hasDiffs).toBe(true);
- });
-
- it('does not run when there are no differences', () => {
- props = {
- origFormData: props.origFormData,
- currentFormData: props.origFormData,
- };
- wrapper = mount();
- expect(wrapper.instance().state.rows).toEqual([]);
- expect(wrapper.instance().state.hasDiffs).toBe(false);
- expect(wrapper.instance().render()).toBeNull();
- });
-
- it('does not run when temporary controls have changes', () => {
- props = {
- origFormData: { ...props.origFormData, url_params: { foo: 'foo' } },
- currentFormData: { ...props.origFormData, url_params: { bar: 'bar' } },
- };
- wrapper = mount();
- expect(wrapper.instance().state.rows).toEqual([]);
- expect(wrapper.instance().state.hasDiffs).toBe(false);
- expect(wrapper.instance().render()).toBeNull();
- });
-
- it('sets new rows when receiving new props', () => {
- const testRows = ['testValue'];
- const getRowsFromDiffsStub = jest
- .spyOn(AlteredSliceTag.prototype, 'getRowsFromDiffs')
- .mockReturnValueOnce(testRows);
- const newProps = {
- currentFormData: { ...props.currentFormData },
- origFormData: { ...props.origFormData },
- };
- wrapper = mount();
- const wrapperInstance = wrapper.instance();
- wrapperInstance.UNSAFE_componentWillReceiveProps(newProps);
- expect(getRowsFromDiffsStub).toHaveBeenCalled();
- expect(wrapperInstance.state.rows).toEqual(testRows);
- });
-
- it('does not set new state when props are the same', () => {
- const currentRows = wrapper.instance().state.rows;
- wrapper.instance().UNSAFE_componentWillReceiveProps(props);
- // Check equal references
- expect(wrapper.instance().state.rows).toBe(currentRows);
- });
-
- it('renders a ModalTrigger', () => {
- expect(wrapper.find(ModalTrigger)).toExist();
- });
-
- describe('renderTriggerNode', () => {
- it('renders a Tooltip', () => {
- const triggerNode = mount(
-
{wrapper.instance().renderTriggerNode()}
,
- );
- expect(triggerNode.find(Tooltip)).toHaveLength(1);
- });
- });
-
- describe('renderModalBody', () => {
- it('renders a Table', () => {
- const modalBody = mount(
- {wrapper.instance().renderModalBody()}
,
- );
- expect(modalBody.find(TableView)).toHaveLength(1);
- });
-
- it('renders a thead', () => {
- const modalBody = mount(
- {wrapper.instance().renderModalBody()}
,
- );
- expect(
- getTableWrapperFromModalBody(modalBody).find('thead'),
- ).toHaveLength(1);
- });
-
- it('renders th', () => {
- const modalBody = mount(
- {wrapper.instance().renderModalBody()}
,
- );
- const th = getTableWrapperFromModalBody(modalBody).find('th');
- expect(th).toHaveLength(3);
- ['Control', 'Before', 'After'].forEach(async (v, i) => {
- await expect(th.at(i).find('span').get(0).props.children[0]).toBe(v);
- });
- });
-
- it('renders the correct number of Tr', () => {
- const modalBody = mount(
- {wrapper.instance().renderModalBody()}
,
- );
- const tr = getTableWrapperFromModalBody(modalBody).find('tr');
- expect(tr).toHaveLength(8);
- });
-
- it('renders the correct number of td', () => {
- const modalBody = mount(
- {wrapper.instance().renderModalBody()}
,
- );
- const td = getTableWrapperFromModalBody(modalBody).find('td');
- expect(td).toHaveLength(21);
- ['control', 'before', 'after'].forEach((v, i) => {
- expect(td.find('defaultRenderer').get(0).props.columns[i].id).toBe(v);
- });
- });
- });
-
- describe('renderRows', () => {
- it('returns an array of rows with one tr and three td', () => {
- const modalBody = mount(
- {wrapper.instance().renderModalBody()}
,
- );
- const rows = getTableWrapperFromModalBody(modalBody).find('tr');
- expect(rows).toHaveLength(8);
- const slice = mount(
- ,
- );
- expect(slice.find('tr')).toHaveLength(1);
- expect(slice.find('td')).toHaveLength(3);
- });
- });
-
- describe('formatValue', () => {
- it('returns "N/A" for undefined values', () => {
- expect(wrapper.instance().formatValue(undefined, 'b', controlsMap)).toBe(
- 'N/A',
- );
- });
-
- it('returns "null" for null values', () => {
- expect(wrapper.instance().formatValue(null, 'b', controlsMap)).toBe(
- 'null',
- );
- });
-
- it('returns "Max" and "Min" for BoundsControl', () => {
- // need to pass the viz type to the wrapper
- expect(
- wrapper.instance().formatValue([5, 6], 'y_axis_bounds', controlsMap),
- ).toBe('Min: 5, Max: 6');
- });
-
- it('returns stringified objects for CollectionControl', () => {
- const value = [
- { 1: 2, alpha: 'bravo' },
- { sent: 'imental', w0ke: 5 },
- ];
- const expected = '{"1":2,"alpha":"bravo"}, {"sent":"imental","w0ke":5}';
- expect(
- wrapper.instance().formatValue(value, 'column_collection', controlsMap),
- ).toBe(expected);
- });
-
- it('returns boolean values as string', () => {
- expect(wrapper.instance().formatValue(true, 'b', controlsMap)).toBe(
- 'true',
- );
- expect(wrapper.instance().formatValue(false, 'b', controlsMap)).toBe(
- 'false',
- );
- });
-
- it('returns Array joined by commas', () => {
- const value = [5, 6, 7, 8, 'hello', 'goodbye'];
- const expected = '5, 6, 7, 8, hello, goodbye';
- expect(
- wrapper.instance().formatValue(value, undefined, controlsMap),
- ).toBe(expected);
- });
-
- it('returns Metrics if the field type is metrics', () => {
- const value = [
- {
- label: 'SUM(Sales)',
- },
- ];
- const expected = 'SUM(Sales)';
- expect(
- wrapper.instance().formatValue(value, 'metrics', controlsMap),
- ).toBe(expected);
- });
-
- it('stringifies objects', () => {
- const value = { 1: 2, alpha: 'bravo' };
- const expected = '{"1":2,"alpha":"bravo"}';
- expect(
- wrapper.instance().formatValue(value, undefined, controlsMap),
- ).toBe(expected);
- });
-
- it('does nothing to strings and numbers', () => {
- expect(wrapper.instance().formatValue(5, undefined, controlsMap)).toBe(5);
- expect(
- wrapper.instance().formatValue('hello', undefined, controlsMap),
- ).toBe('hello');
- });
-
- it('returns "[]" for empty filters', () => {
- expect(
- wrapper.instance().formatValue([], 'adhoc_filters', controlsMap),
- ).toBe('[]');
- });
-
- it('correctly formats filters with array values', () => {
- const filters = [
- {
- clause: 'WHERE',
- comparator: ['1', 'g', '7', 'ho'],
- expressionType: 'SIMPLE',
- operator: 'IN',
- subject: 'a',
- },
- {
- clause: 'WHERE',
- comparator: ['hu', 'ho', 'ha'],
- expressionType: 'SIMPLE',
- operator: 'NOT IN',
- subject: 'b',
- },
- ];
- const expected = 'a IN [1, g, 7, ho], b NOT IN [hu, ho, ha]';
- expect(
- wrapper.instance().formatValue(filters, 'adhoc_filters', controlsMap),
- ).toBe(expected);
- });
-
- it('correctly formats filters with string values', () => {
- const filters = [
- {
- clause: 'WHERE',
- comparator: 'gucci',
- expressionType: 'SIMPLE',
- operator: '==',
- subject: 'a',
- },
- {
- clause: 'WHERE',
- comparator: 'moshi moshi',
- expressionType: 'SIMPLE',
- operator: 'LIKE',
- subject: 'b',
- },
- ];
- const expected = 'a == gucci, b LIKE moshi moshi';
- expect(
- wrapper.instance().formatValue(filters, 'adhoc_filters', controlsMap),
- ).toBe(expected);
- });
- });
- describe('isEqualish', () => {
- it('considers null, undefined, {} and [] as equal', () => {
- const inst = wrapper.instance();
- expect(inst.isEqualish(null, undefined)).toBe(true);
- expect(inst.isEqualish(null, [])).toBe(true);
- expect(inst.isEqualish(null, {})).toBe(true);
- expect(inst.isEqualish(undefined, {})).toBe(true);
- });
- it('considers empty strings are the same as null', () => {
- const inst = wrapper.instance();
- expect(inst.isEqualish(undefined, '')).toBe(true);
- expect(inst.isEqualish(null, '')).toBe(true);
- });
- it('considers deeply equal objects as equal', () => {
- const inst = wrapper.instance();
- expect(inst.isEqualish('', '')).toBe(true);
- expect(inst.isEqualish({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 })).toBe(
- true,
- );
- // Out of order
- expect(inst.isEqualish({ a: 1, b: 2, c: 3 }, { b: 2, a: 1, c: 3 })).toBe(
- true,
- );
-
- // Actually not equal
- expect(inst.isEqualish({ a: 1, b: 2, z: 9 }, { a: 1, b: 2, c: 3 })).toBe(
- false,
- );
- });
- });
+ const alteredLabel = screen.getByText('Altered');
+ expect(alteredLabel).toBeInTheDocument();
+});
+
+test('opens the modal on click', () => {
+ render(
+ ,
+ );
+
+ const alteredLabel = screen.getByText('Altered');
+ userEvent.click(alteredLabel);
+
+ const modalTitle = screen.getByText('Chart changes');
+ expect(modalTitle).toBeInTheDocument();
+});
+
+test('displays the differences in the modal', () => {
+ render(
+ ,
+ );
+
+ const alteredLabel = screen.getByText('Altered');
+ userEvent.click(alteredLabel);
+
+ const beforeValue = screen.getByText('1, 2, 3, 4');
+ const afterValue = screen.getByText('a, b, c, d');
+ expect(beforeValue).toBeInTheDocument();
+ expect(afterValue).toBeInTheDocument();
+});
+
+test('does not render anything if there are no differences', () => {
+ render(
+ ,
+ );
+
+ const alteredLabel = screen.queryByText('Altered');
+ expect(alteredLabel).not.toBeInTheDocument();
+});
+
+test('alterForComparison returns null for undefined value', () => {
+ expect(alterForComparison(undefined)).toBeNull();
+});
+
+test('alterForComparison returns null for null value', () => {
+ expect(alterForComparison(null)).toBeNull();
+});
+
+test('alterForComparison returns null for empty string value', () => {
+ expect(alterForComparison('')).toBeNull();
+});
+
+test('alterForComparison returns null for empty array value', () => {
+ expect(alterForComparison([])).toBeNull();
+});
+
+test('alterForComparison returns null for empty object value', () => {
+ expect(alterForComparison({})).toBeNull();
+});
+
+test('alterForComparison returns value for non-empty array', () => {
+ const value = [1, 2, 3];
+ expect(alterForComparison(value)).toEqual(value);
+});
+
+test('alterForComparison returns value for non-empty object', () => {
+ const value = { key: 'value' };
+ expect(alterForComparison(value)).toEqual(value);
+});
+
+test('formatValueHandler handles undefined value', () => {
+ const value = undefined;
+ const key = 'b';
+ const formattedValue = formatValueHandler(value, key, controlsMap);
+ expect(formattedValue).toBe('N/A');
+});
+
+test('formatValueHandler handles null value', () => {
+ const value = null;
+ const key = 'b';
+ const formattedValue = formatValueHandler(value, key, controlsMap);
+ expect(formattedValue).toBe('null');
+});
+
+test('formatValueHandler returns "[]" for empty filters', () => {
+ const value = [];
+ const key = 'adhoc_filters';
+ const formattedValue = formatValueHandler(value, key, controlsMap);
+ expect(formattedValue).toBe('[]');
+});
+
+test('formatValueHandler formats filters with array values', () => {
+ const filters = [
+ {
+ clause: 'WHERE',
+ comparator: ['1', 'g', '7', 'ho'],
+ expressionType: 'SIMPLE',
+ operator: 'IN',
+ subject: 'a',
+ },
+ {
+ clause: 'WHERE',
+ comparator: ['hu', 'ho', 'ha'],
+ expressionType: 'SIMPLE',
+ operator: 'NOT IN',
+ subject: 'b',
+ },
+ ];
+ const key = 'adhoc_filters';
+ const formattedValue = formatValueHandler(filters, key, controlsMap);
+ const expected = 'a IN [1, g, 7, ho], b NOT IN [hu, ho, ha]';
+ expect(formattedValue).toBe(expected);
+});
+
+test('formatValueHandler formats filters with string values', () => {
+ const filters = [
+ {
+ clause: 'WHERE',
+ comparator: 'gucci',
+ expressionType: 'SIMPLE',
+ operator: '==',
+ subject: 'a',
+ },
+ {
+ clause: 'WHERE',
+ comparator: 'moshi moshi',
+ expressionType: 'SIMPLE',
+ operator: 'LIKE',
+ subject: 'b',
+ },
+ ];
+ const key = 'adhoc_filters';
+ const expected = 'a == gucci, b LIKE moshi moshi';
+ const formattedValue = formatValueHandler(filters, key, controlsMap);
+ expect(formattedValue).toBe(expected);
+});
+
+test('formatValueHandler formats "Min" and "Max" for BoundsControl', () => {
+ const value = [1, 2];
+ const key = 'b';
+ const result = formatValueHandler(value, key, controlsMap);
+ expect(result).toEqual('Min: 1, Max: 2');
+});
+
+test('formatValueHandler formats stringified objects for CollectionControl', () => {
+ const value = [{ a: 1 }, { b: 2 }];
+ const key = 'column_collection';
+ const result = formatValueHandler(value, key, controlsMap);
+ expect(result).toEqual(
+ `${JSON.stringify(value[0])}, ${JSON.stringify(value[1])}`,
+ );
+});
+
+test('formatValueHandler formats MetricsControl values correctly', () => {
+ const value = [{ label: 'SUM(Sales)' }, { label: 'Metric2' }];
+ const key = 'metrics';
+ const result = formatValueHandler(value, key, controlsMap);
+ expect(result).toEqual('SUM(Sales), Metric2');
+});
+
+test('formatValueHandler formats boolean values as string', () => {
+ const value1 = true;
+ const value2 = false;
+ const key = 'b';
+ const formattedValue1 = formatValueHandler(value1, key, controlsMap);
+ const formattedValue2 = formatValueHandler(value2, key, controlsMap);
+ expect(formattedValue1).toBe('true');
+ expect(formattedValue2).toBe('false');
+});
+
+test('formatValueHandler formats array values correctly', () => {
+ const value = [
+ { label: 'Label1' },
+ { label: 'Label2' },
+ 5,
+ 6,
+ 7,
+ 8,
+ 'hello',
+ 'goodbye',
+ ];
+ const result = formatValueHandler(value, undefined, controlsMap);
+ const expected = 'Label1, Label2, 5, 6, 7, 8, hello, goodbye';
+ expect(result).toEqual(expected);
+});
+
+test('formatValueHandler formats string values correctly', () => {
+ const value = 'test';
+ const key = 'other_control';
+ const result = formatValueHandler(value, key, controlsMap);
+ expect(result).toEqual('test');
+});
+
+test('formatValueHandler formats number values correctly', () => {
+ const value = 123;
+ const key = 'other_control';
+ const result = formatValueHandler(value, key, controlsMap);
+ expect(result).toEqual(123);
+});
+
+test('formatValueHandler formats object values correctly', () => {
+ const value = { 1: 2, alpha: 'bravo' };
+ const key = 'other_control';
+ const expected = '{"1":2,"alpha":"bravo"}';
+ const result = formatValueHandler(value, key, controlsMap);
+ expect(result).toEqual(expected);
+});
+
+test('isEqualish considers null, undefined, {} and [] as equal', () => {
+ expect(isEqualish(null, undefined)).toBe(true);
+ expect(isEqualish(null, [])).toBe(true);
+ expect(isEqualish(null, {})).toBe(true);
+ expect(isEqualish(undefined, {})).toBe(true);
+});
+
+test('isEqualish considers empty strings equal to null', () => {
+ expect(isEqualish(undefined, '')).toBe(true);
+ expect(isEqualish(null, '')).toBe(true);
+});
+
+test('isEqualish considers deeply equal objects equal', () => {
+ const obj1 = { a: { b: { c: 1 } } };
+ const obj2 = { a: { b: { c: 1 } } };
+ expect(isEqualish('', '')).toBe(true);
+ expect(isEqualish(obj1, obj2)).toBe(true);
+ // Actually not equal
+ expect(isEqualish({ a: 1, b: 2, z: 9 }, { a: 1, b: 2, c: 3 })).toBe(false);
});
diff --git a/superset-frontend/src/components/AlteredSliceTag/index.tsx b/superset-frontend/src/components/AlteredSliceTag/index.tsx
index 28f47657b..99ab59e01 100644
--- a/superset-frontend/src/components/AlteredSliceTag/index.tsx
+++ b/superset-frontend/src/components/AlteredSliceTag/index.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { isEqual, isEmpty } from 'lodash';
import { QueryFormData, styled, t } from '@superset-ui/core';
import { sanitizeFormData } from 'src/explore/exploreUtils/formData';
@@ -67,12 +67,6 @@ export type RowType = {
control: string;
};
-interface AlteredSliceTagState {
- rows: RowType[];
- hasDiffs: boolean;
- controlsMap: ControlMap;
-}
-
const StyledLabel = styled.span`
${({ theme }) => `
font-size: ${theme.typography.sizes.s}px;
@@ -85,7 +79,9 @@ const StyledLabel = styled.span`
`}
`;
-function alterForComparison(value?: string | null | []): string | null {
+export const alterForComparison = (
+ value?: string | null | [],
+): string | null => {
// Treat `null`, `undefined`, and empty strings as equivalent
if (value === undefined || value === null || value === '') {
return null;
@@ -98,48 +94,82 @@ function alterForComparison(value?: string | null | []): string | null {
return null;
}
return value;
-}
+};
-class AlteredSliceTag extends React.Component<
- AlteredSliceTagProps,
- AlteredSliceTagState
-> {
- constructor(props: AlteredSliceTagProps) {
- super(props);
- const diffs = this.getDiffs(props);
- const controlsMap: ControlMap = getControlsForVizType(
- props.origFormData.viz_type,
- ) as ControlMap;
- const rows = this.getRowsFromDiffs(diffs, controlsMap);
-
- this.state = { rows, hasDiffs: !isEmpty(diffs), controlsMap };
+export const formatValueHandler = (
+ value: DiffItemType,
+ key: string,
+ controlsMap: ControlMap,
+): string | number => {
+ if (value === undefined) {
+ return 'N/A';
}
-
- UNSAFE_componentWillReceiveProps(newProps: AlteredSliceTagProps): void {
- if (isEqual(this.props, newProps)) {
- return;
+ if (value === null) {
+ return 'null';
+ }
+ if (typeof value === 'boolean') {
+ return value ? 'true' : 'false';
+ }
+ if (controlsMap[key]?.type === 'AdhocFilterControl' && Array.isArray(value)) {
+ if (!value.length) {
+ return '[]';
}
- const diffs = this.getDiffs(newProps);
- this.setState(prevState => ({
- rows: this.getRowsFromDiffs(diffs, prevState.controlsMap),
- hasDiffs: !isEmpty(diffs),
- }));
+ return value
+ .map((v: FilterItemType) => {
+ const filterVal =
+ v.comparator && v.comparator.constructor === Array
+ ? `[${v.comparator.join(', ')}]`
+ : v.comparator;
+ return `${v.subject} ${v.operator} ${filterVal}`;
+ })
+ .join(', ');
}
-
- getRowsFromDiffs(
- diffs: { [key: string]: DiffType },
- controlsMap: ControlMap,
- ): RowType[] {
- return Object.entries(diffs).map(([key, diff]) => ({
- control: controlsMap[key]?.label || key,
- before: this.formatValue(diff.before, key, controlsMap),
- after: this.formatValue(diff.after, key, controlsMap),
- }));
+ if (controlsMap[key]?.type === 'BoundsControl') {
+ return `Min: ${value[0]}, Max: ${value[1]}`;
}
+ if (controlsMap[key]?.type === 'CollectionControl' && Array.isArray(value)) {
+ return value.map((v: FilterItemType) => safeStringify(v)).join(', ');
+ }
+ if (
+ controlsMap[key]?.type === 'MetricsControl' &&
+ value.constructor === Array
+ ) {
+ const formattedValue = value.map((v: FilterItemType) => v?.label ?? v);
+ return formattedValue.length ? formattedValue.join(', ') : '[]';
+ }
+ if (Array.isArray(value)) {
+ const formattedValue = value.map((v: FilterItemType) => v?.label ?? v);
+ return formattedValue.length ? formattedValue.join(', ') : '[]';
+ }
+ if (typeof value === 'string' || typeof value === 'number') {
+ return value;
+ }
+ return safeStringify(value);
+};
- getDiffs(props: AlteredSliceTagProps): { [key: string]: DiffType } {
+export const getRowsFromDiffs = (
+ diffs: { [key: string]: DiffType },
+ controlsMap: ControlMap,
+): RowType[] =>
+ Object.entries(diffs).map(([key, diff]) => ({
+ control: controlsMap[key]?.label || key,
+ before: formatValueHandler(diff.before, key, controlsMap),
+ after: formatValueHandler(diff.after, key, controlsMap),
+ }));
+
+export const isEqualish = (val1: string, val2: string): boolean =>
+ isEqual(alterForComparison(val1), alterForComparison(val2));
+
+const AlteredSliceTag: React.FC = props => {
+ const [rows, setRows] = useState([]);
+ const [hasDiffs, setHasDiffs] = useState(false);
+
+ const getDiffs = useCallback(() => {
+ // Returns all properties that differ in the
+ // current form data and the saved form data
const ofd = sanitizeFormData(props.origFormData);
const cfd = sanitizeFormData(props.currentFormData);
+
const fdKeys = Object.keys(cfd);
const diffs: { [key: string]: DiffType } = {};
fdKeys.forEach(fdKey => {
@@ -149,72 +179,23 @@ class AlteredSliceTag extends React.Component<
if (['filters', 'having', 'where'].includes(fdKey)) {
return;
}
- if (!this.isEqualish(ofd[fdKey], cfd[fdKey])) {
+ if (!isEqualish(ofd[fdKey], cfd[fdKey])) {
diffs[fdKey] = { before: ofd[fdKey], after: cfd[fdKey] };
}
});
return diffs;
- }
+ }, [props.currentFormData, props.origFormData]);
- isEqualish(val1: string, val2: string): boolean {
- return isEqual(alterForComparison(val1), alterForComparison(val2));
- }
+ useEffect(() => {
+ const diffs = getDiffs();
+ const controlsMap = getControlsForVizType(
+ props.origFormData?.viz_type,
+ ) as ControlMap;
+ setRows(getRowsFromDiffs(diffs, controlsMap));
+ setHasDiffs(!isEmpty(diffs));
+ }, [getDiffs, props.origFormData?.viz_type]);
- formatValue(
- value: DiffItemType,
- key: string,
- controlsMap: ControlMap,
- ): string | number {
- if (value === undefined) {
- return 'N/A';
- }
- if (value === null) {
- return 'null';
- }
- if (
- controlsMap[key]?.type === 'AdhocFilterControl' &&
- Array.isArray(value)
- ) {
- if (!value.length) {
- return '[]';
- }
- return value
- .map((v: FilterItemType) => {
- const filterVal =
- v.comparator && v.comparator.constructor === Array
- ? `[${v.comparator.join(', ')}]`
- : v.comparator;
- return `${v.subject} ${v.operator} ${filterVal}`;
- })
- .join(', ');
- }
- if (controlsMap[key]?.type === 'BoundsControl') {
- return `Min: ${value[0]}, Max: ${value[1]}`;
- }
- if (
- controlsMap[key]?.type === 'CollectionControl' &&
- Array.isArray(value)
- ) {
- return value.map(v => safeStringify(v)).join(', ');
- }
- if (controlsMap[key]?.type === 'MetricsControl' && Array.isArray(value)) {
- const formattedValue = value.map((v: FilterItemType) => v?.label ?? v);
- return formattedValue.length ? formattedValue.join(', ') : '[]';
- }
- if (typeof value === 'boolean') {
- return value ? 'true' : 'false';
- }
- if (Array.isArray(value)) {
- const formattedValue = value.map((v: FilterItemType) => v?.label ?? v);
- return formattedValue.length ? formattedValue.join(', ') : '[]';
- }
- if (typeof value === 'string' || typeof value === 'number') {
- return value;
- }
- return safeStringify(value);
- }
-
- renderModalBody(): React.ReactNode {
+ const modalBody = useMemo(() => {
const columns = [
{
accessor: 'control',
@@ -235,39 +216,35 @@ class AlteredSliceTag extends React.Component<
return (
);
- }
+ }, [rows]);
- renderTriggerNode(): React.ReactNode {
- return (
+ const triggerNode = useMemo(
+ () => (
{t('Altered')}
- );
+ ),
+ [],
+ );
+
+ if (!hasDiffs) {
+ return null;
}
- render() {
- // Return nothing if there are no differences
- if (!this.state.hasDiffs) {
- return null;
- }
- // Render the label-warning 'Altered' tag which the user may
- // click to open a modal containing a table summarizing the
- // differences in the slice
- return (
-
- );
- }
-}
+ return (
+
+ );
+};
export default AlteredSliceTag;