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