refactor(monorepo): stage 1 (#17427)
* skip geojson in pre-commit update prettier * update package.json update package.json u package pkg pkg2 * lint main repo 2 lint main repo lint * lintrc lintrc 2 lintrc2 lintrc 3 lintrc * fix import * refresh lock file * fix break line make @ts-ignore invalid * update rat-excludes rat-excludes update rat-excludes * update eslintrc.js * lint lint lint
This commit is contained in:
parent
34d7f0a860
commit
9070b6b19c
|
|
@ -33,6 +33,7 @@ repos:
|
|||
hooks:
|
||||
- id: check-docstring-first
|
||||
- id: check-added-large-files
|
||||
exclude: \.(geojson)$
|
||||
- id: check-yaml
|
||||
exclude: ^helm/superset/templates/
|
||||
- id: debug-statements
|
||||
|
|
@ -45,7 +46,7 @@ repos:
|
|||
- id: black
|
||||
language_version: python3
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.2.1 # Use the sha or tag you want to point at
|
||||
rev: v2.4.1 # Use the sha or tag you want to point at
|
||||
hooks:
|
||||
- id: prettier
|
||||
files: 'superset-frontend'
|
||||
|
|
|
|||
|
|
@ -48,3 +48,16 @@ vendor/*
|
|||
# github configuration
|
||||
.github/*
|
||||
.*mdx
|
||||
|
||||
# skip license check in superset-ui
|
||||
tmp/*
|
||||
lib/*
|
||||
esm/*
|
||||
tsconfig.tsbuildinfo
|
||||
.*ipynb
|
||||
.*yml
|
||||
.*iml
|
||||
.esprintrc
|
||||
.prettierignore
|
||||
superset-frontend/packages/generator-superset
|
||||
superset-frontend/temporary_superset_ui
|
||||
|
|
|
|||
|
|
@ -16,6 +16,15 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
const packageConfig = require('./package');
|
||||
|
||||
const importCoreModules = [];
|
||||
Object.entries(packageConfig.dependencies).forEach(([pkg]) => {
|
||||
if (/@superset-ui/.test(pkg)) {
|
||||
importCoreModules.push(pkg);
|
||||
}
|
||||
});
|
||||
module.exports = {
|
||||
extends: [
|
||||
'airbnb',
|
||||
|
|
@ -33,7 +42,15 @@ module.exports = {
|
|||
browser: true,
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': 'webpack',
|
||||
'import/resolver': {
|
||||
webpack: {},
|
||||
node: {
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
|
||||
},
|
||||
},
|
||||
// Allow only core/src and core/test, avoid import modules from lib
|
||||
'import/internal-regex': /^@superset-ui\/core\/(src|test)\/.*/,
|
||||
'import/core-modules': importCoreModules,
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
|
|
@ -76,11 +93,11 @@ module.exports = {
|
|||
'@typescript-eslint/no-empty-function': 0,
|
||||
'@typescript-eslint/no-explicit-any': 0,
|
||||
'@typescript-eslint/no-use-before-define': 1, // disabled temporarily
|
||||
'@typescript-eslint/no-non-null-assertion': 0, // disabled temporarily
|
||||
'@typescript-eslint/explicit-function-return-type': 0,
|
||||
'@typescript-eslint/explicit-module-boundary-types': 0, // re-enable up for discussion
|
||||
camelcase: 0,
|
||||
'class-methods-use-this': 0,
|
||||
curly: 1,
|
||||
'func-names': 0,
|
||||
'guard-for-in': 0,
|
||||
'import/no-cycle': 0, // re-enable up for discussion, might require some major refactors
|
||||
|
|
@ -170,11 +187,11 @@ module.exports = {
|
|||
},
|
||||
{
|
||||
files: [
|
||||
'src/**/*.test.ts',
|
||||
'src/**/*.test.tsx',
|
||||
'src/**/*.test.js',
|
||||
'src/**/*.test.jsx',
|
||||
'src/**/fixtures.*',
|
||||
'*.test.ts',
|
||||
'*.test.tsx',
|
||||
'*.test.js',
|
||||
'*.test.jsx',
|
||||
'fixtures.*',
|
||||
],
|
||||
plugins: ['jest', 'jest-dom', 'no-only-tests', 'testing-library'],
|
||||
env: {
|
||||
|
|
@ -195,9 +212,28 @@ module.exports = {
|
|||
'error',
|
||||
{ devDependencies: true },
|
||||
],
|
||||
'jest/consistent-test-it': 'error',
|
||||
'no-only-tests/no-only-tests': 'error',
|
||||
'max-classes-per-file': 0,
|
||||
'@typescript-eslint/no-non-null-assertion': 0,
|
||||
// TODO: disabled temporarily, re-enable after monorepo
|
||||
'jest/consistent-test-it': 'error',
|
||||
'jest/expect-expect': 0,
|
||||
'jest/no-test-prefixes': 0,
|
||||
'jest/valid-expect-in-promise': 0,
|
||||
'jest/valid-expect': 0,
|
||||
'jest/valid-title': 0,
|
||||
'jest-dom/prefer-to-have-attribute': 0,
|
||||
'jest-dom/prefer-to-have-text-content': 0,
|
||||
'jest-dom/prefer-to-have-style': 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: './packages/generator-superset/**/*.test.*',
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
rules: {
|
||||
'jest/expect-expect': 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -210,7 +246,7 @@ module.exports = {
|
|||
},
|
||||
],
|
||||
'class-methods-use-this': 0,
|
||||
curly: 1,
|
||||
curly: 2,
|
||||
'func-names': 0,
|
||||
'guard-for-in': 0,
|
||||
'import/extensions': [
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
const packageConfig = require('./package.json');
|
||||
const packageConfig = require('./package');
|
||||
|
||||
module.exports = {
|
||||
sourceMaps: true,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
export default function parsePostForm(requestBody: ArrayBuffer) {
|
||||
type ParsedFields = Record<string, string[] | string>;
|
||||
if (requestBody.constructor.name !== 'ArrayBuffer') {
|
||||
return (requestBody as unknown) as ParsedFields;
|
||||
return requestBody as unknown as ParsedFields;
|
||||
}
|
||||
const lines = new TextDecoder('utf-8').decode(requestBody).split('\n');
|
||||
const fields: ParsedFields = {};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,37 +2,6 @@
|
|||
"name": "superset",
|
||||
"version": "0.0.0dev",
|
||||
"description": "Superset is a data exploration platform designed to be visual, intuitive, and interactive.",
|
||||
"license": "Apache-2.0",
|
||||
"directories": {
|
||||
"doc": "docs",
|
||||
"test": "spec"
|
||||
},
|
||||
"scripts": {
|
||||
"tdd": "cross-env NODE_ENV=test jest --watch",
|
||||
"test": "cross-env NODE_ENV=test jest",
|
||||
"type": "tsc --noEmit",
|
||||
"cover": "cross-env NODE_ENV=test jest --coverage",
|
||||
"dev": "webpack --mode=development --color --watch",
|
||||
"dev-server": "cross-env NODE_ENV=development BABEL_ENV=development node --max_old_space_size=4096 ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --mode=development",
|
||||
"prod": "npm run build",
|
||||
"build-dev": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=development webpack --mode=development --color",
|
||||
"build-instrumented": "cross-env NODE_ENV=production BABEL_ENV=instrumented webpack --mode=production --color",
|
||||
"build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=production BABEL_ENV=\"${BABEL_ENV:=production}\" webpack --mode=production --color",
|
||||
"lint": "eslint --ignore-path=.eslintignore --ext .js,.jsx,.ts,.tsx . && npm run type",
|
||||
"prettier-check": "prettier --check 'src/**/*.{css,less,sass,scss}'",
|
||||
"lint-fix": "eslint --fix --ignore-path=.eslintignore --ext .js,.jsx,.ts,tsx . && npm run clean-css && npm run type",
|
||||
"clean-css": "prettier --write 'src/**/*.{css,less,sass,scss}'",
|
||||
"format": "prettier --write './{src,spec,cypress-base}/**/*{.js,.jsx,.ts,.tsx,.css,.less,.scss,.sass}'",
|
||||
"prettier": "npm run format",
|
||||
"check-translation": "prettier --check ../superset/translations/**/LC_MESSAGES/*.json",
|
||||
"clean-translation": "prettier --write ../superset/translations/**/LC_MESSAGES/*.json",
|
||||
"storybook": "cross-env NODE_ENV=development BABEL_ENV=development start-storybook -s ./src/assets/images -p 6006",
|
||||
"build-storybook": "build-storybook"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/apache/superset.git"
|
||||
},
|
||||
"keywords": [
|
||||
"big",
|
||||
"data",
|
||||
|
|
@ -45,21 +14,66 @@
|
|||
"database",
|
||||
"flask"
|
||||
],
|
||||
"author": "Apache",
|
||||
"homepage": "https://superset.apache.org/",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache/superset/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/apache/superset.git"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "Apache",
|
||||
"directories": {
|
||||
"doc": "docs",
|
||||
"test": "spec"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=production BABEL_ENV=\"${BABEL_ENV:=production}\" webpack --mode=production --color",
|
||||
"build-dev": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=development webpack --mode=development --color",
|
||||
"build-instrumented": "cross-env NODE_ENV=production BABEL_ENV=instrumented webpack --mode=production --color",
|
||||
"build-storybook": "build-storybook",
|
||||
"check-translation": "prettier --check ../superset/translations/**/LC_MESSAGES/*.json",
|
||||
"clean-css": "prettier --write 'src/**/*.{css,less,sass,scss}'",
|
||||
"clean-translation": "prettier --write ../superset/translations/**/LC_MESSAGES/*.json",
|
||||
"cover": "cross-env NODE_ENV=test jest --coverage",
|
||||
"dev": "webpack --mode=development --color --watch",
|
||||
"dev-server": "cross-env NODE_ENV=development BABEL_ENV=development node --max_old_space_size=4096 ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --mode=development",
|
||||
"format": "prettier --write './{src,spec,cypress-base,plugins,packages}/**/*{.js,.jsx,.ts,.tsx,.css,.less,.scss,.sass}'",
|
||||
"lint": "eslint --ignore-path=.eslintignore --ext .js,.jsx,.ts,.tsx . && npm run type",
|
||||
"lint-fix": "eslint --fix --ignore-path=.eslintignore --ext .js,.jsx,.ts,tsx . && npm run clean-css && npm run type",
|
||||
"prettier": "npm run format",
|
||||
"prettier-check": "prettier --check 'src/**/*.{css,less,sass,scss}'",
|
||||
"prod": "npm run build",
|
||||
"prune": "rm -rf ./{packages,plugins}/*/{lib,esm,tsconfig.tsbuildinfo,package-lock.json}",
|
||||
"storybook": "cross-env NODE_ENV=development BABEL_ENV=development start-storybook -s ./src/assets/images -p 6006",
|
||||
"tdd": "cross-env NODE_ENV=test jest --watch",
|
||||
"test": "cross-env NODE_ENV=test jest",
|
||||
"type": "tsc --noEmit"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 3 chrome versions",
|
||||
"last 3 firefox versions",
|
||||
"last 3 safari versions",
|
||||
"last 3 edge versions"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^16.9.1",
|
||||
"npm": "^7.5.4"
|
||||
"stylelint": {
|
||||
"rules": {
|
||||
"block-opening-brace-space-before": "always",
|
||||
"no-missing-end-of-source-newline": "never",
|
||||
"rule-empty-line-before": [
|
||||
"always",
|
||||
{
|
||||
"except": [
|
||||
"first-nested"
|
||||
],
|
||||
"ignore": [
|
||||
"after-comment"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"homepage": "https://superset.apache.org/",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.2.2",
|
||||
"@babel/runtime-corejs3": "^7.12.5",
|
||||
|
|
@ -178,12 +192,15 @@
|
|||
"redux-thunk": "^2.1.0",
|
||||
"redux-undo": "^1.0.0-beta9-9-7",
|
||||
"regenerator-runtime": "^0.13.5",
|
||||
"rimraf": "^3.0.2",
|
||||
"rison": "^0.1.1",
|
||||
"scroll-into-view-if-needed": "^2.2.28",
|
||||
"shortid": "^2.2.6",
|
||||
"src": "file:./src",
|
||||
"urijs": "^1.19.6",
|
||||
"use-immer": "^0.6.0",
|
||||
"use-query-params": "^1.1.9"
|
||||
"use-query-params": "^1.1.9",
|
||||
"yargs": "^15.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.15.7",
|
||||
|
|
@ -258,7 +275,7 @@
|
|||
"css-minimizer-webpack-plugin": "^3.0.2",
|
||||
"enzyme": "^3.10.0",
|
||||
"enzyme-adapter-react-16": "^1.14.0",
|
||||
"eslint": "^7.17.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-prettier": "^7.1.0",
|
||||
"eslint-import-resolver-typescript": "^2.5.0",
|
||||
|
|
@ -284,17 +301,18 @@
|
|||
"jest-environment-enzyme": "^7.1.2",
|
||||
"jest-enzyme": "^7.1.2",
|
||||
"jest-websocket-mock": "^2.2.0",
|
||||
"lerna": "^3.22.1",
|
||||
"jsdom": "^16.4.0",
|
||||
"lerna": "^3.22.1",
|
||||
"less": "^3.12.2",
|
||||
"less-loader": "^5.0.0",
|
||||
"mini-css-extract-plugin": "^2.3.0",
|
||||
"mock-socket": "^9.0.3",
|
||||
"node-fetch": "^2.6.1",
|
||||
"prettier": "^2.2.1",
|
||||
"prettier": "^2.4.1",
|
||||
"prettier-plugin-packagejson": "^2.2.15",
|
||||
"process": "^0.11.10",
|
||||
"react-test-renderer": "^16.9.0",
|
||||
"react-resizable": "^3.0.4",
|
||||
"react-test-renderer": "^16.9.0",
|
||||
"redux-mock-store": "^1.5.4",
|
||||
"sinon": "^9.0.2",
|
||||
"source-map-support": "^0.5.16",
|
||||
|
|
@ -312,24 +330,10 @@
|
|||
"webpack-cli": "^4.8.0",
|
||||
"webpack-dev-server": "^4.2.0",
|
||||
"webpack-manifest-plugin": "^4.0.2",
|
||||
"webpack-sources": "^3.2.0",
|
||||
"yargs": "^15.4.1"
|
||||
"webpack-sources": "^3.2.0"
|
||||
},
|
||||
"stylelint": {
|
||||
"rules": {
|
||||
"block-opening-brace-space-before": "always",
|
||||
"no-missing-end-of-source-newline": "never",
|
||||
"rule-empty-line-before": [
|
||||
"always",
|
||||
{
|
||||
"except": [
|
||||
"first-nested"
|
||||
],
|
||||
"ignore": [
|
||||
"after-comment"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
"engines": {
|
||||
"node": "^16.9.1",
|
||||
"npm": "^7.5.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,8 @@ export const getMockStoreWithChartsInTabsAndRoot = () =>
|
|||
);
|
||||
|
||||
export const mockStoreWithTabs = getMockStoreWithTabs();
|
||||
export const mockStoreWithChartsInTabsAndRoot = getMockStoreWithChartsInTabsAndRoot();
|
||||
export const mockStoreWithChartsInTabsAndRoot =
|
||||
getMockStoreWithChartsInTabsAndRoot();
|
||||
|
||||
export const sliceIdWithAppliedFilter = sliceId + 1;
|
||||
export const sliceIdWithRejectedFilter = sliceId + 2;
|
||||
|
|
|
|||
|
|
@ -93,9 +93,8 @@ describe('dashboardState actions', () => {
|
|||
// mock redux work: dispatch an event, cause modify redux state
|
||||
const mockParentsList = ['ROOT_ID'];
|
||||
dispatch.callsFake(() => {
|
||||
mockState.dashboardLayout.present[
|
||||
DASHBOARD_GRID_ID
|
||||
].parents = mockParentsList;
|
||||
mockState.dashboardLayout.present[DASHBOARD_GRID_ID].parents =
|
||||
mockParentsList;
|
||||
});
|
||||
|
||||
// call saveDashboardRequest, it should post dashboard data with updated
|
||||
|
|
|
|||
|
|
@ -183,7 +183,8 @@ describe('Tabs', () => {
|
|||
expect(wrapper.state('tabIndex')).toBe(0);
|
||||
|
||||
// display child in directPathToChild list
|
||||
const directPathToChild = dashboardLayoutWithTabs.present.ROW_ID2.parents.slice();
|
||||
const directPathToChild =
|
||||
dashboardLayoutWithTabs.present.ROW_ID2.parents.slice();
|
||||
const directLinkProps = {
|
||||
...props,
|
||||
directPathToChild,
|
||||
|
|
|
|||
|
|
@ -65,13 +65,13 @@ describe('getFormDataWithExtraFilters', () => {
|
|||
nativeFilters: {
|
||||
filterSets: {},
|
||||
filters: {
|
||||
[filterId]: ({
|
||||
[filterId]: {
|
||||
id: filterId,
|
||||
scope: {
|
||||
rootPath: [DASHBOARD_ROOT_ID],
|
||||
excluded: [],
|
||||
},
|
||||
} as unknown) as Filter,
|
||||
} as unknown as Filter,
|
||||
},
|
||||
},
|
||||
dataMask: {
|
||||
|
|
@ -82,7 +82,7 @@ describe('getFormDataWithExtraFilters', () => {
|
|||
ownState: {},
|
||||
},
|
||||
},
|
||||
layout: (dashboardLayout.present as unknown) as {
|
||||
layout: dashboardLayout.present as unknown as {
|
||||
[key: string]: LayoutItem;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -29,9 +29,8 @@ describe('getLeafComponentIdFromPath', () => {
|
|||
});
|
||||
|
||||
it('should not return label component', () => {
|
||||
const updatedPath = dashboardFilters[filterId].directPathToFilter.concat(
|
||||
'LABEL-test123',
|
||||
);
|
||||
const updatedPath =
|
||||
dashboardFilters[filterId].directPathToFilter.concat('LABEL-test123');
|
||||
expect(getLeafComponentIdFromPath(updatedPath)).toBe(leaf);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43,10 +43,10 @@ const getKnownControlState = (...args: Parameters<typeof getControlState>) =>
|
|||
|
||||
describe('controlUtils', () => {
|
||||
const state: ControlPanelState = {
|
||||
datasource: ({
|
||||
datasource: {
|
||||
columns: [{ column_name: 'a' }],
|
||||
metrics: [{ metric_name: 'first' }, { metric_name: 'second' }],
|
||||
} as unknown) as DatasourceMeta,
|
||||
} as unknown as DatasourceMeta,
|
||||
controls: {},
|
||||
form_data: { datasource: '1__table', viz_type: 'table' },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -68,39 +68,41 @@ export const controlPanelSectionsChartOptions: ControlPanelSectionConfig[] = [
|
|||
},
|
||||
];
|
||||
|
||||
export const controlPanelSectionsChartOptionsOnlyColorScheme: ControlPanelSectionConfig[] = [
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [['color_scheme']],
|
||||
},
|
||||
];
|
||||
export const controlPanelSectionsChartOptionsOnlyColorScheme: ControlPanelSectionConfig[] =
|
||||
[
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [['color_scheme']],
|
||||
},
|
||||
];
|
||||
|
||||
export const controlPanelSectionsChartOptionsTable: ControlPanelSectionConfig[] = [
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
'metric',
|
||||
'metrics',
|
||||
{
|
||||
name: 'all_columns',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
multi: true,
|
||||
label: t('Columns'),
|
||||
default: [],
|
||||
description: t('Columns to display'),
|
||||
optionRenderer: c => <ColumnOption column={c} showType />,
|
||||
valueKey: 'column_name',
|
||||
mapStateToProps: stateRef => ({
|
||||
options: stateRef.datasource ? stateRef.datasource.columns : [],
|
||||
}),
|
||||
freeForm: true,
|
||||
} as ControlConfig<'SelectControl', ColumnMeta>,
|
||||
},
|
||||
export const controlPanelSectionsChartOptionsTable: ControlPanelSectionConfig[] =
|
||||
[
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
'metric',
|
||||
'metrics',
|
||||
{
|
||||
name: 'all_columns',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
multi: true,
|
||||
label: t('Columns'),
|
||||
default: [],
|
||||
description: t('Columns to display'),
|
||||
optionRenderer: c => <ColumnOption column={c} showType />,
|
||||
valueKey: 'column_name',
|
||||
mapStateToProps: stateRef => ({
|
||||
options: stateRef.datasource ? stateRef.datasource.columns : [],
|
||||
}),
|
||||
freeForm: true,
|
||||
} as ControlConfig<'SelectControl', ColumnMeta>,
|
||||
},
|
||||
],
|
||||
],
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -252,9 +252,9 @@ export default class CRUDCollection extends React.PureComponent<
|
|||
}
|
||||
|
||||
// newly ordered collection
|
||||
const sorted = [
|
||||
...this.state.collectionArray,
|
||||
].sort((a: object, b: object) => compareSort(a[col], b[col]));
|
||||
const sorted = [...this.state.collectionArray].sort(
|
||||
(a: object, b: object) => compareSort(a[col], b[col]),
|
||||
);
|
||||
const newCollection =
|
||||
sort === SortOrder.asc ? sorted : sorted.reverse();
|
||||
|
||||
|
|
@ -280,12 +280,8 @@ export default class CRUDCollection extends React.PureComponent<
|
|||
|
||||
renderHeaderRow() {
|
||||
const cols = this.effectiveTableColumns();
|
||||
const {
|
||||
allowDeletes,
|
||||
expandFieldset,
|
||||
extraButtons,
|
||||
sortColumns,
|
||||
} = this.props;
|
||||
const { allowDeletes, expandFieldset, extraButtons, sortColumns } =
|
||||
this.props;
|
||||
return (
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -322,12 +318,8 @@ export default class CRUDCollection extends React.PureComponent<
|
|||
}
|
||||
|
||||
renderItem(record: any) {
|
||||
const {
|
||||
allowAddItem,
|
||||
allowDeletes,
|
||||
expandFieldset,
|
||||
tableColumns,
|
||||
} = this.props;
|
||||
const { allowAddItem, allowDeletes, expandFieldset, tableColumns } =
|
||||
this.props;
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
const isExpanded =
|
||||
!!this.state.expandedColumns[record.id] || record.__expanded;
|
||||
|
|
|
|||
|
|
@ -44,9 +44,8 @@ class ExploreResultsButton extends React.PureComponent {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this.getInvalidColumns = this.getInvalidColumns.bind(this);
|
||||
this.renderInvalidColumnMessage = this.renderInvalidColumnMessage.bind(
|
||||
this,
|
||||
);
|
||||
this.renderInvalidColumnMessage =
|
||||
this.renderInvalidColumnMessage.bind(this);
|
||||
}
|
||||
|
||||
getColumns() {
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ function QuerySearch({ actions, displayLimit }: QuerySearchProps) {
|
|||
value: xt,
|
||||
label: xt,
|
||||
}))}
|
||||
value={(from as unknown) as undefined}
|
||||
value={from as unknown as undefined}
|
||||
autosize={false}
|
||||
onChange={(selected: any) => setFrom(selected?.value)}
|
||||
/>
|
||||
|
|
@ -230,7 +230,7 @@ function QuerySearch({ actions, displayLimit }: QuerySearchProps) {
|
|||
name="select-to"
|
||||
placeholder={t('[To]-')}
|
||||
options={TIME_OPTIONS.map(xt => ({ value: xt, label: xt }))}
|
||||
value={(to as unknown) as undefined}
|
||||
value={to as unknown as undefined}
|
||||
autosize={false}
|
||||
onChange={(selected: any) => setTo(selected?.value)}
|
||||
/>
|
||||
|
|
@ -242,7 +242,7 @@ function QuerySearch({ actions, displayLimit }: QuerySearchProps) {
|
|||
value: s,
|
||||
label: s,
|
||||
}))}
|
||||
value={(status as unknown) as undefined}
|
||||
value={status as unknown as undefined}
|
||||
isLoading={false}
|
||||
autosize={false}
|
||||
onChange={(selected: any) => setStatus(selected?.value)}
|
||||
|
|
|
|||
|
|
@ -203,30 +203,25 @@ export default class ResultSet extends React.PureComponent<
|
|||
this.fetchResults = this.fetchResults.bind(this);
|
||||
this.popSelectStar = this.popSelectStar.bind(this);
|
||||
this.reFetchQueryResults = this.reFetchQueryResults.bind(this);
|
||||
this.toggleExploreResultsButton = this.toggleExploreResultsButton.bind(
|
||||
this,
|
||||
);
|
||||
this.toggleExploreResultsButton =
|
||||
this.toggleExploreResultsButton.bind(this);
|
||||
this.handleSaveInDataset = this.handleSaveInDataset.bind(this);
|
||||
this.handleHideSaveModal = this.handleHideSaveModal.bind(this);
|
||||
this.handleDatasetNameChange = this.handleDatasetNameChange.bind(this);
|
||||
this.handleSaveDatasetRadioBtnState = this.handleSaveDatasetRadioBtnState.bind(
|
||||
this,
|
||||
);
|
||||
this.handleSaveDatasetRadioBtnState =
|
||||
this.handleSaveDatasetRadioBtnState.bind(this);
|
||||
this.handleOverwriteCancel = this.handleOverwriteCancel.bind(this);
|
||||
this.handleOverwriteDataset = this.handleOverwriteDataset.bind(this);
|
||||
this.handleOverwriteDatasetOption = this.handleOverwriteDatasetOption.bind(
|
||||
this,
|
||||
);
|
||||
this.handleOverwriteDatasetOption =
|
||||
this.handleOverwriteDatasetOption.bind(this);
|
||||
this.handleSaveDatasetModalSearch = debounce(
|
||||
this.handleSaveDatasetModalSearch.bind(this),
|
||||
1000,
|
||||
);
|
||||
this.handleFilterAutocompleteOption = this.handleFilterAutocompleteOption.bind(
|
||||
this,
|
||||
);
|
||||
this.handleOnChangeAutoComplete = this.handleOnChangeAutoComplete.bind(
|
||||
this,
|
||||
);
|
||||
this.handleFilterAutocompleteOption =
|
||||
this.handleFilterAutocompleteOption.bind(this);
|
||||
this.handleOnChangeAutoComplete =
|
||||
this.handleOnChangeAutoComplete.bind(this);
|
||||
this.handleExploreBtnClick = this.handleExploreBtnClick.bind(this);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -184,9 +184,8 @@ class SqlEditor extends React.PureComponent {
|
|||
);
|
||||
this.queryPane = this.queryPane.bind(this);
|
||||
this.renderQueryLimit = this.renderQueryLimit.bind(this);
|
||||
this.getAceEditorAndSouthPaneHeights = this.getAceEditorAndSouthPaneHeights.bind(
|
||||
this,
|
||||
);
|
||||
this.getAceEditorAndSouthPaneHeights =
|
||||
this.getAceEditorAndSouthPaneHeights.bind(this);
|
||||
this.getSqlEditorHeight = this.getSqlEditorHeight.bind(this);
|
||||
this.requestValidation = debounce(
|
||||
this.requestValidation.bind(this),
|
||||
|
|
@ -456,14 +455,12 @@ class SqlEditor extends React.PureComponent {
|
|||
|
||||
queryPane() {
|
||||
const hotkeys = this.getHotkeyConfig();
|
||||
const {
|
||||
aceEditorHeight,
|
||||
southPaneHeight,
|
||||
} = this.getAceEditorAndSouthPaneHeights(
|
||||
this.state.height,
|
||||
this.state.northPercent,
|
||||
this.state.southPercent,
|
||||
);
|
||||
const { aceEditorHeight, southPaneHeight } =
|
||||
this.getAceEditorAndSouthPaneHeights(
|
||||
this.state.height,
|
||||
this.state.northPercent,
|
||||
this.state.southPercent,
|
||||
);
|
||||
return (
|
||||
<Split
|
||||
expandToMin
|
||||
|
|
|
|||
|
|
@ -84,9 +84,8 @@ class TabbedSqlEditors extends React.PureComponent {
|
|||
this.removeQueryEditor = this.removeQueryEditor.bind(this);
|
||||
this.renameTab = this.renameTab.bind(this);
|
||||
this.toggleLeftBar = this.toggleLeftBar.bind(this);
|
||||
this.removeAllOtherQueryEditors = this.removeAllOtherQueryEditors.bind(
|
||||
this,
|
||||
);
|
||||
this.removeAllOtherQueryEditors =
|
||||
this.removeAllOtherQueryEditors.bind(this);
|
||||
this.duplicateQueryEditor = this.duplicateQueryEditor.bind(this);
|
||||
this.handleSelect = this.handleSelect.bind(this);
|
||||
this.handleEdit = this.handleEdit.bind(this);
|
||||
|
|
|
|||
|
|
@ -110,9 +110,8 @@ const RefreshOverlayWrapper = styled.div`
|
|||
class Chart extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleRenderContainerFailure = this.handleRenderContainerFailure.bind(
|
||||
this,
|
||||
);
|
||||
this.handleRenderContainerFailure =
|
||||
this.handleRenderContainerFailure.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
|
|
|||
|
|
@ -162,13 +162,8 @@ class ChartRenderer extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
chartAlert,
|
||||
chartStatus,
|
||||
vizType,
|
||||
chartId,
|
||||
refreshOverlayVisible,
|
||||
} = this.props;
|
||||
const { chartAlert, chartStatus, vizType, chartId, refreshOverlayVisible } =
|
||||
this.props;
|
||||
|
||||
// Skip chart rendering
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -574,8 +574,8 @@ export function redirectSQLLab(formData) {
|
|||
export function refreshChart(chartKey, force, dashboardId) {
|
||||
return (dispatch, getState) => {
|
||||
const chart = (getState().charts || {})[chartKey];
|
||||
const timeout = getState().dashboardInfo.common.conf
|
||||
.SUPERSET_WEBSERVER_TIMEOUT;
|
||||
const timeout =
|
||||
getState().dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT;
|
||||
|
||||
if (
|
||||
!chart.latestQueryFormData ||
|
||||
|
|
|
|||
|
|
@ -70,12 +70,8 @@ class AnchorLink extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
anchorLinkId,
|
||||
filters,
|
||||
showShortLinkButton,
|
||||
placement,
|
||||
} = this.props;
|
||||
const { anchorLinkId, filters, showShortLinkButton, placement } =
|
||||
this.props;
|
||||
return (
|
||||
<span className="anchor-link-container" id={anchorLinkId}>
|
||||
{showShortLinkButton && (
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ function DefaultPlaceholder({
|
|||
*/
|
||||
export default function AsyncEsmComponent<
|
||||
P = PlaceholderProps,
|
||||
M = React.ComponentType<P> | { default: React.ComponentType<P> }
|
||||
M = React.ComponentType<P> | { default: React.ComponentType<P> },
|
||||
>(
|
||||
/**
|
||||
* A promise generator that returns the React component to render.
|
||||
|
|
|
|||
|
|
@ -25,10 +25,12 @@ export interface BadgeProps extends AntdBadgeProps {
|
|||
textColor?: string;
|
||||
}
|
||||
|
||||
const Badge = styled((
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
{ textColor, ...props }: BadgeProps,
|
||||
) => <AntdBadge {...props} />)`
|
||||
const Badge = styled(
|
||||
(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
{ textColor, ...props }: BadgeProps,
|
||||
) => <AntdBadge {...props} />,
|
||||
)`
|
||||
& > sup {
|
||||
padding: 0 ${({ theme }) => theme.gridUnit * 2}px;
|
||||
background: ${({ theme, color }) => color || theme.colors.primary.base};
|
||||
|
|
|
|||
|
|
@ -93,8 +93,8 @@ test('renders with custom properties', () => {
|
|||
});
|
||||
|
||||
const header = document.getElementsByClassName('ant-collapse-header')[0];
|
||||
const arrow = document.getElementsByClassName('ant-collapse-arrow')[0]
|
||||
.children[0];
|
||||
const arrow =
|
||||
document.getElementsByClassName('ant-collapse-arrow')[0].children[0];
|
||||
|
||||
const headerStyle = window.getComputedStyle(header);
|
||||
const arrowStyle = window.getComputedStyle(arrow);
|
||||
|
|
|
|||
|
|
@ -146,61 +146,62 @@ export default function DatabaseSelector({
|
|||
const [refresh, setRefresh] = useState(0);
|
||||
|
||||
const loadDatabases = useMemo(
|
||||
() => async (
|
||||
search: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
): Promise<{
|
||||
data: DatabaseValue[];
|
||||
totalCount: number;
|
||||
}> => {
|
||||
const queryParams = rison.encode({
|
||||
order_columns: 'database_name',
|
||||
order_direction: 'asc',
|
||||
page,
|
||||
page_size: pageSize,
|
||||
...(formMode || !sqlLabMode
|
||||
? { filters: [{ col: 'database_name', opr: 'ct', value: search }] }
|
||||
: {
|
||||
filters: [
|
||||
{ col: 'database_name', opr: 'ct', value: search },
|
||||
{
|
||||
col: 'expose_in_sqllab',
|
||||
opr: 'eq',
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
const endpoint = `/api/v1/database/?q=${queryParams}`;
|
||||
return SupersetClient.get({ endpoint }).then(({ json }) => {
|
||||
const { result } = json;
|
||||
if (getDbList) {
|
||||
getDbList(result);
|
||||
}
|
||||
if (result.length === 0) {
|
||||
handleError(t("It seems you don't have access to any database"));
|
||||
}
|
||||
const options = result.map((row: DatabaseObject) => ({
|
||||
label: (
|
||||
<SelectLabel
|
||||
backend={row.backend}
|
||||
databaseName={row.database_name}
|
||||
/>
|
||||
),
|
||||
value: row.id,
|
||||
id: row.id,
|
||||
database_name: row.database_name,
|
||||
backend: row.backend,
|
||||
allow_multi_schema_metadata_fetch:
|
||||
row.allow_multi_schema_metadata_fetch,
|
||||
}));
|
||||
return {
|
||||
data: options,
|
||||
totalCount: options.length,
|
||||
};
|
||||
});
|
||||
},
|
||||
() =>
|
||||
async (
|
||||
search: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
): Promise<{
|
||||
data: DatabaseValue[];
|
||||
totalCount: number;
|
||||
}> => {
|
||||
const queryParams = rison.encode({
|
||||
order_columns: 'database_name',
|
||||
order_direction: 'asc',
|
||||
page,
|
||||
page_size: pageSize,
|
||||
...(formMode || !sqlLabMode
|
||||
? { filters: [{ col: 'database_name', opr: 'ct', value: search }] }
|
||||
: {
|
||||
filters: [
|
||||
{ col: 'database_name', opr: 'ct', value: search },
|
||||
{
|
||||
col: 'expose_in_sqllab',
|
||||
opr: 'eq',
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
const endpoint = `/api/v1/database/?q=${queryParams}`;
|
||||
return SupersetClient.get({ endpoint }).then(({ json }) => {
|
||||
const { result } = json;
|
||||
if (getDbList) {
|
||||
getDbList(result);
|
||||
}
|
||||
if (result.length === 0) {
|
||||
handleError(t("It seems you don't have access to any database"));
|
||||
}
|
||||
const options = result.map((row: DatabaseObject) => ({
|
||||
label: (
|
||||
<SelectLabel
|
||||
backend={row.backend}
|
||||
databaseName={row.database_name}
|
||||
/>
|
||||
),
|
||||
value: row.id,
|
||||
id: row.id,
|
||||
database_name: row.database_name,
|
||||
backend: row.backend,
|
||||
allow_multi_schema_metadata_fetch:
|
||||
row.allow_multi_schema_metadata_fetch,
|
||||
}));
|
||||
return {
|
||||
data: options,
|
||||
totalCount: options.length,
|
||||
};
|
||||
});
|
||||
},
|
||||
[formMode, getDbList, handleError, sqlLabMode],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -452,9 +452,8 @@ class DatasourceEditor extends React.PureComponent {
|
|||
this.onChangeEditMode = this.onChangeEditMode.bind(this);
|
||||
this.onDatasourcePropChange = this.onDatasourcePropChange.bind(this);
|
||||
this.onDatasourceChange = this.onDatasourceChange.bind(this);
|
||||
this.tableChangeAndSyncMetadata = this.tableChangeAndSyncMetadata.bind(
|
||||
this,
|
||||
);
|
||||
this.tableChangeAndSyncMetadata =
|
||||
this.tableChangeAndSyncMetadata.bind(this);
|
||||
this.syncMetadata = this.syncMetadata.bind(this);
|
||||
this.setColumns = this.setColumns.bind(this);
|
||||
this.validateAndChange = this.validateAndChange.bind(this);
|
||||
|
|
|
|||
|
|
@ -52,10 +52,8 @@ export default function EditableTitle({
|
|||
const [isEditing, setIsEditing] = useState(editing);
|
||||
const [currentTitle, setCurrentTitle] = useState(title);
|
||||
const [lastTitle, setLastTitle] = useState(title);
|
||||
const [
|
||||
contentBoundingRect,
|
||||
setContentBoundingRect,
|
||||
] = useState<DOMRect | null>(null);
|
||||
const [contentBoundingRect, setContentBoundingRect] =
|
||||
useState<DOMRect | null>(null);
|
||||
// Used so we can access the DOM element if a user clicks on this component.
|
||||
|
||||
const contentRef = useRef<any | HTMLInputElement | HTMLTextAreaElement>();
|
||||
|
|
|
|||
|
|
@ -38,10 +38,9 @@ function TimeoutErrorMessage({
|
|||
}: ErrorMessageComponentProps<TimeoutErrorExtra>) {
|
||||
const { extra, level } = error;
|
||||
|
||||
const isVisualization = (['dashboard', 'explore'] as (
|
||||
| string
|
||||
| undefined
|
||||
)[]).includes(source);
|
||||
const isVisualization = (
|
||||
['dashboard', 'explore'] as (string | undefined)[]
|
||||
).includes(source);
|
||||
|
||||
const subtitle = isVisualization
|
||||
? tn(
|
||||
|
|
|
|||
|
|
@ -96,12 +96,12 @@ export type SupersetError<ExtraType = Record<string, any> | null> = {
|
|||
message: string;
|
||||
};
|
||||
|
||||
export type ErrorMessageComponentProps<
|
||||
ExtraType = Record<string, any> | null
|
||||
> = {
|
||||
error: SupersetError<ExtraType>;
|
||||
source?: ErrorSource;
|
||||
subtitle?: React.ReactNode;
|
||||
};
|
||||
export type ErrorMessageComponentProps<ExtraType = Record<string, any> | null> =
|
||||
{
|
||||
error: SupersetError<ExtraType>;
|
||||
source?: ErrorSource;
|
||||
subtitle?: React.ReactNode;
|
||||
};
|
||||
|
||||
export type ErrorMessageComponent = React.ComponentType<ErrorMessageComponentProps>;
|
||||
export type ErrorMessageComponent =
|
||||
React.ComponentType<ErrorMessageComponentProps>;
|
||||
|
|
|
|||
|
|
@ -126,9 +126,8 @@ const ImportModelsModal: FunctionComponent<ImportModelsModalProps> = ({
|
|||
}) => {
|
||||
const [isHidden, setIsHidden] = useState<boolean>(true);
|
||||
const [passwords, setPasswords] = useState<Record<string, string>>({});
|
||||
const [needsOverwriteConfirm, setNeedsOverwriteConfirm] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
const [needsOverwriteConfirm, setNeedsOverwriteConfirm] =
|
||||
useState<boolean>(false);
|
||||
const [confirmedOverwrite, setConfirmedOverwrite] = useState<boolean>(false);
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const [importingModel, setImportingModel] = useState<boolean>(false);
|
||||
|
|
|
|||
|
|
@ -45,15 +45,8 @@ export default function Label(props: LabelProps) {
|
|||
const theme = useTheme();
|
||||
const { colors, transitionTiming } = theme;
|
||||
const { type, onClick, children, ...rest } = props;
|
||||
const {
|
||||
primary,
|
||||
secondary,
|
||||
grayscale,
|
||||
success,
|
||||
warning,
|
||||
error,
|
||||
info,
|
||||
} = colors;
|
||||
const { primary, secondary, grayscale, success, warning, error, info } =
|
||||
colors;
|
||||
|
||||
let backgroundColor = grayscale.light3;
|
||||
let backgroundColorHover = onClick ? primary.light2 : grayscale.light3;
|
||||
|
|
|
|||
|
|
@ -41,19 +41,18 @@ export const InteractivePopoverDropdown = (props: Props) => {
|
|||
const { value, buttonType, optionType, ...rest } = props;
|
||||
const [currentValue, setCurrentValue] = useState(value);
|
||||
|
||||
const newElementHandler = (type: ElementType) => ({
|
||||
label,
|
||||
value,
|
||||
}: OptionProps) => {
|
||||
if (type === 'button') {
|
||||
return (
|
||||
<button type="button" key={value}>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return <span>{label}</span>;
|
||||
};
|
||||
const newElementHandler =
|
||||
(type: ElementType) =>
|
||||
({ label, value }: OptionProps) => {
|
||||
if (type === 'button') {
|
||||
return (
|
||||
<button type="button" key={value}>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return <span>{label}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<PopoverDropdown
|
||||
|
|
|
|||
|
|
@ -43,10 +43,8 @@ export default function HeaderReportActionsDropDown({
|
|||
const reports = useSelector<any, AlertObject>(state => state.reports);
|
||||
const reportsIds = Object.keys(reports);
|
||||
const report = reports[reportsIds[0]];
|
||||
const [
|
||||
currentReportDeleting,
|
||||
setCurrentReportDeleting,
|
||||
] = useState<AlertObject | null>(null);
|
||||
const [currentReportDeleting, setCurrentReportDeleting] =
|
||||
useState<AlertObject | null>(null);
|
||||
const theme = useTheme();
|
||||
|
||||
const toggleActiveKey = async (data: AlertObject, checked: boolean) => {
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ type AnyReactSelect<OptionType extends OptionTypeBase> =
|
|||
|
||||
export type SupersetStyledSelectProps<
|
||||
OptionType extends OptionTypeBase,
|
||||
T extends WindowedSelectProps<OptionType> = WindowedSelectProps<OptionType>
|
||||
T extends WindowedSelectProps<OptionType> = WindowedSelectProps<OptionType>,
|
||||
> = T & {
|
||||
// additional props for easier usage or backward compatibility
|
||||
labelKey?: string;
|
||||
|
|
@ -103,7 +103,7 @@ function styled<
|
|||
| WindowedSelectComponentType<OptionType>
|
||||
| ComponentType<
|
||||
SelectProps<OptionType>
|
||||
> = WindowedSelectComponentType<OptionType>
|
||||
> = WindowedSelectComponentType<OptionType>,
|
||||
>(SelectComponent: SelectComponentType) {
|
||||
type SelectProps = SupersetStyledSelectProps<OptionType>;
|
||||
type Components = SelectComponents<OptionType>;
|
||||
|
|
@ -113,7 +113,8 @@ function styled<
|
|||
});
|
||||
|
||||
// default components for the given OptionType
|
||||
const supersetDefaultComponents: SelectComponentsConfig<OptionType> = DEFAULT_COMPONENTS;
|
||||
const supersetDefaultComponents: SelectComponentsConfig<OptionType> =
|
||||
DEFAULT_COMPONENTS;
|
||||
|
||||
const getSortableMultiValue = (MultiValue: Components['MultiValue']) =>
|
||||
SortableElement((props: MultiValueProps<OptionType>) => {
|
||||
|
|
|
|||
|
|
@ -246,15 +246,13 @@ const defaultSortComparator = (a: AntdLabeledValue, b: AntdLabeledValue) => {
|
|||
* It creates a comparator to check for a specific property.
|
||||
* Can be used with string and number property values.
|
||||
* */
|
||||
export const propertyComparator = (property: string) => (
|
||||
a: AntdLabeledValue,
|
||||
b: AntdLabeledValue,
|
||||
) => {
|
||||
if (typeof a[property] === 'string' && typeof b[property] === 'string') {
|
||||
return a[property].localeCompare(b[property]);
|
||||
}
|
||||
return (a[property] as number) - (b[property] as number);
|
||||
};
|
||||
export const propertyComparator =
|
||||
(property: string) => (a: AntdLabeledValue, b: AntdLabeledValue) => {
|
||||
if (typeof a[property] === 'string' && typeof b[property] === 'string') {
|
||||
return a[property].localeCompare(b[property]);
|
||||
}
|
||||
return (a[property] as number) - (b[property] as number);
|
||||
};
|
||||
|
||||
/**
|
||||
* This component is a customized version of the Antdesign 4.X Select component
|
||||
|
|
@ -300,9 +298,8 @@ const Select = ({
|
|||
const shouldShowSearch = isAsync || allowNewOptions ? true : showSearch;
|
||||
const initialOptions =
|
||||
options && Array.isArray(options) ? options : EMPTY_OPTIONS;
|
||||
const [selectOptions, setSelectOptions] = useState<OptionsType>(
|
||||
initialOptions,
|
||||
);
|
||||
const [selectOptions, setSelectOptions] =
|
||||
useState<OptionsType>(initialOptions);
|
||||
const shouldUseChildrenOptions = !!selectOptions.find(
|
||||
opt => opt?.customLabel,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -59,15 +59,14 @@ type MenuListPropsChildren<OptionType> =
|
|||
| Component<OptionProps<OptionType>>[]
|
||||
| ReactElement[];
|
||||
|
||||
export type MenuListProps<
|
||||
OptionType extends OptionTypeBase
|
||||
> = MenuListComponentProps<OptionType> & {
|
||||
children: MenuListPropsChildren<OptionType>;
|
||||
// theme is not present with built-in @types/react-select, but is actually
|
||||
// available via CommonProps.
|
||||
theme?: ThemeConfig;
|
||||
className?: string;
|
||||
} & WindowedMenuListProps;
|
||||
export type MenuListProps<OptionType extends OptionTypeBase> =
|
||||
MenuListComponentProps<OptionType> & {
|
||||
children: MenuListPropsChildren<OptionType>;
|
||||
// theme is not present with built-in @types/react-select, but is actually
|
||||
// available via CommonProps.
|
||||
theme?: ThemeConfig;
|
||||
className?: string;
|
||||
} & WindowedMenuListProps;
|
||||
|
||||
const DEFAULT_OPTION_HEIGHT = 30;
|
||||
|
||||
|
|
|
|||
|
|
@ -29,15 +29,13 @@ const { MenuList: DefaultMenuList } = defaultComponents;
|
|||
|
||||
export const DEFAULT_WINDOW_THRESHOLD = 100;
|
||||
|
||||
export type WindowedSelectProps<
|
||||
OptionType extends OptionTypeBase
|
||||
> = SelectProps<OptionType> & {
|
||||
windowThreshold?: number;
|
||||
} & WindowedMenuListProps['selectProps'];
|
||||
export type WindowedSelectProps<OptionType extends OptionTypeBase> =
|
||||
SelectProps<OptionType> & {
|
||||
windowThreshold?: number;
|
||||
} & WindowedMenuListProps['selectProps'];
|
||||
|
||||
export type WindowedSelectComponentType<
|
||||
OptionType extends OptionTypeBase
|
||||
> = FunctionComponent<WindowedSelectProps<OptionType>>;
|
||||
export type WindowedSelectComponentType<OptionType extends OptionTypeBase> =
|
||||
FunctionComponent<WindowedSelectProps<OptionType>>;
|
||||
|
||||
export function MenuList<OptionType extends OptionTypeBase>({
|
||||
children,
|
||||
|
|
|
|||
|
|
@ -71,12 +71,11 @@ export type ThemeConfig = {
|
|||
colors: {
|
||||
// add known colors
|
||||
[key in keyof typeof reactSelectColors]: string;
|
||||
} &
|
||||
{
|
||||
[key in keyof ReturnType<typeof colors>]: string;
|
||||
} & {
|
||||
[key: string]: string; // any other colors
|
||||
};
|
||||
} & {
|
||||
[key in keyof ReturnType<typeof colors>]: string;
|
||||
} & {
|
||||
[key: string]: string; // any other colors
|
||||
};
|
||||
spacing: Theme['spacing'] & {
|
||||
// line height and font size must be pixels for easier computation
|
||||
// of option item height in WindowedMenuList
|
||||
|
|
@ -89,21 +88,20 @@ export type ThemeConfig = {
|
|||
|
||||
export type PartialThemeConfig = RecursivePartial<ThemeConfig>;
|
||||
|
||||
export const defaultTheme: (
|
||||
theme: SupersetTheme,
|
||||
) => PartialThemeConfig = theme => ({
|
||||
borderRadius: theme.borderRadius,
|
||||
zIndex: 11,
|
||||
colors: colors(theme),
|
||||
spacing: {
|
||||
baseUnit: 3,
|
||||
menuGutter: 0,
|
||||
controlHeight: 34,
|
||||
lineHeight: 19,
|
||||
fontSize: 14,
|
||||
minWidth: '6.5em',
|
||||
},
|
||||
});
|
||||
export const defaultTheme: (theme: SupersetTheme) => PartialThemeConfig =
|
||||
theme => ({
|
||||
borderRadius: theme.borderRadius,
|
||||
zIndex: 11,
|
||||
colors: colors(theme),
|
||||
spacing: {
|
||||
baseUnit: 3,
|
||||
menuGutter: 0,
|
||||
controlHeight: 34,
|
||||
lineHeight: 19,
|
||||
fontSize: 14,
|
||||
minWidth: '6.5em',
|
||||
},
|
||||
});
|
||||
|
||||
// let styles accept serialized CSS, too
|
||||
type CSSStyles = CSSProperties | SerializedStyles;
|
||||
|
|
@ -314,13 +312,8 @@ export type InputProps = ReactSelectInputProps & {
|
|||
inputStyle?: object;
|
||||
};
|
||||
|
||||
const {
|
||||
ClearIndicator,
|
||||
DropdownIndicator,
|
||||
Option,
|
||||
Input,
|
||||
SelectContainer,
|
||||
} = defaultComponents as Required<DeepNonNullable<SelectComponentsType>>;
|
||||
const { ClearIndicator, DropdownIndicator, Option, Input, SelectContainer } =
|
||||
defaultComponents as Required<DeepNonNullable<SelectComponentsType>>;
|
||||
|
||||
export const DEFAULT_COMPONENTS: SelectComponentsType = {
|
||||
SelectContainer: ({ children, ...props }) => {
|
||||
|
|
|
|||
|
|
@ -60,41 +60,41 @@ export interface SetChartConfigFail {
|
|||
type: typeof SET_CHART_CONFIG_FAIL;
|
||||
chartConfiguration: ChartConfiguration;
|
||||
}
|
||||
export const setChartConfiguration = (
|
||||
chartConfiguration: ChartConfiguration,
|
||||
) => async (dispatch: Dispatch, getState: () => any) => {
|
||||
dispatch({
|
||||
type: SET_CHART_CONFIG_BEGIN,
|
||||
chartConfiguration,
|
||||
});
|
||||
const { id, metadata } = getState().dashboardInfo;
|
||||
|
||||
// TODO extract this out when makeApi supports url parameters
|
||||
const updateDashboard = makeApi<
|
||||
Partial<DashboardInfo>,
|
||||
{ result: DashboardInfo }
|
||||
>({
|
||||
method: 'PUT',
|
||||
endpoint: `/api/v1/dashboard/${id}`,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await updateDashboard({
|
||||
json_metadata: JSON.stringify({
|
||||
...metadata,
|
||||
chart_configuration: chartConfiguration,
|
||||
}),
|
||||
});
|
||||
dispatch(
|
||||
dashboardInfoChanged({
|
||||
metadata: JSON.parse(response.result.json_metadata),
|
||||
}),
|
||||
);
|
||||
export const setChartConfiguration =
|
||||
(chartConfiguration: ChartConfiguration) =>
|
||||
async (dispatch: Dispatch, getState: () => any) => {
|
||||
dispatch({
|
||||
type: SET_CHART_CONFIG_COMPLETE,
|
||||
type: SET_CHART_CONFIG_BEGIN,
|
||||
chartConfiguration,
|
||||
});
|
||||
} catch (err) {
|
||||
dispatch({ type: SET_CHART_CONFIG_FAIL, chartConfiguration });
|
||||
}
|
||||
};
|
||||
const { id, metadata } = getState().dashboardInfo;
|
||||
|
||||
// TODO extract this out when makeApi supports url parameters
|
||||
const updateDashboard = makeApi<
|
||||
Partial<DashboardInfo>,
|
||||
{ result: DashboardInfo }
|
||||
>({
|
||||
method: 'PUT',
|
||||
endpoint: `/api/v1/dashboard/${id}`,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await updateDashboard({
|
||||
json_metadata: JSON.stringify({
|
||||
...metadata,
|
||||
chart_configuration: chartConfiguration,
|
||||
}),
|
||||
});
|
||||
dispatch(
|
||||
dashboardInfoChanged({
|
||||
metadata: JSON.parse(response.result.json_metadata),
|
||||
}),
|
||||
);
|
||||
dispatch({
|
||||
type: SET_CHART_CONFIG_COMPLETE,
|
||||
chartConfiguration,
|
||||
});
|
||||
} catch (err) {
|
||||
dispatch({ type: SET_CHART_CONFIG_FAIL, chartConfiguration });
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,28 +38,29 @@ export const UPDATE_COMPONENTS = 'UPDATE_COMPONENTS';
|
|||
// an additional setUnsavedChanges(true) action after the dispatch in the case
|
||||
// that dashboardState.hasUnsavedChanges is false.
|
||||
function setUnsavedChangesAfterAction(action) {
|
||||
return (...args) => (dispatch, getState) => {
|
||||
const result = action(...args);
|
||||
if (typeof result === 'function') {
|
||||
dispatch(result(dispatch, getState));
|
||||
} else {
|
||||
dispatch(result);
|
||||
}
|
||||
return (...args) =>
|
||||
(dispatch, getState) => {
|
||||
const result = action(...args);
|
||||
if (typeof result === 'function') {
|
||||
dispatch(result(dispatch, getState));
|
||||
} else {
|
||||
dispatch(result);
|
||||
}
|
||||
|
||||
const isComponentLevelEvent =
|
||||
result.type === UPDATE_COMPONENTS &&
|
||||
result.payload &&
|
||||
result.payload.nextComponents;
|
||||
// trigger dashboardFilters state update if dashboard layout is changed.
|
||||
if (!isComponentLevelEvent) {
|
||||
const components = getState().dashboardLayout.present;
|
||||
dispatch(updateLayoutComponents(components));
|
||||
}
|
||||
const isComponentLevelEvent =
|
||||
result.type === UPDATE_COMPONENTS &&
|
||||
result.payload &&
|
||||
result.payload.nextComponents;
|
||||
// trigger dashboardFilters state update if dashboard layout is changed.
|
||||
if (!isComponentLevelEvent) {
|
||||
const components = getState().dashboardLayout.present;
|
||||
dispatch(updateLayoutComponents(components));
|
||||
}
|
||||
|
||||
if (!getState().dashboardState.hasUnsavedChanges) {
|
||||
dispatch(setUnsavedChanges(true));
|
||||
}
|
||||
};
|
||||
if (!getState().dashboardState.hasUnsavedChanges) {
|
||||
dispatch(setUnsavedChanges(true));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const updateComponents = setUnsavedChangesAfterAction(
|
||||
|
|
|
|||
|
|
@ -62,347 +62,352 @@ import getNativeFilterConfig from '../util/filterboxMigrationHelper';
|
|||
|
||||
export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD';
|
||||
|
||||
export const hydrateDashboard = (
|
||||
dashboardData,
|
||||
chartData,
|
||||
filterboxMigrationState = FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||
) => (dispatch, getState) => {
|
||||
const { user, common } = getState();
|
||||
export const hydrateDashboard =
|
||||
(
|
||||
dashboardData,
|
||||
chartData,
|
||||
filterboxMigrationState = FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||
) =>
|
||||
(dispatch, getState) => {
|
||||
const { user, common } = getState();
|
||||
|
||||
const { metadata } = dashboardData;
|
||||
const regularUrlParams = extractUrlParams('regular');
|
||||
const reservedUrlParams = extractUrlParams('reserved');
|
||||
const editMode = reservedUrlParams.edit === 'true';
|
||||
const { metadata } = dashboardData;
|
||||
const regularUrlParams = extractUrlParams('regular');
|
||||
const reservedUrlParams = extractUrlParams('reserved');
|
||||
const editMode = reservedUrlParams.edit === 'true';
|
||||
|
||||
let preselectFilters = {};
|
||||
let preselectFilters = {};
|
||||
|
||||
chartData.forEach(chart => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
chart.slice_id = chart.form_data.slice_id;
|
||||
});
|
||||
try {
|
||||
// allow request parameter overwrite dashboard metadata
|
||||
preselectFilters =
|
||||
getUrlParam(URL_PARAMS.preselectFilters) ||
|
||||
JSON.parse(metadata.default_filters);
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
// Priming the color palette with user's label-color mapping provided in
|
||||
// the dashboard's JSON metadata
|
||||
if (metadata?.label_colors) {
|
||||
const namespace = metadata.color_namespace;
|
||||
const colorMap = isString(metadata.label_colors)
|
||||
? JSON.parse(metadata.label_colors)
|
||||
: metadata.label_colors;
|
||||
const categoricalNamespace = CategoricalColorNamespace.getNamespace(
|
||||
namespace,
|
||||
);
|
||||
|
||||
Object.keys(colorMap).forEach(label => {
|
||||
categoricalNamespace.setColor(label, colorMap[label]);
|
||||
chartData.forEach(chart => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
chart.slice_id = chart.form_data.slice_id;
|
||||
});
|
||||
}
|
||||
|
||||
// dashboard layout
|
||||
const { position_data } = dashboardData;
|
||||
// new dash: position_json could be {} or null
|
||||
const layout =
|
||||
position_data && Object.keys(position_data).length > 0
|
||||
? position_data
|
||||
: getEmptyLayout();
|
||||
|
||||
// create a lookup to sync layout names with slice names
|
||||
const chartIdToLayoutId = {};
|
||||
Object.values(layout).forEach(layoutComponent => {
|
||||
if (layoutComponent.type === CHART_TYPE) {
|
||||
chartIdToLayoutId[layoutComponent.meta.chartId] = layoutComponent.id;
|
||||
try {
|
||||
// allow request parameter overwrite dashboard metadata
|
||||
preselectFilters =
|
||||
getUrlParam(URL_PARAMS.preselectFilters) ||
|
||||
JSON.parse(metadata.default_filters);
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
});
|
||||
|
||||
// find root level chart container node for newly-added slices
|
||||
const parentId = findFirstParentContainerId(layout);
|
||||
const parent = layout[parentId];
|
||||
let newSlicesContainer;
|
||||
let newSlicesContainerWidth = 0;
|
||||
// Priming the color palette with user's label-color mapping provided in
|
||||
// the dashboard's JSON metadata
|
||||
if (metadata?.label_colors) {
|
||||
const namespace = metadata.color_namespace;
|
||||
const colorMap = isString(metadata.label_colors)
|
||||
? JSON.parse(metadata.label_colors)
|
||||
: metadata.label_colors;
|
||||
const categoricalNamespace =
|
||||
CategoricalColorNamespace.getNamespace(namespace);
|
||||
|
||||
const filterScopes = metadata?.filter_scopes || {};
|
||||
Object.keys(colorMap).forEach(label => {
|
||||
categoricalNamespace.setColor(label, colorMap[label]);
|
||||
});
|
||||
}
|
||||
|
||||
const chartQueries = {};
|
||||
const dashboardFilters = {};
|
||||
const slices = {};
|
||||
const sliceIds = new Set();
|
||||
chartData.forEach(slice => {
|
||||
const key = slice.slice_id;
|
||||
const form_data = {
|
||||
...slice.form_data,
|
||||
url_params: {
|
||||
...slice.form_data.url_params,
|
||||
...regularUrlParams,
|
||||
},
|
||||
};
|
||||
chartQueries[key] = {
|
||||
...chart,
|
||||
id: key,
|
||||
form_data,
|
||||
formData: applyDefaultFormData(form_data),
|
||||
};
|
||||
// dashboard layout
|
||||
const { position_data } = dashboardData;
|
||||
// new dash: position_json could be {} or null
|
||||
const layout =
|
||||
position_data && Object.keys(position_data).length > 0
|
||||
? position_data
|
||||
: getEmptyLayout();
|
||||
|
||||
slices[key] = {
|
||||
slice_id: key,
|
||||
slice_url: slice.slice_url,
|
||||
slice_name: slice.slice_name,
|
||||
form_data: slice.form_data,
|
||||
viz_type: slice.form_data.viz_type,
|
||||
datasource: slice.form_data.datasource,
|
||||
description: slice.description,
|
||||
description_markeddown: slice.description_markeddown,
|
||||
owners: slice.owners,
|
||||
modified: slice.modified,
|
||||
changed_on: new Date(slice.changed_on).getTime(),
|
||||
};
|
||||
// create a lookup to sync layout names with slice names
|
||||
const chartIdToLayoutId = {};
|
||||
Object.values(layout).forEach(layoutComponent => {
|
||||
if (layoutComponent.type === CHART_TYPE) {
|
||||
chartIdToLayoutId[layoutComponent.meta.chartId] = layoutComponent.id;
|
||||
}
|
||||
});
|
||||
|
||||
sliceIds.add(key);
|
||||
// find root level chart container node for newly-added slices
|
||||
const parentId = findFirstParentContainerId(layout);
|
||||
const parent = layout[parentId];
|
||||
let newSlicesContainer;
|
||||
let newSlicesContainerWidth = 0;
|
||||
|
||||
// if there are newly added slices from explore view, fill slices into 1 or more rows
|
||||
if (!chartIdToLayoutId[key] && layout[parentId]) {
|
||||
if (
|
||||
newSlicesContainerWidth === 0 ||
|
||||
newSlicesContainerWidth + GRID_DEFAULT_CHART_WIDTH > GRID_COLUMN_COUNT
|
||||
) {
|
||||
newSlicesContainer = newComponentFactory(
|
||||
ROW_TYPE,
|
||||
(parent.parents || []).slice(),
|
||||
const filterScopes = metadata?.filter_scopes || {};
|
||||
|
||||
const chartQueries = {};
|
||||
const dashboardFilters = {};
|
||||
const slices = {};
|
||||
const sliceIds = new Set();
|
||||
chartData.forEach(slice => {
|
||||
const key = slice.slice_id;
|
||||
const form_data = {
|
||||
...slice.form_data,
|
||||
url_params: {
|
||||
...slice.form_data.url_params,
|
||||
...regularUrlParams,
|
||||
},
|
||||
};
|
||||
chartQueries[key] = {
|
||||
...chart,
|
||||
id: key,
|
||||
form_data,
|
||||
formData: applyDefaultFormData(form_data),
|
||||
};
|
||||
|
||||
slices[key] = {
|
||||
slice_id: key,
|
||||
slice_url: slice.slice_url,
|
||||
slice_name: slice.slice_name,
|
||||
form_data: slice.form_data,
|
||||
viz_type: slice.form_data.viz_type,
|
||||
datasource: slice.form_data.datasource,
|
||||
description: slice.description,
|
||||
description_markeddown: slice.description_markeddown,
|
||||
owners: slice.owners,
|
||||
modified: slice.modified,
|
||||
changed_on: new Date(slice.changed_on).getTime(),
|
||||
};
|
||||
|
||||
sliceIds.add(key);
|
||||
|
||||
// if there are newly added slices from explore view, fill slices into 1 or more rows
|
||||
if (!chartIdToLayoutId[key] && layout[parentId]) {
|
||||
if (
|
||||
newSlicesContainerWidth === 0 ||
|
||||
newSlicesContainerWidth + GRID_DEFAULT_CHART_WIDTH > GRID_COLUMN_COUNT
|
||||
) {
|
||||
newSlicesContainer = newComponentFactory(
|
||||
ROW_TYPE,
|
||||
(parent.parents || []).slice(),
|
||||
);
|
||||
layout[newSlicesContainer.id] = newSlicesContainer;
|
||||
parent.children.push(newSlicesContainer.id);
|
||||
newSlicesContainerWidth = 0;
|
||||
}
|
||||
|
||||
const chartHolder = newComponentFactory(
|
||||
CHART_TYPE,
|
||||
{
|
||||
chartId: slice.slice_id,
|
||||
},
|
||||
(newSlicesContainer.parents || []).slice(),
|
||||
);
|
||||
layout[newSlicesContainer.id] = newSlicesContainer;
|
||||
parent.children.push(newSlicesContainer.id);
|
||||
newSlicesContainerWidth = 0;
|
||||
|
||||
layout[chartHolder.id] = chartHolder;
|
||||
newSlicesContainer.children.push(chartHolder.id);
|
||||
chartIdToLayoutId[chartHolder.meta.chartId] = chartHolder.id;
|
||||
newSlicesContainerWidth += GRID_DEFAULT_CHART_WIDTH;
|
||||
}
|
||||
|
||||
const chartHolder = newComponentFactory(
|
||||
CHART_TYPE,
|
||||
{
|
||||
chartId: slice.slice_id,
|
||||
},
|
||||
(newSlicesContainer.parents || []).slice(),
|
||||
);
|
||||
// build DashboardFilters for interactive filter features
|
||||
if (slice.form_data.viz_type === 'filter_box') {
|
||||
const configs = getFilterConfigsFromFormdata(slice.form_data);
|
||||
let { columns } = configs;
|
||||
const { labels } = configs;
|
||||
if (preselectFilters[key]) {
|
||||
Object.keys(columns).forEach(col => {
|
||||
if (preselectFilters[key][col]) {
|
||||
columns = {
|
||||
...columns,
|
||||
[col]: preselectFilters[key][col],
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
layout[chartHolder.id] = chartHolder;
|
||||
newSlicesContainer.children.push(chartHolder.id);
|
||||
chartIdToLayoutId[chartHolder.meta.chartId] = chartHolder.id;
|
||||
newSlicesContainerWidth += GRID_DEFAULT_CHART_WIDTH;
|
||||
}
|
||||
const scopesByChartId = Object.keys(columns).reduce((map, column) => {
|
||||
const scopeSettings = {
|
||||
...filterScopes[key],
|
||||
};
|
||||
const { scope, immune } = {
|
||||
...DASHBOARD_FILTER_SCOPE_GLOBAL,
|
||||
...scopeSettings[column],
|
||||
};
|
||||
|
||||
// build DashboardFilters for interactive filter features
|
||||
if (slice.form_data.viz_type === 'filter_box') {
|
||||
const configs = getFilterConfigsFromFormdata(slice.form_data);
|
||||
let { columns } = configs;
|
||||
const { labels } = configs;
|
||||
if (preselectFilters[key]) {
|
||||
Object.keys(columns).forEach(col => {
|
||||
if (preselectFilters[key][col]) {
|
||||
columns = {
|
||||
...columns,
|
||||
[col]: preselectFilters[key][col],
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const scopesByChartId = Object.keys(columns).reduce((map, column) => {
|
||||
const scopeSettings = {
|
||||
...filterScopes[key],
|
||||
};
|
||||
const { scope, immune } = {
|
||||
...DASHBOARD_FILTER_SCOPE_GLOBAL,
|
||||
...scopeSettings[column],
|
||||
};
|
||||
|
||||
return {
|
||||
...map,
|
||||
[column]: {
|
||||
scope,
|
||||
immune,
|
||||
},
|
||||
};
|
||||
}, {});
|
||||
|
||||
const componentId = chartIdToLayoutId[key];
|
||||
const directPathToFilter = (layout[componentId].parents || []).slice();
|
||||
directPathToFilter.push(componentId);
|
||||
if (
|
||||
[
|
||||
FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||
FILTER_BOX_MIGRATION_STATES.SNOOZED,
|
||||
].includes(filterboxMigrationState)
|
||||
) {
|
||||
dashboardFilters[key] = {
|
||||
...dashboardFilter,
|
||||
chartId: key,
|
||||
componentId,
|
||||
datasourceId: slice.form_data.datasource,
|
||||
filterName: slice.slice_name,
|
||||
directPathToFilter,
|
||||
columns,
|
||||
labels,
|
||||
scopes: scopesByChartId,
|
||||
isDateFilter: Object.keys(columns).includes(TIME_RANGE),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// sync layout names with current slice names in case a slice was edited
|
||||
// in explore since the layout was updated. name updates go through layout for undo/redo
|
||||
// functionality and python updates slice names based on layout upon dashboard save
|
||||
const layoutId = chartIdToLayoutId[key];
|
||||
if (layoutId && layout[layoutId]) {
|
||||
layout[layoutId].meta.sliceName = slice.slice_name;
|
||||
}
|
||||
});
|
||||
buildActiveFilters({
|
||||
dashboardFilters,
|
||||
components: layout,
|
||||
});
|
||||
|
||||
// store the header as a layout component so we can undo/redo changes
|
||||
layout[DASHBOARD_HEADER_ID] = {
|
||||
id: DASHBOARD_HEADER_ID,
|
||||
type: DASHBOARD_HEADER_TYPE,
|
||||
meta: {
|
||||
text: dashboardData.dashboard_title,
|
||||
},
|
||||
};
|
||||
|
||||
const dashboardLayout = {
|
||||
past: [],
|
||||
present: layout,
|
||||
future: [],
|
||||
};
|
||||
|
||||
// find direct link component and path from root
|
||||
const directLinkComponentId = getLocationHash();
|
||||
let directPathToChild = [];
|
||||
if (layout[directLinkComponentId]) {
|
||||
directPathToChild = (layout[directLinkComponentId].parents || []).slice();
|
||||
directPathToChild.push(directLinkComponentId);
|
||||
}
|
||||
|
||||
// should convert filter_box to filter component?
|
||||
let filterConfig = metadata?.native_filter_configuration || [];
|
||||
if (filterboxMigrationState === FILTER_BOX_MIGRATION_STATES.REVIEWING) {
|
||||
filterConfig = getNativeFilterConfig(
|
||||
chartData,
|
||||
filterScopes,
|
||||
preselectFilters,
|
||||
);
|
||||
metadata.native_filter_configuration = filterConfig;
|
||||
metadata.show_native_filters = true;
|
||||
}
|
||||
const nativeFilters = getInitialNativeFilterState({
|
||||
filterConfig,
|
||||
});
|
||||
metadata.show_native_filters =
|
||||
dashboardData?.metadata?.show_native_filters ??
|
||||
(isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) &&
|
||||
[
|
||||
FILTER_BOX_MIGRATION_STATES.CONVERTED,
|
||||
FILTER_BOX_MIGRATION_STATES.REVIEWING,
|
||||
FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||
].includes(filterboxMigrationState));
|
||||
|
||||
if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) {
|
||||
// If user just added cross filter to dashboard it's not saving it scope on server,
|
||||
// so we tweak it until user will update scope and will save it in server
|
||||
Object.values(dashboardLayout.present).forEach(layoutItem => {
|
||||
const chartId = layoutItem.meta?.chartId;
|
||||
const behaviors =
|
||||
(
|
||||
getChartMetadataRegistry().get(
|
||||
chartQueries[chartId]?.formData?.viz_type,
|
||||
) ?? {}
|
||||
)?.behaviors ?? [];
|
||||
|
||||
if (!metadata.chart_configuration) {
|
||||
metadata.chart_configuration = {};
|
||||
}
|
||||
if (
|
||||
behaviors.includes(Behavior.INTERACTIVE_CHART) &&
|
||||
!metadata.chart_configuration[chartId]
|
||||
) {
|
||||
metadata.chart_configuration[chartId] = {
|
||||
id: chartId,
|
||||
crossFilters: {
|
||||
scope: {
|
||||
rootPath: [DASHBOARD_ROOT_ID],
|
||||
excluded: [chartId], // By default it doesn't affects itself
|
||||
return {
|
||||
...map,
|
||||
[column]: {
|
||||
scope,
|
||||
immune,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
}, {});
|
||||
|
||||
const componentId = chartIdToLayoutId[key];
|
||||
const directPathToFilter = (layout[componentId].parents || []).slice();
|
||||
directPathToFilter.push(componentId);
|
||||
if (
|
||||
[
|
||||
FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||
FILTER_BOX_MIGRATION_STATES.SNOOZED,
|
||||
].includes(filterboxMigrationState)
|
||||
) {
|
||||
dashboardFilters[key] = {
|
||||
...dashboardFilter,
|
||||
chartId: key,
|
||||
componentId,
|
||||
datasourceId: slice.form_data.datasource,
|
||||
filterName: slice.slice_name,
|
||||
directPathToFilter,
|
||||
columns,
|
||||
labels,
|
||||
scopes: scopesByChartId,
|
||||
isDateFilter: Object.keys(columns).includes(TIME_RANGE),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// sync layout names with current slice names in case a slice was edited
|
||||
// in explore since the layout was updated. name updates go through layout for undo/redo
|
||||
// functionality and python updates slice names based on layout upon dashboard save
|
||||
const layoutId = chartIdToLayoutId[key];
|
||||
if (layoutId && layout[layoutId]) {
|
||||
layout[layoutId].meta.sliceName = slice.slice_name;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const { roles } = user;
|
||||
const canEdit = canUserEditDashboard(dashboardData, user);
|
||||
|
||||
return dispatch({
|
||||
type: HYDRATE_DASHBOARD,
|
||||
data: {
|
||||
sliceEntities: { ...initSliceEntities, slices, isLoading: false },
|
||||
charts: chartQueries,
|
||||
// read-only data
|
||||
dashboardInfo: {
|
||||
...dashboardData,
|
||||
metadata,
|
||||
userId: user.userId ? String(user.userId) : null, // legacy, please use state.user instead
|
||||
dash_edit_perm: canEdit,
|
||||
dash_save_perm: findPermission('can_save_dash', 'Superset', roles),
|
||||
dash_share_perm: findPermission(
|
||||
'can_share_dashboard',
|
||||
'Superset',
|
||||
roles,
|
||||
),
|
||||
superset_can_explore: findPermission('can_explore', 'Superset', roles),
|
||||
superset_can_share: findPermission(
|
||||
'can_share_chart',
|
||||
'Superset',
|
||||
roles,
|
||||
),
|
||||
superset_can_csv: findPermission('can_csv', 'Superset', roles),
|
||||
slice_can_edit: findPermission('can_slice', 'Superset', roles),
|
||||
common: {
|
||||
// legacy, please use state.common instead
|
||||
flash_messages: common.flash_messages,
|
||||
conf: common.conf,
|
||||
},
|
||||
},
|
||||
buildActiveFilters({
|
||||
dashboardFilters,
|
||||
nativeFilters,
|
||||
dashboardState: {
|
||||
preselectNativeFilters: getUrlParam(URL_PARAMS.nativeFilters),
|
||||
sliceIds: Array.from(sliceIds),
|
||||
directPathToChild,
|
||||
directPathLastUpdated: Date.now(),
|
||||
focusedFilterField: null,
|
||||
expandedSlices: metadata?.expanded_slices || {},
|
||||
refreshFrequency: metadata?.refresh_frequency || 0,
|
||||
// dashboard viewers can set refresh frequency for the current visit,
|
||||
// only persistent refreshFrequency will be saved to backend
|
||||
shouldPersistRefreshFrequency: false,
|
||||
css: dashboardData.css || '',
|
||||
colorNamespace: metadata?.color_namespace || null,
|
||||
colorScheme: metadata?.color_scheme || null,
|
||||
editMode: canEdit && editMode,
|
||||
isPublished: dashboardData.published,
|
||||
hasUnsavedChanges: false,
|
||||
maxUndoHistoryExceeded: false,
|
||||
lastModifiedTime: dashboardData.changed_on,
|
||||
isRefreshing: false,
|
||||
activeTabs: [],
|
||||
filterboxMigrationState,
|
||||
components: layout,
|
||||
});
|
||||
|
||||
// store the header as a layout component so we can undo/redo changes
|
||||
layout[DASHBOARD_HEADER_ID] = {
|
||||
id: DASHBOARD_HEADER_ID,
|
||||
type: DASHBOARD_HEADER_TYPE,
|
||||
meta: {
|
||||
text: dashboardData.dashboard_title,
|
||||
},
|
||||
dashboardLayout,
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const dashboardLayout = {
|
||||
past: [],
|
||||
present: layout,
|
||||
future: [],
|
||||
};
|
||||
|
||||
// find direct link component and path from root
|
||||
const directLinkComponentId = getLocationHash();
|
||||
let directPathToChild = [];
|
||||
if (layout[directLinkComponentId]) {
|
||||
directPathToChild = (layout[directLinkComponentId].parents || []).slice();
|
||||
directPathToChild.push(directLinkComponentId);
|
||||
}
|
||||
|
||||
// should convert filter_box to filter component?
|
||||
let filterConfig = metadata?.native_filter_configuration || [];
|
||||
if (filterboxMigrationState === FILTER_BOX_MIGRATION_STATES.REVIEWING) {
|
||||
filterConfig = getNativeFilterConfig(
|
||||
chartData,
|
||||
filterScopes,
|
||||
preselectFilters,
|
||||
);
|
||||
metadata.native_filter_configuration = filterConfig;
|
||||
metadata.show_native_filters = true;
|
||||
}
|
||||
const nativeFilters = getInitialNativeFilterState({
|
||||
filterConfig,
|
||||
});
|
||||
metadata.show_native_filters =
|
||||
dashboardData?.metadata?.show_native_filters ??
|
||||
(isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) &&
|
||||
[
|
||||
FILTER_BOX_MIGRATION_STATES.CONVERTED,
|
||||
FILTER_BOX_MIGRATION_STATES.REVIEWING,
|
||||
FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||
].includes(filterboxMigrationState));
|
||||
|
||||
if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) {
|
||||
// If user just added cross filter to dashboard it's not saving it scope on server,
|
||||
// so we tweak it until user will update scope and will save it in server
|
||||
Object.values(dashboardLayout.present).forEach(layoutItem => {
|
||||
const chartId = layoutItem.meta?.chartId;
|
||||
const behaviors =
|
||||
(
|
||||
getChartMetadataRegistry().get(
|
||||
chartQueries[chartId]?.formData?.viz_type,
|
||||
) ?? {}
|
||||
)?.behaviors ?? [];
|
||||
|
||||
if (!metadata.chart_configuration) {
|
||||
metadata.chart_configuration = {};
|
||||
}
|
||||
if (
|
||||
behaviors.includes(Behavior.INTERACTIVE_CHART) &&
|
||||
!metadata.chart_configuration[chartId]
|
||||
) {
|
||||
metadata.chart_configuration[chartId] = {
|
||||
id: chartId,
|
||||
crossFilters: {
|
||||
scope: {
|
||||
rootPath: [DASHBOARD_ROOT_ID],
|
||||
excluded: [chartId], // By default it doesn't affects itself
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const { roles } = user;
|
||||
const canEdit = canUserEditDashboard(dashboardData, user);
|
||||
|
||||
return dispatch({
|
||||
type: HYDRATE_DASHBOARD,
|
||||
data: {
|
||||
sliceEntities: { ...initSliceEntities, slices, isLoading: false },
|
||||
charts: chartQueries,
|
||||
// read-only data
|
||||
dashboardInfo: {
|
||||
...dashboardData,
|
||||
metadata,
|
||||
userId: user.userId ? String(user.userId) : null, // legacy, please use state.user instead
|
||||
dash_edit_perm: canEdit,
|
||||
dash_save_perm: findPermission('can_save_dash', 'Superset', roles),
|
||||
dash_share_perm: findPermission(
|
||||
'can_share_dashboard',
|
||||
'Superset',
|
||||
roles,
|
||||
),
|
||||
superset_can_explore: findPermission(
|
||||
'can_explore',
|
||||
'Superset',
|
||||
roles,
|
||||
),
|
||||
superset_can_share: findPermission(
|
||||
'can_share_chart',
|
||||
'Superset',
|
||||
roles,
|
||||
),
|
||||
superset_can_csv: findPermission('can_csv', 'Superset', roles),
|
||||
slice_can_edit: findPermission('can_slice', 'Superset', roles),
|
||||
common: {
|
||||
// legacy, please use state.common instead
|
||||
flash_messages: common.flash_messages,
|
||||
conf: common.conf,
|
||||
},
|
||||
},
|
||||
dashboardFilters,
|
||||
nativeFilters,
|
||||
dashboardState: {
|
||||
preselectNativeFilters: getUrlParam(URL_PARAMS.nativeFilters),
|
||||
sliceIds: Array.from(sliceIds),
|
||||
directPathToChild,
|
||||
directPathLastUpdated: Date.now(),
|
||||
focusedFilterField: null,
|
||||
expandedSlices: metadata?.expanded_slices || {},
|
||||
refreshFrequency: metadata?.refresh_frequency || 0,
|
||||
// dashboard viewers can set refresh frequency for the current visit,
|
||||
// only persistent refreshFrequency will be saved to backend
|
||||
shouldPersistRefreshFrequency: false,
|
||||
css: dashboardData.css || '',
|
||||
colorNamespace: metadata?.color_namespace || null,
|
||||
colorScheme: metadata?.color_scheme || null,
|
||||
editMode: canEdit && editMode,
|
||||
isPublished: dashboardData.published,
|
||||
hasUnsavedChanges: false,
|
||||
maxUndoHistoryExceeded: false,
|
||||
lastModifiedTime: dashboardData.changed_on,
|
||||
isRefreshing: false,
|
||||
activeTabs: [],
|
||||
filterboxMigrationState,
|
||||
},
|
||||
dashboardLayout,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -111,82 +111,84 @@ export interface UpdateFilterSetFail {
|
|||
type: typeof UPDATE_FILTER_SET_FAIL;
|
||||
}
|
||||
|
||||
export const setFilterConfiguration = (
|
||||
filterConfig: FilterConfiguration,
|
||||
) => async (dispatch: Dispatch, getState: () => any) => {
|
||||
dispatch({
|
||||
type: SET_FILTER_CONFIG_BEGIN,
|
||||
filterConfig,
|
||||
});
|
||||
const { id, metadata } = getState().dashboardInfo;
|
||||
const oldFilters = getState().nativeFilters?.filters;
|
||||
export const setFilterConfiguration =
|
||||
(filterConfig: FilterConfiguration) =>
|
||||
async (dispatch: Dispatch, getState: () => any) => {
|
||||
dispatch({
|
||||
type: SET_FILTER_CONFIG_BEGIN,
|
||||
filterConfig,
|
||||
});
|
||||
const { id, metadata } = getState().dashboardInfo;
|
||||
const oldFilters = getState().nativeFilters?.filters;
|
||||
|
||||
// TODO extract this out when makeApi supports url parameters
|
||||
const updateDashboard = makeApi<
|
||||
Partial<DashboardInfo>,
|
||||
{ result: DashboardInfo }
|
||||
>({
|
||||
method: 'PUT',
|
||||
endpoint: `/api/v1/dashboard/${id}`,
|
||||
});
|
||||
// TODO extract this out when makeApi supports url parameters
|
||||
const updateDashboard = makeApi<
|
||||
Partial<DashboardInfo>,
|
||||
{ result: DashboardInfo }
|
||||
>({
|
||||
method: 'PUT',
|
||||
endpoint: `/api/v1/dashboard/${id}`,
|
||||
});
|
||||
|
||||
const mergedFilterConfig = filterConfig.map(filter => {
|
||||
const oldFilter = oldFilters[filter.id];
|
||||
if (!oldFilter) {
|
||||
return filter;
|
||||
const mergedFilterConfig = filterConfig.map(filter => {
|
||||
const oldFilter = oldFilters[filter.id];
|
||||
if (!oldFilter) {
|
||||
return filter;
|
||||
}
|
||||
return { ...oldFilter, ...filter };
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await updateDashboard({
|
||||
json_metadata: JSON.stringify({
|
||||
...metadata,
|
||||
native_filter_configuration: mergedFilterConfig,
|
||||
}),
|
||||
});
|
||||
dispatch(
|
||||
dashboardInfoChanged({
|
||||
metadata: JSON.parse(response.result.json_metadata),
|
||||
}),
|
||||
);
|
||||
dispatch({
|
||||
type: SET_FILTER_CONFIG_COMPLETE,
|
||||
filterConfig: mergedFilterConfig,
|
||||
});
|
||||
dispatch(
|
||||
setDataMaskForFilterConfigComplete(mergedFilterConfig, oldFilters),
|
||||
);
|
||||
} catch (err) {
|
||||
dispatch({
|
||||
type: SET_FILTER_CONFIG_FAIL,
|
||||
filterConfig: mergedFilterConfig,
|
||||
});
|
||||
dispatch({
|
||||
type: SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL,
|
||||
filterConfig: mergedFilterConfig,
|
||||
});
|
||||
}
|
||||
return { ...oldFilter, ...filter };
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await updateDashboard({
|
||||
json_metadata: JSON.stringify({
|
||||
...metadata,
|
||||
native_filter_configuration: mergedFilterConfig,
|
||||
}),
|
||||
});
|
||||
dispatch(
|
||||
dashboardInfoChanged({
|
||||
metadata: JSON.parse(response.result.json_metadata),
|
||||
}),
|
||||
);
|
||||
export const setInScopeStatusOfFilters =
|
||||
(
|
||||
filterScopes: {
|
||||
filterId: string;
|
||||
chartsInScope: number[];
|
||||
tabsInScope: string[];
|
||||
}[],
|
||||
) =>
|
||||
async (dispatch: Dispatch, getState: () => any) => {
|
||||
const filters = getState().nativeFilters?.filters;
|
||||
const filtersWithScopes = filterScopes.map(scope => ({
|
||||
...filters[scope.filterId],
|
||||
chartsInScope: scope.chartsInScope,
|
||||
tabsInScope: scope.tabsInScope,
|
||||
}));
|
||||
dispatch({
|
||||
type: SET_FILTER_CONFIG_COMPLETE,
|
||||
filterConfig: mergedFilterConfig,
|
||||
type: SET_IN_SCOPE_STATUS_OF_FILTERS,
|
||||
filterConfig: filtersWithScopes,
|
||||
});
|
||||
dispatch(
|
||||
setDataMaskForFilterConfigComplete(mergedFilterConfig, oldFilters),
|
||||
);
|
||||
} catch (err) {
|
||||
dispatch({
|
||||
type: SET_FILTER_CONFIG_FAIL,
|
||||
filterConfig: mergedFilterConfig,
|
||||
});
|
||||
dispatch({
|
||||
type: SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL,
|
||||
filterConfig: mergedFilterConfig,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const setInScopeStatusOfFilters = (
|
||||
filterScopes: {
|
||||
filterId: string;
|
||||
chartsInScope: number[];
|
||||
tabsInScope: string[];
|
||||
}[],
|
||||
) => async (dispatch: Dispatch, getState: () => any) => {
|
||||
const filters = getState().nativeFilters?.filters;
|
||||
const filtersWithScopes = filterScopes.map(scope => ({
|
||||
...filters[scope.filterId],
|
||||
chartsInScope: scope.chartsInScope,
|
||||
tabsInScope: scope.tabsInScope,
|
||||
}));
|
||||
dispatch({
|
||||
type: SET_IN_SCOPE_STATUS_OF_FILTERS,
|
||||
filterConfig: filtersWithScopes,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
type BootstrapData = {
|
||||
nativeFilters: {
|
||||
|
|
@ -201,138 +203,134 @@ export interface SetBootstrapData {
|
|||
data: BootstrapData;
|
||||
}
|
||||
|
||||
export const getFilterSets = () => async (
|
||||
dispatch: Dispatch,
|
||||
getState: () => RootState,
|
||||
) => {
|
||||
const dashboardId = getState().dashboardInfo.id;
|
||||
const fetchFilterSets = makeApi<
|
||||
null,
|
||||
{
|
||||
count: number;
|
||||
ids: number[];
|
||||
result: FilterSetFullData[];
|
||||
}
|
||||
>({
|
||||
method: 'GET',
|
||||
endpoint: `/api/v1/dashboard/${dashboardId}/filtersets`,
|
||||
});
|
||||
export const getFilterSets =
|
||||
() => async (dispatch: Dispatch, getState: () => RootState) => {
|
||||
const dashboardId = getState().dashboardInfo.id;
|
||||
const fetchFilterSets = makeApi<
|
||||
null,
|
||||
{
|
||||
count: number;
|
||||
ids: number[];
|
||||
result: FilterSetFullData[];
|
||||
}
|
||||
>({
|
||||
method: 'GET',
|
||||
endpoint: `/api/v1/dashboard/${dashboardId}/filtersets`,
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: SET_FILTER_SETS_BEGIN,
|
||||
});
|
||||
dispatch({
|
||||
type: SET_FILTER_SETS_BEGIN,
|
||||
});
|
||||
|
||||
const response = await fetchFilterSets(null);
|
||||
const response = await fetchFilterSets(null);
|
||||
|
||||
dispatch({
|
||||
type: SET_FILTER_SETS_COMPLETE,
|
||||
filterSets: response.ids.map((id, i) => ({
|
||||
...response.result[i].params,
|
||||
id,
|
||||
name: response.result[i].name,
|
||||
})),
|
||||
});
|
||||
};
|
||||
|
||||
export const createFilterSet = (filterSet: Omit<FilterSet, 'id'>) => async (
|
||||
dispatch: Function,
|
||||
getState: () => RootState,
|
||||
) => {
|
||||
const dashboardId = getState().dashboardInfo.id;
|
||||
const postFilterSets = makeApi<
|
||||
Partial<FilterSetFullData & { json_metadata: any }>,
|
||||
{
|
||||
count: number;
|
||||
ids: number[];
|
||||
result: FilterSetFullData[];
|
||||
}
|
||||
>({
|
||||
method: 'POST',
|
||||
endpoint: `/api/v1/dashboard/${dashboardId}/filtersets`,
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: CREATE_FILTER_SET_BEGIN,
|
||||
});
|
||||
|
||||
const serverFilterSet: Omit<FilterSet, 'id' | 'name'> & { name?: string } = {
|
||||
...filterSet,
|
||||
dispatch({
|
||||
type: SET_FILTER_SETS_COMPLETE,
|
||||
filterSets: response.ids.map((id, i) => ({
|
||||
...response.result[i].params,
|
||||
id,
|
||||
name: response.result[i].name,
|
||||
})),
|
||||
});
|
||||
};
|
||||
|
||||
delete serverFilterSet.name;
|
||||
export const createFilterSet =
|
||||
(filterSet: Omit<FilterSet, 'id'>) =>
|
||||
async (dispatch: Function, getState: () => RootState) => {
|
||||
const dashboardId = getState().dashboardInfo.id;
|
||||
const postFilterSets = makeApi<
|
||||
Partial<FilterSetFullData & { json_metadata: any }>,
|
||||
{
|
||||
count: number;
|
||||
ids: number[];
|
||||
result: FilterSetFullData[];
|
||||
}
|
||||
>({
|
||||
method: 'POST',
|
||||
endpoint: `/api/v1/dashboard/${dashboardId}/filtersets`,
|
||||
});
|
||||
|
||||
await postFilterSets({
|
||||
name: filterSet.name,
|
||||
owner_type: 'Dashboard',
|
||||
owner_id: dashboardId,
|
||||
json_metadata: JSON.stringify(serverFilterSet),
|
||||
});
|
||||
dispatch({
|
||||
type: CREATE_FILTER_SET_BEGIN,
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: CREATE_FILTER_SET_COMPLETE,
|
||||
});
|
||||
dispatch(getFilterSets());
|
||||
};
|
||||
const serverFilterSet: Omit<FilterSet, 'id' | 'name'> & { name?: string } =
|
||||
{
|
||||
...filterSet,
|
||||
};
|
||||
|
||||
export const updateFilterSet = (filterSet: FilterSet) => async (
|
||||
dispatch: Function,
|
||||
getState: () => RootState,
|
||||
) => {
|
||||
const dashboardId = getState().dashboardInfo.id;
|
||||
const postFilterSets = makeApi<
|
||||
Partial<FilterSetFullData & { json_metadata: any }>,
|
||||
{}
|
||||
>({
|
||||
method: 'PUT',
|
||||
endpoint: `/api/v1/dashboard/${dashboardId}/filtersets/${filterSet.id}`,
|
||||
});
|
||||
delete serverFilterSet.name;
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_FILTER_SET_BEGIN,
|
||||
});
|
||||
await postFilterSets({
|
||||
name: filterSet.name,
|
||||
owner_type: 'Dashboard',
|
||||
owner_id: dashboardId,
|
||||
json_metadata: JSON.stringify(serverFilterSet),
|
||||
});
|
||||
|
||||
const serverFilterSet: Omit<FilterSet, 'id' | 'name'> & {
|
||||
name?: string;
|
||||
id?: number;
|
||||
} = {
|
||||
...filterSet,
|
||||
dispatch({
|
||||
type: CREATE_FILTER_SET_COMPLETE,
|
||||
});
|
||||
dispatch(getFilterSets());
|
||||
};
|
||||
|
||||
delete serverFilterSet.id;
|
||||
delete serverFilterSet.name;
|
||||
export const updateFilterSet =
|
||||
(filterSet: FilterSet) =>
|
||||
async (dispatch: Function, getState: () => RootState) => {
|
||||
const dashboardId = getState().dashboardInfo.id;
|
||||
const postFilterSets = makeApi<
|
||||
Partial<FilterSetFullData & { json_metadata: any }>,
|
||||
{}
|
||||
>({
|
||||
method: 'PUT',
|
||||
endpoint: `/api/v1/dashboard/${dashboardId}/filtersets/${filterSet.id}`,
|
||||
});
|
||||
|
||||
await postFilterSets({
|
||||
name: filterSet.name,
|
||||
json_metadata: JSON.stringify(serverFilterSet),
|
||||
});
|
||||
dispatch({
|
||||
type: UPDATE_FILTER_SET_BEGIN,
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_FILTER_SET_COMPLETE,
|
||||
});
|
||||
dispatch(getFilterSets());
|
||||
};
|
||||
const serverFilterSet: Omit<FilterSet, 'id' | 'name'> & {
|
||||
name?: string;
|
||||
id?: number;
|
||||
} = {
|
||||
...filterSet,
|
||||
};
|
||||
|
||||
export const deleteFilterSet = (filterSetId: number) => async (
|
||||
dispatch: Function,
|
||||
getState: () => RootState,
|
||||
) => {
|
||||
const dashboardId = getState().dashboardInfo.id;
|
||||
const deleteFilterSets = makeApi<{}, {}>({
|
||||
method: 'DELETE',
|
||||
endpoint: `/api/v1/dashboard/${dashboardId}/filtersets/${filterSetId}`,
|
||||
});
|
||||
delete serverFilterSet.id;
|
||||
delete serverFilterSet.name;
|
||||
|
||||
dispatch({
|
||||
type: DELETE_FILTER_SET_BEGIN,
|
||||
});
|
||||
await postFilterSets({
|
||||
name: filterSet.name,
|
||||
json_metadata: JSON.stringify(serverFilterSet),
|
||||
});
|
||||
|
||||
await deleteFilterSets({});
|
||||
dispatch({
|
||||
type: UPDATE_FILTER_SET_COMPLETE,
|
||||
});
|
||||
dispatch(getFilterSets());
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: DELETE_FILTER_SET_COMPLETE,
|
||||
});
|
||||
dispatch(getFilterSets());
|
||||
};
|
||||
export const deleteFilterSet =
|
||||
(filterSetId: number) =>
|
||||
async (dispatch: Function, getState: () => RootState) => {
|
||||
const dashboardId = getState().dashboardInfo.id;
|
||||
const deleteFilterSets = makeApi<{}, {}>({
|
||||
method: 'DELETE',
|
||||
endpoint: `/api/v1/dashboard/${dashboardId}/filtersets/${filterSetId}`,
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: DELETE_FILTER_SET_BEGIN,
|
||||
});
|
||||
|
||||
await deleteFilterSets({});
|
||||
|
||||
dispatch({
|
||||
type: DELETE_FILTER_SET_COMPLETE,
|
||||
});
|
||||
dispatch(getFilterSets());
|
||||
};
|
||||
|
||||
export const SET_FOCUSED_NATIVE_FILTER = 'SET_FOCUSED_NATIVE_FILTER';
|
||||
export interface SetFocusedNativeFilter {
|
||||
|
|
|
|||
|
|
@ -56,45 +56,40 @@ interface FilterBoxMigrationModalProps {
|
|||
hideFooter: boolean;
|
||||
}
|
||||
|
||||
const FilterBoxMigrationModal: FunctionComponent<FilterBoxMigrationModalProps> = ({
|
||||
onClickReview,
|
||||
onClickSnooze,
|
||||
onHide,
|
||||
show,
|
||||
hideFooter = false,
|
||||
}) => (
|
||||
<StyledFilterBoxMigrationModal
|
||||
show={show}
|
||||
onHide={onHide}
|
||||
title={t('Ready to review filters in this dashboard?')}
|
||||
hideFooter={hideFooter}
|
||||
footer={
|
||||
<>
|
||||
<Button buttonSize="small" onClick={onClickSnooze}>
|
||||
{t('Remind me in 24 hours')}
|
||||
</Button>
|
||||
<Button buttonSize="small" onClick={onHide}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
buttonSize="small"
|
||||
buttonStyle="primary"
|
||||
onClick={onClickReview}
|
||||
>
|
||||
{t('Start Review')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
responsive
|
||||
>
|
||||
<div>
|
||||
{t(
|
||||
'filter_box will be deprecated ' +
|
||||
'in a future version of Superset. ' +
|
||||
'Please replace filter_box by dashboard filter components.',
|
||||
)}
|
||||
</div>
|
||||
</StyledFilterBoxMigrationModal>
|
||||
);
|
||||
const FilterBoxMigrationModal: FunctionComponent<FilterBoxMigrationModalProps> =
|
||||
({ onClickReview, onClickSnooze, onHide, show, hideFooter = false }) => (
|
||||
<StyledFilterBoxMigrationModal
|
||||
show={show}
|
||||
onHide={onHide}
|
||||
title={t('Ready to review filters in this dashboard?')}
|
||||
hideFooter={hideFooter}
|
||||
footer={
|
||||
<>
|
||||
<Button buttonSize="small" onClick={onClickSnooze}>
|
||||
{t('Remind me in 24 hours')}
|
||||
</Button>
|
||||
<Button buttonSize="small" onClick={onHide}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
buttonSize="small"
|
||||
buttonStyle="primary"
|
||||
onClick={onClickReview}
|
||||
>
|
||||
{t('Start Review')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
responsive
|
||||
>
|
||||
<div>
|
||||
{t(
|
||||
'filter_box will be deprecated ' +
|
||||
'in a future version of Superset. ' +
|
||||
'Please replace filter_box by dashboard filter components.',
|
||||
)}
|
||||
</div>
|
||||
</StyledFilterBoxMigrationModal>
|
||||
);
|
||||
|
||||
export default FilterBoxMigrationModal;
|
||||
|
|
|
|||
|
|
@ -611,10 +611,8 @@ class Header extends React.PureComponent {
|
|||
onHide={this.hidePropertiesModal}
|
||||
colorScheme={this.props.colorScheme}
|
||||
onSubmit={updates => {
|
||||
const {
|
||||
dashboardInfoChanged,
|
||||
dashboardTitleChanged,
|
||||
} = this.props;
|
||||
const { dashboardInfoChanged, dashboardTitleChanged } =
|
||||
this.props;
|
||||
dashboardInfoChanged({
|
||||
slug: updates.slug,
|
||||
metadata: JSON.parse(updates.jsonMetadata),
|
||||
|
|
|
|||
|
|
@ -85,24 +85,26 @@ const handleErrorResponse = async response => {
|
|||
});
|
||||
};
|
||||
|
||||
const loadAccessOptions = accessType => (input = '') => {
|
||||
const query = rison.encode({ filter: input });
|
||||
return SupersetClient.get({
|
||||
endpoint: `/api/v1/dashboard/related/${accessType}?q=${query}`,
|
||||
}).then(
|
||||
response => ({
|
||||
data: response.json.result.map(item => ({
|
||||
value: item.value,
|
||||
label: item.text,
|
||||
})),
|
||||
totalCount: response.json.count,
|
||||
}),
|
||||
badResponse => {
|
||||
handleErrorResponse(badResponse);
|
||||
return [];
|
||||
},
|
||||
);
|
||||
};
|
||||
const loadAccessOptions =
|
||||
accessType =>
|
||||
(input = '') => {
|
||||
const query = rison.encode({ filter: input });
|
||||
return SupersetClient.get({
|
||||
endpoint: `/api/v1/dashboard/related/${accessType}?q=${query}`,
|
||||
}).then(
|
||||
response => ({
|
||||
data: response.json.result.map(item => ({
|
||||
value: item.value,
|
||||
label: item.text,
|
||||
})),
|
||||
totalCount: response.json.count,
|
||||
}),
|
||||
badResponse => {
|
||||
handleErrorResponse(badResponse);
|
||||
return [];
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const loadOwners = loadAccessOptions('owners');
|
||||
const loadRoles = loadAccessOptions('roles');
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ const defaultProps = {
|
|||
};
|
||||
|
||||
test('renders with unpublished status and readonly permissions', async () => {
|
||||
const tooltip = /This dashboard is not published which means it will not show up in the list of dashboards/;
|
||||
const tooltip =
|
||||
/This dashboard is not published which means it will not show up in the list of dashboards/;
|
||||
render(<PublishedStatus {...defaultProps} />);
|
||||
expect(screen.getByText('Draft')).toBeInTheDocument();
|
||||
userEvent.hover(screen.getByText('Draft'));
|
||||
|
|
@ -38,7 +39,8 @@ test('renders with unpublished status and readonly permissions', async () => {
|
|||
});
|
||||
|
||||
test('renders with unpublished status and write permissions', async () => {
|
||||
const tooltip = /This dashboard is not published, it will not show up in the list of dashboards/;
|
||||
const tooltip =
|
||||
/This dashboard is not published, it will not show up in the list of dashboards/;
|
||||
const savePublished = jest.fn();
|
||||
render(
|
||||
<PublishedStatus
|
||||
|
|
|
|||
|
|
@ -179,11 +179,8 @@ export default class FilterScopeSelector extends React.PureComponent {
|
|||
}
|
||||
|
||||
onCheckFilterScope(checked = []) {
|
||||
const {
|
||||
activeFilterField,
|
||||
filterScopeMap,
|
||||
checkedFilterFields,
|
||||
} = this.state;
|
||||
const { activeFilterField, filterScopeMap, checkedFilterFields } =
|
||||
this.state;
|
||||
|
||||
const key = getKeyForFilterScopeTree({
|
||||
activeFilterField,
|
||||
|
|
@ -213,11 +210,8 @@ export default class FilterScopeSelector extends React.PureComponent {
|
|||
}
|
||||
|
||||
onExpandFilterScope(expanded = []) {
|
||||
const {
|
||||
activeFilterField,
|
||||
checkedFilterFields,
|
||||
filterScopeMap,
|
||||
} = this.state;
|
||||
const { activeFilterField, checkedFilterFields, filterScopeMap } =
|
||||
this.state;
|
||||
const key = getKeyForFilterScopeTree({
|
||||
activeFilterField,
|
||||
checkedFilterFields,
|
||||
|
|
@ -347,11 +341,8 @@ export default class FilterScopeSelector extends React.PureComponent {
|
|||
// Reset nodes back to unfiltered state
|
||||
if (!this.state.searchText) {
|
||||
this.setState(prevState => {
|
||||
const {
|
||||
activeFilterField,
|
||||
checkedFilterFields,
|
||||
filterScopeMap,
|
||||
} = prevState;
|
||||
const { activeFilterField, checkedFilterFields, filterScopeMap } =
|
||||
prevState;
|
||||
const key = getKeyForFilterScopeTree({
|
||||
activeFilterField,
|
||||
checkedFilterFields,
|
||||
|
|
@ -370,11 +361,8 @@ export default class FilterScopeSelector extends React.PureComponent {
|
|||
});
|
||||
} else {
|
||||
const updater = prevState => {
|
||||
const {
|
||||
activeFilterField,
|
||||
checkedFilterFields,
|
||||
filterScopeMap,
|
||||
} = prevState;
|
||||
const { activeFilterField, checkedFilterFields, filterScopeMap } =
|
||||
prevState;
|
||||
const key = getKeyForFilterScopeTree({
|
||||
activeFilterField,
|
||||
checkedFilterFields,
|
||||
|
|
|
|||
|
|
@ -167,10 +167,8 @@ class ChartHolder extends React.Component {
|
|||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
const { component, directPathToChild, directPathLastUpdated } = props;
|
||||
const {
|
||||
label: columnName,
|
||||
chart: chartComponentId,
|
||||
} = getChartAndLabelComponentIdFromPath(directPathToChild);
|
||||
const { label: columnName, chart: chartComponentId } =
|
||||
getChartAndLabelComponentIdFromPath(directPathToChild);
|
||||
|
||||
if (
|
||||
directPathLastUpdated !== state.directPathLastUpdated &&
|
||||
|
|
|
|||
|
|
@ -83,8 +83,9 @@ describe('ChartHolder', () => {
|
|||
it('should render full size', async () => {
|
||||
renderWrapper();
|
||||
|
||||
const chart = (screen.getByTestId('slice-container')
|
||||
.firstChild as HTMLElement).style;
|
||||
const chart = (
|
||||
screen.getByTestId('slice-container').firstChild as HTMLElement
|
||||
).style;
|
||||
|
||||
await waitFor(() => expect(chart?.width).toBe('992px'));
|
||||
expect(chart?.height).toBe('714px');
|
||||
|
|
|
|||
|
|
@ -110,13 +110,8 @@ class Markdown extends React.PureComponent {
|
|||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, state) {
|
||||
const {
|
||||
hasError,
|
||||
editorMode,
|
||||
markdownSource,
|
||||
undoLength,
|
||||
redoLength,
|
||||
} = state;
|
||||
const { hasError, editorMode, markdownSource, undoLength, redoLength } =
|
||||
state;
|
||||
const {
|
||||
component: nextComponent,
|
||||
undoLength: nextUndoLength,
|
||||
|
|
|
|||
|
|
@ -135,10 +135,10 @@ const CascadePopover: React.FC<CascadePopoverProps> = ({
|
|||
};
|
||||
|
||||
const allFilters = getAllFilters(filter);
|
||||
const activeFilters = useMemo(() => getActiveChildren(filter) || [filter], [
|
||||
filter,
|
||||
getActiveChildren,
|
||||
]);
|
||||
const activeFilters = useMemo(
|
||||
() => getActiveChildren(filter) || [filter],
|
||||
[filter, getActiveChildren],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const focusedFilterId = currentPathToChild?.[0];
|
||||
|
|
|
|||
|
|
@ -73,9 +73,8 @@ const FilterControls: FC<FilterControlsProps> = ({
|
|||
}, [filterValues, dataMaskSelected]);
|
||||
const cascadeFilterIds = new Set(cascadeFilters.map(item => item.id));
|
||||
|
||||
const [filtersInScope, filtersOutOfScope] = useSelectFiltersInScope(
|
||||
cascadeFilters,
|
||||
);
|
||||
const [filtersInScope, filtersOutOfScope] =
|
||||
useSelectFiltersInScope(cascadeFilters);
|
||||
const dashboardHasTabs = useDashboardHasTabs();
|
||||
const showCollapsePanel = dashboardHasTabs && cascadeFilters.length > 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -213,9 +213,10 @@ const FilterValue: React.FC<FilterProps> = ({
|
|||
() => dispatchFocusAction(dispatch, id),
|
||||
[dispatch, id],
|
||||
);
|
||||
const unsetFocusedFilter = useCallback(() => dispatchFocusAction(dispatch), [
|
||||
dispatch,
|
||||
]);
|
||||
const unsetFocusedFilter = useCallback(
|
||||
() => dispatchFocusAction(dispatch),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const hooks = useMemo(
|
||||
() => ({ setDataMask, setFocusedFilter, unsetFocusedFilter }),
|
||||
|
|
|
|||
|
|
@ -151,9 +151,8 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
|||
const history = useHistory();
|
||||
const dataMaskApplied: DataMaskStateWithId = useNativeFiltersDataMask();
|
||||
const [editFilterSetId, setEditFilterSetId] = useState<number | null>(null);
|
||||
const [dataMaskSelected, setDataMaskSelected] = useImmer<DataMaskStateWithId>(
|
||||
dataMaskApplied,
|
||||
);
|
||||
const [dataMaskSelected, setDataMaskSelected] =
|
||||
useImmer<DataMaskStateWithId>(dataMaskApplied);
|
||||
const dispatch = useDispatch();
|
||||
const filterSets = useFilterSets();
|
||||
const filterSetFilterValues = Object.values(filterSets);
|
||||
|
|
@ -267,9 +266,10 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
|||
});
|
||||
}, [dataMaskSelected, dispatch]);
|
||||
|
||||
const openFiltersBar = useCallback(() => toggleFiltersBar(true), [
|
||||
toggleFiltersBar,
|
||||
]);
|
||||
const openFiltersBar = useCallback(
|
||||
() => toggleFiltersBar(true),
|
||||
[toggleFiltersBar],
|
||||
);
|
||||
|
||||
useFilterUpdates(dataMaskSelected, setDataMaskSelected);
|
||||
const isApplyDisabled = checkIsApplyDisabled(
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ export enum TabIds {
|
|||
FilterSets = 'filterSets',
|
||||
}
|
||||
|
||||
export function mapParentFiltersToChildren(
|
||||
filters: Filter[],
|
||||
): { [id: string]: Filter[] } {
|
||||
export function mapParentFiltersToChildren(filters: Filter[]): {
|
||||
[id: string]: Filter[];
|
||||
} {
|
||||
const cascadeChildren = {};
|
||||
filters.forEach(filter => {
|
||||
const [parentId] = filter.cascadeParentIds || [];
|
||||
|
|
|
|||
|
|
@ -104,9 +104,8 @@ const FilterTitlePane: React.FC<Props> = ({
|
|||
setTimeout(() => {
|
||||
const element = document.getElementById('native-filters-tabs');
|
||||
if (element) {
|
||||
const navList = element.getElementsByClassName(
|
||||
'ant-tabs-nav-list',
|
||||
)[0];
|
||||
const navList =
|
||||
element.getElementsByClassName('ant-tabs-nav-list')[0];
|
||||
navList.scrollTop = navList.scrollHeight;
|
||||
}
|
||||
}, 0);
|
||||
|
|
|
|||
|
|
@ -77,8 +77,8 @@ export function ColumnSelect({
|
|||
[columns, filterValues],
|
||||
);
|
||||
|
||||
const currentFilterType = form.getFieldValue('filters')?.[filterId]
|
||||
.filterType;
|
||||
const currentFilterType =
|
||||
form.getFieldValue('filters')?.[filterId].filterType;
|
||||
const currentColumn = useMemo(
|
||||
() => columns?.find(column => column.column_name === value),
|
||||
[columns, value],
|
||||
|
|
|
|||
|
|
@ -390,9 +390,9 @@ const FiltersConfigForm = (
|
|||
return currentDataset ? hasTemporalColumns(currentDataset) : true;
|
||||
}, [formFilter?.dataset?.value, loadedDatasets]);
|
||||
|
||||
// @ts-ignore
|
||||
const hasDataset = !!nativeFilterItems[formFilter?.filterType]?.value
|
||||
?.datasourceCount;
|
||||
const hasDataset =
|
||||
// @ts-ignore
|
||||
!!nativeFilterItems[formFilter?.filterType]?.value?.datasourceCount;
|
||||
|
||||
const datasetId =
|
||||
formFilter?.dataset?.value ??
|
||||
|
|
@ -514,12 +514,8 @@ const FiltersConfigForm = (
|
|||
...formFilter,
|
||||
});
|
||||
|
||||
const [
|
||||
hasDefaultValue,
|
||||
isRequired,
|
||||
defaultValueTooltip,
|
||||
setHasDefaultValue,
|
||||
] = useDefaultValue(formFilter, filterToEdit);
|
||||
const [hasDefaultValue, isRequired, defaultValueTooltip, setHasDefaultValue] =
|
||||
useDefaultValue(formFilter, filterToEdit);
|
||||
|
||||
const showDataset =
|
||||
!datasetId || datasetDetails || formFilter?.dataset?.label;
|
||||
|
|
|
|||
|
|
@ -118,53 +118,55 @@ export const validateForm = async (
|
|||
}
|
||||
};
|
||||
|
||||
export const createHandleSave = (
|
||||
filterConfigMap: Record<string, Filter>,
|
||||
filterIds: string[],
|
||||
removedFilters: Record<string, FilterRemoval>,
|
||||
saveForm: Function,
|
||||
values: NativeFiltersForm,
|
||||
) => async () => {
|
||||
const newFilterConfig: FilterConfiguration = filterIds
|
||||
.filter(id => !removedFilters[id])
|
||||
.map(id => {
|
||||
// create a filter config object from the form inputs
|
||||
const formInputs = values.filters?.[id];
|
||||
// if user didn't open a filter, return the original config
|
||||
if (!formInputs) return filterConfigMap[id];
|
||||
const target: Partial<Target> = {};
|
||||
if (formInputs.dataset) {
|
||||
target.datasetId = formInputs.dataset.value;
|
||||
}
|
||||
if (formInputs.dataset && formInputs.column) {
|
||||
target.column = { name: formInputs.column };
|
||||
}
|
||||
return {
|
||||
id,
|
||||
adhoc_filters: formInputs.adhoc_filters,
|
||||
time_range: formInputs.time_range,
|
||||
controlValues: formInputs.controlValues ?? {},
|
||||
granularity_sqla: formInputs.granularity_sqla,
|
||||
requiredFirst: Object.values(formInputs.requiredFirst ?? {}).find(
|
||||
rf => rf,
|
||||
),
|
||||
name: formInputs.name,
|
||||
filterType: formInputs.filterType,
|
||||
// for now there will only ever be one target
|
||||
targets: [target],
|
||||
defaultDataMask: formInputs.defaultDataMask ?? getInitialDataMask(),
|
||||
cascadeParentIds: formInputs.parentFilter
|
||||
? [formInputs.parentFilter.value]
|
||||
: [],
|
||||
scope: formInputs.scope,
|
||||
sortMetric: formInputs.sortMetric,
|
||||
type: formInputs.type,
|
||||
description: (formInputs.description || '').trim(),
|
||||
};
|
||||
});
|
||||
export const createHandleSave =
|
||||
(
|
||||
filterConfigMap: Record<string, Filter>,
|
||||
filterIds: string[],
|
||||
removedFilters: Record<string, FilterRemoval>,
|
||||
saveForm: Function,
|
||||
values: NativeFiltersForm,
|
||||
) =>
|
||||
async () => {
|
||||
const newFilterConfig: FilterConfiguration = filterIds
|
||||
.filter(id => !removedFilters[id])
|
||||
.map(id => {
|
||||
// create a filter config object from the form inputs
|
||||
const formInputs = values.filters?.[id];
|
||||
// if user didn't open a filter, return the original config
|
||||
if (!formInputs) return filterConfigMap[id];
|
||||
const target: Partial<Target> = {};
|
||||
if (formInputs.dataset) {
|
||||
target.datasetId = formInputs.dataset.value;
|
||||
}
|
||||
if (formInputs.dataset && formInputs.column) {
|
||||
target.column = { name: formInputs.column };
|
||||
}
|
||||
return {
|
||||
id,
|
||||
adhoc_filters: formInputs.adhoc_filters,
|
||||
time_range: formInputs.time_range,
|
||||
controlValues: formInputs.controlValues ?? {},
|
||||
granularity_sqla: formInputs.granularity_sqla,
|
||||
requiredFirst: Object.values(formInputs.requiredFirst ?? {}).find(
|
||||
rf => rf,
|
||||
),
|
||||
name: formInputs.name,
|
||||
filterType: formInputs.filterType,
|
||||
// for now there will only ever be one target
|
||||
targets: [target],
|
||||
defaultDataMask: formInputs.defaultDataMask ?? getInitialDataMask(),
|
||||
cascadeParentIds: formInputs.parentFilter
|
||||
? [formInputs.parentFilter.value]
|
||||
: [],
|
||||
scope: formInputs.scope,
|
||||
sortMetric: formInputs.sortMetric,
|
||||
type: formInputs.type,
|
||||
description: (formInputs.description || '').trim(),
|
||||
};
|
||||
});
|
||||
|
||||
await saveForm(newFilterConfig);
|
||||
};
|
||||
await saveForm(newFilterConfig);
|
||||
};
|
||||
export function buildFilterGroup(nodes: FilterHierarchyNode[]) {
|
||||
const buildGroup = (
|
||||
elementId: string,
|
||||
|
|
@ -208,84 +210,88 @@ export function buildFilterGroup(nodes: FilterHierarchyNode[]) {
|
|||
}
|
||||
return group;
|
||||
}
|
||||
export const createHandleTabEdit = (
|
||||
setRemovedFilters: (
|
||||
value:
|
||||
| ((
|
||||
prevState: Record<string, FilterRemoval>,
|
||||
) => Record<string, FilterRemoval>)
|
||||
| Record<string, FilterRemoval>,
|
||||
) => void,
|
||||
setSaveAlertVisible: Function,
|
||||
setOrderedFilters: (
|
||||
val: string[][] | ((prevState: string[][]) => string[][]),
|
||||
) => void,
|
||||
setFilterHierarchy: (
|
||||
state: FilterHierarchy | ((prevState: FilterHierarchy) => FilterHierarchy),
|
||||
) => void,
|
||||
addFilter: Function,
|
||||
filterHierarchy: FilterHierarchy,
|
||||
) => (filterId: string, action: 'add' | 'remove') => {
|
||||
const completeFilterRemoval = (filterId: string) => {
|
||||
const buildNewFilterHierarchy = (hierarchy: FilterHierarchy) =>
|
||||
hierarchy
|
||||
.filter(nativeFilter => nativeFilter.id !== filterId)
|
||||
.map(nativeFilter => {
|
||||
const didRemoveParent = nativeFilter.parentId === filterId;
|
||||
return didRemoveParent
|
||||
? { ...nativeFilter, parentId: null }
|
||||
: nativeFilter;
|
||||
});
|
||||
// the filter state will actually stick around in the form,
|
||||
// and the filterConfig/newFilterIds, but we use removedFilters
|
||||
// to mark it as removed.
|
||||
setRemovedFilters(removedFilters => ({
|
||||
...removedFilters,
|
||||
[filterId]: { isPending: false },
|
||||
}));
|
||||
// Remove the filter from the side tab and de-associate children
|
||||
// in case we removed a parent.
|
||||
setFilterHierarchy(prevFilterHierarchy =>
|
||||
buildNewFilterHierarchy(prevFilterHierarchy),
|
||||
);
|
||||
setOrderedFilters((orderedFilters: string[][]) => {
|
||||
const newOrder = [];
|
||||
for (let index = 0; index < orderedFilters.length; index += 1) {
|
||||
const doesGroupContainDeletedFilter =
|
||||
orderedFilters[index].findIndex(id => id === filterId) >= 0;
|
||||
// Rebuild just the group that contains deleted filter ID.
|
||||
if (doesGroupContainDeletedFilter) {
|
||||
const newGroups = buildFilterGroup(
|
||||
buildNewFilterHierarchy(
|
||||
filterHierarchy.filter(filter =>
|
||||
orderedFilters[index].includes(filter.id),
|
||||
export const createHandleTabEdit =
|
||||
(
|
||||
setRemovedFilters: (
|
||||
value:
|
||||
| ((
|
||||
prevState: Record<string, FilterRemoval>,
|
||||
) => Record<string, FilterRemoval>)
|
||||
| Record<string, FilterRemoval>,
|
||||
) => void,
|
||||
setSaveAlertVisible: Function,
|
||||
setOrderedFilters: (
|
||||
val: string[][] | ((prevState: string[][]) => string[][]),
|
||||
) => void,
|
||||
setFilterHierarchy: (
|
||||
state:
|
||||
| FilterHierarchy
|
||||
| ((prevState: FilterHierarchy) => FilterHierarchy),
|
||||
) => void,
|
||||
addFilter: Function,
|
||||
filterHierarchy: FilterHierarchy,
|
||||
) =>
|
||||
(filterId: string, action: 'add' | 'remove') => {
|
||||
const completeFilterRemoval = (filterId: string) => {
|
||||
const buildNewFilterHierarchy = (hierarchy: FilterHierarchy) =>
|
||||
hierarchy
|
||||
.filter(nativeFilter => nativeFilter.id !== filterId)
|
||||
.map(nativeFilter => {
|
||||
const didRemoveParent = nativeFilter.parentId === filterId;
|
||||
return didRemoveParent
|
||||
? { ...nativeFilter, parentId: null }
|
||||
: nativeFilter;
|
||||
});
|
||||
// the filter state will actually stick around in the form,
|
||||
// and the filterConfig/newFilterIds, but we use removedFilters
|
||||
// to mark it as removed.
|
||||
setRemovedFilters(removedFilters => ({
|
||||
...removedFilters,
|
||||
[filterId]: { isPending: false },
|
||||
}));
|
||||
// Remove the filter from the side tab and de-associate children
|
||||
// in case we removed a parent.
|
||||
setFilterHierarchy(prevFilterHierarchy =>
|
||||
buildNewFilterHierarchy(prevFilterHierarchy),
|
||||
);
|
||||
setOrderedFilters((orderedFilters: string[][]) => {
|
||||
const newOrder = [];
|
||||
for (let index = 0; index < orderedFilters.length; index += 1) {
|
||||
const doesGroupContainDeletedFilter =
|
||||
orderedFilters[index].findIndex(id => id === filterId) >= 0;
|
||||
// Rebuild just the group that contains deleted filter ID.
|
||||
if (doesGroupContainDeletedFilter) {
|
||||
const newGroups = buildFilterGroup(
|
||||
buildNewFilterHierarchy(
|
||||
filterHierarchy.filter(filter =>
|
||||
orderedFilters[index].includes(filter.id),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
newGroups.forEach(group => newOrder.push(group));
|
||||
} else {
|
||||
newOrder.push(orderedFilters[index]);
|
||||
);
|
||||
newGroups.forEach(group => newOrder.push(group));
|
||||
} else {
|
||||
newOrder.push(orderedFilters[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newOrder;
|
||||
});
|
||||
};
|
||||
return newOrder;
|
||||
});
|
||||
};
|
||||
|
||||
if (action === 'remove') {
|
||||
// first set up the timer to completely remove it
|
||||
const timerId = window.setTimeout(() => {
|
||||
completeFilterRemoval(filterId);
|
||||
}, REMOVAL_DELAY_SECS * 1000);
|
||||
// mark the filter state as "removal in progress"
|
||||
setRemovedFilters(removedFilters => ({
|
||||
...removedFilters,
|
||||
[filterId]: { isPending: true, timerId },
|
||||
}));
|
||||
setSaveAlertVisible(false);
|
||||
} else if (action === 'add') {
|
||||
addFilter();
|
||||
}
|
||||
};
|
||||
if (action === 'remove') {
|
||||
// first set up the timer to completely remove it
|
||||
const timerId = window.setTimeout(() => {
|
||||
completeFilterRemoval(filterId);
|
||||
}, REMOVAL_DELAY_SECS * 1000);
|
||||
// mark the filter state as "removal in progress"
|
||||
setRemovedFilters(removedFilters => ({
|
||||
...removedFilters,
|
||||
[filterId]: { isPending: true, timerId },
|
||||
}));
|
||||
setSaveAlertVisible(false);
|
||||
} else if (action === 'add') {
|
||||
addFilter();
|
||||
}
|
||||
};
|
||||
|
||||
export const NATIVE_FILTER_PREFIX = 'NATIVE_FILTER-';
|
||||
export const generateFilterId = () =>
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ function mapStateToProps({
|
|||
).text,
|
||||
expandedSlices: dashboardState.expandedSlices,
|
||||
refreshFrequency: dashboardState.refreshFrequency,
|
||||
shouldPersistRefreshFrequency: !!dashboardState.shouldPersistRefreshFrequency,
|
||||
shouldPersistRefreshFrequency:
|
||||
!!dashboardState.shouldPersistRefreshFrequency,
|
||||
customCss: dashboardState.css,
|
||||
colorNamespace: dashboardState.colorNamespace,
|
||||
colorScheme: dashboardState.colorScheme,
|
||||
|
|
|
|||
|
|
@ -72,15 +72,12 @@ const DashboardPage: FC = () => {
|
|||
);
|
||||
const { addDangerToast } = useToasts();
|
||||
const { idOrSlug } = useParams<{ idOrSlug: string }>();
|
||||
const { result: dashboard, error: dashboardApiError } = useDashboard(
|
||||
idOrSlug,
|
||||
);
|
||||
const { result: charts, error: chartsApiError } = useDashboardCharts(
|
||||
idOrSlug,
|
||||
);
|
||||
const { result: datasets, error: datasetsApiError } = useDashboardDatasets(
|
||||
idOrSlug,
|
||||
);
|
||||
const { result: dashboard, error: dashboardApiError } =
|
||||
useDashboard(idOrSlug);
|
||||
const { result: charts, error: chartsApiError } =
|
||||
useDashboardCharts(idOrSlug);
|
||||
const { result: datasets, error: datasetsApiError } =
|
||||
useDashboardDatasets(idOrSlug);
|
||||
const isDashboardHydrated = useRef(false);
|
||||
|
||||
const error = dashboardApiError || chartsApiError;
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ export function isFilterBox(chartId) {
|
|||
export function getAppliedFilterValues(chartId) {
|
||||
// use cached data if possible
|
||||
if (!(chartId in appliedFilterValuesByChart)) {
|
||||
const applicableFilters = Object.entries(
|
||||
activeFilters,
|
||||
).filter(([, { scope: chartIds }]) => chartIds.includes(chartId));
|
||||
const applicableFilters = Object.entries(activeFilters).filter(
|
||||
([, { scope: chartIds }]) => chartIds.includes(chartId),
|
||||
);
|
||||
appliedFilterValuesByChart[chartId] = flow(
|
||||
keyBy(
|
||||
([filterKey]) => getChartIdAndColumnFromFilterKey(filterKey).column,
|
||||
|
|
|
|||
|
|
@ -103,15 +103,18 @@ enum FILTER_COMPONENT_FILTER_TYPES {
|
|||
FILTER_RANGE = 'filter_range',
|
||||
}
|
||||
|
||||
const getPreselectedValuesFromDashboard = (
|
||||
preselectedFilters: PreselectedFiltersMeatadata,
|
||||
) => (filterKey: string, column: string) => {
|
||||
if (preselectedFilters[filterKey] && preselectedFilters[filterKey][column]) {
|
||||
// overwrite default values by dashboard default_filters
|
||||
return preselectedFilters[filterKey][column];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const getPreselectedValuesFromDashboard =
|
||||
(preselectedFilters: PreselectedFiltersMeatadata) =>
|
||||
(filterKey: string, column: string) => {
|
||||
if (
|
||||
preselectedFilters[filterKey] &&
|
||||
preselectedFilters[filterKey][column]
|
||||
) {
|
||||
// overwrite default values by dashboard default_filters
|
||||
return preselectedFilters[filterKey][column];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getFilterBoxDefaultValues = (config: FilterConfig) => {
|
||||
let defaultValues = config[FILTER_CONFIG_ATTRIBUTES.DEFAULT_VALUE];
|
||||
|
|
@ -218,9 +221,8 @@ export default function getNativeFilterConfig(
|
|||
time_range,
|
||||
} = slice.form_data;
|
||||
|
||||
const getDashboardDefaultValues = getPreselectedValuesFromDashboard(
|
||||
preselectFilters,
|
||||
);
|
||||
const getDashboardDefaultValues =
|
||||
getPreselectedValuesFromDashboard(preselectFilters);
|
||||
|
||||
if (date_filter) {
|
||||
const { scope, immune }: FilterScopeType =
|
||||
|
|
@ -488,9 +490,8 @@ export default function getNativeFilterConfig(
|
|||
}
|
||||
});
|
||||
|
||||
const dependencies: FilterBoxDependencyMap = getFilterboxDependencies(
|
||||
filterScopes,
|
||||
);
|
||||
const dependencies: FilterBoxDependencyMap =
|
||||
getFilterboxDependencies(filterScopes);
|
||||
Object.entries(dependencies).forEach(([key, filterFields]) => {
|
||||
Object.entries(filterFields).forEach(([field, childrenChartIds]) => {
|
||||
const parentComponentId = filterBoxToFilterComponentMap[key][field];
|
||||
|
|
|
|||
|
|
@ -37,35 +37,29 @@ export default function getComponentWidthFromDrop({
|
|||
return component.meta.width;
|
||||
}
|
||||
|
||||
const {
|
||||
width: draggingWidth,
|
||||
minimumWidth: minDraggingWidth,
|
||||
} = getDetailedComponentWidth({
|
||||
component,
|
||||
components,
|
||||
});
|
||||
const { width: draggingWidth, minimumWidth: minDraggingWidth } =
|
||||
getDetailedComponentWidth({
|
||||
component,
|
||||
components,
|
||||
});
|
||||
|
||||
const {
|
||||
width: destinationWidth,
|
||||
occupiedWidth: draggingOccupiedWidth,
|
||||
} = getDetailedComponentWidth({
|
||||
id: destination.id,
|
||||
components,
|
||||
});
|
||||
const { width: destinationWidth, occupiedWidth: draggingOccupiedWidth } =
|
||||
getDetailedComponentWidth({
|
||||
id: destination.id,
|
||||
components,
|
||||
});
|
||||
|
||||
let destinationCapacity = Number(destinationWidth - draggingOccupiedWidth);
|
||||
|
||||
if (Number.isNaN(destinationCapacity)) {
|
||||
const {
|
||||
width: grandparentWidth,
|
||||
occupiedWidth: grandparentOccupiedWidth,
|
||||
} = getDetailedComponentWidth({
|
||||
id: findParentId({
|
||||
childId: destination.id,
|
||||
layout: components,
|
||||
}),
|
||||
components,
|
||||
});
|
||||
const { width: grandparentWidth, occupiedWidth: grandparentOccupiedWidth } =
|
||||
getDetailedComponentWidth({
|
||||
id: findParentId({
|
||||
childId: destination.id,
|
||||
layout: components,
|
||||
}),
|
||||
components,
|
||||
});
|
||||
|
||||
destinationCapacity = Number(grandparentWidth - grandparentOccupiedWidth);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,12 +57,11 @@ function getTabChildrenScope({
|
|||
))
|
||||
) {
|
||||
// get all charts from tabChildren that is not in scope
|
||||
const immuneChartIdsFromTabsNotInScope = getImmuneChartIdsFromTabsNotInScope(
|
||||
{
|
||||
const immuneChartIdsFromTabsNotInScope =
|
||||
getImmuneChartIdsFromTabsNotInScope({
|
||||
tabs: tabChildren,
|
||||
tabsInScope: flatMap(tabScopes, ({ scope }) => scope),
|
||||
},
|
||||
);
|
||||
});
|
||||
const immuneChartIdsFromTabsInScope = flatMap(
|
||||
Object.values(tabScopes),
|
||||
({ immune }) => immune,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export default function injectCustomCss(css: string) {
|
|||
document.querySelector(`.${className}`) || createStyleElement(className);
|
||||
|
||||
if ('styleSheet' in style) {
|
||||
((style as unknown) as MysteryStyleElement).styleSheet.cssText = css;
|
||||
(style as unknown as MysteryStyleElement).styleSheet.cssText = css;
|
||||
} else {
|
||||
style.innerHTML = css;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,9 +49,8 @@ export type ControlProps = {
|
|||
/**
|
||||
*
|
||||
*/
|
||||
export type ControlComponentProps<
|
||||
ValueType extends JsonValue = JsonValue
|
||||
> = Omit<ControlProps, 'value'> & BaseControlComponentProps<ValueType>;
|
||||
export type ControlComponentProps<ValueType extends JsonValue = JsonValue> =
|
||||
Omit<ControlProps, 'value'> & BaseControlComponentProps<ValueType>;
|
||||
|
||||
export default function Control(props: ControlProps) {
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -49,14 +49,8 @@ type ExploreActionButtonsProps = {
|
|||
};
|
||||
|
||||
const ActionButton = (props: ActionButtonProps) => {
|
||||
const {
|
||||
icon,
|
||||
text,
|
||||
tooltip,
|
||||
className,
|
||||
onTooltipVisibilityChange,
|
||||
...rest
|
||||
} = props;
|
||||
const { icon, text, tooltip, className, onTooltipVisibilityChange, ...rest } =
|
||||
props;
|
||||
return (
|
||||
<Tooltip
|
||||
id={`${icon}-tooltip`}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const createProps = () => ({
|
|||
},
|
||||
chartStatus: 'rendered',
|
||||
},
|
||||
slice: ({
|
||||
slice: {
|
||||
cache_timeout: null,
|
||||
changed_on: '2021-03-19T16:30:56.750230',
|
||||
changed_on_humanized: '7 days ago',
|
||||
|
|
@ -85,7 +85,7 @@ const createProps = () => ({
|
|||
slice_id: 318,
|
||||
slice_name: 'Age distribution of respondents',
|
||||
slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20318%7D',
|
||||
} as unknown) as Slice,
|
||||
} as unknown as Slice,
|
||||
slice_name: 'Age distribution of respondents',
|
||||
actions: {
|
||||
postChartFormData: () => null,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import userEvent from '@testing-library/user-event';
|
|||
import PropertiesModal from '.';
|
||||
|
||||
const createProps = () => ({
|
||||
slice: ({
|
||||
slice: {
|
||||
cache_timeout: null,
|
||||
changed_on: '2021-03-19T16:30:56.750230',
|
||||
changed_on_humanized: '7 days ago',
|
||||
|
|
@ -62,7 +62,7 @@ const createProps = () => ({
|
|||
slice_id: 318,
|
||||
slice_name: 'Age distribution of respondents',
|
||||
slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20318%7D',
|
||||
} as unknown) as Slice,
|
||||
} as unknown as Slice,
|
||||
show: true,
|
||||
onHide: jest.fn(),
|
||||
onSave: jest.fn(),
|
||||
|
|
|
|||
|
|
@ -88,20 +88,25 @@ export default function PropertiesModal({
|
|||
);
|
||||
|
||||
const loadOptions = useMemo(
|
||||
() => (input = '', page: number, pageSize: number) => {
|
||||
const query = rison.encode({ filter: input, page, page_size: pageSize });
|
||||
return SupersetClient.get({
|
||||
endpoint: `/api/v1/chart/related/owners?q=${query}`,
|
||||
}).then(response => ({
|
||||
data: response.json.result.map(
|
||||
(item: { value: number; text: string }) => ({
|
||||
value: item.value,
|
||||
label: item.text,
|
||||
}),
|
||||
),
|
||||
totalCount: response.json.count,
|
||||
}));
|
||||
},
|
||||
() =>
|
||||
(input = '', page: number, pageSize: number) => {
|
||||
const query = rison.encode({
|
||||
filter: input,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
});
|
||||
return SupersetClient.get({
|
||||
endpoint: `/api/v1/chart/related/owners?q=${query}`,
|
||||
}).then(response => ({
|
||||
data: response.json.result.map(
|
||||
(item: { value: number; text: string }) => ({
|
||||
value: item.value,
|
||||
label: item.text,
|
||||
}),
|
||||
),
|
||||
totalCount: response.json.count,
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
|
|
@ -115,10 +120,12 @@ export default function PropertiesModal({
|
|||
cache_timeout: cacheTimeout || null,
|
||||
};
|
||||
if (selectedOwners) {
|
||||
payload.owners = (selectedOwners as {
|
||||
value: number;
|
||||
label: string;
|
||||
}[]).map(o => o.value);
|
||||
payload.owners = (
|
||||
selectedOwners as {
|
||||
value: number;
|
||||
label: string;
|
||||
}[]
|
||||
).map(o => o.value);
|
||||
}
|
||||
try {
|
||||
const res = await SupersetClient.put({
|
||||
|
|
|
|||
|
|
@ -164,9 +164,8 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
this.applyAnnotation = this.applyAnnotation.bind(this);
|
||||
this.fetchOptions = this.fetchOptions.bind(this);
|
||||
this.handleAnnotationType = this.handleAnnotationType.bind(this);
|
||||
this.handleAnnotationSourceType = this.handleAnnotationSourceType.bind(
|
||||
this,
|
||||
);
|
||||
this.handleAnnotationSourceType =
|
||||
this.handleAnnotationSourceType.bind(this);
|
||||
this.handleValue = this.handleValue.bind(this);
|
||||
this.isValidForm = this.isValidForm.bind(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,10 +74,8 @@ const ConditionalFormattingControl = ({
|
|||
...props
|
||||
}: ConditionalFormattingControlProps) => {
|
||||
const theme = useTheme();
|
||||
const [
|
||||
conditionalFormattingConfigs,
|
||||
setConditionalFormattingConfigs,
|
||||
] = useState<ConditionalFormattingConfig[]>(value ?? []);
|
||||
const [conditionalFormattingConfigs, setConditionalFormattingConfigs] =
|
||||
useState<ConditionalFormattingConfig[]>(value ?? []);
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
|
|
|
|||
|
|
@ -57,22 +57,22 @@ const operatorOptions = [
|
|||
{ value: COMPARATOR.BETWEEN_OR_RIGHT_EQUAL, label: '< x ≤', order: 10 },
|
||||
];
|
||||
|
||||
const targetValueValidator = (
|
||||
compare: (targetValue: number, compareValue: number) => boolean,
|
||||
rejectMessage: string,
|
||||
) => (targetValue: number | string) => (
|
||||
_: any,
|
||||
compareValue: number | string,
|
||||
) => {
|
||||
if (
|
||||
!targetValue ||
|
||||
!compareValue ||
|
||||
compare(Number(targetValue), Number(compareValue))
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(rejectMessage));
|
||||
};
|
||||
const targetValueValidator =
|
||||
(
|
||||
compare: (targetValue: number, compareValue: number) => boolean,
|
||||
rejectMessage: string,
|
||||
) =>
|
||||
(targetValue: number | string) =>
|
||||
(_: any, compareValue: number | string) => {
|
||||
if (
|
||||
!targetValue ||
|
||||
!compareValue ||
|
||||
compare(Number(targetValue), Number(compareValue))
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(rejectMessage));
|
||||
};
|
||||
|
||||
const targetValueLeftValidator = targetValueValidator(
|
||||
(target: number, val: number) => target > val,
|
||||
|
|
|
|||
|
|
@ -116,9 +116,8 @@ class DatasourceControl extends React.PureComponent {
|
|||
showChangeDatasourceModal: false,
|
||||
};
|
||||
this.onDatasourceSave = this.onDatasourceSave.bind(this);
|
||||
this.toggleChangeDatasourceModal = this.toggleChangeDatasourceModal.bind(
|
||||
this,
|
||||
);
|
||||
this.toggleChangeDatasourceModal =
|
||||
this.toggleChangeDatasourceModal.bind(this);
|
||||
this.toggleEditDatasourceModal = this.toggleEditDatasourceModal.bind(this);
|
||||
this.toggleShowDatasource = this.toggleShowDatasource.bind(this);
|
||||
this.handleMenuItemClick = this.handleMenuItemClick.bind(this);
|
||||
|
|
|
|||
|
|
@ -93,7 +93,8 @@ export const SINCE_MODE_OPTIONS: SelectOptionType[] = [
|
|||
{ value: 'today', label: t('Midnight'), order: 3 },
|
||||
];
|
||||
|
||||
export const UNTIL_MODE_OPTIONS: SelectOptionType[] = SINCE_MODE_OPTIONS.slice();
|
||||
export const UNTIL_MODE_OPTIONS: SelectOptionType[] =
|
||||
SINCE_MODE_OPTIONS.slice();
|
||||
|
||||
export const COMMON_RANGE_SET: Set<CommonRangeType> = new Set([
|
||||
'Last day',
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ export const formatTimeRange = (
|
|||
) => {
|
||||
const splitDateRange = timeRange.split(SEPARATOR);
|
||||
if (splitDateRange.length === 1) return timeRange;
|
||||
const formattedEndpoints = (
|
||||
endpoints || ['unknown', 'unknown']
|
||||
).map((endpoint: string) => (endpoint === 'inclusive' ? '≤' : '<'));
|
||||
const formattedEndpoints = (endpoints || ['unknown', 'unknown']).map(
|
||||
(endpoint: string) => (endpoint === 'inclusive' ? '≤' : '<'),
|
||||
);
|
||||
|
||||
return `${formatDateEndpoint(splitDateRange[0], true)} ${
|
||||
formattedEndpoints[0]
|
||||
|
|
|
|||
|
|
@ -84,12 +84,12 @@ export const customTimeRangeDecode = (
|
|||
|
||||
// specific : specific
|
||||
if (ISO8601_AND_CONSTANT.test(since) && ISO8601_AND_CONSTANT.test(until)) {
|
||||
const sinceMode = (DATETIME_CONSTANT.includes(since)
|
||||
? since
|
||||
: 'specific') as DateTimeModeType;
|
||||
const untilMode = (DATETIME_CONSTANT.includes(until)
|
||||
? until
|
||||
: 'specific') as DateTimeModeType;
|
||||
const sinceMode = (
|
||||
DATETIME_CONSTANT.includes(since) ? since : 'specific'
|
||||
) as DateTimeModeType;
|
||||
const untilMode = (
|
||||
DATETIME_CONSTANT.includes(until) ? until : 'specific'
|
||||
) as DateTimeModeType;
|
||||
return {
|
||||
customRange: {
|
||||
...defaultCustomRange,
|
||||
|
|
@ -110,9 +110,9 @@ export const customTimeRangeDecode = (
|
|||
since.includes(until)
|
||||
) {
|
||||
const [dttm, grainValue, grain] = sinceCapturedGroup.slice(1);
|
||||
const untilMode = (DATETIME_CONSTANT.includes(until)
|
||||
? until
|
||||
: 'specific') as DateTimeModeType;
|
||||
const untilMode = (
|
||||
DATETIME_CONSTANT.includes(until) ? until : 'specific'
|
||||
) as DateTimeModeType;
|
||||
return {
|
||||
customRange: {
|
||||
...defaultCustomRange,
|
||||
|
|
@ -135,9 +135,9 @@ export const customTimeRangeDecode = (
|
|||
until.includes(since)
|
||||
) {
|
||||
const [dttm, grainValue, grain] = [...untilCapturedGroup.slice(1)];
|
||||
const sinceMode = (DATETIME_CONSTANT.includes(since)
|
||||
? since
|
||||
: 'specific') as DateTimeModeType;
|
||||
const sinceMode = (
|
||||
DATETIME_CONSTANT.includes(since) ? since : 'specific'
|
||||
) as DateTimeModeType;
|
||||
return {
|
||||
customRange: {
|
||||
...defaultCustomRange,
|
||||
|
|
|
|||
|
|
@ -88,11 +88,8 @@ const ColumnSelectPopover = ({
|
|||
isAdhocColumnsEnabled,
|
||||
}: ColumnSelectPopoverProps) => {
|
||||
const [initialLabel] = useState(label);
|
||||
const [
|
||||
initialAdhocColumn,
|
||||
initialCalculatedColumn,
|
||||
initialSimpleColumn,
|
||||
] = getInitialColumnValues(editedColumn);
|
||||
const [initialAdhocColumn, initialCalculatedColumn, initialSimpleColumn] =
|
||||
getInitialColumnValues(editedColumn);
|
||||
|
||||
const [adhocColumn, setAdhocColumn] = useState<AdhocColumn | undefined>(
|
||||
initialAdhocColumn,
|
||||
|
|
|
|||
|
|
@ -81,21 +81,18 @@ const ColumnSelectPopoverTrigger = ({
|
|||
setPopoverVisible(false);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
visible,
|
||||
handleTogglePopover,
|
||||
handleClosePopover,
|
||||
} = isControlledComponent
|
||||
? {
|
||||
visible: props.visible,
|
||||
handleTogglePopover: props.togglePopover!,
|
||||
handleClosePopover: props.closePopover!,
|
||||
}
|
||||
: {
|
||||
visible: popoverVisible,
|
||||
handleTogglePopover: togglePopover,
|
||||
handleClosePopover: closePopover,
|
||||
};
|
||||
const { visible, handleTogglePopover, handleClosePopover } =
|
||||
isControlledComponent
|
||||
? {
|
||||
visible: props.visible,
|
||||
handleTogglePopover: props.togglePopover!,
|
||||
handleClosePopover: props.closePopover!,
|
||||
}
|
||||
: {
|
||||
visible: popoverVisible,
|
||||
handleTogglePopover: togglePopover,
|
||||
handleClosePopover: closePopover,
|
||||
};
|
||||
|
||||
const getCurrentTab = useCallback((tab: string) => {
|
||||
setIsTitleEditDisabled(tab !== editableTitleTab);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import {
|
|||
DndFilterSelectProps,
|
||||
} from 'src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
|
||||
import { PLACEHOLDER_DATASOURCE } from 'src/dashboard/constants';
|
||||
import { DEFAULT_FORM_DATA } from '@superset-ui/plugin-chart-echarts/lib/Timeseries/types';
|
||||
import { TimeseriesDefaultFormData } from '@superset-ui/plugin-chart-echarts';
|
||||
|
||||
const defaultProps: DndFilterSelectProps = {
|
||||
type: 'DndFilterSelect',
|
||||
|
|
@ -70,7 +70,7 @@ test('renders options with saved metric', () => {
|
|||
{...defaultProps}
|
||||
formData={{
|
||||
...baseFormData,
|
||||
...DEFAULT_FORM_DATA,
|
||||
...TimeseriesDefaultFormData,
|
||||
metrics: ['saved_metric'],
|
||||
}}
|
||||
/>,
|
||||
|
|
@ -111,7 +111,7 @@ test('renders options with adhoc metric', () => {
|
|||
{...defaultProps}
|
||||
formData={{
|
||||
...baseFormData,
|
||||
...DEFAULT_FORM_DATA,
|
||||
...TimeseriesDefaultFormData,
|
||||
metrics: [adhocMetric],
|
||||
}}
|
||||
/>,
|
||||
|
|
|
|||
|
|
@ -41,13 +41,12 @@ export interface OptionItemInterface {
|
|||
/**
|
||||
* Shared control props for all DnD control.
|
||||
*/
|
||||
export type DndControlProps<
|
||||
ValueType extends JsonValue
|
||||
> = ControlComponentProps<ValueType | ValueType[] | null> & {
|
||||
multi?: boolean;
|
||||
canDelete?: boolean;
|
||||
ghostButtonText?: string;
|
||||
onChange: (value: ValueType | ValueType[] | null | undefined) => void;
|
||||
};
|
||||
export type DndControlProps<ValueType extends JsonValue> =
|
||||
ControlComponentProps<ValueType | ValueType[] | null> & {
|
||||
multi?: boolean;
|
||||
canDelete?: boolean;
|
||||
ghostButtonText?: string;
|
||||
onChange: (value: ValueType | ValueType[] | null | undefined) => void;
|
||||
};
|
||||
|
||||
export type OptionValueType = Record<string, any>;
|
||||
|
|
|
|||
|
|
@ -121,9 +121,9 @@ export default class AdhocFilter {
|
|||
|
||||
this.filterOptionName =
|
||||
adhocFilter.filterOptionName ||
|
||||
`filter_${Math.random()
|
||||
`filter_${Math.random().toString(36).substring(2, 15)}_${Math.random()
|
||||
.toString(36)
|
||||
.substring(2, 15)}_${Math.random().toString(36).substring(2, 15)}`;
|
||||
.substring(2, 15)}`;
|
||||
}
|
||||
|
||||
duplicateWith(nextFields) {
|
||||
|
|
|
|||
|
|
@ -227,10 +227,8 @@ const AdhocFilterEditPopoverSimpleTabContent: React.FC<Props> = props => {
|
|||
} = useSimpleTabFilterProps(props);
|
||||
const [suggestions, setSuggestions] = useState<Record<string, any>>([]);
|
||||
const [comparator, setComparator] = useState(props.adhocFilter.comparator);
|
||||
const [
|
||||
loadingComparatorSuggestions,
|
||||
setLoadingComparatorSuggestions,
|
||||
] = useState(false);
|
||||
const [loadingComparatorSuggestions, setLoadingComparatorSuggestions] =
|
||||
useState(false);
|
||||
|
||||
const onInputComparatorChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
|
|
|
|||
|
|
@ -55,9 +55,8 @@ export default class AdhocFilterEditPopoverSqlTabContent extends React.Component
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this.onSqlExpressionChange = this.onSqlExpressionChange.bind(this);
|
||||
this.onSqlExpressionClauseChange = this.onSqlExpressionClauseChange.bind(
|
||||
this,
|
||||
);
|
||||
this.onSqlExpressionClauseChange =
|
||||
this.onSqlExpressionClauseChange.bind(this);
|
||||
this.handleAceEditorRef = this.handleAceEditorRef.bind(this);
|
||||
|
||||
this.selectProps = {
|
||||
|
|
|
|||
|
|
@ -80,9 +80,9 @@ export default class AdhocMetric {
|
|||
|
||||
this.optionName =
|
||||
adhocMetric.optionName ||
|
||||
`metric_${Math.random()
|
||||
`metric_${Math.random().toString(36).substring(2, 15)}_${Math.random()
|
||||
.toString(36)
|
||||
.substring(2, 15)}_${Math.random().toString(36).substring(2, 15)}`;
|
||||
.substring(2, 15)}`;
|
||||
}
|
||||
|
||||
getDefaultLabel() {
|
||||
|
|
|
|||
|
|
@ -207,19 +207,20 @@ const MetricsControl = ({
|
|||
[value],
|
||||
);
|
||||
|
||||
const isAddNewMetricDisabled = useCallback(() => !multi && value.length > 0, [
|
||||
multi,
|
||||
value.length,
|
||||
]);
|
||||
const isAddNewMetricDisabled = useCallback(
|
||||
() => !multi && value.length > 0,
|
||||
[multi, value.length],
|
||||
);
|
||||
|
||||
const savedMetricOptions = useMemo(
|
||||
() => getOptionsForSavedMetrics(savedMetrics, propsValue, null),
|
||||
[propsValue, savedMetrics],
|
||||
);
|
||||
|
||||
const newAdhocMetric = useMemo(() => new AdhocMetric({ isNew: true }), [
|
||||
value,
|
||||
]);
|
||||
const newAdhocMetric = useMemo(
|
||||
() => new AdhocMetric({ isNew: true }),
|
||||
[value],
|
||||
);
|
||||
const addNewMetricPopoverTrigger = useCallback(
|
||||
trigger => {
|
||||
if (isAddNewMetricDisabled()) {
|
||||
|
|
@ -271,10 +272,10 @@ const MetricsControl = ({
|
|||
setValue(coerceAdhocMetrics(propsValue));
|
||||
}, [propsValue]);
|
||||
|
||||
const onDropLabel = useCallback(() => handleChange(value), [
|
||||
handleChange,
|
||||
value,
|
||||
]);
|
||||
const onDropLabel = useCallback(
|
||||
() => handleChange(value),
|
||||
[handleChange, value],
|
||||
);
|
||||
|
||||
const valueRenderer = useCallback(
|
||||
(option, index) => (
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const safeStringify = (value?: InputValueType | null) =>
|
|||
value == null ? '' : String(value);
|
||||
|
||||
export default class TextControl<
|
||||
T extends InputValueType = InputValueType
|
||||
T extends InputValueType = InputValueType,
|
||||
> extends React.Component<TextControlProps<T>, TextControlState> {
|
||||
initialValue?: TextControlProps['value'];
|
||||
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ import { testWithId } from 'src/utils/testUtils';
|
|||
import {
|
||||
EchartsMixedTimeseriesChartPlugin,
|
||||
EchartsTimeseriesChartPlugin,
|
||||
} from '@superset-ui/plugin-chart-echarts/lib';
|
||||
import { LineChartPlugin } from '@superset-ui/preset-chart-xy/lib';
|
||||
} from '@superset-ui/plugin-chart-echarts';
|
||||
import { LineChartPlugin } from '@superset-ui/preset-chart-xy';
|
||||
import TimeTableChartPlugin from '../../../../visualizations/TimeTable/TimeTableChartPlugin';
|
||||
import VizTypeControl, { VIZ_TYPE_CONTROL_TEST_ID } from './index';
|
||||
|
||||
|
|
|
|||
|
|
@ -102,9 +102,12 @@ export const DISABLE_INPUT_OPERATORS = [
|
|||
Operators.IS_FALSE,
|
||||
];
|
||||
|
||||
export const sqlaAutoGeneratedMetricNameRegex = /^(sum|min|max|avg|count|count_distinct)__.*$/i;
|
||||
export const sqlaAutoGeneratedMetricRegex = /^(LONG|DOUBLE|FLOAT)?(SUM|AVG|MAX|MIN|COUNT)\([A-Z0-9_."]*\)$/i;
|
||||
export const druidAutoGeneratedMetricRegex = /^(LONG|DOUBLE|FLOAT)?(SUM|MAX|MIN|COUNT)\([A-Z0-9_."]*\)$/i;
|
||||
export const sqlaAutoGeneratedMetricNameRegex =
|
||||
/^(sum|min|max|avg|count|count_distinct)__.*$/i;
|
||||
export const sqlaAutoGeneratedMetricRegex =
|
||||
/^(LONG|DOUBLE|FLOAT)?(SUM|AVG|MAX|MIN|COUNT)\([A-Z0-9_."]*\)$/i;
|
||||
export const druidAutoGeneratedMetricRegex =
|
||||
/^(LONG|DOUBLE|FLOAT)?(SUM|MAX|MIN|COUNT)\([A-Z0-9_."]*\)$/i;
|
||||
|
||||
export const TIME_FILTER_LABELS = {
|
||||
time_range: t('Time range'),
|
||||
|
|
|
|||
|
|
@ -47,10 +47,8 @@ export function findControlItem(
|
|||
|
||||
const getMemoizedControlConfig = memoizeOne(
|
||||
(controlKey, controlPanelConfig) => {
|
||||
const {
|
||||
controlOverrides = {},
|
||||
controlPanelSections = [],
|
||||
} = controlPanelConfig;
|
||||
const { controlOverrides = {}, controlPanelSections = [] } =
|
||||
controlPanelConfig;
|
||||
const control = expandControlConfig(
|
||||
findControlItem(controlPanelSections, controlKey),
|
||||
controlOverrides,
|
||||
|
|
|
|||
|
|
@ -20,14 +20,8 @@ import { ChartProps } from '@superset-ui/core';
|
|||
import { DEFAULT_FORM_DATA } from './types';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const {
|
||||
formData,
|
||||
height,
|
||||
hooks,
|
||||
queriesData,
|
||||
width,
|
||||
filterState,
|
||||
} = chartProps;
|
||||
const { formData, height, hooks, queriesData, width, filterState } =
|
||||
chartProps;
|
||||
const {
|
||||
setDataMask = () => {},
|
||||
setFocusedFilter = () => {},
|
||||
|
|
|
|||
|
|
@ -68,12 +68,8 @@ const loggerMiddleware = store => next => action => {
|
|||
return next(action);
|
||||
}
|
||||
|
||||
const {
|
||||
dashboardInfo,
|
||||
explore,
|
||||
impressionId,
|
||||
dashboardLayout,
|
||||
} = store.getState();
|
||||
const { dashboardInfo, explore, impressionId, dashboardLayout } =
|
||||
store.getState();
|
||||
let logMetadata = {
|
||||
impression_id: impressionId,
|
||||
version: 'v2',
|
||||
|
|
|
|||
|
|
@ -17,16 +17,18 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export const cacheWrapper = <T extends Array<any>, U>(
|
||||
fn: (...args: T) => U,
|
||||
cache: Map<string, any>,
|
||||
keyFn: (...args: T) => string = (...args: T) => JSON.stringify([...args]),
|
||||
) => (...args: T): U => {
|
||||
const key = keyFn(...args);
|
||||
if (cache.has(key)) {
|
||||
return cache.get(key);
|
||||
}
|
||||
const result = fn(...args);
|
||||
cache.set(key, result);
|
||||
return result;
|
||||
};
|
||||
export const cacheWrapper =
|
||||
<T extends Array<any>, U>(
|
||||
fn: (...args: T) => U,
|
||||
cache: Map<string, any>,
|
||||
keyFn: (...args: T) => string = (...args: T) => JSON.stringify([...args]),
|
||||
) =>
|
||||
(...args: T): U => {
|
||||
const key = keyFn(...args);
|
||||
if (cache.has(key)) {
|
||||
return cache.get(key);
|
||||
}
|
||||
const result = fn(...args);
|
||||
cache.set(key, result);
|
||||
return result;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,23 +21,25 @@ import { JsonObject } from '@superset-ui/core';
|
|||
type TestWithIdType<T> = T extends string ? string : { 'data-test': string };
|
||||
|
||||
// Using bem standard
|
||||
export const testWithId = <T extends string | JsonObject = JsonObject>(
|
||||
prefix?: string,
|
||||
idOnly = false,
|
||||
) => (id?: string, localIdOnly = false): TestWithIdType<T> => {
|
||||
const resultIdOnly = localIdOnly || idOnly;
|
||||
if (!id && prefix) {
|
||||
return (resultIdOnly
|
||||
? prefix
|
||||
: { 'data-test': prefix }) as TestWithIdType<T>;
|
||||
}
|
||||
if (id && !prefix) {
|
||||
return (resultIdOnly ? id : { 'data-test': id }) as TestWithIdType<T>;
|
||||
}
|
||||
if (!id && !prefix) {
|
||||
console.warn('testWithId function has missed "prefix" and "id" params');
|
||||
return (resultIdOnly ? '' : { 'data-test': '' }) as TestWithIdType<T>;
|
||||
}
|
||||
const newId = `${prefix}__${id}`;
|
||||
return (resultIdOnly ? newId : { 'data-test': newId }) as TestWithIdType<T>;
|
||||
};
|
||||
export const testWithId =
|
||||
<T extends string | JsonObject = JsonObject>(
|
||||
prefix?: string,
|
||||
idOnly = false,
|
||||
) =>
|
||||
(id?: string, localIdOnly = false): TestWithIdType<T> => {
|
||||
const resultIdOnly = localIdOnly || idOnly;
|
||||
if (!id && prefix) {
|
||||
return (
|
||||
resultIdOnly ? prefix : { 'data-test': prefix }
|
||||
) as TestWithIdType<T>;
|
||||
}
|
||||
if (id && !prefix) {
|
||||
return (resultIdOnly ? id : { 'data-test': id }) as TestWithIdType<T>;
|
||||
}
|
||||
if (!id && !prefix) {
|
||||
console.warn('testWithId function has missed "prefix" and "id" params');
|
||||
return (resultIdOnly ? '' : { 'data-test': '' }) as TestWithIdType<T>;
|
||||
}
|
||||
const newId = `${prefix}__${id}`;
|
||||
return (resultIdOnly ? newId : { 'data-test': newId }) as TestWithIdType<T>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -123,10 +123,8 @@ function AlertList({
|
|||
const [currentAlert, setCurrentAlert] = useState<Partial<AlertObject> | null>(
|
||||
null,
|
||||
);
|
||||
const [
|
||||
currentAlertDeleting,
|
||||
setCurrentAlertDeleting,
|
||||
] = useState<AlertObject | null>(null);
|
||||
const [currentAlertDeleting, setCurrentAlertDeleting] =
|
||||
useState<AlertObject | null>(null);
|
||||
|
||||
// Actions
|
||||
function handleAlertEdit(alert: AlertObject | null) {
|
||||
|
|
|
|||
|
|
@ -408,10 +408,8 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
conf?.ALERT_REPORTS_NOTIFICATION_METHODS || DEFAULT_NOTIFICATION_METHODS;
|
||||
|
||||
const [disableSave, setDisableSave] = useState<boolean>(true);
|
||||
const [
|
||||
currentAlert,
|
||||
setCurrentAlert,
|
||||
] = useState<Partial<AlertObject> | null>();
|
||||
const [currentAlert, setCurrentAlert] =
|
||||
useState<Partial<AlertObject> | null>();
|
||||
const [isHidden, setIsHidden] = useState<boolean>(true);
|
||||
const [contentType, setContentType] = useState<string>('dashboard');
|
||||
const [reportFormat, setReportFormat] = useState<string>(
|
||||
|
|
@ -432,10 +430,8 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
contentType === 'chart' &&
|
||||
(isFeatureEnabled(FeatureFlag.ALERTS_ATTACH_REPORTS) || isReport);
|
||||
|
||||
const [
|
||||
notificationAddState,
|
||||
setNotificationAddState,
|
||||
] = useState<NotificationAddStatus>('active');
|
||||
const [notificationAddState, setNotificationAddState] =
|
||||
useState<NotificationAddStatus>('active');
|
||||
const [notificationSettings, setNotificationSettings] = useState<
|
||||
NotificationSetting[]
|
||||
>([]);
|
||||
|
|
@ -581,20 +577,25 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
|
||||
// Fetch data to populate form dropdowns
|
||||
const loadOwnerOptions = useMemo(
|
||||
() => (input = '', page: number, pageSize: number) => {
|
||||
const query = rison.encode({ filter: input, page, page_size: pageSize });
|
||||
return SupersetClient.get({
|
||||
endpoint: `/api/v1/report/related/owners?q=${query}`,
|
||||
}).then(response => ({
|
||||
data: response.json.result.map(
|
||||
(item: { value: number; text: string }) => ({
|
||||
value: item.value,
|
||||
label: item.text,
|
||||
}),
|
||||
),
|
||||
totalCount: response.json.count,
|
||||
}));
|
||||
},
|
||||
() =>
|
||||
(input = '', page: number, pageSize: number) => {
|
||||
const query = rison.encode({
|
||||
filter: input,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
});
|
||||
return SupersetClient.get({
|
||||
endpoint: `/api/v1/report/related/owners?q=${query}`,
|
||||
}).then(response => ({
|
||||
data: response.json.result.map(
|
||||
(item: { value: number; text: string }) => ({
|
||||
value: item.value,
|
||||
label: item.text,
|
||||
}),
|
||||
),
|
||||
totalCount: response.json.count,
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
|
|
@ -629,21 +630,26 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
};
|
||||
|
||||
const loadSourceOptions = useMemo(
|
||||
() => (input = '', page: number, pageSize: number) => {
|
||||
const query = rison.encode({ filter: input, page, page_size: pageSize });
|
||||
return SupersetClient.get({
|
||||
endpoint: `/api/v1/report/related/database?q=${query}`,
|
||||
}).then(response => {
|
||||
const list = response.json.result.map(
|
||||
(item: { value: number; text: string }) => ({
|
||||
value: item.value,
|
||||
label: item.text,
|
||||
}),
|
||||
);
|
||||
setSourceOptions(list);
|
||||
return { data: list, totalCount: response.json.count };
|
||||
});
|
||||
},
|
||||
() =>
|
||||
(input = '', page: number, pageSize: number) => {
|
||||
const query = rison.encode({
|
||||
filter: input,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
});
|
||||
return SupersetClient.get({
|
||||
endpoint: `/api/v1/report/related/database?q=${query}`,
|
||||
}).then(response => {
|
||||
const list = response.json.result.map(
|
||||
(item: { value: number; text: string }) => ({
|
||||
value: item.value,
|
||||
label: item.text,
|
||||
}),
|
||||
);
|
||||
setSourceOptions(list);
|
||||
return { data: list, totalCount: response.json.count };
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
|
|
@ -657,21 +663,26 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
}, [databaseLabel, getSourceData]);
|
||||
|
||||
const loadDashboardOptions = useMemo(
|
||||
() => (input = '', page: number, pageSize: number) => {
|
||||
const query = rison.encode({ filter: input, page, page_size: pageSize });
|
||||
return SupersetClient.get({
|
||||
endpoint: `/api/v1/report/related/dashboard?q=${query}`,
|
||||
}).then(response => {
|
||||
const list = response.json.result.map(
|
||||
(item: { value: number; text: string }) => ({
|
||||
value: item.value,
|
||||
label: item.text,
|
||||
}),
|
||||
);
|
||||
setDashboardOptions(list);
|
||||
return { data: list, totalCount: response.json.count };
|
||||
});
|
||||
},
|
||||
() =>
|
||||
(input = '', page: number, pageSize: number) => {
|
||||
const query = rison.encode({
|
||||
filter: input,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
});
|
||||
return SupersetClient.get({
|
||||
endpoint: `/api/v1/report/related/dashboard?q=${query}`,
|
||||
}).then(response => {
|
||||
const list = response.json.result.map(
|
||||
(item: { value: number; text: string }) => ({
|
||||
value: item.value,
|
||||
label: item.text,
|
||||
}),
|
||||
);
|
||||
setDashboardOptions(list);
|
||||
return { data: list, totalCount: response.json.count };
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
|
|
@ -726,22 +737,27 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
}, [getChartData, noChartLabel]);
|
||||
|
||||
const loadChartOptions = useMemo(
|
||||
() => (input = '', page: number, pageSize: number) => {
|
||||
const query = rison.encode({ filter: input, page, page_size: pageSize });
|
||||
return SupersetClient.get({
|
||||
endpoint: `/api/v1/report/related/chart?q=${query}`,
|
||||
}).then(response => {
|
||||
const list = response.json.result.map(
|
||||
(item: { value: number; text: string }) => ({
|
||||
value: item.value,
|
||||
label: item.text,
|
||||
}),
|
||||
);
|
||||
() =>
|
||||
(input = '', page: number, pageSize: number) => {
|
||||
const query = rison.encode({
|
||||
filter: input,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
});
|
||||
return SupersetClient.get({
|
||||
endpoint: `/api/v1/report/related/chart?q=${query}`,
|
||||
}).then(response => {
|
||||
const list = response.json.result.map(
|
||||
(item: { value: number; text: string }) => ({
|
||||
value: item.value,
|
||||
label: item.text,
|
||||
}),
|
||||
);
|
||||
|
||||
setChartOptions(list);
|
||||
return { data: list, totalCount: response.json.count };
|
||||
});
|
||||
},
|
||||
setChartOptions(list);
|
||||
return { data: list, totalCount: response.json.count };
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue