refactor(explore): convert ControlPanelsContainer to typescript (#13221)
This commit is contained in:
parent
892eef1af6
commit
3c62069bbb
|
|
@ -104,6 +104,7 @@ module.exports = {
|
||||||
'no-mixed-operators': 0,
|
'no-mixed-operators': 0,
|
||||||
'no-multi-assign': 0,
|
'no-multi-assign': 0,
|
||||||
'no-multi-spaces': 0,
|
'no-multi-spaces': 0,
|
||||||
|
'no-nested-ternary': 0,
|
||||||
'no-prototype-builtins': 0,
|
'no-prototype-builtins': 0,
|
||||||
'no-restricted-properties': 0,
|
'no-restricted-properties': 0,
|
||||||
'no-restricted-imports': [
|
'no-restricted-imports': [
|
||||||
|
|
@ -226,6 +227,7 @@ module.exports = {
|
||||||
'no-mixed-operators': 0,
|
'no-mixed-operators': 0,
|
||||||
'no-multi-assign': 0,
|
'no-multi-assign': 0,
|
||||||
'no-multi-spaces': 0,
|
'no-multi-spaces': 0,
|
||||||
|
'no-nested-ternary': 0,
|
||||||
'no-prototype-builtins': 0,
|
'no-prototype-builtins': 0,
|
||||||
'no-restricted-properties': 0,
|
'no-restricted-properties': 0,
|
||||||
'no-restricted-imports': [
|
'no-restricted-imports': [
|
||||||
|
|
|
||||||
|
|
@ -55,44 +55,3 @@ describe('Advanced analytics', () => {
|
||||||
.contains('1 year');
|
.contains('1 year');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Annotations', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.login();
|
|
||||||
cy.intercept('GET', '/superset/explore_json/**').as('getJson');
|
|
||||||
cy.intercept('POST', '/superset/explore_json/**').as('postJson');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Create formula annotation y-axis goal line', () => {
|
|
||||||
cy.visitChartByName('Num Births Trend');
|
|
||||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
|
||||||
|
|
||||||
cy.get('[data-test=annotation_layers]').within(() => {
|
|
||||||
cy.get('button').click();
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get('[data-test="popover-content"]').within(() => {
|
|
||||||
cy.get('[data-test=annotation-layer-name-header]')
|
|
||||||
.siblings()
|
|
||||||
.first()
|
|
||||||
.within(() => {
|
|
||||||
cy.get('input').type('Goal line');
|
|
||||||
});
|
|
||||||
cy.get('[data-test=annotation-layer-value-header]')
|
|
||||||
.siblings()
|
|
||||||
.first()
|
|
||||||
.within(() => {
|
|
||||||
cy.get('input').type('y=1400000');
|
|
||||||
});
|
|
||||||
cy.get('button').contains('OK').click({ force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get('button[data-test="run-query-button"]').click();
|
|
||||||
cy.verifySliceSuccess({
|
|
||||||
waitAlias: '@postJson',
|
|
||||||
chartSelector: 'svg',
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get('.nv-legend-text').should('have.length', 2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
describe('Annotations', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.login();
|
||||||
|
cy.intercept('GET', '/superset/explore_json/**').as('getJson');
|
||||||
|
cy.intercept('POST', '/superset/explore_json/**').as('postJson');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Create formula annotation y-axis goal line', () => {
|
||||||
|
cy.visitChartByName('Num Births Trend');
|
||||||
|
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||||
|
|
||||||
|
const layerLabel = 'Goal line';
|
||||||
|
|
||||||
|
cy.get('[data-test=annotation_layers] button').click();
|
||||||
|
|
||||||
|
cy.get('[data-test="popover-content"]').within(() => {
|
||||||
|
cy.get('[data-test=annotation-layer-name-header]')
|
||||||
|
.siblings()
|
||||||
|
.first()
|
||||||
|
.within(() => {
|
||||||
|
cy.get('input').type(layerLabel);
|
||||||
|
});
|
||||||
|
cy.get('[data-test=annotation-layer-value-header]')
|
||||||
|
.siblings()
|
||||||
|
.first()
|
||||||
|
.within(() => {
|
||||||
|
cy.get('input').type('y=1400000');
|
||||||
|
});
|
||||||
|
cy.get('button').contains('OK').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('button[data-test="run-query-button"]').click();
|
||||||
|
cy.get('[data-test=annotation_layers]').contains(layerLabel);
|
||||||
|
|
||||||
|
cy.verifySliceSuccess({
|
||||||
|
waitAlias: '@postJson',
|
||||||
|
chartSelector: 'svg',
|
||||||
|
});
|
||||||
|
cy.get('.nv-legend-text').should('have.length', 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -28,7 +28,7 @@ describe('Visualization > Pivot Table', () => {
|
||||||
adhoc_filters: [],
|
adhoc_filters: [],
|
||||||
groupby: ['name'],
|
groupby: ['name'],
|
||||||
columns: ['state'],
|
columns: ['state'],
|
||||||
row_limit: 50000,
|
row_limit: 5000,
|
||||||
pandas_aggfunc: 'sum',
|
pandas_aggfunc: 'sum',
|
||||||
pivot_margins: true,
|
pivot_margins: true,
|
||||||
number_format: '.3s',
|
number_format: '.3s',
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,17 @@
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { getChartControlPanelRegistry, t } from '@superset-ui/core';
|
import {
|
||||||
|
DatasourceType,
|
||||||
|
getChartControlPanelRegistry,
|
||||||
|
t,
|
||||||
|
} from '@superset-ui/core';
|
||||||
import { defaultControls } from 'src/explore/store';
|
import { defaultControls } from 'src/explore/store';
|
||||||
import { getFormDataFromControls } from 'src/explore/controlUtils';
|
import { getFormDataFromControls } from 'src/explore/controlUtils';
|
||||||
import { ControlPanelsContainer } from 'src/explore/components/ControlPanelsContainer';
|
import {
|
||||||
|
ControlPanelsContainer,
|
||||||
|
ControlPanelsContainerProps,
|
||||||
|
} from 'src/explore/components/ControlPanelsContainer';
|
||||||
import Collapse from 'src/common/components/Collapse';
|
import Collapse from 'src/common/components/Collapse';
|
||||||
|
|
||||||
describe('ControlPanelsContainer', () => {
|
describe('ControlPanelsContainer', () => {
|
||||||
|
|
@ -78,15 +85,15 @@ describe('ControlPanelsContainer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function getDefaultProps() {
|
function getDefaultProps() {
|
||||||
|
const controls = defaultControls as ControlPanelsContainerProps['controls'];
|
||||||
return {
|
return {
|
||||||
datasource_type: 'table',
|
datasource_type: DatasourceType.Table,
|
||||||
actions: {},
|
actions: {},
|
||||||
controls: defaultControls,
|
controls,
|
||||||
// Note: default viz_type is table
|
form_data: getFormDataFromControls(controls),
|
||||||
form_data: getFormDataFromControls(defaultControls),
|
|
||||||
isDatasourceMetaLoading: false,
|
isDatasourceMetaLoading: false,
|
||||||
exploreState: {},
|
exploreState: {},
|
||||||
};
|
} as ControlPanelsContainerProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
it('renders ControlPanelSections', () => {
|
it('renders ControlPanelSections', () => {
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
export const DATETIME_WITH_TIME_ZONE = 'YYYY-MM-DD HH:mm:ssZ';
|
export const DATETIME_WITH_TIME_ZONE = 'YYYY-MM-DD HH:mm:ssZ';
|
||||||
|
|
||||||
export const TIME_WITH_MS = 'HH:mm:ss.SSS';
|
export const TIME_WITH_MS = 'HH:mm:ss.SSS';
|
||||||
|
|
||||||
export const BOOL_TRUE_DISPLAY = 'True';
|
export const BOOL_TRUE_DISPLAY = 'True';
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ export const SET_FIELD_VALUE = 'SET_FIELD_VALUE';
|
||||||
export function setControlValue(
|
export function setControlValue(
|
||||||
controlName: string,
|
controlName: string,
|
||||||
value: any,
|
value: any,
|
||||||
validationErrors: any[],
|
validationErrors?: any[],
|
||||||
) {
|
) {
|
||||||
return { type: SET_FIELD_VALUE, controlName, value, validationErrors };
|
return { type: SET_FIELD_VALUE, controlName, value, validationErrors };
|
||||||
}
|
}
|
||||||
|
|
@ -109,11 +109,6 @@ export function setExploreControls(formData: QueryFormData) {
|
||||||
return { type: SET_EXPLORE_CONTROLS, formData };
|
return { type: SET_EXPLORE_CONTROLS, formData };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const REMOVE_CONTROL_PANEL_ALERT = 'REMOVE_CONTROL_PANEL_ALERT';
|
|
||||||
export function removeControlPanelAlert() {
|
|
||||||
return { type: REMOVE_CONTROL_PANEL_ALERT };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UPDATE_CHART_TITLE = 'UPDATE_CHART_TITLE';
|
export const UPDATE_CHART_TITLE = 'UPDATE_CHART_TITLE';
|
||||||
export function updateChartTitle(sliceName: string) {
|
export function updateChartTitle(sliceName: string) {
|
||||||
return { type: UPDATE_CHART_TITLE, sliceName };
|
return { type: UPDATE_CHART_TITLE, sliceName };
|
||||||
|
|
@ -154,7 +149,6 @@ export const exploreActions = {
|
||||||
saveFaveStar,
|
saveFaveStar,
|
||||||
setControlValue,
|
setControlValue,
|
||||||
setExploreControls,
|
setExploreControls,
|
||||||
removeControlPanelAlert,
|
|
||||||
updateChartTitle,
|
updateChartTitle,
|
||||||
createNewSlice,
|
createNewSlice,
|
||||||
sliceUpdated,
|
sliceUpdated,
|
||||||
|
|
|
||||||
|
|
@ -18,30 +18,47 @@
|
||||||
*/
|
*/
|
||||||
/* eslint camelcase: 0 */
|
/* eslint camelcase: 0 */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { t, styled, getChartControlPanelRegistry } from '@superset-ui/core';
|
import {
|
||||||
|
t,
|
||||||
|
styled,
|
||||||
|
getChartControlPanelRegistry,
|
||||||
|
QueryFormData,
|
||||||
|
DatasourceType,
|
||||||
|
} from '@superset-ui/core';
|
||||||
|
|
||||||
import Tabs from 'src/common/components/Tabs';
|
import Tabs from 'src/common/components/Tabs';
|
||||||
import Alert from 'src/components/Alert';
|
|
||||||
import Collapse from 'src/common/components/Collapse';
|
import Collapse from 'src/common/components/Collapse';
|
||||||
import { PluginContext } from 'src/components/DynamicPlugins';
|
import { PluginContext } from 'src/components/DynamicPlugins';
|
||||||
import Loading from 'src/components/Loading';
|
import Loading from 'src/components/Loading';
|
||||||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
import {
|
||||||
|
ControlPanelSectionConfig,
|
||||||
|
ControlState,
|
||||||
|
CustomControlItem,
|
||||||
|
ExpandedControlItem,
|
||||||
|
InfoTooltipWithTrigger,
|
||||||
|
} from '@superset-ui/chart-controls';
|
||||||
import ControlRow from './ControlRow';
|
import ControlRow from './ControlRow';
|
||||||
import Control from './Control';
|
import Control from './Control';
|
||||||
import { sectionsToRender } from '../controlUtils';
|
import { sectionsToRender } from '../controlUtils';
|
||||||
import { exploreActions } from '../actions/exploreActions';
|
import { ExploreActions, exploreActions } from '../actions/exploreActions';
|
||||||
|
import { ExploreState } from '../reducers/getInitialState';
|
||||||
|
|
||||||
const propTypes = {
|
export type ControlPanelsContainerProps = {
|
||||||
actions: PropTypes.object.isRequired,
|
actions: ExploreActions;
|
||||||
alert: PropTypes.string,
|
datasource_type: DatasourceType;
|
||||||
datasource_type: PropTypes.string.isRequired,
|
exploreState: Record<string, any>;
|
||||||
exploreState: PropTypes.object.isRequired,
|
controls: Record<string, ControlState>;
|
||||||
controls: PropTypes.object.isRequired,
|
form_data: QueryFormData;
|
||||||
form_data: PropTypes.object.isRequired,
|
isDatasourceMetaLoading: boolean;
|
||||||
isDatasourceMetaLoading: PropTypes.bool.isRequired,
|
};
|
||||||
|
|
||||||
|
export type ExpandedControlPanelSectionConfig = Omit<
|
||||||
|
ControlPanelSectionConfig,
|
||||||
|
'controlSetRows'
|
||||||
|
> & {
|
||||||
|
controlSetRows: ExpandedControlItem[][];
|
||||||
};
|
};
|
||||||
|
|
||||||
const Styles = styled.div`
|
const Styles = styled.div`
|
||||||
|
|
@ -50,9 +67,6 @@ const Styles = styled.div`
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-x: visible;
|
overflow-x: visible;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
.remove-alert {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#controlSections {
|
#controlSections {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|
@ -80,60 +94,34 @@ const ControlPanelsTabs = styled(Tabs)`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
class ControlPanelsContainer extends React.Component {
|
|
||||||
|
class ControlPanelsContainer extends React.Component<ControlPanelsContainerProps> {
|
||||||
// trigger updates to the component when async plugins load
|
// trigger updates to the component when async plugins load
|
||||||
static contextType = PluginContext;
|
static contextType = PluginContext;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: ControlPanelsContainerProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.removeAlert = this.removeAlert.bind(this);
|
|
||||||
this.renderControl = this.renderControl.bind(this);
|
this.renderControl = this.renderControl.bind(this);
|
||||||
this.renderControlPanelSection = this.renderControlPanelSection.bind(this);
|
this.renderControlPanelSection = this.renderControlPanelSection.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
sectionsToRender(): ExpandedControlPanelSectionConfig[] {
|
||||||
const {
|
|
||||||
actions: { setControlValue },
|
|
||||||
} = this.props;
|
|
||||||
if (this.props.form_data.datasource !== prevProps.form_data.datasource) {
|
|
||||||
const defaultValues = [
|
|
||||||
'MetricsControl',
|
|
||||||
'AdhocFilterControl',
|
|
||||||
'TextControl',
|
|
||||||
'SelectControl',
|
|
||||||
'CheckboxControl',
|
|
||||||
'AnnotationLayerControl',
|
|
||||||
];
|
|
||||||
Object.entries(this.props.controls).forEach(([controlName, control]) => {
|
|
||||||
const { type, default: defaultValue } = control;
|
|
||||||
if (defaultValues.includes(type)) {
|
|
||||||
setControlValue(controlName, defaultValue);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sectionsToRender() {
|
|
||||||
return sectionsToRender(
|
return sectionsToRender(
|
||||||
this.props.form_data.viz_type,
|
this.props.form_data.viz_type,
|
||||||
this.props.datasource_type,
|
this.props.datasource_type,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
sectionsToExpand(sections) {
|
sectionsToExpand(sections: ControlPanelSectionConfig[]) {
|
||||||
return sections.reduce(
|
return sections.reduce(
|
||||||
(acc, cur) => (cur.expanded ? [...acc, cur.label] : acc),
|
(acc, section) =>
|
||||||
[],
|
section.expanded ? [...acc, String(section.label)] : acc,
|
||||||
|
[] as string[],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAlert() {
|
renderControl({ name, config }: CustomControlItem) {
|
||||||
this.props.actions.removeControlPanelAlert();
|
const { actions, controls } = this.props;
|
||||||
}
|
|
||||||
|
|
||||||
renderControl({ name, config }) {
|
|
||||||
const { actions, controls, form_data: formData } = this.props;
|
|
||||||
const { visibility } = config;
|
const { visibility } = config;
|
||||||
|
|
||||||
// If the control item is not an object, we have to look up the control data from
|
// If the control item is not an object, we have to look up the control data from
|
||||||
|
|
@ -144,11 +132,9 @@ class ControlPanelsContainer extends React.Component {
|
||||||
...controls[name],
|
...controls[name],
|
||||||
name,
|
name,
|
||||||
};
|
};
|
||||||
const {
|
const { validationErrors, ...restProps } = controlData as ControlState & {
|
||||||
validationErrors,
|
validationErrors?: any[];
|
||||||
provideFormDataToProps,
|
};
|
||||||
...restProps
|
|
||||||
} = controlData;
|
|
||||||
|
|
||||||
// if visibility check says the config is not visible, don't render it
|
// if visibility check says the config is not visible, don't render it
|
||||||
if (visibility && !visibility.call(config, this.props, controlData)) {
|
if (visibility && !visibility.call(config, this.props, controlData)) {
|
||||||
|
|
@ -160,30 +146,42 @@ class ControlPanelsContainer extends React.Component {
|
||||||
name={name}
|
name={name}
|
||||||
validationErrors={validationErrors}
|
validationErrors={validationErrors}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
formData={provideFormDataToProps ? formData : null}
|
|
||||||
datasource={formData?.datasource}
|
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderControlPanelSection(section) {
|
renderControlPanelSection(section: ExpandedControlPanelSectionConfig) {
|
||||||
const { controls } = this.props;
|
const { controls } = this.props;
|
||||||
const { label, description } = section;
|
const { label, description } = section;
|
||||||
|
|
||||||
|
// Section label can be a ReactNode but in some places we want to
|
||||||
|
// have a string ID. Using forced type conversion for now,
|
||||||
|
// should probably add a `id` field to sections in the future.
|
||||||
|
const sectionId = String(label);
|
||||||
|
|
||||||
const hasErrors = section.controlSetRows.some(rows =>
|
const hasErrors = section.controlSetRows.some(rows =>
|
||||||
rows.some(
|
rows.some(item => {
|
||||||
s =>
|
const controlName =
|
||||||
controls[s] &&
|
typeof item === 'string'
|
||||||
controls[s].validationErrors &&
|
? item
|
||||||
controls[s].validationErrors.length > 0,
|
: item && 'name' in item
|
||||||
),
|
? item.name
|
||||||
|
: null;
|
||||||
|
return (
|
||||||
|
controlName &&
|
||||||
|
controlName in controls &&
|
||||||
|
controls[controlName].validationErrors &&
|
||||||
|
controls[controlName].validationErrors.length > 0
|
||||||
|
);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
const PanelHeader = () => (
|
const PanelHeader = () => (
|
||||||
<span>
|
<span>
|
||||||
<span>{label}</span>{' '}
|
<span>{label}</span>{' '}
|
||||||
{description && (
|
{description && (
|
||||||
<InfoTooltipWithTrigger label={label} tooltip={description} />
|
// label is only used in tooltip id (should probably call this prop `id`)
|
||||||
|
<InfoTooltipWithTrigger label={sectionId} tooltip={description} />
|
||||||
)}
|
)}
|
||||||
{hasErrors && (
|
{hasErrors && (
|
||||||
<InfoTooltipWithTrigger
|
<InfoTooltipWithTrigger
|
||||||
|
|
@ -199,7 +197,7 @@ class ControlPanelsContainer extends React.Component {
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
className="control-panel-section"
|
className="control-panel-section"
|
||||||
header={PanelHeader()}
|
header={PanelHeader()}
|
||||||
key={section.label}
|
key={sectionId}
|
||||||
>
|
>
|
||||||
{section.controlSetRows.map((controlSets, i) => {
|
{section.controlSetRows.map((controlSets, i) => {
|
||||||
const renderedControls = controlSets
|
const renderedControls = controlSets
|
||||||
|
|
@ -229,7 +227,6 @@ class ControlPanelsContainer extends React.Component {
|
||||||
return (
|
return (
|
||||||
<ControlRow
|
<ControlRow
|
||||||
key={`controlsetrow-${i}`}
|
key={`controlsetrow-${i}`}
|
||||||
className="control-row"
|
|
||||||
controls={renderedControls}
|
controls={renderedControls}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -247,8 +244,8 @@ class ControlPanelsContainer extends React.Component {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const querySectionsToRender = [];
|
const querySectionsToRender: ExpandedControlPanelSectionConfig[] = [];
|
||||||
const displaySectionsToRender = [];
|
const displaySectionsToRender: ExpandedControlPanelSectionConfig[] = [];
|
||||||
this.sectionsToRender().forEach(section => {
|
this.sectionsToRender().forEach(section => {
|
||||||
// if at least one control in the section is not `renderTrigger`
|
// if at least one control in the section is not `renderTrigger`
|
||||||
// or asks to be displayed at the Data tab
|
// or asks to be displayed at the Data tab
|
||||||
|
|
@ -258,6 +255,8 @@ class ControlPanelsContainer extends React.Component {
|
||||||
rows.some(
|
rows.some(
|
||||||
control =>
|
control =>
|
||||||
control &&
|
control &&
|
||||||
|
typeof control === 'object' &&
|
||||||
|
'config' in control &&
|
||||||
control.config &&
|
control.config &&
|
||||||
(!control.config.renderTrigger ||
|
(!control.config.renderTrigger ||
|
||||||
control.config.tabOverride === 'data'),
|
control.config.tabOverride === 'data'),
|
||||||
|
|
@ -277,14 +276,6 @@ class ControlPanelsContainer extends React.Component {
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Styles>
|
<Styles>
|
||||||
{this.props.alert && (
|
|
||||||
<Alert
|
|
||||||
type="warning"
|
|
||||||
message={this.props.alert}
|
|
||||||
closable
|
|
||||||
onClose={this.removeAlert}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<ControlPanelsTabs
|
<ControlPanelsTabs
|
||||||
id="controlSections"
|
id="controlSections"
|
||||||
data-test="control-tabs"
|
data-test="control-tabs"
|
||||||
|
|
@ -318,26 +309,19 @@ class ControlPanelsContainer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ControlPanelsContainer.propTypes = propTypes;
|
|
||||||
|
|
||||||
function mapStateToProps({ explore }) {
|
|
||||||
return {
|
|
||||||
alert: explore.controlPanelAlert,
|
|
||||||
isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
|
|
||||||
controls: explore.controls,
|
|
||||||
exploreState: explore,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
|
||||||
return {
|
|
||||||
actions: bindActionCreators(exploreActions, dispatch),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ControlPanelsContainer };
|
export { ControlPanelsContainer };
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
function mapStateToProps({ explore }: ExploreState) {
|
||||||
mapDispatchToProps,
|
return {
|
||||||
|
isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
|
||||||
|
controls: explore.controls,
|
||||||
|
exploreState: explore,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators(exploreActions, dispatch),
|
||||||
|
};
|
||||||
|
},
|
||||||
)(ControlPanelsContainer);
|
)(ControlPanelsContainer);
|
||||||
|
|
@ -16,20 +16,20 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
import { ExpandedControlItem } from '@superset-ui/chart-controls';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const NUM_COLUMNS = 12;
|
const NUM_COLUMNS = 12;
|
||||||
|
|
||||||
const propTypes = {
|
export default function ControlRow({
|
||||||
controls: PropTypes.arrayOf(PropTypes.object).isRequired,
|
controls,
|
||||||
};
|
}: {
|
||||||
|
controls: ExpandedControlItem[];
|
||||||
function ControlSetRow(props) {
|
}) {
|
||||||
const colSize = NUM_COLUMNS / props.controls.length;
|
const colSize = NUM_COLUMNS / controls.length;
|
||||||
return (
|
return (
|
||||||
<div className="row space-1">
|
<div className="row space-1">
|
||||||
{props.controls.map((control, i) => (
|
{controls.map((control, i) => (
|
||||||
<div className={`col-lg-${colSize} col-xs-12`} key={i}>
|
<div className={`col-lg-${colSize} col-xs-12`} key={i}>
|
||||||
{control}
|
{control}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -37,6 +37,3 @@ function ControlSetRow(props) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ControlSetRow.propTypes = propTypes;
|
|
||||||
export default ControlSetRow;
|
|
||||||
|
|
@ -222,10 +222,7 @@ function ExploreViewContainer(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onQuery() {
|
function onQuery() {
|
||||||
// remove alerts when query
|
|
||||||
props.actions.removeControlPanelAlert();
|
|
||||||
props.actions.triggerQuery(true, props.chart.id);
|
props.actions.triggerQuery(true, props.chart.id);
|
||||||
|
|
||||||
addHistory();
|
addHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,12 +69,6 @@ export default class CollectionControl extends React.Component {
|
||||||
this.onAdd = this.onAdd.bind(this);
|
this.onAdd = this.onAdd.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (prevProps.datasource.name !== this.props.datasource.name) {
|
|
||||||
this.props.onChange([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(i, value) {
|
onChange(i, value) {
|
||||||
Object.assign(this.props.value[i], value);
|
Object.assign(this.props.value[i], value);
|
||||||
this.props.onChange(this.props.value);
|
this.props.onChange(this.props.value);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import {
|
import {
|
||||||
SupersetClient,
|
SupersetClient,
|
||||||
|
|
@ -25,6 +25,7 @@ import {
|
||||||
t,
|
t,
|
||||||
TimeRangeEndpoints,
|
TimeRangeEndpoints,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
|
import { DatasourceMeta } from '@superset-ui/chart-controls';
|
||||||
import {
|
import {
|
||||||
buildTimeRangeString,
|
buildTimeRangeString,
|
||||||
formatTimeRange,
|
formatTimeRange,
|
||||||
|
|
@ -38,6 +39,8 @@ import { Divider } from 'src/common/components';
|
||||||
import Icon from 'src/components/Icon';
|
import Icon from 'src/components/Icon';
|
||||||
import { Select } from 'src/components/Select';
|
import { Select } from 'src/components/Select';
|
||||||
import { Tooltip } from 'src/common/components/Tooltip';
|
import { Tooltip } from 'src/common/components/Tooltip';
|
||||||
|
import { DEFAULT_TIME_RANGE } from 'src/explore/constants';
|
||||||
|
|
||||||
import { SelectOptionType, FrameType } from './types';
|
import { SelectOptionType, FrameType } from './types';
|
||||||
import {
|
import {
|
||||||
COMMON_RANGE_VALUES_SET,
|
COMMON_RANGE_VALUES_SET,
|
||||||
|
|
@ -165,28 +168,27 @@ const IconWrapper = styled.span`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface DateFilterLabelProps {
|
interface DateFilterControlProps {
|
||||||
name: string;
|
name: string;
|
||||||
onChange: (timeRange: string) => void;
|
onChange: (timeRange: string) => void;
|
||||||
value?: string;
|
value?: string;
|
||||||
endpoints?: TimeRangeEndpoints;
|
endpoints?: TimeRangeEndpoints;
|
||||||
datasource?: string;
|
datasource?: DatasourceMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DateFilterControl(props: DateFilterLabelProps) {
|
export default function DateFilterControl(props: DateFilterControlProps) {
|
||||||
const { value = 'Last week', endpoints, onChange, datasource } = props;
|
const { value = DEFAULT_TIME_RANGE, endpoints, onChange } = props;
|
||||||
const [actualTimeRange, setActualTimeRange] = useState<string>(value);
|
const [actualTimeRange, setActualTimeRange] = useState<string>(value);
|
||||||
|
|
||||||
const [show, setShow] = useState<boolean>(false);
|
const [show, setShow] = useState<boolean>(false);
|
||||||
const [frame, setFrame] = useState<FrameType>(guessFrame(value));
|
const guessedFrame = useMemo(() => guessFrame(value), [value]);
|
||||||
const [isMounted, setIsMounted] = useState<boolean>(false);
|
const [frame, setFrame] = useState<FrameType>(guessedFrame);
|
||||||
const [timeRangeValue, setTimeRangeValue] = useState(value);
|
const [timeRangeValue, setTimeRangeValue] = useState(value);
|
||||||
const [validTimeRange, setValidTimeRange] = useState<boolean>(false);
|
const [validTimeRange, setValidTimeRange] = useState<boolean>(false);
|
||||||
const [evalResponse, setEvalResponse] = useState<string>(value);
|
const [evalResponse, setEvalResponse] = useState<string>(value);
|
||||||
const [tooltipTitle, setTooltipTitle] = useState<string>(value);
|
const [tooltipTitle, setTooltipTitle] = useState<string>(value);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMounted) setIsMounted(true);
|
|
||||||
fetchTimeRange(value, endpoints).then(({ value: actualRange, error }) => {
|
fetchTimeRange(value, endpoints).then(({ value: actualRange, error }) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
setEvalResponse(error || '');
|
setEvalResponse(error || '');
|
||||||
|
|
@ -205,9 +207,9 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
|
||||||
+--------------+------+----------+--------+----------+-----------+
|
+--------------+------+----------+--------+----------+-----------+
|
||||||
*/
|
*/
|
||||||
if (
|
if (
|
||||||
frame === 'Common' ||
|
guessedFrame === 'Common' ||
|
||||||
frame === 'Calendar' ||
|
guessedFrame === 'Calendar' ||
|
||||||
frame === 'No filter'
|
guessedFrame === 'No filter'
|
||||||
) {
|
) {
|
||||||
setActualTimeRange(value);
|
setActualTimeRange(value);
|
||||||
setTooltipTitle(actualRange || '');
|
setTooltipTitle(actualRange || '');
|
||||||
|
|
@ -220,14 +222,6 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
|
||||||
});
|
});
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isMounted) {
|
|
||||||
onChange('Last week');
|
|
||||||
setTimeRangeValue('Last week');
|
|
||||||
setFrame(guessFrame('Last week'));
|
|
||||||
}
|
|
||||||
}, [datasource]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTimeRange(timeRangeValue, endpoints).then(({ value, error }) => {
|
fetchTimeRange(timeRangeValue, endpoints).then(({ value, error }) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
@ -247,13 +241,13 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
|
||||||
|
|
||||||
function onOpen() {
|
function onOpen() {
|
||||||
setTimeRangeValue(value);
|
setTimeRangeValue(value);
|
||||||
setFrame(guessFrame(value));
|
setFrame(guessedFrame);
|
||||||
setShow(true);
|
setShow(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onHide() {
|
function onHide() {
|
||||||
setTimeRangeValue(value);
|
setTimeRangeValue(value);
|
||||||
setFrame(guessFrame(value));
|
setFrame(guessedFrame);
|
||||||
setShow(false);
|
setShow(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,7 +259,7 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function onFrame(option: SelectOptionType) {
|
function onChangeFrame(option: SelectOptionType) {
|
||||||
if (option.value === 'No filter') {
|
if (option.value === 'No filter') {
|
||||||
setTimeRangeValue('No filter');
|
setTimeRangeValue('No filter');
|
||||||
}
|
}
|
||||||
|
|
@ -278,7 +272,7 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
|
||||||
<Select
|
<Select
|
||||||
options={FRAME_OPTIONS}
|
options={FRAME_OPTIONS}
|
||||||
value={FRAME_OPTIONS.filter(({ value }) => value === frame)}
|
value={FRAME_OPTIONS.filter(({ value }) => value === frame)}
|
||||||
onChange={onFrame}
|
onChange={onChangeFrame}
|
||||||
className="frame-dropdown"
|
className="frame-dropdown"
|
||||||
/>
|
/>
|
||||||
{frame !== 'No filter' && <Divider />}
|
{frame !== 'No filter' && <Divider />}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,13 @@
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { t, logging, SupersetClient, withTheme } from '@superset-ui/core';
|
import {
|
||||||
|
t,
|
||||||
|
logging,
|
||||||
|
SupersetClient,
|
||||||
|
withTheme,
|
||||||
|
ensureIsArray,
|
||||||
|
} from '@superset-ui/core';
|
||||||
|
|
||||||
import ControlHeader from 'src/explore/components/ControlHeader';
|
import ControlHeader from 'src/explore/components/ControlHeader';
|
||||||
import adhocMetricType from 'src/explore/components/controls/MetricControl/adhocMetricType';
|
import adhocMetricType from 'src/explore/components/controls/MetricControl/adhocMetricType';
|
||||||
|
|
@ -39,6 +45,11 @@ import AdhocFilterOption from './AdhocFilterOption';
|
||||||
import AdhocFilter, { CLAUSES, EXPRESSION_TYPES } from './AdhocFilter';
|
import AdhocFilter, { CLAUSES, EXPRESSION_TYPES } from './AdhocFilter';
|
||||||
import adhocFilterType from './adhocFilterType';
|
import adhocFilterType from './adhocFilterType';
|
||||||
|
|
||||||
|
const selectedMetricType = PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
adhocMetricType,
|
||||||
|
]);
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
|
@ -46,12 +57,10 @@ const propTypes = {
|
||||||
datasource: PropTypes.object,
|
datasource: PropTypes.object,
|
||||||
columns: PropTypes.arrayOf(columnType),
|
columns: PropTypes.arrayOf(columnType),
|
||||||
savedMetrics: PropTypes.arrayOf(savedMetricType),
|
savedMetrics: PropTypes.arrayOf(savedMetricType),
|
||||||
formData: PropTypes.shape({
|
selectedMetrics: PropTypes.oneOfType([
|
||||||
metric: PropTypes.oneOfType([PropTypes.string, adhocMetricType]),
|
selectedMetricType,
|
||||||
metrics: PropTypes.arrayOf(
|
PropTypes.arrayOf(selectedMetricType),
|
||||||
PropTypes.oneOfType([PropTypes.string, adhocMetricType]),
|
]),
|
||||||
),
|
|
||||||
}),
|
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -60,7 +69,7 @@ const defaultProps = {
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
columns: [],
|
columns: [],
|
||||||
savedMetrics: [],
|
savedMetrics: [],
|
||||||
formData: {},
|
selectedMetrics: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
function isDictionaryForAdhocFilter(value) {
|
function isDictionaryForAdhocFilter(value) {
|
||||||
|
|
@ -141,10 +150,7 @@ class AdhocFilterControl extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
if (
|
if (this.props.columns !== nextProps.columns) {
|
||||||
this.props.columns !== nextProps.columns ||
|
|
||||||
this.props.formData !== nextProps.formData
|
|
||||||
) {
|
|
||||||
this.setState({ options: this.optionsForSelect(nextProps) });
|
this.setState({ options: this.optionsForSelect(nextProps) });
|
||||||
}
|
}
|
||||||
if (this.props.value !== nextProps.value) {
|
if (this.props.value !== nextProps.value) {
|
||||||
|
|
@ -270,7 +276,7 @@ class AdhocFilterControl extends React.Component {
|
||||||
optionsForSelect(props) {
|
optionsForSelect(props) {
|
||||||
const options = [
|
const options = [
|
||||||
...props.columns,
|
...props.columns,
|
||||||
...[...(props.formData?.metrics || []), props.formData?.metric].map(
|
...ensureIsArray(props.selectedMetrics).map(
|
||||||
metric =>
|
metric =>
|
||||||
metric &&
|
metric &&
|
||||||
(typeof metric === 'string'
|
(typeof metric === 'string'
|
||||||
|
|
|
||||||
|
|
@ -123,9 +123,6 @@ export default class AdhocMetricEditPopover extends React.PureComponent {
|
||||||
adhocMetricLabel: this.state.adhocMetric?.getDefaultLabel(),
|
adhocMetricLabel: this.state.adhocMetric?.getDefaultLabel(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (prevProps.datasource !== this.props.datasource) {
|
|
||||||
this.props.onChange(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,6 @@ class AdhocMetricOption extends React.PureComponent {
|
||||||
onMoveLabel,
|
onMoveLabel,
|
||||||
onDropLabel,
|
onDropLabel,
|
||||||
index,
|
index,
|
||||||
datasource,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -70,7 +69,6 @@ class AdhocMetricOption extends React.PureComponent {
|
||||||
columns={columns}
|
columns={columns}
|
||||||
savedMetricsOptions={savedMetricsOptions}
|
savedMetricsOptions={savedMetricsOptions}
|
||||||
savedMetric={savedMetric}
|
savedMetric={savedMetric}
|
||||||
datasource={datasource}
|
|
||||||
datasourceType={datasourceType}
|
datasourceType={datasourceType}
|
||||||
>
|
>
|
||||||
<OptionControlLabel
|
<OptionControlLabel
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ export type AdhocMetricPopoverTriggerProps = {
|
||||||
savedMetricsOptions: savedMetricType[];
|
savedMetricsOptions: savedMetricType[];
|
||||||
savedMetric: savedMetricType;
|
savedMetric: savedMetricType;
|
||||||
datasourceType: string;
|
datasourceType: string;
|
||||||
datasource: string;
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
createNew?: boolean;
|
createNew?: boolean;
|
||||||
};
|
};
|
||||||
|
|
@ -160,7 +159,6 @@ class AdhocMetricPopoverTrigger extends React.PureComponent<
|
||||||
columns={this.props.columns}
|
columns={this.props.columns}
|
||||||
savedMetricsOptions={this.props.savedMetricsOptions}
|
savedMetricsOptions={this.props.savedMetricsOptions}
|
||||||
savedMetric={this.props.savedMetric}
|
savedMetric={this.props.savedMetric}
|
||||||
datasource={this.props.datasource}
|
|
||||||
datasourceType={this.props.datasourceType}
|
datasourceType={this.props.datasourceType}
|
||||||
onResize={this.onPopoverResize}
|
onResize={this.onPopoverResize}
|
||||||
onClose={this.closePopover}
|
onClose={this.closePopover}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ const propTypes = {
|
||||||
savedMetricsOptions: PropTypes.arrayOf(savedMetricType),
|
savedMetricsOptions: PropTypes.arrayOf(savedMetricType),
|
||||||
multi: PropTypes.bool,
|
multi: PropTypes.bool,
|
||||||
datasourceType: PropTypes.string,
|
datasourceType: PropTypes.string,
|
||||||
datasource: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MetricDefinitionValue({
|
export default function MetricDefinitionValue({
|
||||||
|
|
@ -52,16 +51,15 @@ export default function MetricDefinitionValue({
|
||||||
onMoveLabel,
|
onMoveLabel,
|
||||||
onDropLabel,
|
onDropLabel,
|
||||||
index,
|
index,
|
||||||
datasource,
|
|
||||||
}) {
|
}) {
|
||||||
const getSavedMetricByName = metricName =>
|
const getSavedMetricByName = metricName =>
|
||||||
savedMetrics.find(metric => metric.metric_name === metricName);
|
savedMetrics.find(metric => metric.metric_name === metricName);
|
||||||
|
|
||||||
let savedMetric;
|
let savedMetric;
|
||||||
if (option.metric_name) {
|
if (typeof option === 'string') {
|
||||||
savedMetric = option;
|
|
||||||
} else if (typeof option === 'string') {
|
|
||||||
savedMetric = getSavedMetricByName(option);
|
savedMetric = getSavedMetricByName(option);
|
||||||
|
} else if (option.metric_name) {
|
||||||
|
savedMetric = option;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (option instanceof AdhocMetric || savedMetric) {
|
if (option instanceof AdhocMetric || savedMetric) {
|
||||||
|
|
@ -79,7 +77,6 @@ export default function MetricDefinitionValue({
|
||||||
onDropLabel,
|
onDropLabel,
|
||||||
index,
|
index,
|
||||||
savedMetric: savedMetric ?? {},
|
savedMetric: savedMetric ?? {},
|
||||||
datasource,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return <AdhocMetricOption {...metricOptionProps} />;
|
return <AdhocMetricOption {...metricOptionProps} />;
|
||||||
|
|
|
||||||
|
|
@ -179,10 +179,10 @@ class MetricsControl extends React.PureComponent {
|
||||||
) {
|
) {
|
||||||
this.setState({ options: this.optionsForSelect(nextProps) });
|
this.setState({ options: this.optionsForSelect(nextProps) });
|
||||||
|
|
||||||
// Remove metrics if selected value no longer a column
|
// Remove all metrics if selected value no longer a valid column
|
||||||
const containsAllMetrics = columnsContainAllMetrics(value, nextProps);
|
// in the dataset. Must use `nextProps` here because Redux reducers may
|
||||||
|
// have already updated the value for this control.
|
||||||
if (!containsAllMetrics) {
|
if (!columnsContainAllMetrics(nextProps.value, nextProps)) {
|
||||||
this.props.onChange([]);
|
this.props.onChange([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,71 +17,55 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormGroup, FormControl } from 'react-bootstrap';
|
import { FormGroup, FormControl, FormControlProps } from 'react-bootstrap';
|
||||||
import { legacyValidateNumber, legacyValidateInteger } from '@superset-ui/core';
|
import { legacyValidateNumber, legacyValidateInteger } from '@superset-ui/core';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import ControlHeader from '../ControlHeader';
|
import { FAST_DEBOUNCE } from 'src/constants';
|
||||||
|
import ControlHeader from 'src/explore/components/ControlHeader';
|
||||||
|
|
||||||
interface TextControlProps {
|
type InputValueType = string | number;
|
||||||
|
|
||||||
|
export interface TextControlProps<T extends InputValueType = InputValueType> {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
isFloat?: boolean;
|
isFloat?: boolean;
|
||||||
isInt?: boolean;
|
isInt?: boolean;
|
||||||
onChange?: (value: any, errors: any) => {};
|
onChange?: (value: T, errors: any) => {};
|
||||||
onFocus?: () => {};
|
onFocus?: () => {};
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
value?: string | number;
|
value?: T | null;
|
||||||
controlId?: string;
|
controlId?: string;
|
||||||
renderTrigger?: boolean;
|
renderTrigger?: boolean;
|
||||||
datasource?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TextControlState {
|
export interface TextControlState {
|
||||||
controlId: string;
|
controlId: string;
|
||||||
currentDatasource?: string;
|
value: string;
|
||||||
value?: string | number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateControlId = (controlId?: string) =>
|
const generateControlId = (controlId?: string) =>
|
||||||
`formInlineName_${controlId ?? (Math.random() * 1000000).toFixed()}`;
|
`formInlineName_${controlId ?? (Math.random() * 1000000).toFixed()}`;
|
||||||
|
|
||||||
export default class TextControl extends React.Component<
|
const safeStringify = (value?: InputValueType | null) =>
|
||||||
TextControlProps,
|
value == null ? '' : String(value);
|
||||||
TextControlState
|
|
||||||
> {
|
|
||||||
debouncedOnChange = debounce((inputValue: string) => {
|
|
||||||
this.onChange(inputValue);
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
static getDerivedStateFromProps(
|
export default class TextControl<
|
||||||
props: TextControlProps,
|
T extends InputValueType = InputValueType
|
||||||
state: TextControlState,
|
> extends React.Component<TextControlProps<T>, TextControlState> {
|
||||||
) {
|
initialValue?: TextControlProps['value'];
|
||||||
// reset value when datasource changes
|
|
||||||
// props.datasource and props.value don't update in the same re-render,
|
|
||||||
// so we need to synchronize them to update the state with correct values
|
|
||||||
if (
|
|
||||||
props.value !== state.value &&
|
|
||||||
props.datasource !== state.currentDatasource
|
|
||||||
) {
|
|
||||||
return { value: props.value, currentDatasource: props.datasource };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props: TextControlProps) {
|
constructor(props: TextControlProps<T>) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.initialValue = props.value;
|
||||||
// if there's no control id provided, generate a random
|
|
||||||
// number to prevent rendering elements with same ids
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
// if there's no control id provided, generate a random
|
||||||
|
// number to prevent rendering elements with same ids
|
||||||
controlId: generateControlId(props.controlId),
|
controlId: generateControlId(props.controlId),
|
||||||
value: props.value,
|
value: safeStringify(this.initialValue),
|
||||||
currentDatasource: props.datasource,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange = (inputValue: string) => {
|
onChange = (inputValue: string) => {
|
||||||
let parsedValue: string | number = inputValue;
|
let parsedValue: InputValueType = inputValue;
|
||||||
// Validation & casting
|
// Validation & casting
|
||||||
const errors = [];
|
const errors = [];
|
||||||
if (inputValue !== '' && this.props.isFloat) {
|
if (inputValue !== '' && this.props.isFloat) {
|
||||||
|
|
@ -102,26 +86,26 @@ export default class TextControl extends React.Component<
|
||||||
parsedValue = parseInt(inputValue, 10);
|
parsedValue = parseInt(inputValue, 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.props.onChange?.(parsedValue, errors);
|
this.props.onChange?.(parsedValue as T, errors);
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeWrapper = (event: any) => {
|
debouncedOnChange = debounce((inputValue: string) => {
|
||||||
const { value } = event.target;
|
this.onChange(inputValue);
|
||||||
this.setState({ value });
|
}, FAST_DEBOUNCE);
|
||||||
|
|
||||||
// use debounce when change takes effect immediately after user starts typing
|
onChangeWrapper: FormControlProps['onChange'] = event => {
|
||||||
const onChange = this.props.renderTrigger
|
const { value } = event.target as HTMLInputElement;
|
||||||
? this.debouncedOnChange
|
this.setState({ value }, () => {
|
||||||
: this.onChange;
|
this.debouncedOnChange(value);
|
||||||
onChange(value);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render = () => {
|
render = () => {
|
||||||
const { value: rawValue } = this.state;
|
let { value } = this.state;
|
||||||
const value =
|
if (this.initialValue !== this.props.value) {
|
||||||
typeof rawValue !== 'undefined' && rawValue !== null
|
this.initialValue = this.props.value;
|
||||||
? rawValue.toString()
|
value = safeStringify(this.props.value);
|
||||||
: '';
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ControlHeader {...this.props} />
|
<ControlHeader {...this.props} />
|
||||||
|
|
|
||||||
|
|
@ -55,12 +55,7 @@ export const HAVING_OPERATORS = [
|
||||||
OPERATORS['>='],
|
OPERATORS['>='],
|
||||||
OPERATORS['<='],
|
OPERATORS['<='],
|
||||||
];
|
];
|
||||||
export const MULTI_OPERATORS = new Set([
|
export const MULTI_OPERATORS = new Set([OPERATORS.IN, OPERATORS['NOT IN']]);
|
||||||
OPERATORS.in,
|
|
||||||
OPERATORS['not in'],
|
|
||||||
OPERATORS.IN,
|
|
||||||
OPERATORS['NOT IN'],
|
|
||||||
]);
|
|
||||||
// CUSTOM_OPERATORS will show operator in simple mode,
|
// CUSTOM_OPERATORS will show operator in simple mode,
|
||||||
// but will generate customized sqlExpression
|
// but will generate customized sqlExpression
|
||||||
export const CUSTOM_OPERATORS = new Set([OPERATORS['LATEST PARTITION']]);
|
export const CUSTOM_OPERATORS = new Set([OPERATORS['LATEST PARTITION']]);
|
||||||
|
|
@ -103,3 +98,6 @@ export const TIME_FILTER_MAP = {
|
||||||
druid_time_origin: '__time_origin',
|
druid_time_origin: '__time_origin',
|
||||||
granularity: '__granularity',
|
granularity: '__granularity',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: make this configurable per Superset installation
|
||||||
|
export const DEFAULT_TIME_RANGE = 'Last week';
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* 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 { QueryFormData } from '@superset-ui/core';
|
||||||
|
import { ControlStateMapping } from '@superset-ui/chart-controls';
|
||||||
|
|
||||||
|
export function getFormDataFromControls(
|
||||||
|
controlsState: ControlStateMapping,
|
||||||
|
): QueryFormData {
|
||||||
|
const formData: QueryFormData = {
|
||||||
|
viz_type: 'table',
|
||||||
|
datasource: '',
|
||||||
|
};
|
||||||
|
Object.keys(controlsState).forEach(controlName => {
|
||||||
|
const control = controlsState[controlName];
|
||||||
|
formData[controlName] = control.value;
|
||||||
|
});
|
||||||
|
return formData;
|
||||||
|
}
|
||||||
|
|
@ -19,16 +19,9 @@
|
||||||
import memoizeOne from 'memoize-one';
|
import memoizeOne from 'memoize-one';
|
||||||
import { getChartControlPanelRegistry } from '@superset-ui/core';
|
import { getChartControlPanelRegistry } from '@superset-ui/core';
|
||||||
import { expandControlConfig } from '@superset-ui/chart-controls';
|
import { expandControlConfig } from '@superset-ui/chart-controls';
|
||||||
import * as SECTIONS from './controlPanels/sections';
|
import * as SECTIONS from '../controlPanels/sections';
|
||||||
|
|
||||||
export function getFormDataFromControls(controlsState) {
|
export * from './getFormDataFromControls';
|
||||||
const formData = {};
|
|
||||||
Object.keys(controlsState).forEach(controlName => {
|
|
||||||
const control = controlsState[controlName];
|
|
||||||
formData[controlName] = control.value;
|
|
||||||
});
|
|
||||||
return formData;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateControl(control, processedState) {
|
export function validateControl(control, processedState) {
|
||||||
const { validators } = control;
|
const { validators } = control;
|
||||||
|
|
@ -355,8 +355,9 @@ export const controls = {
|
||||||
"using the engine's local timezone. Note one can explicitly set the timezone " +
|
"using the engine's local timezone. Note one can explicitly set the timezone " +
|
||||||
'per the ISO 8601 format if specifying either the start and/or end time.',
|
'per the ISO 8601 format if specifying either the start and/or end time.',
|
||||||
),
|
),
|
||||||
mapStateToProps: state => ({
|
mapStateToProps: ({ form_data: formData }) => ({
|
||||||
endpoints: state.form_data ? state.form_data.time_range_endpoints : null,
|
// eslint-disable-next-line camelcase
|
||||||
|
endpoints: formData?.time_range_endpoints,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -474,7 +475,6 @@ export const controls = {
|
||||||
savedMetrics: state.datasource ? state.datasource.metrics : [],
|
savedMetrics: state.datasource ? state.datasource.metrics : [],
|
||||||
datasource: state.datasource,
|
datasource: state.datasource,
|
||||||
}),
|
}),
|
||||||
provideFormDataToProps: true,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
color_scheme: {
|
color_scheme: {
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,14 @@
|
||||||
*/
|
*/
|
||||||
/* eslint camelcase: 0 */
|
/* eslint camelcase: 0 */
|
||||||
import { DYNAMIC_PLUGIN_CONTROLS_READY } from 'src/chart/chartAction';
|
import { DYNAMIC_PLUGIN_CONTROLS_READY } from 'src/chart/chartAction';
|
||||||
import { getControlsState } from '../store';
|
import { DEFAULT_TIME_RANGE } from 'src/explore/constants';
|
||||||
|
import { getControlsState } from 'src/explore/store';
|
||||||
import {
|
import {
|
||||||
getControlConfig,
|
getControlConfig,
|
||||||
getFormDataFromControls,
|
getFormDataFromControls,
|
||||||
getControlStateFromControlConfig,
|
getControlStateFromControlConfig,
|
||||||
} from '../controlUtils';
|
} from 'src/explore/controlUtils';
|
||||||
import * as actions from '../actions/exploreActions';
|
import * as actions from 'src/explore/actions/exploreActions';
|
||||||
|
|
||||||
export default function exploreReducer(state = {}, action) {
|
export default function exploreReducer(state = {}, action) {
|
||||||
const actionHandlers = {
|
const actionHandlers = {
|
||||||
|
|
@ -61,8 +62,38 @@ export default function exploreReducer(state = {}, action) {
|
||||||
delete newFormData.time_grain_sqla;
|
delete newFormData.time_grain_sqla;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const controls = { ...state.controls };
|
||||||
|
if (
|
||||||
|
action.datasource.id !== state.datasource.id ||
|
||||||
|
action.datasource.type !== state.datasource.type
|
||||||
|
) {
|
||||||
|
// reset time range filter to default
|
||||||
|
newFormData.time_range = DEFAULT_TIME_RANGE;
|
||||||
|
|
||||||
|
// reset control values for column/metric related controls
|
||||||
|
Object.entries(controls).forEach(([controlName, controlState]) => {
|
||||||
|
if (
|
||||||
|
// for direct column select controls
|
||||||
|
controlState.valueKey === 'column_name' ||
|
||||||
|
// for all other controls
|
||||||
|
'columns' in controlState
|
||||||
|
) {
|
||||||
|
// if a control use datasource columns, reset its value to `undefined`,
|
||||||
|
// then `getControlsState` will pick up the default.
|
||||||
|
// TODO: filter out only invalid columns and keep others
|
||||||
|
controls[controlName] = {
|
||||||
|
...controlState,
|
||||||
|
value: undefined,
|
||||||
|
};
|
||||||
|
newFormData[controlName] = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
...state,
|
...state,
|
||||||
|
controls,
|
||||||
datasource: action.datasource,
|
datasource: action.datasource,
|
||||||
datasource_id: action.datasource.id,
|
datasource_id: action.datasource.id,
|
||||||
datasource_type: action.datasource.type,
|
datasource_type: action.datasource.type,
|
||||||
|
|
@ -85,12 +116,6 @@ export default function exploreReducer(state = {}, action) {
|
||||||
datasources: action.datasources,
|
datasources: action.datasources,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[actions.REMOVE_CONTROL_PANEL_ALERT]() {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
controlPanelAlert: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[actions.SET_FIELD_VALUE]() {
|
[actions.SET_FIELD_VALUE]() {
|
||||||
const new_form_data = state.form_data;
|
const new_form_data = state.form_data;
|
||||||
const { controlName, value, validationErrors } = action;
|
const { controlName, value, validationErrors } = action;
|
||||||
|
|
|
||||||
|
|
@ -17,41 +17,72 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import shortid from 'shortid';
|
import shortid from 'shortid';
|
||||||
|
import {
|
||||||
|
Datasource,
|
||||||
|
DatasourceType,
|
||||||
|
JsonObject,
|
||||||
|
QueryFormData,
|
||||||
|
} from '@superset-ui/core';
|
||||||
|
import { Slice } from 'src/types/Chart';
|
||||||
|
import { CommonBootstrapData } from 'src/types/bootstrapTypes';
|
||||||
|
|
||||||
import getToastsFromPyFlashMessages from '../../messageToasts/utils/getToastsFromPyFlashMessages';
|
import getToastsFromPyFlashMessages from 'src/messageToasts/utils/getToastsFromPyFlashMessages';
|
||||||
import { getChartKey } from '../exploreUtils';
|
import { getChartKey } from 'src/explore/exploreUtils';
|
||||||
import { getControlsState } from '../store';
|
import { getControlsState } from 'src/explore/store';
|
||||||
import {
|
import {
|
||||||
getFormDataFromControls,
|
getFormDataFromControls,
|
||||||
applyMapStateToPropsToControl,
|
applyMapStateToPropsToControl,
|
||||||
} from '../controlUtils';
|
} from 'src/explore/controlUtils';
|
||||||
|
import { ControlStateMapping } from '@superset-ui/chart-controls';
|
||||||
|
|
||||||
export default function getInitialState(bootstrapData) {
|
export interface ExlorePageBootstrapData extends JsonObject {
|
||||||
const { form_data: rawFormData } = bootstrapData;
|
can_add: boolean;
|
||||||
|
can_download: boolean;
|
||||||
|
can_overwrite: boolean;
|
||||||
|
datasource: Datasource;
|
||||||
|
form_data: QueryFormData;
|
||||||
|
datasource_id: number;
|
||||||
|
datasource_type: DatasourceType;
|
||||||
|
slice: Slice | null;
|
||||||
|
standalone: boolean;
|
||||||
|
user_id: number;
|
||||||
|
forced_height: string | null;
|
||||||
|
common: CommonBootstrapData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function getInitialState(
|
||||||
|
bootstrapData: ExlorePageBootstrapData,
|
||||||
|
) {
|
||||||
|
const { form_data: initialFormData } = bootstrapData;
|
||||||
const { slice } = bootstrapData;
|
const { slice } = bootstrapData;
|
||||||
const sliceName = slice ? slice.slice_name : null;
|
const sliceName = slice ? slice.slice_name : null;
|
||||||
const bootstrappedState = {
|
|
||||||
|
const exploreState = {
|
||||||
|
// note this will add `form_data` to state,
|
||||||
|
// which will be manipulatable by future reducers.
|
||||||
...bootstrapData,
|
...bootstrapData,
|
||||||
sliceName,
|
sliceName,
|
||||||
common: {
|
common: {
|
||||||
flash_messages: bootstrapData.common.flash_messages,
|
flash_messages: bootstrapData.common.flash_messages,
|
||||||
conf: bootstrapData.common.conf,
|
conf: bootstrapData.common.conf,
|
||||||
},
|
},
|
||||||
rawFormData,
|
|
||||||
filterColumnOpts: [],
|
|
||||||
isDatasourceMetaLoading: false,
|
isDatasourceMetaLoading: false,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
|
// Initial control state will skip `control.mapStateToProps`
|
||||||
|
// because `bootstrapData.controls` is undefined.
|
||||||
|
controls: getControlsState(
|
||||||
|
bootstrapData,
|
||||||
|
initialFormData,
|
||||||
|
) as ControlStateMapping,
|
||||||
};
|
};
|
||||||
const controls = getControlsState(bootstrappedState, rawFormData);
|
|
||||||
bootstrappedState.controls = controls;
|
|
||||||
|
|
||||||
// apply initial mapStateToProps for all controls, must execute AFTER
|
// apply initial mapStateToProps for all controls, must execute AFTER
|
||||||
// bootstrappedState has initialized `controls`. Order of execution is not
|
// bootstrapState has initialized `controls`. Order of execution is not
|
||||||
// guaranteed, so controls shouldn't rely on the each other's mapped state.
|
// guaranteed, so controls shouldn't rely on the each other's mapped state.
|
||||||
Object.entries(controls).forEach(([key, controlState]) => {
|
Object.entries(exploreState.controls).forEach(([key, controlState]) => {
|
||||||
controls[key] = applyMapStateToPropsToControl(
|
exploreState.controls[key] = applyMapStateToPropsToControl(
|
||||||
controlState,
|
controlState,
|
||||||
bootstrappedState,
|
exploreState,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -59,7 +90,7 @@ export default function getInitialState(bootstrapData) {
|
||||||
? getFormDataFromControls(getControlsState(bootstrapData, slice.form_data))
|
? getFormDataFromControls(getControlsState(bootstrapData, slice.form_data))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const chartKey = getChartKey(bootstrappedState);
|
const chartKey: number = getChartKey(bootstrapData);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
charts: {
|
charts: {
|
||||||
|
|
@ -69,7 +100,7 @@ export default function getInitialState(bootstrapData) {
|
||||||
chartStatus: null,
|
chartStatus: null,
|
||||||
chartUpdateEndTime: null,
|
chartUpdateEndTime: null,
|
||||||
chartUpdateStartTime: 0,
|
chartUpdateStartTime: 0,
|
||||||
latestQueryFormData: getFormDataFromControls(controls),
|
latestQueryFormData: getFormDataFromControls(exploreState.controls),
|
||||||
sliceFormData,
|
sliceFormData,
|
||||||
queryController: null,
|
queryController: null,
|
||||||
queriesResponse: null,
|
queriesResponse: null,
|
||||||
|
|
@ -81,10 +112,12 @@ export default function getInitialState(bootstrapData) {
|
||||||
dashboards: [],
|
dashboards: [],
|
||||||
saveModalAlert: null,
|
saveModalAlert: null,
|
||||||
},
|
},
|
||||||
explore: bootstrappedState,
|
explore: exploreState,
|
||||||
impressionId: shortid.generate(),
|
impressionId: shortid.generate(),
|
||||||
messageToasts: getToastsFromPyFlashMessages(
|
messageToasts: getToastsFromPyFlashMessages(
|
||||||
(bootstrapData.common || {}).flash_messages || [],
|
(bootstrapData.common || {}).flash_messages || [],
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ExploreState = ReturnType<typeof getInitialState>;
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
* The Chart model as returned from the API
|
* The Chart model as returned from the API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { QueryFormData } from '@superset-ui/core';
|
||||||
import Owner from './Owner';
|
import Owner from './Owner';
|
||||||
|
|
||||||
export interface Chart {
|
export interface Chart {
|
||||||
|
|
@ -45,7 +46,7 @@ export type Slice = {
|
||||||
slice_name: string;
|
slice_name: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
cache_timeout: number | null;
|
cache_timeout: number | null;
|
||||||
url?: string;
|
form_data?: QueryFormData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Chart;
|
export default Chart;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { JsonObject, Locale } from '@superset-ui/core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
* or more contributor license agreements. See the NOTICE file
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
|
@ -42,3 +44,10 @@ export type Dashboard = {
|
||||||
creator?: string;
|
creator?: string;
|
||||||
creator_url?: string;
|
creator_url?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface CommonBootstrapData {
|
||||||
|
flash_messages: string[][];
|
||||||
|
conf: JsonObject;
|
||||||
|
locale: Locale;
|
||||||
|
feature_flags: Record<string, boolean>;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue