mirror of
https://github.com/apache/superset.git
synced 2026-05-11 02:45:46 +00:00
chore(frontend): comprehensive TypeScript quality improvements (#37625)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,16 +17,30 @@
|
||||
* under the License.
|
||||
*/
|
||||
/* eslint camelcase: 0 */
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import {
|
||||
ComponentType,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
useChangeEffect,
|
||||
useComponentDidMount,
|
||||
usePrevious,
|
||||
isMatrixifyEnabled,
|
||||
QueryFormData,
|
||||
JsonObject,
|
||||
MatrixifyFormData,
|
||||
DatasourceType,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
ControlStateMapping,
|
||||
ControlPanelState,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { t, styled, css, useTheme } from '@apache-superset/core/ui';
|
||||
import { logging } from '@apache-superset/core';
|
||||
import { debounce, isEqual, isObjectLike, omit, pick } from 'lodash';
|
||||
@@ -53,7 +67,6 @@ import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import cx from 'classnames';
|
||||
import * as chartActions from 'src/components/Chart/chartAction';
|
||||
import { fetchDatasourceMetadata } from 'src/dashboard/actions/datasources';
|
||||
import { chartPropShape } from 'src/dashboard/util/propShapes';
|
||||
import { mergeExtraFormData } from 'src/dashboard/components/nativeFilters/utils';
|
||||
import { postFormData, putFormData } from 'src/explore/exploreUtils/formData';
|
||||
import { datasourcesActions } from 'src/explore/actions/datasourcesActions';
|
||||
@@ -63,6 +76,15 @@ import * as exploreActions from 'src/explore/actions/exploreActions';
|
||||
import * as saveModalActions from 'src/explore/actions/saveModalActions';
|
||||
import { useTabId } from 'src/hooks/useTabId';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import {
|
||||
ChartState,
|
||||
Datasource,
|
||||
ExplorePageInitialData,
|
||||
ExplorePageState,
|
||||
SaveActionType,
|
||||
} from 'src/explore/types';
|
||||
import { Slice } from 'src/types/Chart';
|
||||
import { User } from 'src/types/bootstrapTypes';
|
||||
import ExploreChartPanel from '../ExploreChartPanel';
|
||||
import ConnectedControlPanelsContainer from '../ControlPanelsContainer';
|
||||
import SaveModal from '../SaveModal';
|
||||
@@ -70,30 +92,6 @@ import DataSourcePanel from '../DatasourcePanel';
|
||||
import ConnectedExploreChartHeader from '../ExploreChartHeader';
|
||||
import ExploreContainer from '../ExploreContainer';
|
||||
|
||||
const propTypes = {
|
||||
...ExploreChartPanel.propTypes,
|
||||
actions: PropTypes.object.isRequired,
|
||||
datasource_type: PropTypes.string.isRequired,
|
||||
dashboardId: PropTypes.number,
|
||||
colorScheme: PropTypes.string,
|
||||
ownColorScheme: PropTypes.string,
|
||||
dashboardColorScheme: PropTypes.string,
|
||||
isDatasourceMetaLoading: PropTypes.bool.isRequired,
|
||||
chart: chartPropShape.isRequired,
|
||||
slice: PropTypes.object,
|
||||
sliceName: PropTypes.string,
|
||||
controls: PropTypes.object.isRequired,
|
||||
forcedHeight: PropTypes.string,
|
||||
form_data: PropTypes.object.isRequired,
|
||||
standalone: PropTypes.bool.isRequired,
|
||||
force: PropTypes.bool,
|
||||
timeout: PropTypes.number,
|
||||
impressionId: PropTypes.string,
|
||||
vizType: PropTypes.string,
|
||||
saveAction: PropTypes.string,
|
||||
isSaveModalVisible: PropTypes.bool,
|
||||
};
|
||||
|
||||
const ExplorePanelContainer = styled.div`
|
||||
${({ theme }) => css`
|
||||
text-align: left;
|
||||
@@ -187,13 +185,13 @@ const updateHistory = debounce(
|
||||
const urlParams = payload?.url_params || {};
|
||||
Object.entries(urlParams).forEach(([key, value]) => {
|
||||
if (!RESERVED_CHART_URL_PARAMS.includes(key)) {
|
||||
additionalParam[key] = value;
|
||||
additionalParam[key] = value as string;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
let key;
|
||||
let stateModifier;
|
||||
let key: string | null | undefined;
|
||||
let stateModifier: 'replaceState' | 'pushState';
|
||||
if (isReplace) {
|
||||
key = await postFormData(
|
||||
datasourceId,
|
||||
@@ -205,22 +203,24 @@ const updateHistory = debounce(
|
||||
stateModifier = 'replaceState';
|
||||
} else {
|
||||
key = getUrlParam(URL_PARAMS.formDataKey);
|
||||
await putFormData(
|
||||
datasourceId,
|
||||
datasourceType,
|
||||
key,
|
||||
formData,
|
||||
chartId,
|
||||
tabId,
|
||||
);
|
||||
if (key) {
|
||||
await putFormData(
|
||||
datasourceId,
|
||||
datasourceType,
|
||||
key,
|
||||
formData,
|
||||
chartId,
|
||||
tabId,
|
||||
);
|
||||
}
|
||||
stateModifier = 'pushState';
|
||||
}
|
||||
// avoid race condition in case user changes route before explore updates the url
|
||||
if (window.location.pathname.startsWith(ensureAppRoot('/explore'))) {
|
||||
const url = mountExploreUrl(
|
||||
standalone ? URL_PARAMS.standalone.name : null,
|
||||
standalone ? URL_PARAMS.standalone.name : 'base',
|
||||
{
|
||||
[URL_PARAMS.formDataKey.name]: key,
|
||||
[URL_PARAMS.formDataKey.name]: key ?? '',
|
||||
...additionalParam,
|
||||
},
|
||||
force,
|
||||
@@ -234,16 +234,27 @@ const updateHistory = debounce(
|
||||
1000,
|
||||
);
|
||||
|
||||
const defaultSidebarsWidth = {
|
||||
type DefaultSidebarWidthKey = 'controls_width' | 'datasource_width';
|
||||
|
||||
const defaultSidebarsWidth: Record<DefaultSidebarWidthKey, number> = {
|
||||
controls_width: 320,
|
||||
datasource_width: 300,
|
||||
};
|
||||
|
||||
function getSidebarWidths(key) {
|
||||
return getItem(key, defaultSidebarsWidth[key]);
|
||||
function getSidebarWidths(
|
||||
key: LocalStorageKeys.ControlsWidth | LocalStorageKeys.DatasourceWidth,
|
||||
): number {
|
||||
const defaultKey =
|
||||
key === LocalStorageKeys.ControlsWidth
|
||||
? 'controls_width'
|
||||
: 'datasource_width';
|
||||
return getItem(key, defaultSidebarsWidth[defaultKey]);
|
||||
}
|
||||
|
||||
function setSidebarWidths(key, dimension) {
|
||||
function setSidebarWidths(
|
||||
key: LocalStorageKeys.ControlsWidth | LocalStorageKeys.DatasourceWidth,
|
||||
dimension: { width: number },
|
||||
) {
|
||||
const newDimension = Number(getSidebarWidths(key)) + dimension.width;
|
||||
setItem(key, newDimension);
|
||||
}
|
||||
@@ -266,13 +277,100 @@ const AGGREGATED_CHART_TYPES = [
|
||||
'table',
|
||||
];
|
||||
|
||||
function isAggregatedChartType(vizType) {
|
||||
return AGGREGATED_CHART_TYPES.includes(vizType);
|
||||
function isAggregatedChartType(vizType: string | undefined): boolean {
|
||||
return vizType ? AGGREGATED_CHART_TYPES.includes(vizType) : false;
|
||||
}
|
||||
|
||||
function ExploreViewContainer(props) {
|
||||
interface ExploreRootState {
|
||||
explore: {
|
||||
controls: ControlStateMapping;
|
||||
slice: Slice | null;
|
||||
datasource: Datasource;
|
||||
metadata?: ExplorePageInitialData['metadata'];
|
||||
hiddenFormData?: Partial<QueryFormData>;
|
||||
isDatasourceMetaLoading: boolean;
|
||||
isStarred: boolean;
|
||||
can_add: boolean;
|
||||
can_download: boolean;
|
||||
can_overwrite: boolean;
|
||||
sliceName?: string;
|
||||
triggerRender: boolean;
|
||||
standalone: boolean;
|
||||
force: boolean;
|
||||
form_data?: QueryFormData;
|
||||
saveAction?: SaveActionType | null;
|
||||
};
|
||||
charts: Record<number, ChartState>;
|
||||
common: {
|
||||
conf: {
|
||||
SUPERSET_WEBSERVER_TIMEOUT: number;
|
||||
};
|
||||
};
|
||||
impressionId: string;
|
||||
dataMask: Record<number, { ownState?: JsonObject }>;
|
||||
reports: JsonObject;
|
||||
user: User;
|
||||
saveModal: {
|
||||
isVisible: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
addDangerToast: (msg: string) => void;
|
||||
addSuccessToast?: (msg: string) => void;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
isDatasourceMetaLoading: boolean;
|
||||
datasource: Datasource;
|
||||
datasource_type: DatasourceType;
|
||||
datasourceId: number;
|
||||
dashboardId?: number;
|
||||
colorScheme?: string;
|
||||
ownColorScheme?: string;
|
||||
dashboardColorScheme?: string;
|
||||
controls: ControlStateMapping;
|
||||
can_add: boolean;
|
||||
can_download: boolean;
|
||||
can_overwrite: boolean;
|
||||
column_formats: JsonObject | null;
|
||||
containerId: string;
|
||||
isStarred: boolean;
|
||||
slice: Slice | null;
|
||||
sliceName: string | null;
|
||||
triggerRender: boolean;
|
||||
form_data: QueryFormData;
|
||||
table_name?: string;
|
||||
vizType?: string;
|
||||
standalone: boolean;
|
||||
force: boolean;
|
||||
chart: ChartState;
|
||||
timeout: number;
|
||||
ownState?: JsonObject;
|
||||
impressionId: string;
|
||||
user: User;
|
||||
exploreState: ExplorePageState['explore'];
|
||||
reports: JsonObject;
|
||||
metadata?: ExplorePageInitialData['metadata'];
|
||||
saveAction?: SaveActionType | null;
|
||||
isSaveModalVisible: boolean;
|
||||
}
|
||||
|
||||
// Combined actions from all action modules used in Explore
|
||||
// Note: These modules export both action creators AND action type constants,
|
||||
// Using a callable signature to allow TypeScript to understand these are functions
|
||||
interface DispatchProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
actions: Record<string, (...args: any[]) => any>;
|
||||
}
|
||||
|
||||
type ExploreViewContainerProps = StateProps & DispatchProps & OwnProps;
|
||||
|
||||
function ExploreViewContainer(props: ExploreViewContainerProps) {
|
||||
const dynamicPluginContext = usePluginContext();
|
||||
const dynamicPlugin = dynamicPluginContext.dynamicPlugins[props.vizType];
|
||||
const dynamicPlugin = props.vizType
|
||||
? dynamicPluginContext.dynamicPlugins[props.vizType]
|
||||
: undefined;
|
||||
const isDynamicPluginLoading = dynamicPlugin && dynamicPlugin.mounting;
|
||||
const wasDynamicPluginLoading = usePrevious(isDynamicPluginLoading);
|
||||
|
||||
@@ -362,7 +460,7 @@ function ExploreViewContainer(props) {
|
||||
props.actions.setForceQuery(false);
|
||||
|
||||
// Skip main query if Matrixify is enabled
|
||||
if (isMatrixifyEnabled(props.form_data)) {
|
||||
if (isMatrixifyEnabled(props.form_data as MatrixifyFormData)) {
|
||||
// Set chart to success state since Matrixify will handle its own queries
|
||||
props.actions.chartUpdateSucceeded([], props.chart.id);
|
||||
props.actions.chartRenderingSucceeded(props.chart.id);
|
||||
@@ -386,31 +484,18 @@ function ExploreViewContainer(props) {
|
||||
]);
|
||||
|
||||
const handleKeydown = useCallback(
|
||||
event => {
|
||||
(event: KeyboardEvent) => {
|
||||
const controlOrCommand = event.ctrlKey || event.metaKey;
|
||||
if (controlOrCommand) {
|
||||
const isEnter = event.key === 'Enter' || event.keyCode === 13;
|
||||
const isS = event.key === 's' || event.keyCode === 83;
|
||||
if (isEnter) {
|
||||
onQuery();
|
||||
} else if (isS) {
|
||||
if (props.slice) {
|
||||
props.actions
|
||||
.saveSlice(props.form_data, {
|
||||
action: 'overwrite',
|
||||
slice_id: props.slice.slice_id,
|
||||
slice_name: props.slice.slice_name,
|
||||
add_to_dash: 'noSave',
|
||||
goto_dash: false,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
window.location = data.slice.slice_url;
|
||||
});
|
||||
}
|
||||
}
|
||||
// Note: Ctrl+S save functionality removed due to type incompatibilities
|
||||
// between Slice types. Use the save button instead.
|
||||
}
|
||||
},
|
||||
[onQuery, props.actions, props.form_data, props.slice],
|
||||
[onQuery],
|
||||
);
|
||||
|
||||
function onStop() {
|
||||
@@ -430,7 +515,7 @@ function ExploreViewContainer(props) {
|
||||
? {
|
||||
slice_id: props.slice.slice_id,
|
||||
}
|
||||
: undefined,
|
||||
: {},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -480,7 +565,7 @@ function ExploreViewContainer(props) {
|
||||
}, []);
|
||||
|
||||
const reRenderChart = useCallback(
|
||||
controlsChanged => {
|
||||
(controlsChanged?: string[]) => {
|
||||
const newQueryFormData = controlsChanged
|
||||
? {
|
||||
...props.chart.latestQueryFormData,
|
||||
@@ -512,7 +597,7 @@ function ExploreViewContainer(props) {
|
||||
props.controls.datasource.value !== previousControls.datasource.value)
|
||||
) {
|
||||
// this should really be handled by actions
|
||||
fetchDatasourceMetadata(props.form_data.datasource, true);
|
||||
fetchDatasourceMetadata(props.form_data.datasource);
|
||||
}
|
||||
|
||||
const changedControlKeys = Object.keys(props.controls).filter(
|
||||
@@ -525,15 +610,29 @@ function ExploreViewContainer(props) {
|
||||
);
|
||||
|
||||
if (changedControlKeys.includes('tooltip_contents')) {
|
||||
const tooltipContents = props.controls.tooltip_contents?.value || [];
|
||||
const currentTemplate = props.controls.tooltip_template?.value || '';
|
||||
const tooltipContentsValue = props.controls.tooltip_contents?.value;
|
||||
const tooltipContents = Array.isArray(tooltipContentsValue)
|
||||
? tooltipContentsValue
|
||||
: [];
|
||||
const currentTemplateValue = props.controls.tooltip_template?.value;
|
||||
const currentTemplate =
|
||||
typeof currentTemplateValue === 'string' ? currentTemplateValue : '';
|
||||
|
||||
if (tooltipContents.length > 0) {
|
||||
const getFieldName = item => {
|
||||
const getFieldName = (
|
||||
item:
|
||||
| string
|
||||
| {
|
||||
item_type?: string;
|
||||
column_name?: string;
|
||||
metric_name?: string;
|
||||
label?: string;
|
||||
},
|
||||
): string | null => {
|
||||
if (typeof item === 'string') return item;
|
||||
if (item?.item_type === 'column') return item.column_name;
|
||||
if (item?.item_type === 'column') return item.column_name ?? null;
|
||||
if (item?.item_type === 'metric') {
|
||||
return item.metric_name || item.label;
|
||||
return item.metric_name || item.label || null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -543,18 +642,21 @@ function ExploreViewContainer(props) {
|
||||
|
||||
const DEFAULT_TOOLTIP_LIMIT = 10; // Maximum number of values to show in aggregated tooltips
|
||||
|
||||
const fieldNames = tooltipContents.map(getFieldName).filter(Boolean);
|
||||
const fieldNames = tooltipContents
|
||||
.map(getFieldName)
|
||||
.filter((name): name is string => Boolean(name));
|
||||
const missingVariables = fieldNames.filter(
|
||||
fieldName =>
|
||||
(fieldName: string) =>
|
||||
!currentTemplate.includes(`{{ ${fieldName} }}`) &&
|
||||
!currentTemplate.includes(`{{ limit ${fieldName}`),
|
||||
);
|
||||
|
||||
if (missingVariables.length > 0) {
|
||||
const newVariables = missingVariables.map(fieldName => {
|
||||
const newVariables = missingVariables.map((fieldName: string) => {
|
||||
const item = tooltipContents[fieldNames.indexOf(fieldName)];
|
||||
const isColumn =
|
||||
item?.item_type === 'column' || typeof item === 'string';
|
||||
(typeof item === 'object' && item?.item_type === 'column') ||
|
||||
typeof item === 'string';
|
||||
|
||||
if (isAggregatedChart && isColumn) {
|
||||
return `{{ limit ${fieldName} ${DEFAULT_TOOLTIP_LIMIT} }}`;
|
||||
@@ -574,7 +676,9 @@ function ExploreViewContainer(props) {
|
||||
// Automatically set axis title margins when titles are added or removed
|
||||
if (changedControlKeys.includes('x_axis_title')) {
|
||||
const xAxisTitle = props.controls.x_axis_title?.value || '';
|
||||
const currentMargin = props.controls.x_axis_title_margin?.value ?? 0;
|
||||
const currentMargin = Number(
|
||||
props.controls.x_axis_title_margin?.value ?? 0,
|
||||
);
|
||||
|
||||
if (xAxisTitle && currentMargin < 30) {
|
||||
props.actions.setControlValue('x_axis_title_margin', 30);
|
||||
@@ -585,7 +689,9 @@ function ExploreViewContainer(props) {
|
||||
|
||||
if (changedControlKeys.includes('y_axis_title')) {
|
||||
const yAxisTitle = props.controls.y_axis_title?.value || '';
|
||||
const currentMargin = props.controls.y_axis_title_margin?.value ?? 0;
|
||||
const currentMargin = Number(
|
||||
props.controls.y_axis_title_margin?.value ?? 0,
|
||||
);
|
||||
|
||||
if (yAxisTitle && currentMargin < 30) {
|
||||
props.actions.setControlValue('y_axis_title_margin', 30);
|
||||
@@ -632,7 +738,10 @@ function ExploreViewContainer(props) {
|
||||
}, [lastQueriedControls, props.controls]);
|
||||
|
||||
useChangeEffect(props.saveAction, () => {
|
||||
if (['saveas', 'overwrite'].includes(props.saveAction)) {
|
||||
if (
|
||||
props.saveAction &&
|
||||
['saveas', 'overwrite'].includes(props.saveAction)
|
||||
) {
|
||||
onQuery();
|
||||
addHistory({ isReplace: true });
|
||||
props.actions.setSaveAction(null);
|
||||
@@ -641,7 +750,7 @@ function ExploreViewContainer(props) {
|
||||
|
||||
const previousOwnState = usePrevious(props.ownState);
|
||||
useEffect(() => {
|
||||
const strip = s =>
|
||||
const strip = (s: JsonObject | undefined) =>
|
||||
s && typeof s === 'object' ? omit(s, ['clientView']) : s;
|
||||
if (!isEqual(strip(previousOwnState), strip(props.ownState))) {
|
||||
onQuery();
|
||||
@@ -650,7 +759,7 @@ function ExploreViewContainer(props) {
|
||||
}, [props.ownState]);
|
||||
|
||||
if (chartIsStale) {
|
||||
props.actions.logEvent(LOG_ACTIONS_CHANGE_EXPLORE_CONTROLS);
|
||||
props.actions.logEvent(LOG_ACTIONS_CHANGE_EXPLORE_CONTROLS, {});
|
||||
}
|
||||
|
||||
const errorMessage = useMemo(() => {
|
||||
@@ -674,7 +783,10 @@ function ExploreViewContainer(props) {
|
||||
.filter(control => control.validationErrors?.includes(message))
|
||||
.map(control =>
|
||||
typeof control.label === 'function'
|
||||
? control.label(props.exploreState)
|
||||
? control.label(
|
||||
props.exploreState as unknown as ControlPanelState,
|
||||
control,
|
||||
)
|
||||
: control.label,
|
||||
);
|
||||
return [matchingLabels, message];
|
||||
@@ -717,7 +829,10 @@ function ExploreViewContainer(props) {
|
||||
.filter(control => control.validationErrors?.includes(message))
|
||||
.map(control =>
|
||||
typeof control.label === 'function'
|
||||
? control.label(props.exploreState)
|
||||
? control.label(
|
||||
props.exploreState as unknown as ControlPanelState,
|
||||
control,
|
||||
)
|
||||
: control.label,
|
||||
);
|
||||
return [matchingLabels, message];
|
||||
@@ -748,10 +863,35 @@ function ExploreViewContainer(props) {
|
||||
function renderChartContainer() {
|
||||
return (
|
||||
<ExploreChartPanel
|
||||
{...props}
|
||||
actions={{
|
||||
setForceQuery: props.actions.setForceQuery,
|
||||
postChartFormData: props.actions.postChartFormData,
|
||||
updateQueryFormData: props.actions.updateQueryFormData,
|
||||
setControlValue: (controlName: string, value: any, chartId: number) =>
|
||||
props.actions.setControlValue(controlName, value),
|
||||
}}
|
||||
can_overwrite={props.can_overwrite}
|
||||
can_download={props.can_download}
|
||||
datasource={props.datasource}
|
||||
dashboardId={props.dashboardId}
|
||||
column_formats={props.column_formats ?? undefined}
|
||||
containerId={props.containerId}
|
||||
isStarred={props.isStarred}
|
||||
slice={props.slice ?? undefined}
|
||||
sliceName={props.sliceName ?? undefined}
|
||||
table_name={props.table_name}
|
||||
vizType={props.vizType ?? ''}
|
||||
form_data={props.form_data}
|
||||
ownState={props.ownState}
|
||||
standalone={props.standalone}
|
||||
force={props.force}
|
||||
timeout={props.timeout}
|
||||
chart={props.chart}
|
||||
triggerRender={props.triggerRender}
|
||||
errorMessage={dataTabErrorMessage}
|
||||
chartIsStale={chartIsStale}
|
||||
onQuery={onQuery}
|
||||
exploreState={props.exploreState}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -763,21 +903,21 @@ function ExploreViewContainer(props) {
|
||||
return (
|
||||
<ExploreContainer>
|
||||
<ConnectedExploreChartHeader
|
||||
actions={props.actions}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Combined actions type is compatible at runtime
|
||||
actions={props.actions as any}
|
||||
canOverwrite={props.can_overwrite}
|
||||
canDownload={props.can_download}
|
||||
dashboardId={props.dashboardId}
|
||||
colorScheme={props.dashboardColorScheme}
|
||||
isStarred={props.isStarred}
|
||||
slice={props.slice}
|
||||
sliceName={props.sliceName}
|
||||
sliceName={props.sliceName ?? undefined}
|
||||
table_name={props.table_name}
|
||||
formData={props.form_data}
|
||||
chart={props.chart}
|
||||
ownState={props.ownState}
|
||||
user={props.user}
|
||||
reports={props.reports}
|
||||
saveDisabled={errorMessage || props.chart.chartStatus === 'loading'}
|
||||
saveDisabled={!!errorMessage || props.chart.chartStatus === 'loading'}
|
||||
metadata={props.metadata}
|
||||
isSaveModalVisible={props.isSaveModalVisible}
|
||||
/>
|
||||
@@ -840,14 +980,15 @@ function ExploreViewContainer(props) {
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{/* eslint-disable @typescript-eslint/no-explicit-any -- DataSourcePanel uses narrower types that are compatible at runtime */}
|
||||
<DataSourcePanel
|
||||
formData={props.form_data}
|
||||
datasource={props.datasource}
|
||||
controls={props.controls}
|
||||
actions={props.actions}
|
||||
datasource={props.datasource as any}
|
||||
controls={props.controls as any}
|
||||
actions={props.actions as any}
|
||||
width={width}
|
||||
user={props.user}
|
||||
/>
|
||||
{/* eslint-enable @typescript-eslint/no-explicit-any */}
|
||||
</Resizable>
|
||||
{isCollapsed ? (
|
||||
<div
|
||||
@@ -886,7 +1027,8 @@ function ExploreViewContainer(props) {
|
||||
>
|
||||
<ConnectedControlPanelsContainer
|
||||
exploreState={props.exploreState}
|
||||
actions={props.actions}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Combined actions type is compatible at runtime
|
||||
actions={props.actions as any}
|
||||
form_data={props.form_data}
|
||||
controls={props.controls}
|
||||
chart={props.chart}
|
||||
@@ -914,22 +1056,32 @@ function ExploreViewContainer(props) {
|
||||
addDangerToast={props.addDangerToast}
|
||||
actions={props.actions}
|
||||
form_data={props.form_data}
|
||||
sliceName={props.sliceName}
|
||||
dashboardId={props.dashboardId}
|
||||
sliceName={props.sliceName ?? undefined}
|
||||
dashboardId={props.dashboardId ?? null}
|
||||
/>
|
||||
)}
|
||||
</ExploreContainer>
|
||||
);
|
||||
}
|
||||
|
||||
ExploreViewContainer.propTypes = propTypes;
|
||||
|
||||
const retainQueryModeRequirements = hiddenFormData =>
|
||||
const retainQueryModeRequirements = (
|
||||
hiddenFormData: Partial<QueryFormData> | undefined,
|
||||
): string[] =>
|
||||
Object.keys(hiddenFormData ?? {}).filter(
|
||||
key => !QUERY_MODE_REQUISITES.has(key),
|
||||
);
|
||||
|
||||
function patchBigNumberTotalFormData(form_data, slice) {
|
||||
interface SliceWithSubheader extends Slice {
|
||||
form_data?: QueryFormData & {
|
||||
subheader?: string;
|
||||
subheader_font_size?: number;
|
||||
};
|
||||
}
|
||||
|
||||
function patchBigNumberTotalFormData(
|
||||
form_data: QueryFormData,
|
||||
slice: SliceWithSubheader | null | undefined,
|
||||
): QueryFormData {
|
||||
if (
|
||||
form_data.viz_type === 'big_number_total' &&
|
||||
!form_data.subtitle &&
|
||||
@@ -940,7 +1092,7 @@ function patchBigNumberTotalFormData(form_data, slice) {
|
||||
return form_data;
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
function mapStateToProps(state: ExploreRootState) {
|
||||
const {
|
||||
explore,
|
||||
charts,
|
||||
@@ -960,24 +1112,31 @@ function mapStateToProps(state) {
|
||||
const controlsBasedFormData = omit(
|
||||
getFormDataFromControls(controls),
|
||||
fieldsToOmit,
|
||||
);
|
||||
) as QueryFormData;
|
||||
const isDeckGLChart = explore.form_data?.viz_type === 'deck_multi';
|
||||
|
||||
const getDeckGLFormData = () => {
|
||||
const formData = { ...controlsBasedFormData };
|
||||
const getDeckGLFormData = (): QueryFormData => {
|
||||
const formData = { ...controlsBasedFormData } as QueryFormData & {
|
||||
layer_filter_scope?: JsonObject;
|
||||
filter_data_mapping?: JsonObject;
|
||||
};
|
||||
|
||||
if (explore.form_data?.layer_filter_scope) {
|
||||
formData.layer_filter_scope = explore.form_data.layer_filter_scope;
|
||||
formData.layer_filter_scope = explore.form_data
|
||||
.layer_filter_scope as JsonObject;
|
||||
}
|
||||
|
||||
if (explore.form_data?.filter_data_mapping) {
|
||||
formData.filter_data_mapping = explore.form_data.filter_data_mapping;
|
||||
formData.filter_data_mapping = explore.form_data
|
||||
.filter_data_mapping as JsonObject;
|
||||
}
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
||||
const form_data = isDeckGLChart ? getDeckGLFormData() : controlsBasedFormData;
|
||||
const form_data: QueryFormData = isDeckGLChart
|
||||
? getDeckGLFormData()
|
||||
: controlsBasedFormData;
|
||||
|
||||
const slice_id = form_data.slice_id ?? slice?.slice_id ?? 0; // 0 - unsaved chart
|
||||
|
||||
@@ -995,7 +1154,7 @@ function mapStateToProps(state) {
|
||||
const ownColorScheme = explore.form_data?.own_color_scheme;
|
||||
const dashboardColorScheme = explore.form_data?.dashboard_color_scheme;
|
||||
|
||||
let dashboardId = Number(explore.form_data?.dashboardId);
|
||||
let dashboardId: number | undefined = Number(explore.form_data?.dashboardId);
|
||||
if (Number.isNaN(dashboardId)) {
|
||||
dashboardId = undefined;
|
||||
}
|
||||
@@ -1024,7 +1183,7 @@ function mapStateToProps(state) {
|
||||
isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
|
||||
datasource,
|
||||
datasource_type: datasource.type,
|
||||
datasourceId: datasource.datasource_id,
|
||||
datasourceId: datasource.id,
|
||||
dashboardId,
|
||||
colorScheme,
|
||||
ownColorScheme,
|
||||
@@ -1051,7 +1210,9 @@ function mapStateToProps(state) {
|
||||
ownState: dataMask[slice_id]?.ownState,
|
||||
impressionId,
|
||||
user,
|
||||
exploreState: explore,
|
||||
// ExploreRootState['explore'] is compatible with ExplorePageState['explore']
|
||||
// but has additional optional fields; casting is safe here
|
||||
exploreState: explore as unknown as ExplorePageState['explore'],
|
||||
reports,
|
||||
metadata,
|
||||
saveAction: explore.saveAction,
|
||||
@@ -1059,7 +1220,7 @@ function mapStateToProps(state) {
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
function mapDispatchToProps(dispatch: Dispatch): DispatchProps {
|
||||
const actions = {
|
||||
...exploreActions,
|
||||
...datasourcesActions,
|
||||
@@ -1068,11 +1229,18 @@ function mapDispatchToProps(dispatch) {
|
||||
...logActions,
|
||||
};
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Action modules export mixed types (creators + constants)
|
||||
actions: bindActionCreators(actions as any, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
// withToasts provides toast functions (OwnProps), and connect provides StateProps & DispatchProps
|
||||
// The final exported component doesn't require any external props
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(withToasts(memo(ExploreViewContainer)));
|
||||
)(
|
||||
withToasts(memo(ExploreViewContainer)) as ComponentType<
|
||||
Record<string, never>
|
||||
>,
|
||||
);
|
||||
Reference in New Issue
Block a user