feat(fe): upgrade `superset-frontend` to Typescript v5 (#31979)

Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
This commit is contained in:
Đỗ Trọng Hải 2025-01-29 18:40:33 +07:00 committed by GitHub
parent a21f184058
commit 19e8a7049b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
141 changed files with 1095 additions and 572 deletions

View File

@ -290,7 +290,7 @@
"thread-loader": "^4.0.4", "thread-loader": "^4.0.4",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.5",
"ts-loader": "^9.5.1", "ts-loader": "^9.5.1",
"typescript": "^4.8.4", "typescript": "5.1.6",
"vm-browserify": "^1.1.2", "vm-browserify": "^1.1.2",
"webpack": "^5.97.1", "webpack": "^5.97.1",
"webpack-bundle-analyzer": "^4.10.1", "webpack-bundle-analyzer": "^4.10.1",
@ -13499,6 +13499,38 @@
"node": ">=12.0.0" "node": ">=12.0.0"
} }
}, },
"node_modules/@wdio/config/node_modules/@types/node": {
"version": "18.19.74",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.74.tgz",
"integrity": "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@wdio/config/node_modules/@wdio/types": {
"version": "7.30.2",
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-7.30.2.tgz",
"integrity": "sha512-uZ8o7FX8RyBsaXiOWa59UKTCHTtADNvOArYTcHNEIzt+rh4JdB/uwqfc8y4TCNA2kYm7PWaQpUFwpStLeg0H1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "^18.0.0",
"got": "^11.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"typescript": "^4.6.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@wdio/config/node_modules/brace-expansion": { "node_modules/@wdio/config/node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@ -13543,6 +13575,29 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@wdio/config/node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/@wdio/config/node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true,
"license": "MIT"
},
"node_modules/@wdio/logger": { "node_modules/@wdio/logger": {
"version": "7.26.0", "version": "7.26.0",
"resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-7.26.0.tgz", "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-7.26.0.tgz",
@ -13586,7 +13641,32 @@
"node": ">=12.0.0" "node": ">=12.0.0"
} }
}, },
"node_modules/@wdio/types": { "node_modules/@wdio/utils": {
"version": "7.30.2",
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-7.30.2.tgz",
"integrity": "sha512-np7I+smszFUennbQKdzbMN/zUL3s3EZq9pCCUcTRjjs9TE4tnn0wfmGdoz2o7REYu6kn9NfFFJyVIM2VtBbKEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@wdio/logger": "7.26.0",
"@wdio/types": "7.30.2",
"p-iteration": "^1.1.8"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/@wdio/utils/node_modules/@types/node": {
"version": "18.19.74",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.74.tgz",
"integrity": "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@wdio/utils/node_modules/@wdio/types": {
"version": "7.30.2", "version": "7.30.2",
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-7.30.2.tgz", "resolved": "https://registry.npmjs.org/@wdio/types/-/types-7.30.2.tgz",
"integrity": "sha512-uZ8o7FX8RyBsaXiOWa59UKTCHTtADNvOArYTcHNEIzt+rh4JdB/uwqfc8y4TCNA2kYm7PWaQpUFwpStLeg0H1Q==", "integrity": "sha512-uZ8o7FX8RyBsaXiOWa59UKTCHTtADNvOArYTcHNEIzt+rh4JdB/uwqfc8y4TCNA2kYm7PWaQpUFwpStLeg0H1Q==",
@ -13608,38 +13688,29 @@
} }
} }
}, },
"node_modules/@wdio/types/node_modules/@types/node": { "node_modules/@wdio/utils/node_modules/typescript": {
"version": "18.19.74", "version": "4.9.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.74.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A==", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true, "dev": true,
"license": "MIT", "license": "Apache-2.0",
"dependencies": { "optional": true,
"undici-types": "~5.26.4" "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
} }
}, },
"node_modules/@wdio/types/node_modules/undici-types": { "node_modules/@wdio/utils/node_modules/undici-types": {
"version": "5.26.5", "version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@wdio/utils": {
"version": "7.30.2",
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-7.30.2.tgz",
"integrity": "sha512-np7I+smszFUennbQKdzbMN/zUL3s3EZq9pCCUcTRjjs9TE4tnn0wfmGdoz2o7REYu6kn9NfFFJyVIM2VtBbKEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@wdio/logger": "7.26.0",
"@wdio/types": "7.30.2",
"p-iteration": "^1.1.8"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/@webassemblyjs/ast": { "node_modules/@webassemblyjs/ast": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
@ -49029,16 +49100,16 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "4.9.5", "version": "5.1.6",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
}, },
"engines": { "engines": {
"node": ">=4.2.0" "node": ">=14.17"
} }
}, },
"node_modules/typescript-json-schema": { "node_modules/typescript-json-schema": {
@ -50328,6 +50399,44 @@
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
}, },
"node_modules/webdriver/node_modules/@wdio/types": {
"version": "7.30.2",
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-7.30.2.tgz",
"integrity": "sha512-uZ8o7FX8RyBsaXiOWa59UKTCHTtADNvOArYTcHNEIzt+rh4JdB/uwqfc8y4TCNA2kYm7PWaQpUFwpStLeg0H1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "^18.0.0",
"got": "^11.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"typescript": "^4.6.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/webdriver/node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/webdriver/node_modules/undici-types": { "node_modules/webdriver/node_modules/undici-types": {
"version": "5.26.5", "version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",

View File

@ -357,7 +357,7 @@
"thread-loader": "^4.0.4", "thread-loader": "^4.0.4",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.5",
"ts-loader": "^9.5.1", "ts-loader": "^9.5.1",
"typescript": "^4.8.4", "typescript": "5.1.6",
"vm-browserify": "^1.1.2", "vm-browserify": "^1.1.2",
"webpack": "^5.97.1", "webpack": "^5.97.1",
"webpack-bundle-analyzer": "^4.10.1", "webpack-bundle-analyzer": "^4.10.1",

View File

@ -363,6 +363,13 @@ export type CustomControlItem = {
config: BaseControlConfig<any, any, any>; config: BaseControlConfig<any, any, any>;
}; };
export const isCustomControlItem = (obj: unknown): obj is CustomControlItem =>
typeof obj === 'object' &&
obj !== null &&
typeof ('name' in obj && obj.name) === 'string' &&
typeof ('config' in obj && obj.config) === 'object' &&
(obj as CustomControlItem).config !== null;
// use ReactElement instead of ReactNode because `string`, `number`, etc. may // use ReactElement instead of ReactNode because `string`, `number`, etc. may
// interfere with other ControlSetItem types // interfere with other ControlSetItem types
export type ExpandedControlItem = CustomControlItem | ReactElement | null; export type ExpandedControlItem = CustomControlItem | ReactElement | null;

View File

