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:
parent
a21f184058
commit
19e8a7049b
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
//---------------------------------------------------
|
//---------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 &&
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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]));
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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] || {};
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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)) {
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) =>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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} />;
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -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 = (
|
||||||
|
|
|
||||||
|
|
@ -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} />;
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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' },
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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 ||
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
};
|
};
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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' ? (
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
() => (
|
() => (
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Reference in New Issue