mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
feat(explore): Remove default for time range filter and Metrics (#14661)
* feat(explore): Remove default for time range filter and Metrics * Merge errors with same messages * Fix e2e test * Rename a variable * Bump packages * Fix unit tests
This commit is contained in:
committed by
GitHub
parent
add35f9fe4
commit
63dc035d6a
@@ -29,7 +29,7 @@ describe('Visualization > Line', () => {
|
|||||||
it('should show validator error when no metric', () => {
|
it('should show validator error when no metric', () => {
|
||||||
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
|
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(JSON.stringify(formData));
|
||||||
cy.get('.ant-alert-warning').contains(`"Metrics" cannot be empty`);
|
cy.get('.ant-alert-warning').contains(`Metrics: cannot be empty`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should preload mathjs', () => {
|
it('should preload mathjs', () => {
|
||||||
@@ -43,7 +43,7 @@ describe('Visualization > Line', () => {
|
|||||||
it('should not show validator error when metric added', () => {
|
it('should not show validator error when metric added', () => {
|
||||||
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
|
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(JSON.stringify(formData));
|
||||||
cy.get('.ant-alert-warning').contains(`"Metrics" cannot be empty`);
|
cy.get('.ant-alert-warning').contains(`Metrics: cannot be empty`);
|
||||||
cy.get('.text-danger').contains('Metrics');
|
cy.get('.text-danger').contains('Metrics');
|
||||||
|
|
||||||
cy.get('[data-test=metrics]')
|
cy.get('[data-test=metrics]')
|
||||||
@@ -62,6 +62,8 @@ describe('Visualization > Line', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should allow negative values in Y bounds', () => {
|
it('should allow negative values in Y bounds', () => {
|
||||||
|
const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
|
||||||
|
cy.visitChartByParams(JSON.stringify(formData));
|
||||||
cy.get('#controlSections-tab-display').click();
|
cy.get('#controlSections-tab-display').click();
|
||||||
cy.get('span').contains('Y Axis Bounds').scrollIntoView();
|
cy.get('span').contains('Y Axis Bounds').scrollIntoView();
|
||||||
cy.get('input[placeholder="Min"]').type('-0.1', { delay: 100 });
|
cy.get('input[placeholder="Min"]').type('-0.1', { delay: 100 });
|
||||||
|
|||||||
620
superset-frontend/package-lock.json
generated
620
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -67,35 +67,35 @@
|
|||||||
"@emotion/babel-preset-css-prop": "^11.2.0",
|
"@emotion/babel-preset-css-prop": "^11.2.0",
|
||||||
"@emotion/cache": "^11.1.3",
|
"@emotion/cache": "^11.1.3",
|
||||||
"@emotion/react": "^11.1.5",
|
"@emotion/react": "^11.1.5",
|
||||||
"@superset-ui/chart-controls": "^0.17.50",
|
"@superset-ui/chart-controls": "^0.17.52",
|
||||||
"@superset-ui/core": "^0.17.50",
|
"@superset-ui/core": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-calendar": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-calendar": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-chord": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-chord": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-country-map": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-country-map": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-event-flow": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-event-flow": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-force-directed": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-force-directed": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-heatmap": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-heatmap": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-histogram": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-histogram": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-horizon": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-horizon": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-map-box": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-map-box": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-partition": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-partition": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-rose": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-rose": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-sankey": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-sankey": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-sunburst": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-sunburst": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-treemap": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-treemap": "^0.17.52",
|
||||||
"@superset-ui/legacy-plugin-chart-world-map": "^0.17.50",
|
"@superset-ui/legacy-plugin-chart-world-map": "^0.17.52",
|
||||||
"@superset-ui/legacy-preset-chart-big-number": "^0.17.50",
|
"@superset-ui/legacy-preset-chart-big-number": "^0.17.52",
|
||||||
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.6",
|
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.6",
|
||||||
"@superset-ui/legacy-preset-chart-nvd3": "^0.17.50",
|
"@superset-ui/legacy-preset-chart-nvd3": "^0.17.52",
|
||||||
"@superset-ui/plugin-chart-echarts": "^0.17.50",
|
"@superset-ui/plugin-chart-echarts": "^0.17.52",
|
||||||
"@superset-ui/plugin-chart-pivot-table": "^0.17.50",
|
"@superset-ui/plugin-chart-pivot-table": "^0.17.52",
|
||||||
"@superset-ui/plugin-chart-table": "^0.17.50",
|
"@superset-ui/plugin-chart-table": "^0.17.52",
|
||||||
"@superset-ui/plugin-chart-word-cloud": "^0.17.50",
|
"@superset-ui/plugin-chart-word-cloud": "^0.17.52",
|
||||||
"@superset-ui/preset-chart-xy": "^0.17.50",
|
"@superset-ui/preset-chart-xy": "^0.17.52",
|
||||||
"@vx/responsive": "^0.0.195",
|
"@vx/responsive": "^0.0.195",
|
||||||
"abortcontroller-polyfill": "^1.1.9",
|
"abortcontroller-polyfill": "^1.1.9",
|
||||||
"antd": "^4.9.4",
|
"antd": "^4.9.4",
|
||||||
|
|||||||
@@ -149,30 +149,14 @@ describe('controlUtils', () => {
|
|||||||
expect(control).toBeNull();
|
expect(control).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies the default function for metrics', () => {
|
it('metrics control should be empty by default', () => {
|
||||||
const control = getControlState('metrics', 'table', state);
|
const control = getControlState('metrics', 'table', state);
|
||||||
expect(control?.default).toEqual(['first']);
|
expect(control?.default).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies the default function for metric', () => {
|
it('metric control should be empty by default', () => {
|
||||||
const control = getControlState('metric', 'table', state);
|
const control = getControlState('metric', 'table', state);
|
||||||
expect(control?.default).toEqual('first');
|
expect(control?.default).toBeUndefined();
|
||||||
});
|
|
||||||
|
|
||||||
it('applies the default function, prefers count if it exists', () => {
|
|
||||||
const stateWithCount = {
|
|
||||||
...state,
|
|
||||||
datasource: {
|
|
||||||
...(state.datasource as DatasourceMeta),
|
|
||||||
metrics: [
|
|
||||||
{ metric_name: 'first' },
|
|
||||||
{ metric_name: 'second' },
|
|
||||||
{ metric_name: 'count' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const control = getControlState('metrics', 'table', stateWithCount);
|
|
||||||
expect(control?.default).toEqual(['count']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not apply mapStateToProps when initializing', () => {
|
it('should not apply mapStateToProps when initializing', () => {
|
||||||
@@ -180,7 +164,6 @@ describe('controlUtils', () => {
|
|||||||
...state,
|
...state,
|
||||||
controls: undefined,
|
controls: undefined,
|
||||||
});
|
});
|
||||||
expect(typeof control?.default).toBe('function');
|
|
||||||
expect(control?.value).toBe(undefined);
|
expect(control?.value).toBe(undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -370,18 +370,34 @@ function ExploreViewContainer(props) {
|
|||||||
|
|
||||||
function renderErrorMessage() {
|
function renderErrorMessage() {
|
||||||
// Returns an error message as a node if any errors are in the store
|
// Returns an error message as a node if any errors are in the store
|
||||||
const errors = Object.entries(props.controls)
|
const controlsWithErrors = Object.values(props.controls).filter(
|
||||||
.filter(
|
control =>
|
||||||
([, control]) =>
|
control.validationErrors && control.validationErrors.length > 0,
|
||||||
control.validationErrors && control.validationErrors.length > 0,
|
);
|
||||||
)
|
if (controlsWithErrors.length === 0) {
|
||||||
.map(([key, control]) => (
|
return null;
|
||||||
<div key={key}>
|
}
|
||||||
{t('Control labeled ')}
|
|
||||||
<strong>{` "${control.label}" `}</strong>
|
const errorMessages = controlsWithErrors.map(
|
||||||
{control.validationErrors.join('. ')}
|
control => control.validationErrors,
|
||||||
|
);
|
||||||
|
const uniqueErrorMessages = [...new Set(errorMessages.flat())];
|
||||||
|
|
||||||
|
const errors = uniqueErrorMessages
|
||||||
|
.map(message => {
|
||||||
|
const matchingLabels = controlsWithErrors
|
||||||
|
.filter(control => control.validationErrors?.includes(message))
|
||||||
|
.map(control => control.label);
|
||||||
|
return [matchingLabels, message];
|
||||||
|
})
|
||||||
|
.map(([labels, message]) => (
|
||||||
|
<div key={message}>
|
||||||
|
{labels.length > 1 ? t('Controls labeled ') : t('Control labeled ')}
|
||||||
|
<strong>{` ${labels.join(', ')}`}</strong>
|
||||||
|
<span>: {message}</span>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
|
|
||||||
let errorMessage;
|
let errorMessage;
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
errorMessage = <div style={{ textAlign: 'left' }}>{errors}</div>;
|
errorMessage = <div style={{ textAlign: 'left' }}>{errors}</div>;
|
||||||
|
|||||||
@@ -106,4 +106,4 @@ export const TIME_FILTER_MAP = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// TODO: make this configurable per Superset installation
|
// TODO: make this configurable per Superset installation
|
||||||
export const DEFAULT_TIME_RANGE = 'Last week';
|
export const DEFAULT_TIME_RANGE = 'No filter';
|
||||||
|
|||||||
@@ -40,15 +40,17 @@ function execControlValidator<T = ControlType>(
|
|||||||
processedState: ControlState<T>,
|
processedState: ControlState<T>,
|
||||||
) {
|
) {
|
||||||
const validators = control.validators as ControlValueValidator[] | undefined;
|
const validators = control.validators as ControlValueValidator[] | undefined;
|
||||||
const validationErrors: ValidationError[] = [];
|
const { externalValidationErrors = [] } = control;
|
||||||
|
const errors: ValidationError[] = [];
|
||||||
if (validators && validators.length > 0) {
|
if (validators && validators.length > 0) {
|
||||||
validators.forEach(validator => {
|
validators.forEach(validator => {
|
||||||
const error = validator.call(control, control.value, processedState);
|
const error = validator.call(control, control.value, processedState);
|
||||||
if (error) {
|
if (error) {
|
||||||
validationErrors.push(error);
|
errors.push(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const validationErrors = [...errors, ...externalValidationErrors];
|
||||||
// always reset validation errors even when there is no validator
|
// always reset validation errors even when there is no validator
|
||||||
return { ...control, validationErrors };
|
return { ...control, validationErrors };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ import {
|
|||||||
legacyValidateInteger,
|
legacyValidateInteger,
|
||||||
validateNonEmpty,
|
validateNonEmpty,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { formatSelectOptions, mainMetric } from 'src/modules/utils';
|
import { formatSelectOptions } from 'src/modules/utils';
|
||||||
import { TIME_FILTER_LABELS } from './constants';
|
import { TIME_FILTER_LABELS } from './constants';
|
||||||
import { StyledColumnOption } from './components/optionRenderers';
|
import { StyledColumnOption } from './components/optionRenderers';
|
||||||
|
|
||||||
@@ -152,10 +152,6 @@ const metrics = {
|
|||||||
multi: true,
|
multi: true,
|
||||||
label: t('Metrics'),
|
label: t('Metrics'),
|
||||||
validators: [validateNonEmpty],
|
validators: [validateNonEmpty],
|
||||||
default: c => {
|
|
||||||
const metric = mainMetric(c.savedMetrics);
|
|
||||||
return metric ? [metric] : null;
|
|
||||||
},
|
|
||||||
mapStateToProps: state => {
|
mapStateToProps: state => {
|
||||||
const { datasource } = state;
|
const { datasource } = state;
|
||||||
return {
|
return {
|
||||||
@@ -171,7 +167,6 @@ const metric = {
|
|||||||
multi: false,
|
multi: false,
|
||||||
label: t('Metric'),
|
label: t('Metric'),
|
||||||
description: t('Metric'),
|
description: t('Metric'),
|
||||||
default: props => mainMetric(props.savedMetrics),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function columnChoices(datasource) {
|
export function columnChoices(datasource) {
|
||||||
@@ -346,7 +341,7 @@ export const controls = {
|
|||||||
type: 'DateFilterControl',
|
type: 'DateFilterControl',
|
||||||
freeForm: true,
|
freeForm: true,
|
||||||
label: TIME_FILTER_LABELS.time_range,
|
label: TIME_FILTER_LABELS.time_range,
|
||||||
default: t('Last week'), // this value is translated, but the backend wouldn't understand a translated value?
|
default: t('No filter'), // this value is translated, but the backend wouldn't understand a translated value?
|
||||||
description: t(
|
description: t(
|
||||||
'The time range for the visualization. All relative times, e.g. "Last month", ' +
|
'The time range for the visualization. All relative times, e.g. "Last month", ' +
|
||||||
'"Last 7 days", "now", etc. are evaluated on the server using the server\'s ' +
|
'"Last 7 days", "now", etc. are evaluated on the server using the server\'s ' +
|
||||||
|
|||||||
@@ -135,6 +135,24 @@ export default function exploreReducer(state = {}, action) {
|
|||||||
...getControlStateFromControlConfig(controlConfig, state, action.value),
|
...getControlStateFromControlConfig(controlConfig, state, action.value),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
controls: { ...state.controls, [action.controlName]: control },
|
||||||
|
};
|
||||||
|
|
||||||
|
const rerenderedControls = {};
|
||||||
|
if (Array.isArray(control.rerender)) {
|
||||||
|
control.rerender.forEach(controlName => {
|
||||||
|
rerenderedControls[controlName] = {
|
||||||
|
...getControlStateFromControlConfig(
|
||||||
|
newState.controls[controlName],
|
||||||
|
newState,
|
||||||
|
newState.controls[controlName].value,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// combine newly detected errors with errors from `onChange` event of
|
// combine newly detected errors with errors from `onChange` event of
|
||||||
// each control component (passed via reducer action).
|
// each control component (passed via reducer action).
|
||||||
const errors = control.validationErrors || [];
|
const errors = control.validationErrors || [];
|
||||||
@@ -169,6 +187,7 @@ export default function exploreReducer(state = {}, action) {
|
|||||||
...control,
|
...control,
|
||||||
validationErrors: errors,
|
validationErrors: errors,
|
||||||
},
|
},
|
||||||
|
...rerenderedControls,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user