@ -20,8 +20,10 @@ import { AdhocColumn } from '@superset-ui/core';
import { import {
ColumnMeta, ColumnMeta,
ControlPanelSectionConfig, ControlPanelSectionConfig,
CustomControlItem,
isColumnMeta, isColumnMeta,
isControlPanelSectionConfig, isControlPanelSectionConfig,
isCustomControlItem,
isSavedExpression, isSavedExpression,
} from '../src'; } from '../src';
@ -43,6 +45,13 @@ const CONTROL_PANEL_SECTION_CONFIG: ControlPanelSectionConfig = {
description: 'My Description', description: 'My Description',
controlSetRows: [], controlSetRows: [],
}; };
const CUSTOM_CONTROL_ITEM: CustomControlItem = {
name: 'Custom Control Item',
config: {
type: 'config',
foo: 'bar',
},
};
test('isColumnMeta returns false for AdhocColumn', () => { test('isColumnMeta returns false for AdhocColumn', () => {
expect(isColumnMeta(ADHOC_COLUMN)).toEqual(false); expect(isColumnMeta(ADHOC_COLUMN)).toEqual(false);
@ -73,3 +82,11 @@ test('isControlPanelSectionConfig returns true for section', () => {
test('isControlPanelSectionConfig returns true for null value', () => { test('isControlPanelSectionConfig returns true for null value', () => {
expect(isControlPanelSectionConfig(null)).toEqual(false); expect(isControlPanelSectionConfig(null)).toEqual(false);
}); });
test('isCustomControlItem returns true for proper CustomControlItem', () => {
expect(isCustomControlItem(CUSTOM_CONTROL_ITEM)).toEqual(true);
});
test('isCustomControlItem returns false for generic object', () => {
expect(isCustomControlItem({})).toEqual(false);
});

View File

@ -105,7 +105,7 @@ export function parseStringResponse(str: string): string {
} }
export function getErrorFromStatusCode(status: number): string | null { export function getErrorFromStatusCode(status: number): string | null {
return ERROR_CODE_LOOKUP[status] || null; return ERROR_CODE_LOOKUP[status as keyof typeof ERROR_CODE_LOOKUP] || null;
} }
export function retrieveErrorMessage( export function retrieveErrorMessage(

View File

@ -62,6 +62,10 @@ export interface FreeFormAdhocFilter extends BaseAdhocFilter {
sqlExpression: string; sqlExpression: string;
} }
export interface LatestPartitionAdhocFilter extends BaseAdhocFilter {
datasource?: { schema?: string; datasource_name?: string };
}
export type AdhocFilter = SimpleAdhocFilter | FreeFormAdhocFilter; export type AdhocFilter = SimpleAdhocFilter | FreeFormAdhocFilter;
//--------------------------------------------------- //---------------------------------------------------

View File

@ -349,15 +349,17 @@ export type Query = {
}; };
export type QueryResults = { export type QueryResults = {
results: { results: InnerQueryResults;
displayLimitReached: boolean; };
columns: QueryColumn[];
data: Record<string, unknown>[]; export type InnerQueryResults = {
expanded_columns: QueryColumn[]; displayLimitReached: boolean;
selected_columns: QueryColumn[]; columns: QueryColumn[];
query: { limit: number }; data: Record<string, unknown>[];
query_id?: number; expanded_columns: QueryColumn[];
}; selected_columns: QueryColumn[];
query: { limit: number };
query_id?: number;
}; };
export type QueryResponse = Query & QueryResults; export type QueryResponse = Query & QueryResults;

View File

@ -79,6 +79,9 @@ export interface ChartDataResponseResult {
| 'timed_out'; | 'timed_out';
from_dttm: number | null; from_dttm: number | null;
to_dttm: number | null; to_dttm: number | null;
// TODO(hainenber): define proper type for below attributes
rejected_filters?: any[];
applied_filters?: any[];
} }
export interface TimeseriesChartDataResponseResult export interface TimeseriesChartDataResponseResult

View File

@ -93,7 +93,7 @@ export function initFeatureFlags(featureFlags?: FeatureFlagMap) {
export function isFeatureEnabled(feature: FeatureFlag): boolean { export function isFeatureEnabled(feature: FeatureFlag): boolean {
try { try {
return !!window.featureFlags[feature]; return !!window.featureFlags[feature as keyof FeatureFlagMap];
} catch (error) { } catch (error) {
logger.error(`Failed to query feature flag ${feature}`); logger.error(`Failed to query feature flag ${feature}`);
} }

View File

@ -19,7 +19,7 @@
import { t } from '../translation'; import { t } from '../translation';
export default function validateInteger(v: unknown) { export default function validateInteger(v: any) {
if ( if (
(typeof v === 'string' && (typeof v === 'string' &&
v.trim().length > 0 && v.trim().length > 0 &&

View File

@ -202,4 +202,82 @@ describe('customTimeRangeDecode', () => {
matchedFlag: false, matchedFlag: false,
}); });
}); });
it('9) empty string returns default', () => {
const SEVEN_DAYS_AGO = new Date();
SEVEN_DAYS_AGO.setHours(0, 0, 0, 0);
const MIDNIGHT = new Date();
MIDNIGHT.setHours(0, 0, 0, 0);
expect(customTimeRangeDecode('')).toEqual({
customRange: {
sinceDatetime: SEVEN_DAYS_AGO.setDate(
SEVEN_DAYS_AGO.getDate() - 7,
).toString(),
sinceMode: 'relative',
sinceGrain: 'day',
sinceGrainValue: -7,
untilDatetime: MIDNIGHT.toString(),
untilMode: 'specific',
untilGrain: 'day',
untilGrainValue: 7,
anchorMode: 'now',
anchorValue: 'now',
},
matchedFlag: false,
});
});
it('10) both undefined returns default', () => {
const SEVEN_DAYS_AGO = new Date();
SEVEN_DAYS_AGO.setHours(0, 0, 0, 0);
const MIDNIGHT = new Date();
MIDNIGHT.setHours(0, 0, 0, 0);
expect(customTimeRangeDecode('undefined : undefined')).toEqual({
customRange: {
sinceDatetime: SEVEN_DAYS_AGO.setDate(
SEVEN_DAYS_AGO.getDate() - 7,
).toString(),
sinceMode: 'relative',
sinceGrain: 'day',
sinceGrainValue: -7,
untilDatetime: MIDNIGHT.toString(),
untilMode: 'specific',
untilGrain: 'day',
untilGrainValue: 7,
anchorMode: 'now',
anchorValue: 'now',
},
matchedFlag: false,
});
});
it('11) 1 side undefined returns default', () => {
const SEVEN_DAYS_AGO = new Date();
SEVEN_DAYS_AGO.setHours(0, 0, 0, 0);
const MIDNIGHT = new Date();
MIDNIGHT.setHours(0, 0, 0, 0);
expect(customTimeRangeDecode('undefined : now')).toEqual({
customRange: {
sinceDatetime: SEVEN_DAYS_AGO.setDate(
SEVEN_DAYS_AGO.getDate() - 7,
).toString(),
sinceMode: 'relative',
sinceGrain: 'day',
sinceGrainValue: -7,
untilDatetime: MIDNIGHT.toString(),
untilMode: 'specific',
untilGrain: 'day',
untilGrainValue: 7,
anchorMode: 'now',
anchorValue: 'now',
},
matchedFlag: false,
});
});
}); });

View File

@ -54,7 +54,7 @@ function getCategories(fd: QueryFormData, data: JsonObject[]) {
const fixedColor = [c.r, c.g, c.b, 255 * c.a]; const fixedColor = [c.r, c.g, c.b, 255 * c.a];
const appliedScheme = fd.color_scheme; const appliedScheme = fd.color_scheme;
const colorFn = getScale(appliedScheme); const colorFn = getScale(appliedScheme);
const categories = {}; const categories: Record<any, { color: any; enabled: boolean }> = {};
data.forEach(d => { data.forEach(d => {
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) { if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
let color; let color;

View File

@ -97,6 +97,7 @@ const DeckMulti = (props: DeckMultiProps) => {
endpoint: url, endpoint: url,
}) })
.then(({ json }) => { .then(({ json }) => {
// @ts-ignore TODO(hainenber): define proper type for `form_data.viz_type` and call signature for functions in layerGenerators.
const layer = layerGenerators[subsliceCopy.form_data.viz_type]( const layer = layerGenerators[subsliceCopy.form_data.viz_type](
subsliceCopy.form_data, subsliceCopy.form_data,
json, json,

View File

@ -59,7 +59,7 @@ export type LegendProps = {
format: string | null; format: string | null;
forceCategorical?: boolean; forceCategorical?: boolean;
position?: null | 'tl' | 'tr' | 'bl' | 'br'; position?: null | 'tl' | 'tr' | 'bl' | 'br';
categories: Record<string, { enabled: boolean; color: number[] }>; categories: Record<string, { enabled: boolean; color: number[] | undefined }>;
toggleCategory?: (key: string) => void; toggleCategory?: (key: string) => void;
showSingleCategory?: (key: string) => void; showSingleCategory?: (key: string) => void;
}; };
@ -101,7 +101,7 @@ const Legend = ({
} }
const categories = Object.entries(categoriesObject).map(([k, v]) => { const categories = Object.entries(categoriesObject).map(([k, v]) => {
const style = { color: `rgba(${v.color.join(', ')})` }; const style = { color: `rgba(${v.color?.join(', ')})` };
const icon = v.enabled ? '\u25FC' : '\u25FB'; const icon = v.enabled ? '\u25FC' : '\u25FB';
return ( return (

View File

@ -59,7 +59,7 @@ const alterProps = (props: JsonObject, propOverrides: JsonObject) => {
const newProps: JsonObject = {}; const newProps: JsonObject = {};
Object.keys(props).forEach(k => { Object.keys(props).forEach(k => {
if (k in propertyMap) { if (k in propertyMap) {
newProps[propertyMap[k]] = props[k]; newProps[propertyMap[k as keyof typeof propertyMap]] = props[k];
} else { } else {
newProps[k] = props[k]; newProps[k] = props[k];
} }

View File

@ -122,7 +122,11 @@ export function getAggFunc(
sortedArr = arr.sort(d3ascending); sortedArr = arr.sort(d3ascending);
} }
return d3quantile(sortedArr, percentiles[type], acc); return d3quantile(
sortedArr,
percentiles[type as keyof typeof percentiles],
acc,
);
}; };
} else if (type in d3functions) { } else if (type in d3functions) {
d3func = d3functions[type]; d3func = d3functions[type];

View File

@ -172,8 +172,11 @@ export function getBuckets(
) { ) {
const breakPoints = getBreakPoints(fd, features, accessor); const breakPoints = getBreakPoints(fd, features, accessor);
const colorScaler = getBreakPointColorScaler(fd, features, accessor); const colorScaler = getBreakPointColorScaler(fd, features, accessor);
const buckets = {}; const buckets: Record<
breakPoints.slice(1).forEach((value, i) => { string,
{ color: [number, number, number, number] | undefined; enabled: boolean }
> = {};
breakPoints.slice(1).forEach((_, i) => {
const range = `${breakPoints[i]} - ${breakPoints[i + 1]}`; const range = `${breakPoints[i]} - ${breakPoints[i + 1]}`;
const mid = const mid =
0.5 * (parseFloat(breakPoints[i]) + parseFloat(breakPoints[i + 1])); 0.5 * (parseFloat(breakPoints[i]) + parseFloat(breakPoints[i + 1]));

View File

@ -36,7 +36,7 @@ export function getExploreLongUrl(
formData: JsonObject, formData: JsonObject,
endpointType: string, endpointType: string,
allowOverflow = true, allowOverflow = true,
extraSearch = {}, extraSearch: Record<string, any> = {},
): string | undefined { ): string | undefined {
if (!formData.datasource) { if (!formData.datasource) {
return undefined; return undefined;

View File

@ -34,19 +34,26 @@ const GLOBAL_CONTEXT = {
d3array, d3array,
}; };
type GlobalContext = {
console: Console;
_: _.UnderscoreStatic;
colors: typeof colors;
d3array: typeof d3array;
};
// Copied/modified from https://github.com/hacksparrow/safe-eval/blob/master/index.js // Copied/modified from https://github.com/hacksparrow/safe-eval/blob/master/index.js
export default function sandboxedEval( export default function sandboxedEval(
code: string, code: string,
context?: Context, context?: Context,
opts?: RunningScriptOptions | string, opts?: RunningScriptOptions | string,
) { ) {
const sandbox = {}; const sandbox: Context = {};
const resultKey = `SAFE_EVAL_${Math.floor(Math.random() * 1000000)}`; const resultKey = `SAFE_EVAL_${Math.floor(Math.random() * 1000000)}`;
sandbox[resultKey] = {}; sandbox[resultKey] = {};
const codeToEval = `${resultKey}=${code}`; const codeToEval = `${resultKey}=${code}`;
const sandboxContext = { ...GLOBAL_CONTEXT, ...context }; const sandboxContext: GlobalContext = { ...GLOBAL_CONTEXT, ...context };
Object.keys(sandboxContext).forEach(key => { Object.keys(sandboxContext).forEach(key => {
sandbox[key] = sandboxContext[key]; sandbox[key] = sandboxContext[key as keyof GlobalContext];
}); });
try { try {
vm.runInNewContext(codeToEval, sandbox, opts); vm.runInNewContext(codeToEval, sandbox, opts);

View File

@ -71,7 +71,7 @@ export const selectedChartMutator = (
return []; return [];
} }
const data: Record<string, any> = []; const data: any[] = [];
if (value && typeof value === 'string') { if (value && typeof value === 'string') {
const parsedValue = JSON.parse(value); const parsedValue = JSON.parse(value);
let itemFound = false; let itemFound = false;

View File

@ -208,7 +208,7 @@ export const stripGeomColumnFromLabelMap = (
labelMap: { [key: string]: string[] }, labelMap: { [key: string]: string[] },
geomColumn: string, geomColumn: string,
) => { ) => {
const newLabelMap = {}; const newLabelMap: Record<string, string[]> = {};
Object.entries(labelMap).forEach(([key, value]) => { Object.entries(labelMap).forEach(([key, value]) => {
if (key === geomColumn) { if (key === geomColumn) {
return; return;

View File

@ -75,6 +75,7 @@ describe('layerUtil', () => {
// @ts-ignore // @ts-ignore
expect(style!.length).toEqual(3); expect(style!.length).toEqual(3);
// @ts-ignore upgrade `ol` package for better type of StyleLike type.
const colorAtLayer = style![1].getImage().getFill().getColor(); const colorAtLayer = style![1].getImage().getFill().getColor();
expect(colorToExpect).toEqual(colorAtLayer); expect(colorToExpect).toEqual(colorAtLayer);
}); });

View File

@ -165,7 +165,8 @@ export default function transformProps(chartProps: ChartProps) {
percentDifferenceNum = (bigNumber - prevNumber) / Math.abs(prevNumber); percentDifferenceNum = (bigNumber - prevNumber) / Math.abs(prevNumber);
} }
const compType = compTitles[formData.timeComparison]; const compType =
compTitles[formData.timeComparison as keyof typeof compTitles];
bigNumber = numberFormatter(bigNumber); bigNumber = numberFormatter(bigNumber);
prevNumber = numberFormatter(prevNumber); prevNumber = numberFormatter(prevNumber);
valueDifference = numberFormatter(valueDifference); valueDifference = numberFormatter(valueDifference);

View File

@ -35,7 +35,7 @@ const getFontSizeMapping = (
proportionValues: number[], proportionValues: number[],
actualSizes: number[], actualSizes: number[],
) => ) =>
proportionValues.reduce((acc, value, index) => { proportionValues.reduce<Record<number, number>>((acc, value, index) => {
acc[value] = actualSizes[index] ?? actualSizes[actualSizes.length - 1]; acc[value] = actualSizes[index] ?? actualSizes[actualSizes.length - 1];
return acc; return acc;
}, {}); }, {});

View File

@ -112,12 +112,15 @@ export default {
Array.isArray(colnames) && Array.isArray(coltypes) Array.isArray(colnames) && Array.isArray(coltypes)
? colnames ? colnames
.filter( .filter(
(colname: string, index: number) => (_: string, index: number) =>
coltypes[index] === GenericDataType.Numeric, coltypes[index] === GenericDataType.Numeric,
) )
.map(colname => ({ .map((colname: string | number) => ({
value: colname, value: colname,
label: verboseMap[colname] ?? colname, label:
(Array.isArray(verboseMap)
? verboseMap[colname as number]
: verboseMap[colname as string]) ?? colname,
})) }))
: []; : [];
return { return {

View File

@ -38,18 +38,37 @@ import { getPadding } from '../Timeseries/transformers';
import { convertInteger } from '../utils/convertInteger'; import { convertInteger } from '../utils/convertInteger';
import { NULL_STRING } from '../constants'; import { NULL_STRING } from '../constants';
const isIterable = (obj: any): obj is Iterable<any> =>
obj != null && typeof obj[Symbol.iterator] === 'function';
function normalizeSymbolSize( function normalizeSymbolSize(
nodes: ScatterSeriesOption[], nodes: ScatterSeriesOption[],
maxBubbleValue: number, maxBubbleValue: number,
) { ) {
const [bubbleMinValue, bubbleMaxValue] = extent(nodes, x => x.data?.[0]?.[2]); const [bubbleMinValue, bubbleMaxValue] = extent<ScatterSeriesOption, number>(
const nodeSpread = bubbleMaxValue - bubbleMinValue; nodes,
nodes.forEach(node => { x => {
// eslint-disable-next-line no-param-reassign const tmpValue = x.data?.[0];
node.symbolSize = const result = isIterable(tmpValue) ? tmpValue[2] : null;
(((node.data?.[0]?.[2] - bubbleMinValue) / nodeSpread) * if (typeof result === 'number') {
(maxBubbleValue * 2) || 0) + MINIMUM_BUBBLE_SIZE; return result;
}); }
return null;
},
);
if (bubbleMinValue !== undefined && bubbleMaxValue !== undefined) {
const nodeSpread = bubbleMaxValue - bubbleMinValue;
nodes.forEach(node => {
const tmpValue = node.data?.[0];
const calculated = isIterable(tmpValue) ? tmpValue[2] : null;
if (typeof calculated === 'number') {
// eslint-disable-next-line no-param-reassign
node.symbolSize =
(((calculated - bubbleMinValue) / nodeSpread) *
(maxBubbleValue * 2) || 0) + MINIMUM_BUBBLE_SIZE;
}
});
}
} }
export function formatTooltip( export function formatTooltip(

View File

@ -518,7 +518,9 @@ export default function transformProps(
minorTick: { show: minorTicks }, minorTick: { show: minorTicks },
minInterval: minInterval:
xAxisType === AxisType.Time && timeGrainSqla xAxisType === AxisType.Time && timeGrainSqla
? TIMEGRAIN_TO_TIMESTAMP[timeGrainSqla] ? TIMEGRAIN_TO_TIMESTAMP[
timeGrainSqla as keyof typeof TIMEGRAIN_TO_TIMESTAMP
]
: 0, : 0,
...getMinAndMaxFromBounds( ...getMinAndMaxFromBounds(
xAxisType, xAxisType,

View File

@ -29,7 +29,7 @@ export const retainFormDataSuffix = (
* > removeFormDataSuffix(fd, '_b') * > removeFormDataSuffix(fd, '_b')
* { metrics: ['zee'], limit: 100, ... } * { metrics: ['zee'], limit: 100, ... }
* */ * */
const newFormData = {}; const newFormData: Record<string, any> = {};
Object.entries(formData) Object.entries(formData)
.sort(([a], [b]) => { .sort(([a], [b]) => {
@ -63,7 +63,7 @@ export const removeFormDataSuffix = (
* > removeUnusedFormData(fd, '_b') * > removeUnusedFormData(fd, '_b')
* { metrics: ['foo', 'bar'], limit: 100, ... } * { metrics: ['foo', 'bar'], limit: 100, ... }
* */ * */
const newFormData = {}; const newFormData: Record<string, any> = {};
Object.entries(formData).forEach(([key, value]) => { Object.entries(formData).forEach(([key, value]) => {
if (!key.endsWith(controlSuffix)) { if (!key.endsWith(controlSuffix)) {
newFormData[key] = value; newFormData[key] = value;

View File

@ -469,7 +469,7 @@ describe('Does transformProps transform series correctly', () => {
(totals, currentStack) => { (totals, currentStack) => {
const total = Object.keys(currentStack).reduce((stackSum, key) => { const total = Object.keys(currentStack).reduce((stackSum, key) => {
if (key === '__timestamp') return stackSum; if (key === '__timestamp') return stackSum;
return stackSum + currentStack[key]; return stackSum + currentStack[key as keyof typeof currentStack];
}, 0); }, 0);
totals.push(total); totals.push(total);
return totals; return totals;

View File

@ -415,7 +415,12 @@ const config: ControlPanelConfig = {
const chartStatus = chart?.chartStatus; const chartStatus = chart?.chartStatus;
const metricColumn = values.map(value => { const metricColumn = values.map(value => {
if (typeof value === 'string') { if (typeof value === 'string') {
return { value, label: verboseMap[value] ?? value }; return {
value,
label: Array.isArray(verboseMap)
? value
: verboseMap[value],
};
} }
return { value: value.label, label: value.label }; return { value: value.label, label: value.label };
}); });

View File

@ -60,11 +60,12 @@ export function generatePageItems(
items[i] = i + left; items[i] = i + left;
} }
// replace non-ending items with placeholders // replace non-ending items with placeholders
if (items[0] > 0) { if (typeof items[0] === 'number' && items[0] > 0) {
items[0] = 0; items[0] = 0;
items[1] = 'prev-more'; items[1] = 'prev-more';
} }
if (items[items.length - 1] < total - 1) { const lastItem = items[items.length - 1];
if (typeof lastItem === 'number' && lastItem < total - 1) {
items[items.length - 1] = total - 1; items[items.length - 1] = total - 1;
items[items.length - 2] = 'next-more'; items[items.length - 2] = 'next-more';
} }

View File

@ -649,9 +649,11 @@ const config: ControlPanelConfig = {
(colname: string, index: number) => (colname: string, index: number) =>
coltypes[index] === GenericDataType.Numeric, coltypes[index] === GenericDataType.Numeric,
) )
.map(colname => ({ .map((colname: string) => ({
value: colname, value: colname,
label: verboseMap[colname] ?? colname, label: Array.isArray(verboseMap)
? colname
: verboseMap[colname],
})) }))
: []; : [];
const columnOptions = explore?.controls?.time_compare?.value const columnOptions = explore?.controls?.time_compare?.value

View File

@ -16,10 +16,10 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
export const caches = {}; export const caches: Record<string, Record<string, Response>> = {};
export default class Cache { export default class Cache {
cache: object; cache: Record<string, Response>;
constructor(key: string) { constructor(key: string) {
caches[key] = caches[key] || {}; caches[key] = caches[key] || {};

View File

@ -40,9 +40,10 @@ const exposedProperties = ['window', 'navigator', 'document'];
const { defaultView } = document; const { defaultView } = document;
if (defaultView != null) { if (defaultView != null) {
Object.keys(defaultView).forEach(property => { Object.keys(defaultView).forEach(property => {
if (typeof global[property] === 'undefined') { if (typeof global[property as keyof typeof global] === 'undefined') {
exposedProperties.push(property); exposedProperties.push(property);
global[property] = defaultView[property]; // @ts-ignore due to string-type index signature doesn't apply for `typeof globalThis`.
global[property] = defaultView[property as keyof typeof defaultView];
} }
}); });
} }

View File

@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
import { fireEvent, render } from 'spec/helpers/testing-library'; import { fireEvent, render } from 'spec/helpers/testing-library';
import KeyboardShortcutButton, { KEY_MAP } from '.'; import KeyboardShortcutButton, { KEY_MAP, KeyboardShortcut } from '.';
test('renders shortcut description', () => { test('renders shortcut description', () => {
const { getByText, getByRole } = render( const { getByText, getByRole } = render(
@ -26,7 +26,7 @@ test('renders shortcut description', () => {
fireEvent.click(getByRole('button')); fireEvent.click(getByRole('button'));
expect(getByText('Keyboard shortcuts')).toBeInTheDocument(); expect(getByText('Keyboard shortcuts')).toBeInTheDocument();
Object.keys(KEY_MAP) Object.keys(KEY_MAP)
.filter(key => Boolean(KEY_MAP[key])) .filter(key => Boolean(KEY_MAP[key as KeyboardShortcut]))
.forEach(key => { .forEach(key => {
expect(getByText(key)).toBeInTheDocument(); expect(getByText(key)).toBeInTheDocument();
}); });

View File

@ -42,7 +42,7 @@ export enum KeyboardShortcut {
CtrlRight = 'ctrl+]', CtrlRight = 'ctrl+]',
} }
export const KEY_MAP = { export const KEY_MAP: Record<KeyboardShortcut, string | undefined> = {
[KeyboardShortcut.CtrlR]: t('Run query'), [KeyboardShortcut.CtrlR]: t('Run query'),
[KeyboardShortcut.CtrlEnter]: t('Run query'), [KeyboardShortcut.CtrlEnter]: t('Run query'),
[KeyboardShortcut.AltEnter]: t('Run query'), [KeyboardShortcut.AltEnter]: t('Run query'),
@ -62,15 +62,14 @@ export const KEY_MAP = {
[KeyboardShortcut.CtrlH]: userOS !== 'MacOS' ? t('Replace') : undefined, [KeyboardShortcut.CtrlH]: userOS !== 'MacOS' ? t('Replace') : undefined,
}; };
const KeyMapByCommand = Object.entries(KEY_MAP).reduce( const KeyMapByCommand = Object.entries(KEY_MAP).reduce<
(acc, [shortcut, command]) => { Record<string, string[]>
if (command) { >((acc, [shortcut, command]) => {
acc[command] = [...(acc[command] || []), shortcut]; if (command) {
} acc[command] = [...(acc[command] || []), shortcut];
return acc; }
}, return acc;
{} as Record<string, string[]>, }, {});
);
const ShortcutDescription = styled.span` const ShortcutDescription = styled.span`
font-size: ${({ theme }) => theme.typography.sizes.m}px; font-size: ${({ theme }) => theme.typography.sizes.m}px;

View File

@ -103,7 +103,9 @@ const QueryTable = ({
columns.map(column => ({ columns.map(column => ({
accessor: column, accessor: column,
Header: Header:
QUERY_HISTORY_TABLE_HEADERS_LOCALIZED[column] || setHeaders(column), QUERY_HISTORY_TABLE_HEADERS_LOCALIZED[
column as keyof typeof QUERY_HISTORY_TABLE_HEADERS_LOCALIZED
] || setHeaders(column),
disableSortBy: true, disableSortBy: true,
})), })),
[columns], [columns],
@ -221,6 +223,17 @@ const QueryTable = ({
label: t('Unknown Status'), label: t('Unknown Status'),
}, },
}, },
started: {
config: {
icon: (
<Icons.LoadingOutlined
iconColor={theme.colors.primary.base}
iconSize="m"
/>
),
label: t('Started'),
},
},
}; };
return queries return queries

View File

@ -68,10 +68,10 @@ const getValidator = () => {
const rules: any = getValidationRules(); const rules: any = getValidationRules();
return (formData: Record<string, any>, errors: FormValidation) => { return (formData: Record<string, any>, errors: FormValidation) => {
rules.forEach((rule: any) => { rules.forEach((rule: any) => {
const test = validators[rule.name]; const test = validators[rule.name as keyof typeof validators];
const args = rule.arguments.map((name: string) => formData[name]); const args = rule.arguments.map((name: string) => formData[name]);
const container = rule.container || rule.arguments.slice(-1)[0]; const container = rule.container || rule.arguments.slice(-1)[0];
if (!test(...args)) { if (!test(args[0], args[1])) {
errors[container]?.addError(rule.message); errors[container]?.addError(rule.message);
} }
}); });

View File

@ -29,7 +29,7 @@ export const STATE_TYPE_MAP: Record<string, Type> = {
success: 'success', success: 'success',
}; };
export const STATE_TYPE_MAP_LOCALIZED = { export const STATE_TYPE_MAP_LOCALIZED: Record<string, string> = {
offline: t('offline'), offline: t('offline'),
failed: t('failed'), failed: t('failed'),
pending: t('pending'), pending: t('pending'),

View File

@ -16,7 +16,6 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
// TODO: requires redux-localstorage > 1.0 for typescript support
import persistState from 'redux-localstorage'; import persistState from 'redux-localstorage';
import { pickBy } from 'lodash'; import { pickBy } from 'lodash';
import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core'; import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
@ -129,6 +128,8 @@ const sqlLabPersistStateConfig = {
}, },
}; };
// TODO: requires redux-localstorage > 1.0 for typescript support
/** @type {any} */
export const persistSqlLabStateEnhancer = persistState( export const persistSqlLabStateEnhancer = persistState(
sqlLabPersistStateConfig.paths, sqlLabPersistStateConfig.paths,
sqlLabPersistStateConfig.config, sqlLabPersistStateConfig.config,

View File

@ -147,7 +147,7 @@ export default function getInitialState({
}), }),
}; };
const destroyedQueryEditors = {}; const destroyedQueryEditors: Record<string, number> = {};
/** /**
* If the `SQLLAB_BACKEND_PERSISTENCE` feature flag is off, or if the user * If the `SQLLAB_BACKEND_PERSISTENCE` feature flag is off, or if the user

View File

@ -30,7 +30,7 @@ import {
import { queries, defaultQueryEditor } from '../fixtures'; import { queries, defaultQueryEditor } from '../fixtures';
describe('reduxStateToLocalStorageHelper', () => { describe('reduxStateToLocalStorageHelper', () => {
const queriesObj = {}; const queriesObj: Record<string, any> = {};
beforeEach(() => { beforeEach(() => {
queries.forEach(q => { queries.forEach(q => {
queriesObj[q.id] = q; queriesObj[q.id] = q;

View File

@ -16,8 +16,18 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import type { QueryResponse } from '@superset-ui/core'; import type {
import type { QueryEditor, SqlLabRootState, Table } from 'src/SqlLab/types'; InnerQueryResults,
Query,
QueryResponse,
QueryResults,
} from '@superset-ui/core';
import type {
CursorPosition,
QueryEditor,
SqlLabRootState,
Table,
} from 'src/SqlLab/types';
import type { ThunkDispatch } from 'redux-thunk'; import type { ThunkDispatch } from 'redux-thunk';
import { pick } from 'lodash'; import { pick } from 'lodash';
import { tableApiUtil } from 'src/hooks/apiResources/tables'; import { tableApiUtil } from 'src/hooks/apiResources/tables';
@ -71,6 +81,20 @@ export function emptyTablePersistData(tables: Table[]) {
.filter(({ queryEditorId }) => Boolean(queryEditorId)); .filter(({ queryEditorId }) => Boolean(queryEditorId));
} }
type InnerEmptyQueryResults = {
[key in string]: Query &
QueryResults & {
inLocalStorage?: boolean;
};
};
type EmptyQueryResults = Record<
string,
InnerEmptyQueryResults & {
results: InnerQueryResults | {};
}
>;
export function emptyQueryResults( export function emptyQueryResults(
queries: SqlLabRootState['sqlLab']['queries'], queries: SqlLabRootState['sqlLab']['queries'],
) { ) {
@ -86,7 +110,7 @@ export function emptyQueryResults(
[key]: query, [key]: query,
}; };
return updatedQueries; return updatedQueries;
}, {}); }, {} as EmptyQueryResults);
} }
export function clearQueryEditors(queryEditors: QueryEditor[]) { export function clearQueryEditors(queryEditors: QueryEditor[]) {
@ -94,10 +118,15 @@ export function clearQueryEditors(queryEditors: QueryEditor[]) {
// only return selected keys // only return selected keys
Object.keys(editor) Object.keys(editor)
.filter(key => PERSISTENT_QUERY_EDITOR_KEYS.has(key)) .filter(key => PERSISTENT_QUERY_EDITOR_KEYS.has(key))
.reduce( .reduce<
Record<
string,
string | number | boolean | CursorPosition | null | undefined
>
>(
(accumulator, key) => ({ (accumulator, key) => ({
...accumulator, ...accumulator,
[key]: editor[key], [key]: editor[key as keyof QueryEditor],
}), }),
{}, {},
), ),

View File

@ -115,7 +115,7 @@ export const formatValueHandler = (
}) })
.join(', '); .join(', ');
} }
if (controlsMap[key]?.type === 'BoundsControl') { if (controlsMap[key]?.type === 'BoundsControl' && Array.isArray(value)) {
return `Min: ${value[0]}, Max: ${value[1]}`; return `Min: ${value[0]}, Max: ${value[1]}`;
} }
if (controlsMap[key]?.type === 'CollectionControl' && Array.isArray(value)) { if (controlsMap[key]?.type === 'CollectionControl' && Array.isArray(value)) {

View File

@ -28,6 +28,7 @@ import {
t, t,
useTheme, useTheme,
ContextMenuFilters, ContextMenuFilters,
AdhocFilter,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -224,7 +225,7 @@ export default function DrillByModal({
const getFormDataChangesFromConfigs = useCallback( const getFormDataChangesFromConfigs = useCallback(
(configs: DrillByConfigs) => (configs: DrillByConfigs) =>
configs.reduce( configs.reduce<Record<string, any>>(
(acc, config) => { (acc, config) => {
if (config?.groupbyFieldName && config.column) { if (config?.groupbyFieldName && config.column) {
acc.formData[config.groupbyFieldName] = getNewGroupby( acc.formData[config.groupbyFieldName] = getNewGroupby(
@ -246,7 +247,7 @@ export default function DrillByModal({
return acc; return acc;
}, },
{ {
formData: {}, formData: {} as Record<string, string | string[] | Set<string>>,
overriddenGroupbyFields: new Set<string>(), overriddenGroupbyFields: new Set<string>(),
overriddenAdhocFilterFields: new Set<string>(), overriddenAdhocFilterFields: new Set<string>(),
}, },
@ -256,7 +257,7 @@ export default function DrillByModal({
const getFiltersFromConfigsByFieldName = useCallback( const getFiltersFromConfigsByFieldName = useCallback(
() => () =>
drillByConfigs.reduce((acc, config) => { drillByConfigs.reduce<Record<string, AdhocFilter[]>>((acc, config) => {
const adhocFilterFieldName = const adhocFilterFieldName =
config.adhocFilterFieldName || DEFAULT_ADHOC_FILTER_FIELD_NAME; config.adhocFilterFieldName || DEFAULT_ADHOC_FILTER_FIELD_NAME;
acc[adhocFilterFieldName] = [ acc[adhocFilterFieldName] = [
@ -295,7 +296,7 @@ export default function DrillByModal({
...formData, ...formData,
...overrideFormData, ...overrideFormData,
}; };
overriddenAdhocFilterFields.forEach(adhocFilterField => ({ overriddenAdhocFilterFields.forEach((adhocFilterField: string) => ({
...newFormData, ...newFormData,
[adhocFilterField]: [ [adhocFilterField]: [
...formData[adhocFilterField], ...formData[adhocFilterField],

View File

@ -90,7 +90,9 @@ export default function DrillDetailPane({
const [resultsPages, setResultsPages] = useState<Map<number, ResultsPage>>( const [resultsPages, setResultsPages] = useState<Map<number, ResultsPage>>(
new Map(), new Map(),
); );
const [timeFormatting, setTimeFormatting] = useState({}); const [timeFormatting, setTimeFormatting] = useState<
Record<string, TimeFormatting>
>({});
const SAMPLES_ROW_LIMIT = useSelector( const SAMPLES_ROW_LIMIT = useSelector(
(state: { common: { conf: JsonObject } }) => (state: { common: { conf: JsonObject } }) =>
@ -140,7 +142,10 @@ export default function DrillDetailPane({
: TimeFormatting.Formatted : TimeFormatting.Formatted
} }
onChange={value => onChange={value =>
setTimeFormatting(state => ({ ...state, [column]: value })) setTimeFormatting(state => ({
...state,
[column]: parseInt(value, 10) as TimeFormatting,
}))
} }
/> />
) : ( ) : (

View File

@ -34,10 +34,10 @@ export const CheckboxGallery = () =>
<div style={{ marginBottom: '16px' }} key={status}> <div style={{ marginBottom: '16px' }} key={status}>
<Checkbox <Checkbox
onChange={() => {}} onChange={() => {}}
checked={STATUSES[status]} checked={STATUSES[status as keyof typeof STATUSES]}
style={{ marginRight: '8px' }} style={{ marginRight: '8px' }}
/> />
{`I'm a${STATUSES[status] ? '' : 'n'} ${status} checkbox`} {`I'm a${STATUSES[status as keyof typeof STATUSES] ? '' : 'n'} ${status} checkbox`}
</div> </div>
)); ));

View File

@ -24,6 +24,7 @@ import CollectionTable from './CollectionTable';
const props = { const props = {
collection: mockDatasource['7__table'].columns, collection: mockDatasource['7__table'].columns,
tableColumns: ['column_name', 'type', 'groupby'], tableColumns: ['column_name', 'type', 'groupby'],
sortColumns: [],
}; };
test('renders a table', () => { test('renders a table', () => {

View File

@ -36,9 +36,9 @@ import { recurseReactClone } from './utils';
interface CRUDCollectionProps { interface CRUDCollectionProps {
allowAddItem?: boolean; allowAddItem?: boolean;
allowDeletes?: boolean; allowDeletes?: boolean;
collection: Array<object>; collection: Record<PropertyKey, any>[];
columnLabels?: object; columnLabels?: Record<PropertyKey, any>;
columnLabelTooltips?: object; columnLabelTooltips?: Record<PropertyKey, any>;
emptyMessage?: ReactNode; emptyMessage?: ReactNode;
expandFieldset?: ReactNode; expandFieldset?: ReactNode;
extraButtons?: ReactNode; extraButtons?: ReactNode;
@ -58,8 +58,8 @@ interface CRUDCollectionProps {
record: any, record: any,
) => ReactNode)[]; ) => ReactNode)[];
onChange?: (arg0: any) => void; onChange?: (arg0: any) => void;
tableColumns: Array<any>; tableColumns: any[];
sortColumns: Array<string>; sortColumns: string[];
stickyHeader?: boolean; stickyHeader?: boolean;
} }
@ -72,14 +72,14 @@ enum SortOrder {
} }
interface CRUDCollectionState { interface CRUDCollectionState {
collection: object; collection: Record<PropertyKey, any>;
collectionArray: Array<object>; collectionArray: Record<PropertyKey, any>[];
expandedColumns: object; expandedColumns: Record<PropertyKey, any>;
sortColumn: string; sortColumn: string;
sort: SortOrder; sort: SortOrder;
} }
function createCollectionArray(collection: object) { function createCollectionArray(collection: Record<PropertyKey, any>) {
return Object.keys(collection).map(k => collection[k]); return Object.keys(collection).map(k => collection[k]);
} }
@ -89,7 +89,7 @@ function createKeyedCollection(arr: Array<object>) {
id: o.id || nanoid(), id: o.id || nanoid(),
})); }));
const collection = {}; const collection: Record<PropertyKey, any> = {};
collectionArray.forEach((o: any) => { collectionArray.forEach((o: any) => {
collection[o.id] = o; collection[o.id] = o;
}); });
@ -301,7 +301,8 @@ export default class CRUDCollection extends PureComponent<
// newly ordered collection // newly ordered collection
const sorted = [...this.state.collectionArray].sort( const sorted = [...this.state.collectionArray].sort(
(a: object, b: object) => compareSort(a[col], b[col]), (a: Record<PropertyKey, any>, b: Record<PropertyKey, any>) =>
compareSort(a[col], b[col]),
); );
const newCollection = const newCollection =
sort === SortOrder.Asc ? sorted : sorted.reverse(); sort === SortOrder.Asc ? sorted : sorted.reverse();

View File

@ -74,7 +74,7 @@ const interactiveTypes = {
}, },
}; };
export const InteractiveDatePicker = (args: DatePickerProps) => ( export const InteractiveDatePicker: any = (args: DatePickerProps) => (
<DatePicker {...args} /> <DatePicker {...args} />
); );
@ -87,7 +87,7 @@ InteractiveDatePicker.args = {
InteractiveDatePicker.argTypes = interactiveTypes; InteractiveDatePicker.argTypes = interactiveTypes;
export const InteractiveRangePicker = (args: RangePickerProps) => ( export const InteractiveRangePicker: any = (args: RangePickerProps) => (
<RangePicker {...args} /> <RangePicker {...args} />
); );

View File

@ -19,4 +19,8 @@
import { DatePicker as AntdDatePicker } from 'antd-v5'; import { DatePicker as AntdDatePicker } from 'antd-v5';
export const DatePicker = AntdDatePicker; export const DatePicker = AntdDatePicker;
export const { RangePicker } = AntdDatePicker;
// Disable ESLint rule to allow tsc to infer proper type for RangePicker.
// eslint-disable-next-line prefer-destructuring
export const RangePicker: typeof AntdDatePicker.RangePicker =
AntdDatePicker.RangePicker;

View File

@ -144,7 +144,10 @@ const ImageContainer = ({
size: EmptyStateSize; size: EmptyStateSize;
}) => { }) => {
if (!image) return null; if (!image) return null;
const mappedImage = typeof image === 'string' ? imageMap[image] : image; const mappedImage =
typeof image === 'string'
? imageMap[image as keyof typeof imageMap]
: image;
return ( return (
<div role="img" aria-label="empty"> <div role="img" aria-label="empty">
<Empty <Empty

View File

@ -52,7 +52,10 @@ const collapseStyle = (theme: SupersetTheme) => css`
const extractInvalidValues = (messages: object, payload: object): string[] => { const extractInvalidValues = (messages: object, payload: object): string[] => {
const invalidValues: string[] = []; const invalidValues: string[] = [];
const recursiveExtract = (messages: object, payload: object) => { const recursiveExtract = (
messages: Record<string, any>,
payload: Record<string, any>,
) => {
Object.keys(messages).forEach(key => { Object.keys(messages).forEach(key => {
const value = payload[key]; const value = payload[key];
const message = messages[key]; const message = messages[key];
@ -66,7 +69,10 @@ const extractInvalidValues = (messages: object, payload: object): string[] => {
} }
}); });
}; };
recursiveExtract(messages, payload); recursiveExtract(
messages as Record<string, any>,
payload as Record<string, any>,
);
return invalidValues; return invalidValues;
}; };

View File

@ -95,7 +95,7 @@ const FilterableTable = ({
}: FilterableTableProps) => { }: FilterableTableProps) => {
const formatTableData = (data: Record<string, unknown>[]): Datum[] => const formatTableData = (data: Record<string, unknown>[]): Datum[] =>
data.map(row => { data.map(row => {
const newRow = {}; const newRow: Record<string, any> = {};
Object.entries(row).forEach(([key, val]) => { Object.entries(row).forEach(([key, val]) => {
if (['string', 'number'].indexOf(typeof val) >= 0) { if (['string', 'number'].indexOf(typeof val) >= 0) {
newRow[key] = val; newRow[key] = val;
@ -116,7 +116,7 @@ const FilterableTable = ({
const getWidthsForColumns = () => { const getWidthsForColumns = () => {
const PADDING = 50; // accounts for cell padding and width of sorting icon const PADDING = 50; // accounts for cell padding and width of sorting icon
const widthsByColumnKey = {}; const widthsByColumnKey: Record<string, number> = {};
const cellContent = ([] as string[]).concat( const cellContent = ([] as string[]).concat(
...orderedColumnKeys.map(key => { ...orderedColumnKeys.map(key => {
const cellContentList = list.map((data: Datum) => const cellContentList = list.map((data: Datum) =>

View File

@ -41,7 +41,7 @@ export default function FlashProvider({ children, messages }: Props) {
messages.forEach(message => { messages.forEach(message => {
const [type, text] = message; const [type, text] = message;
const flash = flashObj[type]; const flash = flashObj[type];
const toast = toasts[flash]; const toast = toasts[flash as keyof typeof toasts];
if (toast) { if (toast) {
toast(text); toast(text);
} }

View File

@ -127,6 +127,8 @@ const AntdEnhancedIcons = Object.keys(AntdIcons)
.map(k => ({ .map(k => ({
[k]: (props: IconType) => { [k]: (props: IconType) => {
const whatRole = props?.onClick ? 'button' : 'img'; const whatRole = props?.onClick ? 'button' : 'img';
// @ts-ignore TODO(hainenber): fix the type compatiblity between
// StyledIcon component prop and AntdIcon values
return <StyledIcon component={AntdIcons[k]} role={whatRole} {...props} />; return <StyledIcon component={AntdIcons[k]} role={whatRole} {...props} />;
}, },
})) }))

View File

@ -28,7 +28,7 @@ export default {
component: Icon, component: Icon,
}; };
const palette = { Default: null }; const palette: Record<string, string | null> = { Default: null };
Object.entries(supersetTheme.colors).forEach(([familyName, family]) => { Object.entries(supersetTheme.colors).forEach(([familyName, family]) => {
Object.entries(family).forEach(([colorName, colorValue]) => { Object.entries(family).forEach(([colorName, colorValue]) => {
palette[`${familyName} / ${colorName}`] = colorValue; palette[`${familyName} / ${colorName}`] = colorValue;

View File

@ -123,10 +123,11 @@ export default function Label(props: LabelProps) {
borderColor: borderColorHover, borderColor: borderColorHover,
opacity: 1, opacity: 1,
}, },
...(monospace
? { 'font-family': theme.typography.families.monospace }
: {}),
}; };
if (monospace) {
css['font-family'] = theme.typography.families.monospace;
}
return ( return (
<Tag <Tag
onClick={onClick} onClick={onClick}

View File

@ -65,19 +65,21 @@ export type Filters = Filter[];
export type ViewModeType = 'card' | 'table'; export type ViewModeType = 'card' | 'table';
export type InnerFilterValue =
| string
| boolean
| number
| null
| undefined
| string[]
| number[]
| { label: string; value: string | number };
export interface FilterValue { export interface FilterValue {
id: string; id: string;
urlDisplay?: string; urlDisplay?: string;
operator?: string; operator?: string;
value: value: InnerFilterValue;
| string
| boolean
| number
| null
| undefined
| string[]
| number[]
| { label: string; value: string | number };
} }
export interface FetchDataConfig { export interface FetchDataConfig {

View File

@ -39,6 +39,7 @@ import {
FetchDataConfig, FetchDataConfig,
Filter, Filter,
FilterValue, FilterValue,
InnerFilterValue,
InternalFilter, InternalFilter,
SortColumn, SortColumn,
ViewModeType, ViewModeType,
@ -138,7 +139,7 @@ export function convertFiltersRison(
list: Filter[], list: Filter[],
): FilterValue[] { ): FilterValue[] {
const filters: FilterValue[] = []; const filters: FilterValue[] = [];
const refs = {}; const refs: Record<string, FilterValue> = {};
Object.keys(filterObj).forEach(id => { Object.keys(filterObj).forEach(id => {
const filter: FilterValue = { const filter: FilterValue = {
@ -300,7 +301,7 @@ export function useListViewState({
useEffect(() => { useEffect(() => {
// From internalFilters, produce a simplified obj // From internalFilters, produce a simplified obj
const filterObj = {}; const filterObj: Record<string, InnerFilterValue> = {};
internalFilters.forEach(filter => { internalFilters.forEach(filter => {
if ( if (

View File

@ -70,8 +70,10 @@ const loadOptions = async (search: string, page: number, pageSize: number) => {
const optionFilterProps = ['label', 'value', 'gender']; const optionFilterProps = ['label', 'value', 'gender'];
const data = OPTIONS.filter(option => const data = OPTIONS.filter(option =>
optionFilterProps.some(prop => { optionFilterProps.some(prop => {
const optionProp = option?.[prop] const optionProp = option?.[prop as keyof typeof option]
? String(option[prop]).trim().toLowerCase() ? String(option[prop as keyof typeof option])
.trim()
.toLowerCase()
: ''; : '';
return optionProp.includes(searchValue); return optionProp.includes(searchValue);
}), }),

View File

@ -50,8 +50,8 @@ export function getValue(
} }
export function isEqual(a: V | LabeledValue, b: V | LabeledValue, key: string) { export function isEqual(a: V | LabeledValue, b: V | LabeledValue, key: string) {
const actualA = isObject(a) && key in a ? a[key] : a; const actualA = isObject(a) && key in a ? a[key as keyof LabeledValue] : a;
const actualB = isObject(b) && key in b ? b[key] : b; const actualB = isObject(b) && key in b ? b[key as keyof LabeledValue] : b;
// When comparing the values we use the equality // When comparing the values we use the equality
// operator to automatically convert different types // operator to automatically convert different types
// eslint-disable-next-line eqeqeq // eslint-disable-next-line eqeqeq
@ -84,10 +84,15 @@ export function hasOption(
* */ * */
export const propertyComparator = export const propertyComparator =
(property: string) => (a: AntdLabeledValue, b: AntdLabeledValue) => { (property: string) => (a: AntdLabeledValue, b: AntdLabeledValue) => {
if (typeof a[property] === 'string' && typeof b[property] === 'string') { const propertyA = a[property as keyof LabeledValue];
return a[property].localeCompare(b[property]); const propertyB = b[property as keyof LabeledValue];
if (typeof propertyA === 'string' && typeof propertyB === 'string') {
return propertyA.localeCompare(propertyB);
} }
return (a[property] as number) - (b[property] as number); if (typeof propertyA === 'number' && typeof propertyB === 'number') {
return propertyA - propertyB;
}
return String(propertyA).localeCompare(String(propertyB)); // fallback to string comparison
}; };
export const sortSelectedFirstHelper = ( export const sortSelectedFirstHelper = (

View File

@ -16,11 +16,12 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { Steps as AntdSteps } from 'antd-v5';
import { Steps, StepsProps } from '.'; import { Steps, StepsProps } from '.';
export default { export default {
title: 'Steps', title: 'Steps',
component: Steps, component: Steps as typeof AntdSteps,
}; };
export const InteractiveSteps = (args: StepsProps) => <Steps {...args} />; export const InteractiveSteps = (args: StepsProps) => <Steps {...args} />;

View File

@ -46,7 +46,7 @@ export default {
argTypes: { onClick: { action: 'clicked' } }, argTypes: { onClick: { action: 'clicked' } },
} as Meta<typeof Table>; } as Meta<typeof Table>;
export interface BasicData { interface BasicData {
name: string; name: string;
category: string; category: string;
price: number; price: number;
@ -54,7 +54,7 @@ export interface BasicData {
key: number; key: number;
} }
export interface RendererData { interface RendererData {
key: number; key: number;
buttonCell: string; buttonCell: string;
textCell: string; textCell: string;
@ -62,7 +62,7 @@ export interface RendererData {
dollarCell: number; dollarCell: number;
} }
export interface ExampleData { interface ExampleData {
title: string; title: string;
name: string; name: string;
age: number; age: number;
@ -71,8 +71,8 @@ export interface ExampleData {
key: number; key: number;
} }
function generateValues(amount: number, row = 0): object { function generateValues(amount: number, row = 0): Record<string, number> {
const cells = {}; const cells: Record<string, number> = {};
for (let i = 0; i < amount; i += 1) { for (let i = 0; i < amount; i += 1) {
cells[`col-${i}`] = i * row * 0.75; cells[`col-${i}`] = i * row * 0.75;
} }
@ -94,7 +94,12 @@ function generateColumns(amount: number): ColumnsType<ExampleData>[] {
locale={LocaleCode.en_US} locale={LocaleCode.en_US}
/> />
), ),
sorter: (a: BasicData, b: BasicData) => numericalSort(`col-${i}`, a, b), sorter: (a: BasicData, b: BasicData) =>
numericalSort(
`col-${i}`,
a as Record<PropertyKey, any>,
b as Record<PropertyKey, any>,
),
}); });
} }
return newCols as ColumnsType<ExampleData>[]; return newCols as ColumnsType<ExampleData>[];
@ -168,19 +173,34 @@ const basicColumns: ColumnsType<BasicData> = [
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
width: 100, width: 100,
sorter: (a: BasicData, b: BasicData) => alphabeticalSort('name', a, b), sorter: (a: BasicData, b: BasicData) =>
alphabeticalSort(
'name',
a as Record<PropertyKey, any>,
b as Record<PropertyKey, any>,
),
}, },
{ {
title: 'Category', title: 'Category',
dataIndex: 'category', dataIndex: 'category',
key: 'category', key: 'category',
sorter: (a: BasicData, b: BasicData) => alphabeticalSort('category', a, b), sorter: (a: BasicData, b: BasicData) =>
alphabeticalSort(
'category',
a as Record<PropertyKey, any>,
b as Record<PropertyKey, any>,
),
}, },
{ {
title: 'Price', title: 'Price',
dataIndex: 'price', dataIndex: 'price',
key: 'price', key: 'price',
sorter: (a: BasicData, b: BasicData) => numericalSort('price', a, b), sorter: (a: BasicData, b: BasicData) =>
numericalSort(
'price',
a as Record<PropertyKey, any>,
b as Record<PropertyKey, any>,
),
width: 100, width: 100,
}, },
{ {
@ -201,7 +221,12 @@ const bigColumns: ColumnsType<ExampleData> = [
title: 'Age', title: 'Age',
dataIndex: 'age', dataIndex: 'age',
key: 'age', key: 'age',
sorter: (a: ExampleData, b: ExampleData) => numericalSort('age', a, b), sorter: (a: ExampleData, b: ExampleData) =>
numericalSort(
'age',
a as Record<PropertyKey, any>,
b as Record<PropertyKey, any>,
),
width: 75, width: 75,
}, },
{ {
@ -381,7 +406,12 @@ const paginationColumns: ColumnsType<BasicData> = [
locale={LocaleCode.en_US} locale={LocaleCode.en_US}
/> />
), ),
sorter: (a: BasicData, b: BasicData) => numericalSort('price', a, b), sorter: (a: BasicData, b: BasicData) =>
numericalSort(
'price',
a as Record<PropertyKey, any>,
b as Record<PropertyKey, any>,
),
}, },
{ {
title: 'Description', title: 'Description',

View File

@ -95,7 +95,7 @@ function ActionMenu(props: ActionMenuProps) {
const { menuOptions, setVisible } = props; const { menuOptions, setVisible } = props;
const handleClick: MenuProps['onClick'] = ({ key }) => { const handleClick: MenuProps['onClick'] = ({ key }) => {
setVisible?.(false); setVisible?.(false);
const menuItem = menuOptions[key]; const menuItem = menuOptions[parseInt(key, 10)];
if (menuItem) { if (menuItem) {
menuItem?.onClick?.(menuItem); menuItem?.onClick?.(menuItem);
} }

View File

@ -25,6 +25,7 @@ import AntTable, {
import { PaginationProps } from 'antd/lib/pagination'; import { PaginationProps } from 'antd/lib/pagination';
import { t, useTheme, logging, styled } from '@superset-ui/core'; import { t, useTheme, logging, styled } from '@superset-ui/core';
import Loading from 'src/components/Loading'; import Loading from 'src/components/Loading';
import { RowSelectionType } from 'antd/lib/table/interface';
import InteractiveTableUtils from './utils/InteractiveTableUtils'; import InteractiveTableUtils from './utils/InteractiveTableUtils';
import VirtualTable from './VirtualTable'; import VirtualTable from './VirtualTable';
@ -226,11 +227,12 @@ const defaultLocale = {
cancelSort: t('Click to cancel sorting'), cancelSort: t('Click to cancel sorting'),
}; };
const selectionMap = {}; const selectionMap = {
[SelectionType.Multi]: 'checkbox',
[SelectionType.Single]: 'radio',
[SelectionType.Disabled]: null,
};
const noop = () => {}; const noop = () => {};
selectionMap[SelectionType.Multi] = 'checkbox';
selectionMap[SelectionType.Single] = 'radio';
selectionMap[SelectionType.Disabled] = null;
export function Table<RecordType extends object>( export function Table<RecordType extends object>(
props: TableProps<RecordType>, props: TableProps<RecordType>,
@ -277,7 +279,7 @@ export function Table<RecordType extends object>(
const selectionTypeValue = selectionMap[selectionType]; const selectionTypeValue = selectionMap[selectionType];
const rowSelection = { const rowSelection = {
type: selectionTypeValue, type: selectionMap[selectionType] as RowSelectionType,
selectedRowKeys, selectedRowKeys,
onChange: onSelectChange, onChange: onSelectChange,
}; };
@ -398,7 +400,7 @@ export function Table<RecordType extends object>(
{!virtualize && ( {!virtualize && (
<StyledTable <StyledTable
{...sharedProps} {...sharedProps}
rowSelection={selectionTypeValue ? rowSelection : undefined} rowSelection={selectionTypeValue !== null ? rowSelection : undefined}
sticky={sticky} sticky={sticky}
/> />
)} )}

View File

@ -45,14 +45,20 @@ const rows = [
* 1 or greater means the first item comes before the second item * 1 or greater means the first item comes before the second item
*/ */
test('alphabeticalSort sorts correctly', () => { test('alphabeticalSort sorts correctly', () => {
// @ts-ignore
expect(alphabeticalSort('name', rows[0], rows[1])).toBe(-1); expect(alphabeticalSort('name', rows[0], rows[1])).toBe(-1);
// @ts-ignore
expect(alphabeticalSort('name', rows[1], rows[0])).toBe(1); expect(alphabeticalSort('name', rows[1], rows[0])).toBe(1);
// @ts-ignore
expect(alphabeticalSort('category', rows[1], rows[0])).toBe(0); expect(alphabeticalSort('category', rows[1], rows[0])).toBe(0);
}); });
test('numericalSort sorts correctly', () => { test('numericalSort sorts correctly', () => {
// @ts-ignore
expect(numericalSort('cost', rows[1], rows[2])).toBe(0); expect(numericalSort('cost', rows[1], rows[2])).toBe(0);
// @ts-ignore
expect(numericalSort('cost', rows[1], rows[0])).toBeLessThan(0); expect(numericalSort('cost', rows[1], rows[0])).toBeLessThan(0);
// @ts-ignore
expect(numericalSort('cost', rows[4], rows[1])).toBeGreaterThan(0); expect(numericalSort('cost', rows[4], rows[1])).toBeGreaterThan(0);
}); });
@ -73,6 +79,7 @@ test('alphabeticalSort bad inputs no errors', () => {
expect( expect(
alphabeticalSort( alphabeticalSort(
'name', 'name',
// @ts-ignore
{ name: { title: 'the name attribute should not be an object' } }, { name: { title: 'the name attribute should not be an object' } },
{ name: 'Doug' }, { name: 'Doug' },
), ),
@ -93,6 +100,7 @@ test('numericalSort bad inputs no errors', () => {
expect( expect(
numericalSort( numericalSort(
'name', 'name',
// @ts-ignore
{ name: { title: 'the name attribute should not be an object' } }, { name: { title: 'the name attribute should not be an object' } },
{ name: 'Doug' }, { name: 'Doug' },
), ),

View File

@ -23,8 +23,11 @@
* @param b Second row object to compare * @param b Second row object to compare
* @returns number * @returns number
*/ */
export const alphabeticalSort = (key: string, a: object, b: object): number => export const alphabeticalSort = (
a?.[key]?.localeCompare?.(b?.[key]); key: string,
a: Record<PropertyKey, string>,
b: Record<PropertyKey, string>,
): number => a?.[key]?.localeCompare?.(b?.[key]);
/** /**
* @param key The name of the row's attribute used to compare values for numerical sorting * @param key The name of the row's attribute used to compare values for numerical sorting
@ -32,5 +35,8 @@ export const alphabeticalSort = (key: string, a: object, b: object): number =>
* @param b Second row object to compare * @param b Second row object to compare
* @returns number * @returns number
*/ */
export const numericalSort = (key: string, a: object, b: object): number => export const numericalSort = (
a?.[key] - b?.[key]; key: string,
a: Record<PropertyKey, number>,
b: Record<PropertyKey, number>,
): number => a?.[key] - b?.[key];

View File

@ -70,8 +70,8 @@ export default function TimezoneSelector({
const offsets = getOffsetKey(name); const offsets = getOffsetKey(name);
return ( return (
(isDST(currentDate.tz(name), name) (isDST(currentDate.tz(name), name)
? offsetsToName[offsets]?.[1] ? offsetsToName[offsets as keyof typeof offsetsToName]?.[1]
: offsetsToName[offsets]?.[0]) || name : offsetsToName[offsets as keyof typeof offsetsToName]?.[0]) || name
); );
}; };

View File

@ -1,76 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-env browser */
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { getCategoricalSchemeRegistry, t } from '@superset-ui/core';
import ColorSchemeControl from 'src/explore/components/controls/ColorSchemeControl';
const propTypes = {
onChange: PropTypes.func,
labelMargin: PropTypes.number,
colorScheme: PropTypes.string,
hasCustomLabelsColor: PropTypes.bool,
};
const defaultProps = {
hasCustomLabelsColor: false,
colorScheme: undefined,
onChange: () => {},
};
class ColorSchemeControlWrapper extends PureComponent {
constructor(props) {
super(props);
this.state = { hovered: false };
this.categoricalSchemeRegistry = getCategoricalSchemeRegistry();
this.choices = this.categoricalSchemeRegistry.keys().map(s => [s, s]);
this.schemes = this.categoricalSchemeRegistry.getMap();
}
setHover(hovered) {
this.setState({ hovered });
}
render() {
const { colorScheme, labelMargin = 0, hasCustomLabelsColor } = this.props;
return (
<ColorSchemeControl
description={t(
"Any color palette selected here will override the colors applied to this dashboard's individual charts",
)}
labelMargin={labelMargin}
name="color_scheme"
onChange={this.props.onChange}
value={colorScheme}
choices={this.choices}
clearable
schemes={this.schemes}
hovered={this.state.hovered}
hasCustomLabelsColor={hasCustomLabelsColor}
/>
);
}
}
ColorSchemeControlWrapper.propTypes = propTypes;
ColorSchemeControlWrapper.defaultProps = defaultProps;
export default ColorSchemeControlWrapper;

View File

@ -0,0 +1,64 @@
/**
* 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.
*/
/* eslint-env browser */
import { getCategoricalSchemeRegistry, t } from '@superset-ui/core';
import { useEffect, useState } from 'react';
import ColorSchemeControl from 'src/explore/components/controls/ColorSchemeControl';
interface ColorSchemeControlWrapperProps {
colorScheme?: string;
hasCustomLabelsColor: boolean;
hovered?: boolean;
onChange: () => void;
}
const ColorSchemeControlWrapper = ({
colorScheme,
hasCustomLabelsColor = false,
hovered = false,
onChange = () => {},
}: ColorSchemeControlWrapperProps) => {
const [choices, setChoices] = useState<string[][]>([]);
const [schemes, setSchemes] = useState({});
useEffect(() => {
// Registry initialization
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
setChoices(categoricalSchemeRegistry.keys().map(s => [s, s]));
setSchemes(categoricalSchemeRegistry.getMap());
}, []); // Empty dependency array ensures this runs only once
return (
<ColorSchemeControl
description={t(
"Any color palette selected here will override the colors applied to this dashboard's individual charts",
)}
name="color_scheme"
onChange={onChange}
value={colorScheme ?? ''}
choices={choices}
clearable
hovered={hovered}
schemes={schemes}
hasCustomLabelsColor={hasCustomLabelsColor}
/>
);
};
export default ColorSchemeControlWrapper;

View File

@ -122,7 +122,7 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
const [dashboardLabelsColorInitiated, setDashboardLabelsColorInitiated] = const [dashboardLabelsColorInitiated, setDashboardLabelsColorInitiated] =
useState(false); useState(false);
const prevRenderedChartIds = useRef<number[]>([]); const prevRenderedChartIds = useRef<number[]>([]);
const prevTabIndexRef = useRef(); const prevTabIndexRef = useRef<number>();
const tabIndex = useMemo(() => { const tabIndex = useMemo(() => {
const nextTabIndex = findTabIndexByComponentId({ const nextTabIndex = findTabIndexByComponentId({
currentComponent: getRootLevelTabsComponent(dashboardLayout), currentComponent: getRootLevelTabsComponent(dashboardLayout),

View File

@ -175,10 +175,10 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
setDashboardIndicators(indicatorsInitialState); setDashboardIndicators(indicatorsInitialState);
} else if (prevChartStatus !== 'success') { } else if (prevChartStatus !== 'success') {
if ( if (
chart?.queriesResponse?.[0]?.rejected_filters !== chart?.queriesResponse?.rejected_filters !==
prevChart?.queriesResponse?.[0]?.rejected_filters || prevChart?.queriesResponse?.rejected_filters ||
chart?.queriesResponse?.[0]?.applied_filters !== chart?.queriesResponse?.applied_filters !==
prevChart?.queriesResponse?.[0]?.applied_filters || prevChart?.queriesResponse?.applied_filters ||
dashboardFilters !== prevDashboardFilters || dashboardFilters !== prevDashboardFilters ||
datasources !== prevDatasources datasources !== prevDatasources
) { ) {
@ -215,10 +215,10 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
setNativeIndicators(indicatorsInitialState); setNativeIndicators(indicatorsInitialState);
} else if (prevChartStatus !== 'success') { } else if (prevChartStatus !== 'success') {
if ( if (
chart?.queriesResponse?.[0]?.rejected_filters !== chart?.queriesResponse?.rejected_filters !==
prevChart?.queriesResponse?.[0]?.rejected_filters || prevChart?.queriesResponse?.rejected_filters ||
chart?.queriesResponse?.[0]?.applied_filters !== chart?.queriesResponse?.applied_filters !==
prevChart?.queriesResponse?.[0]?.applied_filters || prevChart?.queriesResponse?.applied_filters ||
nativeFilters !== prevNativeFilters || nativeFilters !== prevNativeFilters ||
chartLayoutItems !== prevChartLayoutItems || chartLayoutItems !== prevChartLayoutItems ||
dataMask !== prevDataMask || dataMask !== prevDataMask ||

View File

@ -463,7 +463,6 @@ const PropertiesModal = ({
hasCustomLabelsColor={hasCustomLabelsColor} hasCustomLabelsColor={hasCustomLabelsColor}
onChange={onColorSchemeChange} onChange={onColorSchemeChange}
colorScheme={colorScheme} colorScheme={colorScheme}
labelMargin={4}
/> />
</Col> </Col>
</Row> </Row>
@ -532,7 +531,6 @@ const PropertiesModal = ({
hasCustomLabelsColor={hasCustomLabelsColor} hasCustomLabelsColor={hasCustomLabelsColor}
onChange={onColorSchemeChange} onChange={onColorSchemeChange}
colorScheme={colorScheme} colorScheme={colorScheme}
labelMargin={4}
/> />
</Col> </Col>
</Row> </Row>

View File

@ -30,6 +30,7 @@ import {
import { RootState } from 'src/dashboard/types'; import { RootState } from 'src/dashboard/types';
import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters'; import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
import { enforceSharedLabelsColorsArray } from 'src/utils/colorScheme'; import { enforceSharedLabelsColorsArray } from 'src/utils/colorScheme';
import { Divider, Filter } from '@superset-ui/core';
type Props = { dashboardPageId: string }; type Props = { dashboardPageId: string };
@ -66,7 +67,9 @@ const selectDashboardContextForExplore = createSelector(
(state: RootState) => state.dataMask, (state: RootState) => state.dataMask,
], ],
(metadata, dashboardId, colorScheme, filters, dataMask) => { (metadata, dashboardId, colorScheme, filters, dataMask) => {
const nativeFilters = Object.keys(filters).reduce((acc, key) => { const nativeFilters = Object.keys(filters).reduce<
Record<string, Pick<Filter | Divider, 'chartsInScope'>>
>((acc, key) => {
acc[key] = pick(filters[key], ['chartsInScope']); acc[key] = pick(filters[key], ['chartsInScope']);
return acc; return acc;
}, {}); }, {});

View File

@ -26,6 +26,7 @@ import {
isCrossFilterScopeGlobal, isCrossFilterScopeGlobal,
GlobalChartCrossFilterConfig, GlobalChartCrossFilterConfig,
GLOBAL_SCOPE_POINTER, GLOBAL_SCOPE_POINTER,
ChartCrossFiltersConfig,
} from 'src/dashboard/types'; } from 'src/dashboard/types';
import { getChartIdsInFilterScope } from 'src/dashboard/util/getChartIdsInFilterScope'; import { getChartIdsInFilterScope } from 'src/dashboard/util/getChartIdsInFilterScope';
import { useChartIds } from 'src/dashboard/util/charts/useChartIds'; import { useChartIds } from 'src/dashboard/util/charts/useChartIds';
@ -39,7 +40,9 @@ const getUpdatedGloballyScopedChartsInScope = (
configs: ChartConfiguration, configs: ChartConfiguration,
globalChartsInScope: number[], globalChartsInScope: number[],
) => ) =>
Object.entries(configs).reduce((acc, [id, config]) => { Object.entries(configs).reduce<
Record<string, { id: number; crossFilters: ChartCrossFiltersConfig }>
>((acc, [id, config]) => {
if (isCrossFilterScopeGlobal(config.crossFilters.scope)) { if (isCrossFilterScopeGlobal(config.crossFilters.scope)) {
acc[id] = { acc[id] = {
id: Number(config.id), id: Number(config.id),

View File

@ -191,7 +191,7 @@ const FilterBar: FC<FiltersBarProps> = ({
useEffect(() => { useEffect(() => {
if (previousFilters && dashboardId === previousDashboardId) { if (previousFilters && dashboardId === previousDashboardId) {
const updates = {}; const updates: Record<string, DataMaskWithId> = {};
Object.values(filters).forEach(currentFilter => { Object.values(filters).forEach(currentFilter => {
const previousFilter = previousFilters?.[currentFilter.id]; const previousFilter = previousFilters?.[currentFilter.id];
if (!previousFilter) { if (!previousFilter) {
@ -208,7 +208,9 @@ const FilterBar: FC<FiltersBarProps> = ({
const dataMaskChanged = !isEqual(currentDataMask, previousDataMask); const dataMaskChanged = !isEqual(currentDataMask, previousDataMask);
if (typeChanged || targetsChanged || dataMaskChanged) { if (typeChanged || targetsChanged || dataMaskChanged) {
updates[currentFilter.id] = getInitialDataMask(currentFilter.id); updates[currentFilter.id] = getInitialDataMask(
currentFilter.id,
) as DataMaskWithId;
} }
}); });

View File

@ -80,7 +80,9 @@ export const hasTemporalColumns = (
export const doesColumnMatchFilterType = (filterType: string, column: Column) => export const doesColumnMatchFilterType = (filterType: string, column: Column) =>
!column.type_generic || !column.type_generic ||
!(filterType in FILTER_SUPPORTED_TYPES) || !(filterType in FILTER_SUPPORTED_TYPES) ||
FILTER_SUPPORTED_TYPES[filterType]?.includes(column.type_generic); FILTER_SUPPORTED_TYPES[
filterType as keyof typeof FILTER_SUPPORTED_TYPES
]?.includes(column.type_generic);
export const mostUsedDataset = ( export const mostUsedDataset = (
datasets: DatasourcesState, datasets: DatasourcesState,

View File

@ -91,7 +91,7 @@ const selectIndicatorValue = (
(columnKey === TIME_FILTER_MAP.time_grain_sqla (columnKey === TIME_FILTER_MAP.time_grain_sqla
? datasource.time_grain_sqla ? datasource.time_grain_sqla
: datasource.granularity) || [] : datasource.granularity) || []
).reduce( ).reduce<Record<string, string>>(
(map, [key, value]) => ({ (map, [key, value]) => ({
...map, ...map,
[key]: value, [key]: value,
@ -189,8 +189,16 @@ export const getCrossFilterIndicator = (
return filterObject; return filterObject;
}; };
const cachedIndicatorsForChart = {}; const cachedIndicatorsForChart: Record<number, Indicator[]> = {};
const cachedDashboardFilterDataForChart = {}; const cachedDashboardFilterDataForChart: Record<
string,
{
appliedColumns: Set<string>;
rejectedColumns: Set<string>;
matchingFilters: Filter[];
matchingDatasources: Datasource[];
}
> = {};
// inspects redux state to find what the filter indicators should be shown for a given chart // inspects redux state to find what the filter indicators should be shown for a given chart
export const selectIndicatorsForChart = ( export const selectIndicatorsForChart = (
chartId: number, chartId: number,
@ -214,10 +222,10 @@ export const selectIndicatorsForChart = (
const cachedFilterData = cachedDashboardFilterDataForChart[chartId]; const cachedFilterData = cachedDashboardFilterDataForChart[chartId];
if ( if (
cachedIndicatorsForChart[chartId] && cachedIndicatorsForChart[chartId] &&
areObjectsEqual(cachedFilterData?.appliedColumns, appliedColumns) && areObjectsEqual(cachedFilterData.appliedColumns, appliedColumns) &&
areObjectsEqual(cachedFilterData?.rejectedColumns, rejectedColumns) && areObjectsEqual(cachedFilterData.rejectedColumns, rejectedColumns) &&
areObjectsEqual(cachedFilterData?.matchingFilters, matchingFilters) && areObjectsEqual(cachedFilterData.matchingFilters, matchingFilters) &&
areObjectsEqual(cachedFilterData?.matchingDatasources, matchingDatasources) areObjectsEqual(cachedFilterData.matchingDatasources, matchingDatasources)
) { ) {
return cachedIndicatorsForChart[chartId]; return cachedIndicatorsForChart[chartId];
} }
@ -286,7 +294,7 @@ export const selectChartCrossFilters = (
rejectedColumns: Set<string>, rejectedColumns: Set<string>,
filterEmitter = false, filterEmitter = false,
): Indicator[] | CrossFilterIndicator[] => { ): Indicator[] | CrossFilterIndicator[] => {
let crossFilterIndicators: any = []; let crossFilterIndicators: Indicator[] | CrossFilterIndicator[] = [];
crossFilterIndicators = Object.values(chartConfiguration) crossFilterIndicators = Object.values(chartConfiguration)
.filter(chartConfig => { .filter(chartConfig => {
const inScope = const inScope =
@ -322,8 +330,18 @@ export const selectChartCrossFilters = (
return crossFilterIndicators; return crossFilterIndicators;
}; };
const cachedNativeIndicatorsForChart = {}; const cachedNativeIndicatorsForChart: Record<number, any> = {};
const cachedNativeFilterDataForChart: any = {}; const cachedNativeFilterDataForChart: Record<
number,
{
nativeFilters: Filters;
chartLayoutItems: LayoutItem[];
chartConfiguration: ChartConfiguration;
dataMask: DataMaskStateWithId;
appliedColumns: Set<string>;
rejectedColumns: Set<string>;
}
> = {};
export const selectNativeIndicatorsForChart = ( export const selectNativeIndicatorsForChart = (
nativeFilters: Filters, nativeFilters: Filters,
dataMask: DataMaskStateWithId, dataMask: DataMaskStateWithId,
@ -374,7 +392,7 @@ export const selectNativeIndicatorsForChart = (
}; };
}); });
let crossFilterIndicators: any = []; let crossFilterIndicators: (Indicator | CrossFilterIndicator)[] = [];
crossFilterIndicators = selectChartCrossFilters( crossFilterIndicators = selectChartCrossFilters(
dataMask, dataMask,
chartId, chartId,
@ -383,7 +401,9 @@ export const selectNativeIndicatorsForChart = (
appliedColumns, appliedColumns,
rejectedColumns, rejectedColumns,
); );
const indicators = crossFilterIndicators.concat(nativeFilterIndicators); const indicators = crossFilterIndicators.concat(
nativeFilterIndicators as Indicator[],
);
cachedNativeIndicatorsForChart[chartId] = indicators; cachedNativeIndicatorsForChart[chartId] = indicators;
cachedNativeFilterDataForChart[chartId] = { cachedNativeFilterDataForChart[chartId] = {
nativeFilters, nativeFilters,

View File

@ -45,12 +45,12 @@ export function useFilterConfigMap() {
const filterConfig = useFilterConfiguration(); const filterConfig = useFilterConfiguration();
return useMemo( return useMemo(
() => () =>
filterConfig.reduce( filterConfig.reduce<Record<string, Filter | Divider>>(
(acc: Record<string, Filter | Divider>, filter: Filter) => { (acc: Record<string, Filter | Divider>, filter: Filter) => {
acc[filter.id] = filter; acc[filter.id] = filter;
return acc; return acc;
}, },
{} as Record<string, Filter | Divider>, {},
), ),
[filterConfig], [filterConfig],
); );

View File

@ -27,9 +27,13 @@ import {
getChartMetadataRegistry, getChartMetadataRegistry,
QueryFormData, QueryFormData,
t, t,
ExtraFormDataOverride,
TimeGranularity,
ExtraFormDataAppend,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { LayoutItem } from 'src/dashboard/types'; import { LayoutItem } from 'src/dashboard/types';
import extractUrlParams from 'src/dashboard/util/extractUrlParams'; import extractUrlParams from 'src/dashboard/util/extractUrlParams';
import { isIterable, OnlyKeyWithType } from 'src/utils/types';
import { TAB_TYPE } from '../../util/componentTypes'; import { TAB_TYPE } from '../../util/componentTypes';
import getBootstrapData from '../../../utils/getBootstrapData'; import getBootstrapData from '../../../utils/getBootstrapData';
@ -103,22 +107,26 @@ export function mergeExtraFormData(
): ExtraFormData { ): ExtraFormData {
const mergedExtra: ExtraFormData = {}; const mergedExtra: ExtraFormData = {};
EXTRA_FORM_DATA_APPEND_KEYS.forEach((key: string) => { EXTRA_FORM_DATA_APPEND_KEYS.forEach((key: string) => {
const originalExtraData = originalExtra[key as keyof ExtraFormDataAppend];
const newExtraData = newExtra[key as keyof ExtraFormDataAppend];
const mergedValues = [ const mergedValues = [
...(originalExtra[key] || []), ...(isIterable(originalExtraData) ? originalExtraData : []),
...(newExtra[key] || []), ...(isIterable(newExtraData) ? newExtraData : []),
]; ];
if (mergedValues.length) { if (mergedValues.length) {
mergedExtra[key] = mergedValues; mergedExtra[key as OnlyKeyWithType<ExtraFormData, any[]>] = mergedValues;
} }
}); });
EXTRA_FORM_DATA_OVERRIDE_KEYS.forEach((key: string) => { EXTRA_FORM_DATA_OVERRIDE_KEYS.forEach((key: string) => {
const originalValue = originalExtra[key]; const originalValue = originalExtra[key as keyof ExtraFormDataOverride];
if (originalValue !== undefined) { if (originalValue !== undefined) {
mergedExtra[key] = originalValue; mergedExtra[key as OnlyKeyWithType<ExtraFormData, typeof originalValue>] =
originalValue as TimeGranularity;
} }
const newValue = newExtra[key]; const newValue = newExtra[key as keyof ExtraFormDataOverride];
if (newValue !== undefined) { if (newValue !== undefined) {
mergedExtra[key] = newValue; mergedExtra[key as OnlyKeyWithType<ExtraFormData, typeof newValue>] =
newValue as TimeGranularity;
} }
}); });
return mergedExtra; return mergedExtra;

View File

@ -27,6 +27,7 @@ import {
UPDATE_CASCADE_PARENT_IDS, UPDATE_CASCADE_PARENT_IDS,
} from 'src/dashboard/actions/nativeFilters'; } from 'src/dashboard/actions/nativeFilters';
import { import {
Divider,
Filter, Filter,
FilterConfiguration, FilterConfiguration,
NativeFiltersState, NativeFiltersState,
@ -41,7 +42,7 @@ export function getInitialState({
state?: NativeFiltersState; state?: NativeFiltersState;
}): NativeFiltersState { }): NativeFiltersState {
const state: Partial<NativeFiltersState> = {}; const state: Partial<NativeFiltersState> = {};
const filters = {}; const filters: Record<string, Filter | Divider> = {};
if (filterConfig) { if (filterConfig) {
filterConfig.forEach(filter => { filterConfig.forEach(filter => {
const { id } = filter; const { id } = filter;

View File

@ -25,6 +25,7 @@ import {
JsonObject, JsonObject,
NativeFilterScope, NativeFilterScope,
NativeFiltersState, NativeFiltersState,
NativeFilterTarget,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { Dataset } from '@superset-ui/chart-controls'; import { Dataset } from '@superset-ui/chart-controls';
import { chart } from 'src/components/Chart/chartReducer'; import { chart } from 'src/components/Chart/chartReducer';
@ -183,26 +184,30 @@ export type Charts = { [key: number]: Chart };
type ComponentTypesKeys = keyof typeof componentTypes; type ComponentTypesKeys = keyof typeof componentTypes;
export type ComponentType = (typeof componentTypes)[ComponentTypesKeys]; export type ComponentType = (typeof componentTypes)[ComponentTypesKeys];
export type LayoutItemMeta = {
chartId: number;
defaultText?: string;
height: number;
placeholder?: string;
sliceName?: string;
sliceNameOverride?: string;
text?: string;
uuid: string;
width: number;
};
/** State of dashboardLayout item in redux */ /** State of dashboardLayout item in redux */
export type LayoutItem = { export type LayoutItem = {
children: string[]; children: string[];
parents?: string[]; parents?: string[];
type: ComponentType; type: ComponentType;
id: string; id: string;
meta: { meta: LayoutItemMeta;
chartId: number;
defaultText?: string;
height: number;
placeholder?: string;
sliceName?: string;
sliceNameOverride?: string;
text?: string;
uuid: string;
width: number;
};
}; };
type ActiveFilter = { type ActiveFilter = {
filterType?: string;
targets: number[] | [Partial<NativeFilterTarget>];
scope: number[]; scope: number[];
values: ExtraFormData; values: ExtraFormData;
}; };

View File

@ -20,6 +20,7 @@ import {
DataMaskStateWithId, DataMaskStateWithId,
PartialFilters, PartialFilters,
JsonObject, JsonObject,
DataMaskWithId,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { ActiveFilters, ChartConfiguration } from '../types'; import { ActiveFilters, ChartConfiguration } from '../types';
@ -28,9 +29,12 @@ export const getRelevantDataMask = (
prop: string, prop: string,
): JsonObject | DataMaskStateWithId => ): JsonObject | DataMaskStateWithId =>
Object.values(dataMask) Object.values(dataMask)
.filter(item => item[prop]) .filter(item => item[prop as keyof DataMaskWithId])
.reduce( .reduce(
(prev, next) => ({ ...prev, [next.id]: prop ? next[prop] : next }), (prev, next) => ({
...prev,
[next.id]: prop ? next[prop as keyof DataMaskWithId] : next,
}),
{}, {},
); );
@ -45,13 +49,14 @@ export const getAllActiveFilters = ({
nativeFilters: PartialFilters; nativeFilters: PartialFilters;
allSliceIds: number[]; allSliceIds: number[];
}): ActiveFilters => { }): ActiveFilters => {
const activeFilters = {}; const activeFilters: ActiveFilters = {};
// Combine native filters with cross filters, because they have similar logic // Combine native filters with cross filters, because they have similar logic
Object.values(dataMask).forEach(({ id: filterId, extraFormData }) => { Object.values(dataMask).forEach(({ id: filterId, extraFormData = {} }) => {
const scope = const scope =
nativeFilters?.[filterId]?.chartsInScope ?? nativeFilters?.[filterId]?.chartsInScope ??
chartConfiguration?.[filterId]?.crossFilters?.chartsInScope ?? chartConfiguration?.[parseInt(filterId, 10)]?.crossFilters
?.chartsInScope ??
allSliceIds ?? allSliceIds ??
[]; [];
const filterType = nativeFilters?.[filterId]?.filterType; const filterType = nativeFilters?.[filterId]?.filterType;

View File

@ -17,8 +17,10 @@
* under the License. * under the License.
*/ */
import { import {
DataMask,
DataMaskStateWithId, DataMaskStateWithId,
DataRecordFilters, DataRecordFilters,
DataRecordValue,
JsonObject, JsonObject,
PartialFilters, PartialFilters,
} from '@superset-ui/core'; } from '@superset-ui/core';
@ -29,10 +31,36 @@ import { isEqual } from 'lodash';
import getEffectiveExtraFilters from './getEffectiveExtraFilters'; import getEffectiveExtraFilters from './getEffectiveExtraFilters';
import { getAllActiveFilters } from '../activeAllDashboardFilters'; import { getAllActiveFilters } from '../activeAllDashboardFilters';
interface CachedFormData {
extra_form_data?: JsonObject;
extra_filters: {
col: string;
op: string;
val: DataRecordValue[];
}[];
own_color_scheme?: string;
color_scheme?: string;
color_namespace?: string;
chart_id: number;
label_colors?: Record<string, string>;
shared_label_colors?: string[];
map_label_colors?: Record<string, string>;
}
export type CachedFormDataWithExtraControls = CachedFormData & {
[key: string]: any;
};
// We cache formData objects so that our connected container components don't always trigger // We cache formData objects so that our connected container components don't always trigger
// render cascades. we cannot leverage the reselect library because our cache size is >1 // render cascades. we cannot leverage the reselect library because our cache size is >1
const cachedFiltersByChart = {}; const cachedFiltersByChart: Record<number, DataRecordFilters> = {};
const cachedFormdataByChart = {}; const cachedFormdataByChart: Record<
number,
CachedFormData & {
dataMask: DataMask;
extraControls: Record<string, string | boolean | null>;
}
> = {};
export interface GetFormDataWithExtraFiltersArguments { export interface GetFormDataWithExtraFiltersArguments {
chartConfiguration: ChartConfiguration; chartConfiguration: ChartConfiguration;
@ -113,7 +141,7 @@ export default function getFormDataWithExtraFilters({
}; };
} }
const formData = { const formData: CachedFormDataWithExtraControls = {
...chart.form_data, ...chart.form_data,
chart_id: chart.id, chart_id: chart.id,
label_colors: labelsColor, label_colors: labelsColor,

View File

@ -21,9 +21,11 @@ import {
Behavior, Behavior,
getChartMetadataRegistry, getChartMetadataRegistry,
isDefined, isDefined,
NativeFilterScope,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { getChartIdsInFilterScope } from './getChartIdsInFilterScope'; import { getChartIdsInFilterScope } from './getChartIdsInFilterScope';
import { import {
ChartConfiguration,
ChartsState, ChartsState,
DashboardInfo, DashboardInfo,
DashboardLayout, DashboardLayout,
@ -66,7 +68,7 @@ export const getCrossFiltersConfiguration = (
// If user just added cross filter to dashboard it's not saving its scope on server, // If user just added cross filter to dashboard it's not saving its scope on server,
// so we tweak it until user will update scope and will save it in server // so we tweak it until user will update scope and will save it in server
const chartConfiguration = {}; const chartConfiguration: ChartConfiguration = {};
chartLayoutItems.forEach(layoutItem => { chartLayoutItems.forEach(layoutItem => {
const chartId = layoutItem.meta?.chartId; const chartId = layoutItem.meta?.chartId;
@ -92,6 +94,7 @@ export const getCrossFiltersConfiguration = (
id: chartId, id: chartId,
crossFilters: { crossFilters: {
scope: GLOBAL_SCOPE_POINTER, scope: GLOBAL_SCOPE_POINTER,
chartsInScope: [],
}, },
}; };
} }
@ -101,7 +104,8 @@ export const getCrossFiltersConfiguration = (
id => id !== Number(chartId), id => id !== Number(chartId),
) )
: getChartIdsInFilterScope( : getChartIdsInFilterScope(
chartConfiguration[chartId].crossFilters.scope, chartConfiguration[chartId].crossFilters
.scope as NativeFilterScope,
Object.values(charts).map(chart => chart.id), Object.values(charts).map(chart => chart.id),
chartLayoutItems, chartLayoutItems,
); );

View File

@ -48,15 +48,17 @@ function findParentId(structure: IStructure): string | null {
return parentId; return parentId;
} }
const cache = {}; const cache: Record<string, string | null> = {};
export default function findParentIdWithCache( export default function findParentIdWithCache(
structure: IStructure, structure: IStructure,
): string | null { ): string | null {
let parentId = null; let parentId = null;
if (structure) { if (structure) {
const { childId, layout = {} } = structure; const { childId, layout = {} } = structure;
if (cache[childId]) { const cachedValue = cache[childId];
const lastParent = layout?.[cache[childId]] || {}; if (cachedValue) {
const lastParent = layout?.[cachedValue] || {};
if (lastParent?.children && lastParent?.children?.includes?.(childId)) { if (lastParent?.children && lastParent?.children?.includes?.(childId)) {
return lastParent.id; return lastParent.id;
} }

View File

@ -17,16 +17,17 @@
* under the License. * under the License.
*/ */
import findTabIndexByComponentId from 'src/dashboard/util/findTabIndexByComponentId'; import findTabIndexByComponentId from 'src/dashboard/util/findTabIndexByComponentId';
import { LayoutItem, LayoutItemMeta } from '../types';
describe('findTabIndexByComponentId', () => { describe('findTabIndexByComponentId', () => {
const topLevelTabsComponent = { const topLevelTabsComponent: LayoutItem = {
children: ['TAB-0g-5l347I2', 'TAB-qrwN_9VB5'], children: ['TAB-0g-5l347I2', 'TAB-qrwN_9VB5'],
id: 'TABS-MNQQSW-kyd', id: 'TABS-MNQQSW-kyd',
meta: {}, meta: {} as LayoutItemMeta,
parents: ['ROOT_ID'], parents: ['ROOT_ID'],
type: 'TABS', type: 'TABS',
}; };
const rowLevelTabsComponent = { const rowLevelTabsComponent: LayoutItem = {
children: [ children: [
'TAB-TwyUUGp2Bg', 'TAB-TwyUUGp2Bg',
'TAB-Zl1BQAUvN', 'TAB-Zl1BQAUvN',
@ -34,7 +35,7 @@ describe('findTabIndexByComponentId', () => {
'TAB---e53RNei', 'TAB---e53RNei',
], ],
id: 'TABS-Oduxop1L7I', id: 'TABS-Oduxop1L7I',
meta: {}, meta: {} as LayoutItemMeta,
parents: ['ROOT_ID', 'TABS-MNQQSW-kyd', 'TAB-qrwN_9VB5'], parents: ['ROOT_ID', 'TABS-MNQQSW-kyd', 'TAB-qrwN_9VB5'],
type: 'TABS', type: 'TABS',
}; };

View File

@ -16,10 +16,15 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
export default function findTabIndexByComponentId({ import { LayoutItem } from '../types';
const findTabIndexByComponentId = ({
currentComponent, currentComponent,
directPathToChild = [], directPathToChild = [],
}) { }: {
currentComponent: LayoutItem;
directPathToChild: string[];
}): number => {
if ( if (
!currentComponent || !currentComponent ||
directPathToChild.length === 0 || directPathToChild.length === 0 ||
@ -38,4 +43,6 @@ export default function findTabIndexByComponentId({
); );
} }
return -1; return -1;
} };
export default findTabIndexByComponentId;

View File

@ -17,6 +17,7 @@
* under the License. * under the License.
*/ */
import getFormDataWithExtraFilters, { import getFormDataWithExtraFilters, {
CachedFormDataWithExtraControls,
GetFormDataWithExtraFiltersArguments, GetFormDataWithExtraFiltersArguments,
} from 'src/dashboard/util/charts/getFormDataWithExtraFilters'; } from 'src/dashboard/util/charts/getFormDataWithExtraFilters';
import { sliceId as chartId } from 'spec/fixtures/mockChartQueries'; import { sliceId as chartId } from 'spec/fixtures/mockChartQueries';
@ -87,7 +88,8 @@ describe('getFormDataWithExtraFilters', () => {
}); });
it('should compose extra control', () => { it('should compose extra control', () => {
const result = getFormDataWithExtraFilters(mockArgs); const result: CachedFormDataWithExtraControls =
getFormDataWithExtraFilters(mockArgs);
expect(result.stack).toEqual('Stacked'); expect(result.stack).toEqual('Stacked');
}); });
}); });

View File

@ -43,7 +43,7 @@ describe('isValidChild', () => {
// every unique parent > child relationship is tested, but because this // every unique parent > child relationship is tested, but because this
// test representation WILL result in duplicates, we hash each test // test representation WILL result in duplicates, we hash each test
// to keep track of which we've run // to keep track of which we've run
const didTest = {}; const didTest: Record<string, boolean> = {};
const validExamples = [ const validExamples = [
[ROOT, GRID, CHART], // chart is valid because it is wrapped in a row [ROOT, GRID, CHART], // chart is valid because it is wrapped in a row
[ROOT, GRID, MARKDOWN], // markdown is valid because it is wrapped in a row [ROOT, GRID, MARKDOWN], // markdown is valid because it is wrapped in a row

View File

@ -55,7 +55,7 @@ const depthFour = rootDepth + 4;
const depthFive = rootDepth + 5; const depthFive = rootDepth + 5;
// when moving components around the depth of child is irrelevant, note these are parent depths // when moving components around the depth of child is irrelevant, note these are parent depths
const parentMaxDepthLookup = { const parentMaxDepthLookup: Record<string, Record<string, number>> = {
[DASHBOARD_ROOT_TYPE]: { [DASHBOARD_ROOT_TYPE]: {
[TABS_TYPE]: rootDepth, [TABS_TYPE]: rootDepth,
[DASHBOARD_GRID_TYPE]: rootDepth, [DASHBOARD_GRID_TYPE]: rootDepth,

View File

@ -41,25 +41,15 @@ import { areObjectsEqual } from '../reduxUtils';
export function getInitialDataMask( export function getInitialDataMask(
id?: string | number, id?: string | number,
moreProps?: DataMask,
): DataMask;
export function getInitialDataMask(
id: string | number,
moreProps: DataMask = {}, moreProps: DataMask = {},
): DataMaskWithId { ): DataMask | DataMaskWithId {
let otherProps = {};
if (id) {
otherProps = {
id,
};
}
return { return {
...otherProps, ...(id !== undefined ? { id } : {}),
extraFormData: {}, extraFormData: {},
filterState: {}, filterState: {},
ownState: {}, ownState: {},
...moreProps, ...moreProps,
} as DataMaskWithId; } as DataMask | DataMaskWithId;
} }
function fillNativeFilters( function fillNativeFilters(
@ -132,7 +122,7 @@ function updateDataMaskForFilterChanges(
const dataMaskReducer = produce( const dataMaskReducer = produce(
(draft: DataMaskStateWithId, action: AnyDataMaskAction) => { (draft: DataMaskStateWithId, action: AnyDataMaskAction) => {
const cleanState = {}; const cleanState: DataMaskStateWithId = {};
switch (action.type) { switch (action.type) {
case CLEAR_DATA_MASK_STATE: case CLEAR_DATA_MASK_STATE:
return cleanState; return cleanState;
@ -151,7 +141,7 @@ const dataMaskReducer = produce(
action.data.dashboardInfo?.metadata?.chart_configuration, action.data.dashboardInfo?.metadata?.chart_configuration,
).forEach(id => { ).forEach(id => {
cleanState[id] = { cleanState[id] = {
...getInitialDataMask(id), // take initial data ...(getInitialDataMask(id) as DataMaskWithId), // take initial data
}; };
}); });
fillNativeFilters( fillNativeFilters(

View File

@ -102,7 +102,10 @@ export default function Control(props: ControlProps) {
if (!type || isVisible === false) return null; if (!type || isVisible === false) return null;
const ControlComponent = typeof type === 'string' ? controlMap[type] : type; const ControlComponent =
typeof type === 'string'
? controlMap[type as keyof typeof controlMap]
: type;
if (!ControlComponent) { if (!ControlComponent) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn(`Unknown controlType: ${type}`); console.warn(`Unknown controlType: ${type}`);

View File

@ -48,6 +48,7 @@ import {
CustomControlItem, CustomControlItem,
Dataset, Dataset,
ExpandedControlItem, ExpandedControlItem,
isCustomControlItem,
isTemporalColumn, isTemporalColumn,
sections, sections,
} from '@superset-ui/chart-controls'; } from '@superset-ui/chart-controls';
@ -678,8 +679,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
return controlItem; return controlItem;
} }
if ( if (
controlItem.name && isCustomControlItem(controlItem) &&
controlItem.config &&
controlItem.name !== 'datasource' controlItem.name !== 'datasource'
) { ) {
return renderControl(controlItem); return renderControl(controlItem);

View File

@ -320,7 +320,7 @@ export const useTableColumns = (
return { return {
// react-table requires a non-empty id, therefore we introduce a fallback value in case the key is empty // react-table requires a non-empty id, therefore we introduce a fallback value in case the key is empty
id: key || index, id: key || index,
accessor: row => row[key], accessor: (row: Record<string, any>) => row[key],
Header: Header:
colType === GenericDataType.Temporal && colType === GenericDataType.Temporal &&
typeof firstValue !== 'string' ? ( typeof firstValue !== 'string' ? (

View File

@ -277,7 +277,8 @@ export default function DataSourcePanel({
}; };
const datasourceIsSaveable = const datasourceIsSaveable =
datasource.type && saveableDatasets[datasource.type]; datasource.type &&
saveableDatasets[datasource.type as keyof typeof saveableDatasets];
const mainBody = useMemo( const mainBody = useMemo(
() => ( () => (

View File

@ -19,7 +19,7 @@
import { forwardRef, RefObject, MouseEvent } from 'react'; import { forwardRef, RefObject, MouseEvent } from 'react';
import { css, styled } from '@superset-ui/core'; import { css, styled } from '@superset-ui/core';
import Button from 'src/components/Button'; import Button, { ButtonStyle } from 'src/components/Button';
interface ControlPanelAlertProps { interface ControlPanelAlertProps {
title: string; title: string;
@ -88,6 +88,8 @@ const Title = styled.p`
const typeChart = { const typeChart = {
warning: 'warning', warning: 'warning',
danger: 'danger', danger: 'danger',
error: 'primary',
info: 'primary',
}; };
export const ExploreAlert = forwardRef( export const ExploreAlert = forwardRef(
@ -119,7 +121,7 @@ export const ExploreAlert = forwardRef(
</Button> </Button>
)} )}
<Button <Button
buttonStyle={type in typeChart ? typeChart[type] : 'primary'} buttonStyle={typeChart[type] as ButtonStyle}
buttonSize="small" buttonSize="small"
onClick={primaryButtonAction} onClick={primaryButtonAction}
> >

View File

@ -31,7 +31,6 @@ const defaultProps = () => ({
hasCustomLabelsColor: false, hasCustomLabelsColor: false,
sharedLabelsColors: [], sharedLabelsColors: [],
label: 'Color scheme', label: 'Color scheme',
labelMargin: 0,
name: 'color', name: 'color',
value: 'supersetDefault', value: 'supersetDefault',
clearable: true, clearable: true,

View File

@ -57,7 +57,7 @@ export interface ColorSchemeControlProps {
colorNamespace?: string; colorNamespace?: string;
chartId?: number; chartId?: number;
dashboardId?: number; dashboardId?: number;
label: string; label?: string;
name: string; name: string;
onChange?: (value: string) => void; onChange?: (value: string) => void;
value: string; value: string;
@ -65,7 +65,9 @@ export interface ColorSchemeControlProps {
defaultScheme?: string; defaultScheme?: string;
choices: string[][] | (() => string[][]); choices: string[][] | (() => string[][]);
schemes: ColorSchemes | (() => ColorSchemes); schemes: ColorSchemes | (() => ColorSchemes);
isLinear: boolean; isLinear?: boolean;
description?: string;
hovered?: boolean;
} }
const StyledAlert = styled(Icons.AlertSolid)` const StyledAlert = styled(Icons.AlertSolid)`

Some files were not shown because too many files have changed in this diff Show More