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-multi-assign': 0,
|
||||
'no-multi-spaces': 0,
|
||||
'no-nested-ternary': 0,
|
||||
'no-prototype-builtins': 0,
|
||||
'no-restricted-properties': 0,
|
||||
'no-restricted-imports': [
|
||||
|
|
@ -226,6 +227,7 @@ module.exports = {
|
|||
'no-mixed-operators': 0,
|
||||
'no-multi-assign': 0,
|
||||
'no-multi-spaces': 0,
|
||||
'no-nested-ternary': 0,
|
||||
'no-prototype-builtins': 0,
|
||||
'no-restricted-properties': 0,
|
||||
'no-restricted-imports': [
|
||||
|
|
|
|||
|
|
@ -55,44 +55,3 @@ describe('Advanced analytics', () => {
|
|||
.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: [],
|
||||
groupby: ['name'],
|
||||
columns: ['state'],
|
||||
row_limit: 50000,
|
||||
row_limit: 5000,
|
||||
pandas_aggfunc: 'sum',
|
||||
pivot_margins: true,
|
||||
number_format: '.3s',
|
||||
|
|
|
|||
|
|
@ -18,10 +18,17 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
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 { 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';
|
||||
|
||||
describe('ControlPanelsContainer', () => {
|
||||
|
|
@ -78,15 +85,15 @@ describe('ControlPanelsContainer', () => {
|
|||
});
|
||||
|
||||
function getDefaultProps() {
|
||||
const controls = defaultControls as ControlPanelsContainerProps['controls'];
|
||||
return {
|
||||
datasource_type: 'table',
|
||||
datasource_type: DatasourceType.Table,
|
||||
actions: {},
|
||||
controls: defaultControls,
|
||||
// Note: default viz_type is table
|
||||
form_data: getFormDataFromControls(defaultControls),
|
||||
controls,
|
||||
form_data: getFormDataFromControls(controls),
|
||||
isDatasourceMetaLoading: false,
|
||||
exploreState: {},
|
||||
};
|
||||
} as ControlPanelsContainerProps;
|
||||
}
|
||||
|
||||
it('renders ControlPanelSections', () => {
|
||||
|
|
@ -17,7 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
export const DATETIME_WITH_TIME_ZONE = 'YYYY-MM-DD HH:mm:ssZ';
|
||||
|
||||
export const TIME_WITH_MS = 'HH:mm:ss.SSS';
|
||||
|
||||
export const BOOL_TRUE_DISPLAY = 'True';
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export const SET_FIELD_VALUE = 'SET_FIELD_VALUE';
|
|||
export function setControlValue(
|
||||
controlName: string,
|
||||
value: any,
|
||||
validationErrors: any[],
|
||||
validationErrors?: any[],
|
||||
) {
|
||||
return { type: SET_FIELD_VALUE, controlName, value, validationErrors };
|
||||
}
|
||||
|
|
@ -109,11 +109,6 @@ export function setExploreControls(formData: QueryFormData) {
|
|||
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 function updateChartTitle(sliceName: string) {
|
||||
return { type: UPDATE_CHART_TITLE, sliceName };
|
||||
|
|
@ -154,7 +149,6 @@ export const exploreActions = {
|
|||
saveFaveStar,
|
||||
setControlValue,
|
||||
setExploreControls,
|
||||
removeControlPanelAlert,
|
||||
updateChartTitle,
|
||||
createNewSlice,
|
||||
sliceUpdated,
|
||||
|
|
|
|||
|
|
@ -18,30 +18,47 @@
|
|||
*/
|
||||
/* eslint camelcase: 0 */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from '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 Alert from 'src/components/Alert';
|
||||
import Collapse from 'src/common/components/Collapse';
|
||||
import { PluginContext } from 'src/components/DynamicPlugins';
|
||||
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 Control from './Control';
|
||||
import { sectionsToRender } from '../controlUtils';
|
||||
import { exploreActions } from '../actions/exploreActions';
|
||||
import { ExploreActions, exploreActions } from '../actions/exploreActions';
|
||||
import { ExploreState } from '../reducers/getInitialState';
|
||||
|
||||
const propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
alert: PropTypes.string,
|
||||
datasource_type: PropTypes.string.isRequired,
|
||||
exploreState: PropTypes.object.isRequired,
|
||||
controls: PropTypes.object.isRequired,
|
||||
form_data: PropTypes.object.isRequired,
|
||||
isDatasourceMetaLoading: PropTypes.bool.isRequired,
|
||||
export type ControlPanelsContainerProps = {
|
||||
actions: ExploreActions;
|
||||
datasource_type: DatasourceType;
|
||||
exploreState: Record<string, any>;
|
||||
controls: Record<string, ControlState>;
|
||||
form_data: QueryFormData;
|
||||
isDatasourceMetaLoading: boolean;
|
||||
};
|
||||
|
||||
export type ExpandedControlPanelSectionConfig = Omit<
|
||||
ControlPanelSectionConfig,
|
||||
'controlSetRows'
|
||||
> & {
|
||||
controlSetRows: ExpandedControlItem[][];
|
||||
};
|
||||
|
||||
const Styles = styled.div`
|
||||
|
|
@ -50,9 +67,6 @@ const Styles = styled.div`
|
|||
overflow: auto;
|
||||
overflow-x: visible;
|
||||
overflow-y: auto;
|
||||
.remove-alert {
|
||||
cursor: pointer;
|
||||
}
|
||||
#controlSections {
|
||||
min-height: 100%;
|
||||
overflow: visible;
|
||||
|
|
@ -80,60 +94,34 @@ const ControlPanelsTabs = styled(Tabs)`
|
|||
height: 100%;
|
||||
}
|
||||
`;
|
||||
class ControlPanelsContainer extends React.Component {
|
||||
|
||||
class ControlPanelsContainer extends React.Component<ControlPanelsContainerProps> {
|
||||
// trigger updates to the component when async plugins load
|
||||
static contextType = PluginContext;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: ControlPanelsContainerProps) {
|
||||
super(props);
|
||||
|
||||
this.removeAlert = this.removeAlert.bind(this);
|
||||
this.renderControl = this.renderControl.bind(this);
|
||||
this.renderControlPanelSection = this.renderControlPanelSection.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
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() {
|
||||
sectionsToRender(): ExpandedControlPanelSectionConfig[] {
|
||||
return sectionsToRender(
|
||||
this.props.form_data.viz_type,
|
||||
this.props.datasource_type,
|
||||
);
|
||||
}
|
||||
|
||||
sectionsToExpand(sections) {
|
||||
sectionsToExpand(sections: ControlPanelSectionConfig[]) {
|
||||
return sections.reduce(
|
||||
(acc, cur) => (cur.expanded ? [...acc, cur.label] : acc),
|
||||
[],
|
||||
(acc, section) =>
|
||||
section.expanded ? [...acc, String(section.label)] : acc,
|
||||
[] as string[],
|
||||
);
|
||||
}
|
||||
|
||||
removeAlert() {
|
||||
this.props.actions.removeControlPanelAlert();
|
||||
}
|
||||
|
||||
renderControl({ name, config }) {
|
||||
const { actions, controls, form_data: formData } = this.props;
|
||||
renderControl({ name, config }: CustomControlItem) {
|
||||
const { actions, controls } = this.props;
|
||||
const { visibility } = config;
|
||||
|
||||
// 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],
|
||||
name,
|
||||
};
|
||||
const {
|
||||
validationErrors,
|
||||
provideFormDataToProps,
|
||||
...restProps
|
||||
} = controlData;
|
||||
const { validationErrors, ...restProps } = controlData as ControlState & {
|
||||
validationErrors?: any[];
|
||||
};
|
||||
|
||||
// if visibility check says the config is not visible, don't render it
|
||||
if (visibility && !visibility.call(config, this.props, controlData)) {
|
||||
|
|
@ -160,30 +146,42 @@ class ControlPanelsContainer extends React.Component {
|
|||
name={name}
|
||||
validationErrors={validationErrors}
|
||||
actions={actions}
|
||||
formData={provideFormDataToProps ? formData : null}
|
||||
datasource={formData?.datasource}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderControlPanelSection(section) {
|
||||
renderControlPanelSection(section: ExpandedControlPanelSectionConfig) {
|
||||
const { controls } = this.props;
|
||||
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 =>
|
||||
rows.some(
|
||||
s =>
|
||||
controls[s] &&
|
||||
controls[s].validationErrors &&
|
||||
controls[s].validationErrors.length > 0,
|
||||
),
|
||||
rows.some(item => {
|
||||
const controlName =
|
||||
typeof item === 'string'
|
||||
? item
|
||||
: item && 'name' in item
|
||||
? item.name
|
||||
: null;
|
||||
return (
|
||||
controlName &&
|
||||
controlName in controls &&
|
||||
controls[controlName].validationErrors &&
|
||||
controls[controlName].validationErrors.length > 0
|
||||
);
|
||||
}),
|
||||
);
|
||||
const PanelHeader = () => (
|
||||
<span>
|
||||
<span>{label}</span>{' '}
|
||||
{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 && (
|
||||
<InfoTooltipWithTrigger
|
||||
|
|
@ -199,7 +197,7 @@ class ControlPanelsContainer extends React.Component {
|
|||
<Collapse.Panel
|
||||
className="control-panel-section"
|
||||
header={PanelHeader()}
|
||||
key={section.label}
|
||||
key={sectionId}
|
||||
>
|
||||
{section.controlSetRows.map((controlSets, i) => {
|
||||
const renderedControls = controlSets
|
||||
|
|
@ -229,7 +227,6 @@ class ControlPanelsContainer extends React.Component {
|
|||
return (
|
||||
<ControlRow
|
||||
key={`controlsetrow-${i}`}
|
||||
className="control-row"
|
||||
controls={renderedControls}
|
||||
/>
|
||||
);
|
||||
|
|
@ -247,8 +244,8 @@ class ControlPanelsContainer extends React.Component {
|
|||
return <Loading />;
|
||||
}
|
||||
|
||||
const querySectionsToRender = [];
|
||||
const displaySectionsToRender = [];
|
||||
const querySectionsToRender: ExpandedControlPanelSectionConfig[] = [];
|
||||
const displaySectionsToRender: ExpandedControlPanelSectionConfig[] = [];
|
||||
this.sectionsToRender().forEach(section => {
|
||||
// if at least one control in the section is not `renderTrigger`
|
||||
// or asks to be displayed at the Data tab
|
||||
|
|
@ -258,6 +255,8 @@ class ControlPanelsContainer extends React.Component {
|
|||
rows.some(
|
||||
control =>
|
||||
control &&
|
||||
typeof control === 'object' &&
|
||||
'config' in control &&
|
||||
control.config &&
|
||||
(!control.config.renderTrigger ||
|
||||
control.config.tabOverride === 'data'),
|
||||
|
|
@ -277,14 +276,6 @@ class ControlPanelsContainer extends React.Component {
|
|||
);
|
||||
return (
|
||||
<Styles>
|
||||
{this.props.alert && (
|
||||
<Alert
|
||||
type="warning"
|
||||
message={this.props.alert}
|
||||
closable
|
||||
onClose={this.removeAlert}
|
||||
/>
|
||||
)}
|
||||
<ControlPanelsTabs
|
||||
id="controlSections"
|
||||
data-test="control-tabs"
|
||||
|
|
@ -318,26 +309,19 @@ class ControlPanelsContainer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
ControlPanelsContainer.propTypes = propTypes;
|
||||
export { ControlPanelsContainer };
|
||||
|
||||
function mapStateToProps({ explore }) {
|
||||
export default connect(
|
||||
function mapStateToProps({ explore }: ExploreState) {
|
||||
return {
|
||||
alert: explore.controlPanelAlert,
|
||||
isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
|
||||
controls: explore.controls,
|
||||
exploreState: explore,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
},
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(exploreActions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export { ControlPanelsContainer };
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
},
|
||||
)(ControlPanelsContainer);
|
||||
|
|
@ -16,20 +16,20 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ExpandedControlItem } from '@superset-ui/chart-controls';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const NUM_COLUMNS = 12;
|
||||
|
||||
const propTypes = {
|
||||
controls: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
function ControlSetRow(props) {
|
||||
const colSize = NUM_COLUMNS / props.controls.length;
|
||||
export default function ControlRow({
|
||||
controls,
|
||||
}: {
|
||||
controls: ExpandedControlItem[];
|
||||
}) {
|
||||
const colSize = NUM_COLUMNS / controls.length;
|
||||
return (
|
||||
<div className="row space-1">
|
||||
{props.controls.map((control, i) => (
|
||||
{controls.map((control, i) => (
|
||||
<div className={`col-lg-${colSize} col-xs-12`} key={i}>
|
||||
{control}
|
||||
</div>
|
||||
|
|
@ -37,6 +37,3 @@ function ControlSetRow(props) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ControlSetRow.propTypes = propTypes;
|
||||
export default ControlSetRow;
|
||||
|
|
@ -222,10 +222,7 @@ function ExploreViewContainer(props) {
|
|||
}
|
||||
|
||||
function onQuery() {
|
||||
// remove alerts when query
|
||||
props.actions.removeControlPanelAlert();
|
||||
props.actions.triggerQuery(true, props.chart.id);
|
||||
|
||||
addHistory();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,12 +69,6 @@ export default class CollectionControl extends React.Component {
|
|||
this.onAdd = this.onAdd.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.datasource.name !== this.props.datasource.name) {
|
||||
this.props.onChange([]);
|
||||
}
|
||||
}
|
||||
|
||||
onChange(i, value) {
|
||||
Object.assign(this.props.value[i], value);
|
||||
this.props.onChange(this.props.value);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import rison from 'rison';
|
||||
import {
|
||||
SupersetClient,
|
||||
|
|
@ -25,6 +25,7 @@ import {
|
|||
t,
|
||||
TimeRangeEndpoints,
|
||||
} from '@superset-ui/core';
|
||||
import { DatasourceMeta } from '@superset-ui/chart-controls';
|
||||
import {
|
||||
buildTimeRangeString,
|
||||
formatTimeRange,
|
||||
|
|
@ -38,6 +39,8 @@ import { Divider } from 'src/common/components';
|
|||
import Icon from 'src/components/Icon';
|
||||
import { Select } from 'src/components/Select';
|
||||
import { Tooltip } from 'src/common/components/Tooltip';
|
||||
import { DEFAULT_TIME_RANGE } from 'src/explore/constants';
|
||||
|
||||
import { SelectOptionType, FrameType } from './types';
|
||||
import {
|
||||
COMMON_RANGE_VALUES_SET,
|
||||
|
|
@ -165,28 +168,27 @@ const IconWrapper = styled.span`
|
|||
}
|
||||
`;
|
||||
|
||||
interface DateFilterLabelProps {
|
||||
interface DateFilterControlProps {
|
||||
name: string;
|
||||
onChange: (timeRange: string) => void;
|
||||
value?: string;
|
||||
endpoints?: TimeRangeEndpoints;
|
||||
datasource?: string;
|
||||
datasource?: DatasourceMeta;
|
||||
}
|
||||
|
||||
export default function DateFilterControl(props: DateFilterLabelProps) {
|
||||
const { value = 'Last week', endpoints, onChange, datasource } = props;
|
||||
export default function DateFilterControl(props: DateFilterControlProps) {
|
||||
const { value = DEFAULT_TIME_RANGE, endpoints, onChange } = props;
|
||||
const [actualTimeRange, setActualTimeRange] = useState<string>(value);
|
||||
|
||||
const [show, setShow] = useState<boolean>(false);
|
||||
const [frame, setFrame] = useState<FrameType>(guessFrame(value));
|
||||
const [isMounted, setIsMounted] = useState<boolean>(false);
|
||||
const guessedFrame = useMemo(() => guessFrame(value), [value]);
|
||||
const [frame, setFrame] = useState<FrameType>(guessedFrame);
|
||||
const [timeRangeValue, setTimeRangeValue] = useState(value);
|
||||
const [validTimeRange, setValidTimeRange] = useState<boolean>(false);
|
||||
const [evalResponse, setEvalResponse] = useState<string>(value);
|
||||
const [tooltipTitle, setTooltipTitle] = useState<string>(value);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMounted) setIsMounted(true);
|
||||
fetchTimeRange(value, endpoints).then(({ value: actualRange, error }) => {
|
||||
if (error) {
|
||||
setEvalResponse(error || '');
|
||||
|
|
@ -205,9 +207,9 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
|
|||
+--------------+------+----------+--------+----------+-----------+
|
||||
*/
|
||||
if (
|
||||
frame === 'Common' ||
|
||||
frame === 'Calendar' ||
|
||||
frame === 'No filter'
|
||||
guessedFrame === 'Common' ||
|
||||
guessedFrame === 'Calendar' ||
|
||||
guessedFrame === 'No filter'
|
||||
) {
|
||||
setActualTimeRange(value);
|
||||
setTooltipTitle(actualRange || '');
|
||||
|
|
@ -220,14 +222,6 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
|
|||
});
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMounted) {
|
||||
onChange('Last week');
|
||||
setTimeRangeValue('Last week');
|
||||
setFrame(guessFrame('Last week'));
|
||||
}
|
||||
}, [datasource]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTimeRange(timeRangeValue, endpoints).then(({ value, error }) => {
|
||||
if (error) {
|
||||
|
|
@ -247,13 +241,13 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
|
|||
|
||||
function onOpen() {
|
||||
setTimeRangeValue(value);
|
||||
setFrame(guessFrame(value));
|
||||
setFrame(guessedFrame);
|
||||
setShow(true);
|
||||
}
|
||||
|
||||
function onHide() {
|
||||
setTimeRangeValue(value);
|
||||
setFrame(guessFrame(value));
|
||||
setFrame(guessedFrame);
|
||||
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') {
|
||||
setTimeRangeValue('No filter');
|
||||
}
|
||||
|
|
@ -278,7 +272,7 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
|
|||
<Select
|
||||
options={FRAME_OPTIONS}
|
||||
value={FRAME_OPTIONS.filter(({ value }) => value === frame)}
|
||||
onChange={onFrame}
|
||||
onChange={onChangeFrame}
|
||||
className="frame-dropdown"
|
||||
/>
|
||||
{frame !== 'No filter' && <Divider />}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,13 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
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 adhocMetricType from 'src/explore/components/controls/MetricControl/adhocMetricType';
|
||||
|
|
@ -39,6 +45,11 @@ import AdhocFilterOption from './AdhocFilterOption';
|
|||
import AdhocFilter, { CLAUSES, EXPRESSION_TYPES } from './AdhocFilter';
|
||||
import adhocFilterType from './adhocFilterType';
|
||||
|
||||
const selectedMetricType = PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
adhocMetricType,
|
||||
]);
|
||||
|
||||
const propTypes = {
|
||||
name: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
|
|
@ -46,12 +57,10 @@ const propTypes = {
|
|||
datasource: PropTypes.object,
|
||||
columns: PropTypes.arrayOf(columnType),
|
||||
savedMetrics: PropTypes.arrayOf(savedMetricType),
|
||||
formData: PropTypes.shape({
|
||||
metric: PropTypes.oneOfType([PropTypes.string, adhocMetricType]),
|
||||
metrics: PropTypes.arrayOf(
|
||||
PropTypes.oneOfType([PropTypes.string, adhocMetricType]),
|
||||
),
|
||||
}),
|
||||
selectedMetrics: PropTypes.oneOfType([
|
||||
selectedMetricType,
|
||||
PropTypes.arrayOf(selectedMetricType),
|
||||
]),
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
|
@ -60,7 +69,7 @@ const defaultProps = {
|
|||
onChange: () => {},
|
||||
columns: [],
|
||||
savedMetrics: [],
|
||||
formData: {},
|
||||
selectedMetrics: [],
|
||||
};
|
||||
|
||||
function isDictionaryForAdhocFilter(value) {
|
||||
|
|
@ -141,10 +150,7 @@ class AdhocFilterControl extends React.Component {
|
|||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (
|
||||
this.props.columns !== nextProps.columns ||
|
||||
this.props.formData !== nextProps.formData
|
||||
) {
|
||||
if (this.props.columns !== nextProps.columns) {
|
||||
this.setState({ options: this.optionsForSelect(nextProps) });
|
||||
}
|
||||
if (this.props.value !== nextProps.value) {
|
||||
|
|
@ -270,7 +276,7 @@ class AdhocFilterControl extends React.Component {
|
|||
optionsForSelect(props) {
|
||||
const options = [
|
||||
...props.columns,
|
||||
...[...(props.formData?.metrics || []), props.formData?.metric].map(
|
||||
...ensureIsArray(props.selectedMetrics).map(
|
||||
metric =>
|
||||
metric &&
|
||||
(typeof metric === 'string'
|
||||
|
|
|
|||
|
|
@ -123,9 +123,6 @@ export default class AdhocMetricEditPopover extends React.PureComponent {
|
|||
adhocMetricLabel: this.state.adhocMetric?.getDefaultLabel(),
|
||||
});
|
||||
}
|
||||
if (prevProps.datasource !== this.props.datasource) {
|
||||
this.props.onChange(null);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@ class AdhocMetricOption extends React.PureComponent {
|
|||
onMoveLabel,
|
||||
onDropLabel,
|
||||
index,
|
||||
datasource,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
|
@ -70,7 +69,6 @@ class AdhocMetricOption extends React.PureComponent {
|
|||
columns={columns}
|
||||
savedMetricsOptions={savedMetricsOptions}
|
||||
savedMetric={savedMetric}
|
||||
datasource={datasource}
|
||||
datasourceType={datasourceType}
|
||||
>
|
||||
<OptionControlLabel
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ export type AdhocMetricPopoverTriggerProps = {
|
|||
savedMetricsOptions: savedMetricType[];
|
||||
savedMetric: savedMetricType;
|
||||
datasourceType: string;
|
||||
datasource: string;
|
||||
children: ReactNode;
|
||||
createNew?: boolean;
|
||||
};
|
||||
|
|
@ -160,7 +159,6 @@ class AdhocMetricPopoverTrigger extends React.PureComponent<
|
|||
columns={this.props.columns}
|
||||
savedMetricsOptions={this.props.savedMetricsOptions}
|
||||
savedMetric={this.props.savedMetric}
|
||||
datasource={this.props.datasource}
|
||||
datasourceType={this.props.datasourceType}
|
||||
onResize={this.onPopoverResize}
|
||||
onClose={this.closePopover}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ const propTypes = {
|
|||
savedMetricsOptions: PropTypes.arrayOf(savedMetricType),
|
||||
multi: PropTypes.bool,
|
||||
datasourceType: PropTypes.string,
|
||||
datasource: PropTypes.string,
|
||||
};
|
||||
|
||||
export default function MetricDefinitionValue({
|
||||
|
|
@ -52,16 +51,15 @@ export default function MetricDefinitionValue({
|
|||
onMoveLabel,
|
||||
onDropLabel,
|
||||
index,
|
||||
datasource,
|
||||
}) {
|
||||
const getSavedMetricByName = metricName =>
|
||||
savedMetrics.find(metric => metric.metric_name === metricName);
|
||||
|
||||
let savedMetric;
|
||||
if (option.metric_name) {
|
||||
savedMetric = option;
|
||||
} else if (typeof option === 'string') {
|
||||
if (typeof option === 'string') {
|
||||
savedMetric = getSavedMetricByName(option);
|
||||
} else if (option.metric_name) {
|
||||
savedMetric = option;
|
||||
}
|
||||
|
||||
if (option instanceof AdhocMetric || savedMetric) {
|
||||
|
|
@ -79,7 +77,6 @@ export default function MetricDefinitionValue({
|
|||
onDropLabel,
|
||||
index,
|
||||
savedMetric: savedMetric ?? {},
|
||||
datasource,
|
||||
};
|
||||
|
||||
return <AdhocMetricOption {...metricOptionProps} />;
|
||||
|
|
|
|||
|
|
@ -179,10 +179,10 @@ class MetricsControl extends React.PureComponent {
|
|||
) {
|
||||
this.setState({ options: this.optionsForSelect(nextProps) });
|
||||
|
||||
// Remove metrics if selected value no longer a column
|
||||
const containsAllMetrics = columnsContainAllMetrics(value, nextProps);
|
||||
|
||||
if (!containsAllMetrics) {
|
||||
// Remove all metrics if selected value no longer a valid column
|
||||
// in the dataset. Must use `nextProps` here because Redux reducers may
|
||||
// have already updated the value for this control.
|
||||
if (!columnsContainAllMetrics(nextProps.value, nextProps)) {
|
||||
this.props.onChange([]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,71 +17,55 @@
|
|||
* under the License.
|
||||
*/
|
||||
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 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;
|
||||
isFloat?: boolean;
|
||||
isInt?: boolean;
|
||||
onChange?: (value: any, errors: any) => {};
|
||||
onChange?: (value: T, errors: any) => {};
|
||||
onFocus?: () => {};
|
||||
placeholder?: string;
|
||||
value?: string | number;
|
||||
value?: T | null;
|
||||
controlId?: string;
|
||||
renderTrigger?: boolean;
|
||||
datasource?: string;
|
||||
}
|
||||
|
||||
interface TextControlState {
|
||||
export interface TextControlState {
|
||||
controlId: string;
|
||||
currentDatasource?: string;
|
||||
value?: string | number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const generateControlId = (controlId?: string) =>
|
||||
`formInlineName_${controlId ?? (Math.random() * 1000000).toFixed()}`;
|
||||
|
||||
export default class TextControl extends React.Component<
|
||||
TextControlProps,
|
||||
TextControlState
|
||||
> {
|
||||
debouncedOnChange = debounce((inputValue: string) => {
|
||||
this.onChange(inputValue);
|
||||
}, 500);
|
||||
const safeStringify = (value?: InputValueType | null) =>
|
||||
value == null ? '' : String(value);
|
||||
|
||||
static getDerivedStateFromProps(
|
||||
props: TextControlProps,
|
||||
state: TextControlState,
|
||||
) {
|
||||
// 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;
|
||||
}
|
||||
export default class TextControl<
|
||||
T extends InputValueType = InputValueType
|
||||
> extends React.Component<TextControlProps<T>, TextControlState> {
|
||||
initialValue?: TextControlProps['value'];
|
||||
|
||||
constructor(props: TextControlProps) {
|
||||
constructor(props: TextControlProps<T>) {
|
||||
super(props);
|
||||
|
||||
this.initialValue = props.value;
|
||||
this.state = {
|
||||
// if there's no control id provided, generate a random
|
||||
// number to prevent rendering elements with same ids
|
||||
this.state = {
|
||||
controlId: generateControlId(props.controlId),
|
||||
value: props.value,
|
||||
currentDatasource: props.datasource,
|
||||
value: safeStringify(this.initialValue),
|
||||
};
|
||||
}
|
||||
|
||||
onChange = (inputValue: string) => {
|
||||
let parsedValue: string | number = inputValue;
|
||||
let parsedValue: InputValueType = inputValue;
|
||||
// Validation & casting
|
||||
const errors = [];
|
||||
if (inputValue !== '' && this.props.isFloat) {
|
||||
|
|
@ -102,26 +86,26 @@ export default class TextControl extends React.Component<
|
|||
parsedValue = parseInt(inputValue, 10);
|
||||
}
|
||||
}
|
||||
this.props.onChange?.(parsedValue, errors);
|
||||
this.props.onChange?.(parsedValue as T, errors);
|
||||
};
|
||||
|
||||
onChangeWrapper = (event: any) => {
|
||||
const { value } = event.target;
|
||||
this.setState({ value });
|
||||
debouncedOnChange = debounce((inputValue: string) => {
|
||||
this.onChange(inputValue);
|
||||
}, FAST_DEBOUNCE);
|
||||
|
||||
// use debounce when change takes effect immediately after user starts typing
|
||||
const onChange = this.props.renderTrigger
|
||||
? this.debouncedOnChange
|
||||
: this.onChange;
|
||||
onChange(value);
|
||||
onChangeWrapper: FormControlProps['onChange'] = event => {
|
||||
const { value } = event.target as HTMLInputElement;
|
||||
this.setState({ value }, () => {
|
||||
this.debouncedOnChange(value);
|
||||
});
|
||||
};
|
||||
|
||||
render = () => {
|
||||
const { value: rawValue } = this.state;
|
||||
const value =
|
||||
typeof rawValue !== 'undefined' && rawValue !== null
|
||||
? rawValue.toString()
|
||||
: '';
|
||||
let { value } = this.state;
|
||||
if (this.initialValue !== this.props.value) {
|
||||
this.initialValue = this.props.value;
|
||||
value = safeStringify(this.props.value);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...this.props} />
|
||||
|
|
|
|||
|
|
@ -55,12 +55,7 @@ export const HAVING_OPERATORS = [
|
|||
OPERATORS['>='],
|
||||
OPERATORS['<='],
|
||||
];
|
||||
export const MULTI_OPERATORS = new Set([
|
||||
OPERATORS.in,
|
||||
OPERATORS['not in'],
|
||||
OPERATORS.IN,
|
||||
OPERATORS['NOT IN'],
|
||||
]);
|
||||
export const MULTI_OPERATORS = new Set([OPERATORS.IN, OPERATORS['NOT IN']]);
|
||||
// CUSTOM_OPERATORS will show operator in simple mode,
|
||||
// but will generate customized sqlExpression
|
||||
export const CUSTOM_OPERATORS = new Set([OPERATORS['LATEST PARTITION']]);
|
||||
|
|
@ -103,3 +98,6 @@ export const TIME_FILTER_MAP = {
|
|||
druid_time_origin: '__time_origin',
|
||||
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 { getChartControlPanelRegistry } from '@superset-ui/core';
|
||||
import { expandControlConfig } from '@superset-ui/chart-controls';
|
||||
import * as SECTIONS from './controlPanels/sections';
|
||||
import * as SECTIONS from '../controlPanels/sections';
|
||||
|
||||
export function getFormDataFromControls(controlsState) {
|
||||
const formData = {};
|
||||
Object.keys(controlsState).forEach(controlName => {
|
||||
const control = controlsState[controlName];
|
||||
formData[controlName] = control.value;
|
||||
});
|
||||
return formData;
|
||||
}
|
||||
export * from './getFormDataFromControls';
|
||||
|
||||
export function validateControl(control, processedState) {
|
||||
const { validators } = control;
|
||||
|
|
@ -355,8 +355,9 @@ export const controls = {
|
|||
"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.',
|
||||
),
|
||||
mapStateToProps: state => ({
|
||||
endpoints: state.form_data ? state.form_data.time_range_endpoints : null,
|
||||
mapStateToProps: ({ form_data: formData }) => ({
|
||||
// eslint-disable-next-line camelcase
|
||||
endpoints: formData?.time_range_endpoints,
|
||||
}),
|
||||
},
|
||||
|
||||
|
|
@ -474,7 +475,6 @@ export const controls = {
|
|||
savedMetrics: state.datasource ? state.datasource.metrics : [],
|
||||
datasource: state.datasource,
|
||||
}),
|
||||
provideFormDataToProps: true,
|
||||
},
|
||||
|
||||
color_scheme: {
|
||||
|
|
|
|||
|
|
@ -18,13 +18,14 @@
|
|||
*/
|
||||
/* eslint camelcase: 0 */
|
||||
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 {
|
||||
getControlConfig,
|
||||
getFormDataFromControls,
|
||||
getControlStateFromControlConfig,
|
||||
} from '../controlUtils';
|
||||
import * as actions from '../actions/exploreActions';
|
||||
} from 'src/explore/controlUtils';
|
||||
import * as actions from 'src/explore/actions/exploreActions';
|
||||
|
||||
export default function exploreReducer(state = {}, action) {
|
||||
const actionHandlers = {
|
||||
|
|
@ -61,8 +62,38 @@ export default function exploreReducer(state = {}, action) {
|
|||
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 = {
|
||||
...state,
|
||||
controls,
|
||||
datasource: action.datasource,
|
||||
datasource_id: action.datasource.id,
|
||||
datasource_type: action.datasource.type,
|
||||
|
|
@ -85,12 +116,6 @@ export default function exploreReducer(state = {}, action) {
|
|||
datasources: action.datasources,
|
||||
};
|
||||
},
|
||||
[actions.REMOVE_CONTROL_PANEL_ALERT]() {
|
||||
return {
|
||||
...state,
|
||||
controlPanelAlert: null,
|
||||
};
|
||||
},
|
||||
[actions.SET_FIELD_VALUE]() {
|
||||
const new_form_data = state.form_data;
|
||||
const { controlName, value, validationErrors } = action;
|
||||
|
|
|
|||
|
|
@ -17,41 +17,72 @@
|
|||
* under the License.
|
||||
*/
|
||||
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 { getChartKey } from '../exploreUtils';
|
||||
import { getControlsState } from '../store';
|
||||
import getToastsFromPyFlashMessages from 'src/messageToasts/utils/getToastsFromPyFlashMessages';
|
||||
import { getChartKey } from 'src/explore/exploreUtils';
|
||||
import { getControlsState } from 'src/explore/store';
|
||||
import {
|
||||
getFormDataFromControls,
|
||||
applyMapStateToPropsToControl,
|
||||
} from '../controlUtils';
|
||||
} from 'src/explore/controlUtils';
|
||||
import { ControlStateMapping } from '@superset-ui/chart-controls';
|
||||
|
||||
export default function getInitialState(bootstrapData) {
|
||||
const { form_data: rawFormData } = bootstrapData;
|
||||
export interface ExlorePageBootstrapData extends JsonObject {
|
||||
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 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,
|
||||
sliceName,
|
||||
common: {
|
||||
flash_messages: bootstrapData.common.flash_messages,
|
||||
conf: bootstrapData.common.conf,
|
||||
},
|
||||
rawFormData,
|
||||
filterColumnOpts: [],
|
||||
isDatasourceMetaLoading: 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
|
||||
// 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.
|
||||
Object.entries(controls).forEach(([key, controlState]) => {
|
||||
controls[key] = applyMapStateToPropsToControl(
|
||||
Object.entries(exploreState.controls).forEach(([key, controlState]) => {
|
||||
exploreState.controls[key] = applyMapStateToPropsToControl(
|
||||
controlState,
|
||||
bootstrappedState,
|
||||
exploreState,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -59,7 +90,7 @@ export default function getInitialState(bootstrapData) {
|
|||
? getFormDataFromControls(getControlsState(bootstrapData, slice.form_data))
|
||||
: null;
|
||||
|
||||
const chartKey = getChartKey(bootstrappedState);
|
||||
const chartKey: number = getChartKey(bootstrapData);
|
||||
|
||||
return {
|
||||
charts: {
|
||||
|
|
@ -69,7 +100,7 @@ export default function getInitialState(bootstrapData) {
|
|||
chartStatus: null,
|
||||
chartUpdateEndTime: null,
|
||||
chartUpdateStartTime: 0,
|
||||
latestQueryFormData: getFormDataFromControls(controls),
|
||||
latestQueryFormData: getFormDataFromControls(exploreState.controls),
|
||||
sliceFormData,
|
||||
queryController: null,
|
||||
queriesResponse: null,
|
||||
|
|
@ -81,10 +112,12 @@ export default function getInitialState(bootstrapData) {
|
|||
dashboards: [],
|
||||
saveModalAlert: null,
|
||||
},
|
||||
explore: bootstrappedState,
|
||||
explore: exploreState,
|
||||
impressionId: shortid.generate(),
|
||||
messageToasts: getToastsFromPyFlashMessages(
|
||||
(bootstrapData.common || {}).flash_messages || [],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export type ExploreState = ReturnType<typeof getInitialState>;
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
* The Chart model as returned from the API
|
||||
*/
|
||||
|
||||
import { QueryFormData } from '@superset-ui/core';
|
||||
import Owner from './Owner';
|
||||
|
||||
export interface Chart {
|
||||
|
|
@ -45,7 +46,7 @@ export type Slice = {
|
|||
slice_name: string;
|
||||
description: string | null;
|
||||
cache_timeout: number | null;
|
||||
url?: string;
|
||||
form_data?: QueryFormData;
|
||||
};
|
||||
|
||||
export default Chart;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { JsonObject, Locale } from '@superset-ui/core';
|
||||
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
|
|
@ -42,3 +44,10 @@ export type Dashboard = {
|
|||
creator?: string;
|
||||
creator_url?: string;
|
||||
};
|
||||
|
||||
export interface CommonBootstrapData {
|
||||
flash_messages: string[][];
|
||||
conf: JsonObject;
|
||||
locale: Locale;
|
||||
feature_flags: Record<string, boolean>;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue