chore(AlteredSliceTag): Migrate to functional (#27891)

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>
This commit is contained in:
Ross Mabbett 2024-04-30 07:55:11 -04:00 committed by GitHub
parent 44690fb299
commit efda57e8a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 360 additions and 443 deletions

View File

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

View File

@ -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;