fix: Color consistency (#17089)
* Update label colors on the fly * Clean up * Improve getFormDataWithExtraFilters * Improve code structure * Remove labelColors from formData * Exclude label_colors from URL * Refactor color scheme implementation * Clean up * Refactor and simplify * Fix lint * Remove unnecessary ColorMapControl * Lint * Give json color scheme precedence * Add label_colors prop in metadata * Separate owners and dashboard meta requests * Remove label_colors control * bump superset-ui 0.18.19 * Fix end of file * Update tests * Fix lint * Update Cypress * Update setColorScheme method * Use Antd modal body
This commit is contained in:
parent
85a19a9cc2
commit
59a6502efe
|
|
@ -24,10 +24,10 @@ import { WORLD_HEALTH_DASHBOARD } from './dashboard.helper';
|
|||
|
||||
function selectColorScheme(color: string) {
|
||||
// open color scheme dropdown
|
||||
cy.get('.modal-body')
|
||||
.contains('Color Scheme')
|
||||
cy.get('.ant-modal-body')
|
||||
.contains('Color scheme')
|
||||
.parents('.ControlHeader')
|
||||
.next('.Select')
|
||||
.next('.ant-select')
|
||||
.click()
|
||||
.then($colorSelect => {
|
||||
// select a new color scheme
|
||||
|
|
@ -37,7 +37,7 @@ function selectColorScheme(color: string) {
|
|||
|
||||
function assertMetadata(text: string) {
|
||||
const regex = new RegExp(text);
|
||||
cy.get('.modal-body')
|
||||
cy.get('.ant-modal-body')
|
||||
.find('#json_metadata')
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
|
|
@ -50,12 +50,15 @@ function assertMetadata(text: string) {
|
|||
}
|
||||
|
||||
function typeMetadata(text: string) {
|
||||
cy.get('.modal-body').find('#json_metadata').should('be.visible').type(text);
|
||||
cy.get('.ant-modal-body')
|
||||
.find('#json_metadata')
|
||||
.should('be.visible')
|
||||
.type(text);
|
||||
}
|
||||
|
||||
function openAdvancedProperties() {
|
||||
return cy
|
||||
.get('.modal-body')
|
||||
.get('.ant-modal-body')
|
||||
.contains('Advanced')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
|
|
@ -94,6 +97,10 @@ describe('Dashboard edit action', () => {
|
|||
.get('[data-test="dashboard-title-input"]')
|
||||
.type(`{selectall}{backspace}${dashboardTitle}`);
|
||||
|
||||
cy.wait('@dashboardGet').then(() => {
|
||||
selectColorScheme('d3Category20b');
|
||||
});
|
||||
|
||||
// save edit changes
|
||||
cy.get('.ant-modal-footer')
|
||||
.contains('Save')
|
||||
|
|
@ -146,7 +153,7 @@ describe('Dashboard edit action', () => {
|
|||
.click()
|
||||
.then(() => {
|
||||
// assert that modal edit window has closed
|
||||
cy.get('.modal-body').should('not.exist');
|
||||
cy.get('.ant-modal-body').should('not.exist');
|
||||
|
||||
// assert color has been updated
|
||||
openDashboardEditProperties();
|
||||
|
|
@ -177,7 +184,7 @@ describe('Dashboard edit action', () => {
|
|||
.click()
|
||||
.then(() => {
|
||||
// assert that modal edit window has closed
|
||||
cy.get('.modal-body')
|
||||
cy.get('.ant-modal-body')
|
||||
.contains('A valid color scheme is required')
|
||||
.should('be.visible');
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -68,35 +68,35 @@
|
|||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@superset-ui/chart-controls": "^0.18.18",
|
||||
"@superset-ui/core": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-calendar": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-chord": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-country-map": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-event-flow": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-force-directed": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-heatmap": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-histogram": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-horizon": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-map-box": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-partition": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-rose": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-sankey": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-sunburst": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-treemap": "^0.18.18",
|
||||
"@superset-ui/legacy-plugin-chart-world-map": "^0.18.18",
|
||||
"@superset-ui/legacy-preset-chart-big-number": "^0.18.18",
|
||||
"@superset-ui/chart-controls": "^0.18.19",
|
||||
"@superset-ui/core": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-calendar": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-chord": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-country-map": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-event-flow": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-force-directed": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-heatmap": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-histogram": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-horizon": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-map-box": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-partition": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-rose": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-sankey": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-sunburst": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-treemap": "^0.18.19",
|
||||
"@superset-ui/legacy-plugin-chart-world-map": "^0.18.19",
|
||||
"@superset-ui/legacy-preset-chart-big-number": "^0.18.19",
|
||||
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.13",
|
||||
"@superset-ui/legacy-preset-chart-nvd3": "^0.18.18",
|
||||
"@superset-ui/plugin-chart-echarts": "^0.18.18",
|
||||
"@superset-ui/plugin-chart-pivot-table": "^0.18.18",
|
||||
"@superset-ui/plugin-chart-table": "^0.18.18",
|
||||
"@superset-ui/plugin-chart-word-cloud": "^0.18.18",
|
||||
"@superset-ui/preset-chart-xy": "^0.18.18",
|
||||
"@superset-ui/legacy-preset-chart-nvd3": "^0.18.19",
|
||||
"@superset-ui/plugin-chart-echarts": "^0.18.19",
|
||||
"@superset-ui/plugin-chart-pivot-table": "^0.18.19",
|
||||
"@superset-ui/plugin-chart-table": "^0.18.19",
|
||||
"@superset-ui/plugin-chart-word-cloud": "^0.18.19",
|
||||
"@superset-ui/preset-chart-xy": "^0.18.19",
|
||||
"@vx/responsive": "^0.0.195",
|
||||
"abortcontroller-polyfill": "^1.1.9",
|
||||
"antd": "^4.9.4",
|
||||
|
|
|
|||
|
|
@ -94,10 +94,12 @@ describe('PropertiesModal', () => {
|
|||
describe('without metadata', () => {
|
||||
const wrapper = setup({ colorScheme: 'SUPERSET_DEFAULT' });
|
||||
const modalInstance = wrapper.find('PropertiesModal').instance();
|
||||
it('does not update the color scheme in the metadata', () => {
|
||||
it('updates the color scheme in the metadata', () => {
|
||||
const spy = jest.spyOn(modalInstance, 'onMetadataChange');
|
||||
modalInstance.onColorSchemeChange('SUPERSET_DEFAULT');
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
'{"something": "foo", "color_scheme": "SUPERSET_DEFAULT", "label_colors": {}}',
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('with metadata', () => {
|
||||
|
|
@ -125,10 +127,12 @@ describe('PropertiesModal', () => {
|
|||
json_metadata: '{"timed_refresh_immune_slices": []}',
|
||||
},
|
||||
});
|
||||
it('will not update the metadata', () => {
|
||||
it('will update the metadata', () => {
|
||||
const spy = jest.spyOn(modalInstance, 'onMetadataChange');
|
||||
modalInstance.onColorSchemeChange('SUPERSET_DEFAULT');
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
'{"something": "foo", "color_scheme": "SUPERSET_DEFAULT", "label_colors": {}}',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ const propTypes = {
|
|||
// formData contains chart's own filter parameter
|
||||
// and merged with extra filter that current dashboard applying
|
||||
formData: PropTypes.object.isRequired,
|
||||
labelColors: PropTypes.object,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
setControlValue: PropTypes.func,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ const propTypes = {
|
|||
datasource: PropTypes.object,
|
||||
initialValues: PropTypes.object,
|
||||
formData: PropTypes.object.isRequired,
|
||||
labelColors: PropTypes.object,
|
||||
height: PropTypes.number,
|
||||
width: PropTypes.number,
|
||||
setControlValue: PropTypes.func,
|
||||
|
|
@ -100,6 +101,7 @@ class ChartRenderer extends React.Component {
|
|||
nextProps.height !== this.props.height ||
|
||||
nextProps.width !== this.props.width ||
|
||||
nextProps.triggerRender ||
|
||||
nextProps.labelColors !== this.props.labelColors ||
|
||||
nextProps.formData.color_scheme !== this.props.formData.color_scheme ||
|
||||
nextProps.cacheBusterProp !== this.props.cacheBusterProp
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,13 +17,32 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { Dispatch } from 'redux';
|
||||
import { makeApi } from '@superset-ui/core';
|
||||
import { makeApi, CategoricalColorNamespace } from '@superset-ui/core';
|
||||
import { isString } from 'lodash';
|
||||
import { ChartConfiguration, DashboardInfo } from '../reducers/types';
|
||||
|
||||
export const DASHBOARD_INFO_UPDATED = 'DASHBOARD_INFO_UPDATED';
|
||||
|
||||
// updates partially changed dashboard info
|
||||
export function dashboardInfoChanged(newInfo: { metadata: any }) {
|
||||
const { metadata } = newInfo;
|
||||
|
||||
const categoricalNamespace = CategoricalColorNamespace.getNamespace(
|
||||
metadata?.color_namespace,
|
||||
);
|
||||
|
||||
categoricalNamespace.resetColors();
|
||||
|
||||
if (metadata?.label_colors) {
|
||||
const labelColors = metadata.label_colors;
|
||||
const colorMap = isString(labelColors)
|
||||
? JSON.parse(labelColors)
|
||||
: labelColors;
|
||||
Object.keys(colorMap).forEach(label => {
|
||||
categoricalNamespace.setColor(label, colorMap[label]);
|
||||
});
|
||||
}
|
||||
|
||||
return { type: DASHBOARD_INFO_UPDATED, newInfo };
|
||||
}
|
||||
export const SET_CHART_CONFIG_BEGIN = 'SET_CHART_CONFIG_BEGIN';
|
||||
|
|
|
|||
|
|
@ -88,16 +88,16 @@ export const hydrateDashboard = (dashboardData, chartData) => (
|
|||
// Priming the color palette with user's label-color mapping provided in
|
||||
// the dashboard's JSON metadata
|
||||
if (metadata?.label_colors) {
|
||||
const scheme = metadata.color_scheme;
|
||||
const namespace = metadata.color_namespace;
|
||||
const colorMap = isString(metadata.label_colors)
|
||||
? JSON.parse(metadata.label_colors)
|
||||
: metadata.label_colors;
|
||||
const categoricalNamespace = CategoricalColorNamespace.getNamespace(
|
||||
namespace,
|
||||
);
|
||||
|
||||
Object.keys(colorMap).forEach(label => {
|
||||
CategoricalColorNamespace.getScale(scheme, namespace).setColor(
|
||||
label,
|
||||
colorMap[label],
|
||||
);
|
||||
categoricalNamespace.setColor(label, colorMap[label]);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { styled, CategoricalColorNamespace, t } from '@superset-ui/core';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import ButtonGroup from 'src/components/ButtonGroup';
|
||||
|
||||
import {
|
||||
|
|
@ -348,19 +348,10 @@ class Header extends React.PureComponent {
|
|||
lastModifiedTime,
|
||||
} = this.props;
|
||||
|
||||
const scale = CategoricalColorNamespace.getScale(
|
||||
colorScheme,
|
||||
colorNamespace,
|
||||
);
|
||||
|
||||
// use the colorScheme for default labels
|
||||
let labelColors = colorScheme ? scale.getColorMap() : {};
|
||||
// but allow metadata to overwrite if it exists
|
||||
// eslint-disable-next-line camelcase
|
||||
const metadataLabelColors = dashboardInfo.metadata?.label_colors;
|
||||
if (metadataLabelColors) {
|
||||
labelColors = { ...labelColors, ...metadataLabelColors };
|
||||
}
|
||||
const labelColors =
|
||||
colorScheme && dashboardInfo?.metadata?.label_colors
|
||||
? dashboardInfo.metadata.label_colors
|
||||
: {};
|
||||
|
||||
// check refresh frequency is for current session or persist
|
||||
const refreshFrequency = shouldPersistRefreshFrequency
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import {
|
|||
t,
|
||||
SupersetClient,
|
||||
getCategoricalSchemeRegistry,
|
||||
CategoricalColorNamespace,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
import Modal from 'src/components/Modal';
|
||||
|
|
@ -140,14 +139,15 @@ class PropertiesModal extends React.PureComponent {
|
|||
JsonEditor.preload();
|
||||
}
|
||||
|
||||
onColorSchemeChange(value, { updateMetadata = true } = {}) {
|
||||
onColorSchemeChange(colorScheme, { updateMetadata = true } = {}) {
|
||||
// check that color_scheme is valid
|
||||
const colorChoices = getCategoricalSchemeRegistry().keys();
|
||||
const { json_metadata: jsonMetadata } = this.state.values;
|
||||
const jsonMetadataObj = jsonMetadata?.length
|
||||
? JSON.parse(jsonMetadata)
|
||||
: {};
|
||||
if (!colorChoices.includes(value)) {
|
||||
|
||||
if (!colorScheme || !colorChoices.includes(colorScheme)) {
|
||||
Modal.error({
|
||||
title: 'Error',
|
||||
content: t('A valid color scheme is required'),
|
||||
|
|
@ -157,24 +157,14 @@ class PropertiesModal extends React.PureComponent {
|
|||
}
|
||||
|
||||
// update metadata to match selection
|
||||
if (
|
||||
updateMetadata &&
|
||||
Object.keys(jsonMetadataObj).includes('color_scheme')
|
||||
) {
|
||||
jsonMetadataObj.color_scheme = value;
|
||||
jsonMetadataObj.label_colors = Object.keys(
|
||||
jsonMetadataObj.label_colors ?? {},
|
||||
).reduce(
|
||||
(prev, next) => ({
|
||||
...prev,
|
||||
[next]: CategoricalColorNamespace.getScale(value)(next),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
if (updateMetadata) {
|
||||
jsonMetadataObj.color_scheme = colorScheme;
|
||||
jsonMetadataObj.label_colors = jsonMetadataObj.label_colors || {};
|
||||
|
||||
this.onMetadataChange(jsonStringify(jsonMetadataObj));
|
||||
}
|
||||
|
||||
this.updateFormState('colorScheme', value);
|
||||
this.updateFormState('colorScheme', colorScheme);
|
||||
}
|
||||
|
||||
onOwnersChange(value) {
|
||||
|
|
@ -261,21 +251,22 @@ class PropertiesModal extends React.PureComponent {
|
|||
roles: rolesValue,
|
||||
},
|
||||
} = this.state;
|
||||
|
||||
const { onlyApply } = this.props;
|
||||
const owners = ownersValue?.map(o => o.value) ?? [];
|
||||
const roles = rolesValue?.map(o => o.value) ?? [];
|
||||
let metadataColorScheme;
|
||||
let currentColorScheme = colorScheme;
|
||||
|
||||
// update color scheme to match metadata
|
||||
// color scheme in json metadata has precedence over selection
|
||||
if (jsonMetadata?.length) {
|
||||
const { color_scheme: metadataColorScheme } = JSON.parse(jsonMetadata);
|
||||
if (metadataColorScheme) {
|
||||
this.onColorSchemeChange(metadataColorScheme, {
|
||||
updateMetadata: false,
|
||||
});
|
||||
}
|
||||
const metadata = JSON.parse(jsonMetadata);
|
||||
currentColorScheme = metadata?.color_scheme || colorScheme;
|
||||
}
|
||||
|
||||
this.onColorSchemeChange(currentColorScheme, {
|
||||
updateMetadata: false,
|
||||
});
|
||||
|
||||
const moreProps = {};
|
||||
const morePutProps = {};
|
||||
if (isFeatureEnabled(FeatureFlag.DASHBOARD_RBAC)) {
|
||||
|
|
@ -289,7 +280,7 @@ class PropertiesModal extends React.PureComponent {
|
|||
slug,
|
||||
jsonMetadata,
|
||||
ownerIds: owners,
|
||||
colorScheme: metadataColorScheme || colorScheme,
|
||||
colorScheme: currentColorScheme,
|
||||
...moreProps,
|
||||
});
|
||||
this.props.onHide();
|
||||
|
|
@ -316,7 +307,7 @@ class PropertiesModal extends React.PureComponent {
|
|||
slug: result.slug,
|
||||
jsonMetadata: result.json_metadata,
|
||||
ownerIds: result.owners,
|
||||
colorScheme: metadataColorScheme || colorScheme,
|
||||
colorScheme: currentColorScheme,
|
||||
...moreResultProps,
|
||||
});
|
||||
this.props.onHide();
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import React from 'react';
|
|||
import { Radio } from 'src/components/Radio';
|
||||
import { RadioChangeEvent, Input } from 'src/common/components';
|
||||
import Button from 'src/components/Button';
|
||||
import { t, CategoricalColorNamespace, JsonResponse } from '@superset-ui/core';
|
||||
import { t, JsonResponse } from '@superset-ui/core';
|
||||
|
||||
import ModalTrigger from 'src/components/ModalTrigger';
|
||||
import Checkbox from 'src/components/Checkbox';
|
||||
|
|
@ -131,11 +131,11 @@ class SaveModal extends React.PureComponent<SaveModalProps, SaveModalState> {
|
|||
lastModifiedTime,
|
||||
} = this.props;
|
||||
|
||||
const scale = CategoricalColorNamespace.getScale(
|
||||
colorScheme,
|
||||
colorNamespace,
|
||||
);
|
||||
const labelColors = colorScheme ? scale.getColorMap() : {};
|
||||
const labelColors =
|
||||
colorScheme && dashboardInfo?.metadata?.label_colors
|
||||
? dashboardInfo.metadata.label_colors
|
||||
: {};
|
||||
|
||||
// check refresh frequency is for current session or persist
|
||||
const refreshFrequency = shouldPersistRefreshFrequency
|
||||
? currentRefreshFrequency
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ const propTypes = {
|
|||
// from redux
|
||||
chart: chartPropShape.isRequired,
|
||||
formData: PropTypes.object.isRequired,
|
||||
labelColors: PropTypes.object,
|
||||
datasource: PropTypes.object,
|
||||
slice: slicePropShape.isRequired,
|
||||
sliceName: PropTypes.string.isRequired,
|
||||
|
|
@ -277,6 +278,7 @@ export default class Chart extends React.Component {
|
|||
editMode,
|
||||
filters,
|
||||
formData,
|
||||
labelColors,
|
||||
updateSliceName,
|
||||
sliceName,
|
||||
toggleExpandSlice,
|
||||
|
|
@ -399,6 +401,7 @@ export default class Chart extends React.Component {
|
|||
dashboardId={dashboardId}
|
||||
initialValues={initialValues}
|
||||
formData={formData}
|
||||
labelColors={labelColors}
|
||||
ownState={ownState}
|
||||
filterState={filterState}
|
||||
queriesResponse={chart.queriesResponse}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ function mapStateToProps(
|
|||
(chart && chart.form_data && datasources[chart.form_data.datasource]) ||
|
||||
PLACEHOLDER_DATASOURCE;
|
||||
const { colorScheme, colorNamespace } = dashboardState;
|
||||
|
||||
const labelColors = dashboardInfo?.metadata?.label_colors || {};
|
||||
// note: this method caches filters if possible to prevent render cascades
|
||||
const formData = getFormDataWithExtraFilters({
|
||||
layout: dashboardLayout.present,
|
||||
|
|
@ -75,6 +75,7 @@ function mapStateToProps(
|
|||
sliceId: id,
|
||||
nativeFilters,
|
||||
dataMask,
|
||||
labelColors,
|
||||
});
|
||||
|
||||
formData.dashboardId = dashboardInfo.id;
|
||||
|
|
@ -82,6 +83,7 @@ function mapStateToProps(
|
|||
return {
|
||||
chart,
|
||||
datasource,
|
||||
labelColors,
|
||||
slice: sliceEntities.slices[id],
|
||||
timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
|
||||
filters: getActiveFilters() || EMPTY_OBJECT,
|
||||
|
|
|
|||
|
|
@ -16,11 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
CategoricalColorNamespace,
|
||||
DataRecordFilters,
|
||||
JsonObject,
|
||||
} from '@superset-ui/core';
|
||||
import { DataRecordFilters, JsonObject } from '@superset-ui/core';
|
||||
import { ChartQueryPayload, Charts, LayoutItem } from 'src/dashboard/types';
|
||||
import { getExtraFormData } from 'src/dashboard/components/nativeFilters/utils';
|
||||
import { DataMaskStateWithId } from 'src/dataMask/types';
|
||||
|
|
@ -45,6 +41,7 @@ export interface GetFormDataWithExtraFiltersArguments {
|
|||
sliceId: number;
|
||||
dataMask: DataMaskStateWithId;
|
||||
nativeFilters: NativeFiltersState;
|
||||
labelColors?: Record<string, string>;
|
||||
}
|
||||
|
||||
// this function merge chart's formData with dashboard filters value,
|
||||
|
|
@ -61,11 +58,8 @@ export default function getFormDataWithExtraFilters({
|
|||
sliceId,
|
||||
layout,
|
||||
dataMask,
|
||||
labelColors,
|
||||
}: GetFormDataWithExtraFiltersArguments) {
|
||||
// Propagate color mapping to chart
|
||||
const scale = CategoricalColorNamespace.getScale(colorScheme, colorNamespace);
|
||||
const labelColors = scale.getColorMap();
|
||||
|
||||
// if dashboard metadata + filters have not changed, use cache if possible
|
||||
const cachedFormData = cachedFormdataByChart[sliceId];
|
||||
if (
|
||||
|
|
@ -109,11 +103,12 @@ export default function getFormDataWithExtraFilters({
|
|||
|
||||
const formData = {
|
||||
...chart.formData,
|
||||
...(colorScheme && { color_scheme: colorScheme }),
|
||||
label_colors: labelColors,
|
||||
...(colorScheme && { color_scheme: colorScheme }),
|
||||
extra_filters: getEffectiveExtraFilters(filters),
|
||||
...extraData,
|
||||
};
|
||||
|
||||
cachedFiltersByChart[sliceId] = filters;
|
||||
cachedFormdataByChart[sliceId] = { ...formData, dataMask };
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,9 @@ export default function Control(props: ControlProps) {
|
|||
|
||||
const ControlComponent = typeof type === 'string' ? controlMap[type] : type;
|
||||
if (!ControlComponent) {
|
||||
return <>Unknown controlType: {type}</>;
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`Unknown controlType: ${type}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -21,7 +21,12 @@ import { connect } from 'react-redux';
|
|||
import { bindActionCreators } from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import {
|
||||
CategoricalColorNamespace,
|
||||
SupersetClient,
|
||||
styled,
|
||||
t,
|
||||
} from '@superset-ui/core';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import ReportModal from 'src/components/ReportModal';
|
||||
import {
|
||||
|
|
@ -53,6 +58,7 @@ const propTypes = {
|
|||
addHistory: PropTypes.func,
|
||||
can_overwrite: PropTypes.bool.isRequired,
|
||||
can_download: PropTypes.bool.isRequired,
|
||||
dashboardId: PropTypes.number,
|
||||
isStarred: PropTypes.bool.isRequired,
|
||||
slice: PropTypes.object,
|
||||
sliceName: PropTypes.string,
|
||||
|
|
@ -114,9 +120,11 @@ export class ExploreChartHeader extends React.PureComponent {
|
|||
this.showReportModal = this.showReportModal.bind(this);
|
||||
this.hideReportModal = this.hideReportModal.bind(this);
|
||||
this.renderReportModal = this.renderReportModal.bind(this);
|
||||
this.fetchChartDashboardData = this.fetchChartDashboardData.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { dashboardId } = this.props;
|
||||
if (this.canAddReports()) {
|
||||
const { user, chart } = this.props;
|
||||
// this is in the case that there is an anonymous user.
|
||||
|
|
@ -127,6 +135,33 @@ export class ExploreChartHeader extends React.PureComponent {
|
|||
chart.id,
|
||||
);
|
||||
}
|
||||
if (dashboardId) {
|
||||
this.fetchChartDashboardData();
|
||||
}
|
||||
}
|
||||
|
||||
async fetchChartDashboardData() {
|
||||
const { dashboardId, slice } = this.props;
|
||||
const response = await SupersetClient.get({
|
||||
endpoint: `/api/v1/chart/${slice.slice_id}`,
|
||||
});
|
||||
const chart = response.json.result;
|
||||
const dashboards = chart.dashboards || [];
|
||||
const dashboard =
|
||||
dashboardId &&
|
||||
dashboards.length &&
|
||||
dashboards.find(d => d.id === dashboardId);
|
||||
|
||||
if (dashboard && dashboard.json_metadata) {
|
||||
// setting the chart to use the dashboard custom label colors if any
|
||||
const labelColors =
|
||||
JSON.parse(dashboard.json_metadata).label_colors || {};
|
||||
const categoricalNamespace = CategoricalColorNamespace.getNamespace();
|
||||
|
||||
Object.keys(labelColors).forEach(label => {
|
||||
categoricalNamespace.setColor(label, labelColors[label]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSliceName() {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ const propTypes = {
|
|||
can_overwrite: PropTypes.bool.isRequired,
|
||||
can_download: PropTypes.bool.isRequired,
|
||||
datasource: PropTypes.object,
|
||||
dashboardId: PropTypes.number,
|
||||
column_formats: PropTypes.object,
|
||||
containerId: PropTypes.string.isRequired,
|
||||
height: PropTypes.string.isRequired,
|
||||
|
|
@ -291,6 +292,7 @@ const ExploreChartPanel = props => {
|
|||
addHistory={props.addHistory}
|
||||
can_overwrite={props.can_overwrite}
|
||||
can_download={props.can_download}
|
||||
dashboardId={props.dashboardId}
|
||||
isStarred={props.isStarred}
|
||||
slice={props.slice}
|
||||
sliceName={props.sliceName}
|
||||
|
|
|
|||
|
|
@ -591,6 +591,7 @@ function mapStateToProps(state) {
|
|||
);
|
||||
const chartKey = Object.keys(charts)[0];
|
||||
const chart = charts[chartKey];
|
||||
|
||||
return {
|
||||
isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
|
||||
datasource: explore.datasource,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import React, { useMemo, useState, useCallback, useEffect } from 'react';
|
||||
import Modal from 'src/components/Modal';
|
||||
import { Row, Col, Input, TextArea } from 'src/common/components';
|
||||
import Button from 'src/components/Button';
|
||||
|
|
@ -33,6 +33,8 @@ type PropertiesModalProps = {
|
|||
show: boolean;
|
||||
onHide: () => void;
|
||||
onSave: (chart: Chart) => void;
|
||||
permissionsError?: string;
|
||||
existingOwners?: SelectValue;
|
||||
};
|
||||
|
||||
export default function PropertiesModal({
|
||||
|
|
@ -42,16 +44,15 @@ export default function PropertiesModal({
|
|||
show,
|
||||
}: PropertiesModalProps) {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const [selectedOwners, setSelectedOwners] = useState<SelectValue | null>(
|
||||
null,
|
||||
);
|
||||
// values of form inputs
|
||||
const [name, setName] = useState(slice.slice_name || '');
|
||||
const [description, setDescription] = useState(slice.description || '');
|
||||
const [cacheTimeout, setCacheTimeout] = useState(
|
||||
slice.cache_timeout != null ? slice.cache_timeout : '',
|
||||
);
|
||||
const [selectedOwners, setSelectedOwners] = useState<SelectValue | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
function showError({ error, statusText, message }: any) {
|
||||
let errorText = error || statusText || t('An error has occurred');
|
||||
|
|
@ -65,8 +66,8 @@ export default function PropertiesModal({
|
|||
});
|
||||
}
|
||||
|
||||
const fetchChartData = useCallback(
|
||||
async function fetchChartData() {
|
||||
const fetchChartOwners = useCallback(
|
||||
async function fetchChartOwners() {
|
||||
try {
|
||||
const response = await SupersetClient.get({
|
||||
endpoint: `/api/v1/chart/${slice.slice_id}`,
|
||||
|
|
@ -143,8 +144,8 @@ export default function PropertiesModal({
|
|||
|
||||
// get the owners of this slice
|
||||
useEffect(() => {
|
||||
fetchChartData();
|
||||
}, [fetchChartData]);
|
||||
fetchChartOwners();
|
||||
}, [fetchChartOwners]);
|
||||
|
||||
// update name after it's changed in another modal
|
||||
useEffect(() => {
|
||||
|
|
@ -242,8 +243,8 @@ export default function PropertiesModal({
|
|||
mode="multiple"
|
||||
name="owners"
|
||||
value={selectedOwners || []}
|
||||
options={loadOptions}
|
||||
onChange={setSelectedOwners}
|
||||
options={loadOptions}
|
||||
disabled={!selectedOwners}
|
||||
allowClear
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { CategoricalColorNamespace } from '@superset-ui/core';
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.object,
|
||||
colorScheme: PropTypes.string,
|
||||
colorNamespace: PropTypes.string,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
value: {},
|
||||
colorScheme: undefined,
|
||||
colorNamespace: undefined,
|
||||
};
|
||||
|
||||
export default class ColorMapControl extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
Object.keys(this.props.value).forEach(label => {
|
||||
CategoricalColorNamespace.getScale(
|
||||
this.props.colorScheme,
|
||||
this.props.colorNamespace,
|
||||
).setColor(label, this.props.value[label]);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ColorMapControl.propTypes = propTypes;
|
||||
ColorMapControl.defaultProps = defaultProps;
|
||||
|
|
@ -21,7 +21,6 @@ import AnnotationLayerControl from './AnnotationLayerControl';
|
|||
import BoundsControl from './BoundsControl';
|
||||
import CheckboxControl from './CheckboxControl';
|
||||
import CollectionControl from './CollectionControl';
|
||||
import ColorMapControl from './ColorMapControl';
|
||||
import ColorPickerControl from './ColorPickerControl';
|
||||
import ColorSchemeControl from './ColorSchemeControl';
|
||||
import DatasourceControl from './DatasourceControl';
|
||||
|
|
@ -52,7 +51,6 @@ const controlMap = {
|
|||
BoundsControl,
|
||||
CheckboxControl,
|
||||
CollectionControl,
|
||||
ColorMapControl,
|
||||
ColorPickerControl,
|
||||
ColorSchemeControl,
|
||||
DatasourceControl,
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export const datasourceAndVizType: ControlPanelSectionConfig = {
|
|||
|
||||
export const colorScheme: ControlPanelSectionConfig = {
|
||||
label: t('Color scheme'),
|
||||
controlSetRows: [['color_scheme', 'label_colors']],
|
||||
controlSetRows: [['color_scheme']],
|
||||
};
|
||||
|
||||
export const sqlaTimeSeries: ControlPanelSectionConfig = {
|
||||
|
|
|
|||
|
|
@ -476,15 +476,4 @@ export const controls = {
|
|||
description: t('The color scheme for rendering chart'),
|
||||
schemes: () => categoricalSchemeRegistry.getMap(),
|
||||
},
|
||||
|
||||
label_colors: {
|
||||
type: 'ColorMapControl',
|
||||
label: t('Color map'),
|
||||
default: {},
|
||||
renderTrigger: true,
|
||||
mapStateToProps: state => ({
|
||||
colorNamespace: state.form_data.color_namespace,
|
||||
colorScheme: state.form_data.color_scheme,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -132,7 +132,11 @@ export function getExploreUrlFromDashboard(formData) {
|
|||
// These are present when generating explore urls from the dashboard page.
|
||||
// This should be superseded by some sort of "exploration context" system
|
||||
// where form data and other context is referenced by id.
|
||||
const trimmedFormData = omit(formData, ['dataMask', 'url_params']);
|
||||
const trimmedFormData = omit(formData, [
|
||||
'dataMask',
|
||||
'url_params',
|
||||
'label_colors',
|
||||
]);
|
||||
return getExploreLongUrl(trimmedFormData, null, false);
|
||||
}
|
||||
|
||||
|
|
@ -169,6 +173,11 @@ export function getExploreUrl({
|
|||
if (!formData.datasource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// label_colors should not pollute the URL
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
delete formData.label_colors;
|
||||
|
||||
let uri = getChartDataUri({ path: '/', allowDomainSharding });
|
||||
if (curUrl) {
|
||||
uri = URI(URI(curUrl).search());
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { getChartControlPanelRegistry, t } from '@superset-ui/core';
|
||||
import { getChartControlPanelRegistry } from '@superset-ui/core';
|
||||
import getControlsForVizType from 'src/utils/getControlsForVizType';
|
||||
|
||||
const fakePluginControls = {
|
||||
|
|
@ -26,7 +26,6 @@ const fakePluginControls = {
|
|||
label: 'Fake Control Panel Sections',
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['label_colors'],
|
||||
[
|
||||
{
|
||||
name: 'y_axis_bounds',
|
||||
|
|
@ -81,16 +80,6 @@ describe('getControlsForVizType', () => {
|
|||
JSON.stringify(getControlsForVizType('chart_controls_inventory_fake')),
|
||||
).toEqual(
|
||||
JSON.stringify({
|
||||
label_colors: {
|
||||
type: 'ColorMapControl',
|
||||
label: t('Color map'),
|
||||
default: {},
|
||||
renderTrigger: true,
|
||||
mapStateToProps: state => ({
|
||||
colorNamespace: state.form_data.color_namespace,
|
||||
colorScheme: state.form_data.color_scheme,
|
||||
}),
|
||||
},
|
||||
y_axis_bounds: {
|
||||
type: 'BoundsControl',
|
||||
label: 'Value bounds',
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||
"cache_timeout",
|
||||
"dashboards.dashboard_title",
|
||||
"dashboards.id",
|
||||
"dashboards.json_metadata",
|
||||
"description",
|
||||
"owners.first_name",
|
||||
"owners.id",
|
||||
|
|
|
|||
Loading…
Reference in New Issue