From 6311a9ec8ce7a77e3d4789f2b3f5b7b8826f2557 Mon Sep 17 00:00:00 2001 From: adam-stasiak-polidea Date: Mon, 14 Dec 2020 17:06:19 +0100 Subject: [PATCH 01/43] feat: Added setup for running Cypress tests in docker locally (#11207) * Working configuration * Fixes * Set database volume directory. Added info in CONTRIBUTING how to avoid malformed database in tests. --- CONTRIBUTING.md | 24 ++++++++++++++++++++++++ docker-compose.yml | 5 +++++ docker/.env | 1 + docker/docker-bootstrap.sh | 8 +++++++- docker/docker-init.sh | 23 ++++++++++++++++++----- 5 files changed, 55 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4e5a860fd3e42..30da73333545b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -672,6 +672,30 @@ CYPRESS_BASE_URL= npm run cypress open See [`superset-frontend/cypress_build.sh`](https://github.com/apache/incubator-superset/blob/master/superset-frontend/cypress_build.sh). +As an alternative you can use docker-compose environment for testing: + +Make sure you have added below line to your /etc/hosts file: +```127.0.0.1 db``` + +If you already have launched Docker environment please use the following command to assure a fresh database instance: +```docker-compose down -v``` + +Launch environment: + +CYPRESS_CONFIG=true docker-compose up + +It will serve backend and frontend on port 8088. + +Run Cypres tests: + +```bash +cd cypress-base +npm install +``` + +# run tests via headless Chrome browser (requires Chrome 64+) +npm run cypress-run-chrome + ### Storybook Superset includes a [Storybook](https://storybook.js.org/) to preview the layout/styling of various Superset components, and variations thereof. To open and view the Storybook: diff --git a/docker-compose.yml b/docker-compose.yml index c58f12f308a55..0fa2d252dcc03 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,7 @@ x-superset-volumes: &superset-volumes - ./superset:/app/superset - ./superset-frontend:/app/superset-frontend - superset_home:/app/superset_home + - ./tests:/app/tests version: "3.7" services: @@ -57,6 +58,8 @@ services: user: "root" depends_on: *superset-depends-on volumes: *superset-volumes + environment: + CYPRESS_CONFIG: "${CYPRESS_CONFIG}" superset-init: image: *superset-image @@ -66,6 +69,8 @@ services: depends_on: *superset-depends-on user: "root" volumes: *superset-volumes + environment: + CYPRESS_CONFIG: "${CYPRESS_CONFIG}" superset-node: image: node:12 diff --git a/docker/.env b/docker/.env index 4857d44b38943..9594adb8a8150 100644 --- a/docker/.env +++ b/docker/.env @@ -42,3 +42,4 @@ REDIS_PORT=6379 FLASK_ENV=development SUPERSET_ENV=development SUPERSET_LOAD_EXAMPLES=yes +CYPRESS_CONFIG=false diff --git a/docker/docker-bootstrap.sh b/docker/docker-bootstrap.sh index c60a6d21f75a9..f44cf3aa0f303 100755 --- a/docker/docker-bootstrap.sh +++ b/docker/docker-bootstrap.sh @@ -19,7 +19,13 @@ set -eo pipefail REQUIREMENTS_LOCAL="/app/docker/requirements-local.txt" - +# If Cypress run – overwrite the password for admin and export env variables +if [ "$CYPRESS_CONFIG" == "true" ]; then + export SUPERSET_CONFIG=tests.superset_test_config + export SUPERSET_TESTENV=true + export ENABLE_REACT_CRUD_VIEWS=true + export SUPERSET__SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset +fi # # Make sure we have dev requirements installed # diff --git a/docker/docker-init.sh b/docker/docker-init.sh index 124e747f5f9ee..d5ead5039857e 100755 --- a/docker/docker-init.sh +++ b/docker/docker-init.sh @@ -37,22 +37,29 @@ Init Step ${1}/${STEP_CNT} [${2}] -- ${3} EOF } - +ADMIN_PASSWORD="admin" +# If Cypress run – overwrite the password for admin and export env variables +if [ "$CYPRESS_CONFIG" == "true" ]; then + ADMIN_PASSWORD="general" + export SUPERSET_CONFIG=tests.superset_test_config + export SUPERSET_TESTENV=true + export ENABLE_REACT_CRUD_VIEWS=true + export SUPERSET__SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset +fi # Initialize the database echo_step "1" "Starting" "Applying DB migrations" superset db upgrade echo_step "1" "Complete" "Applying DB migrations" # Create an admin user -echo_step "2" "Starting" "Setting up admin user ( admin / admin )" +echo_step "2" "Starting" "Setting up admin user ( admin / $ADMIN_PASSWORD )" superset fab create-admin \ --username admin \ --firstname Superset \ --lastname Admin \ --email admin@superset.com \ - --password admin + --password $ADMIN_PASSWORD echo_step "2" "Complete" "Setting up admin user" - # Create default roles and permissions echo_step "3" "Starting" "Setting up roles and perms" superset init @@ -61,6 +68,12 @@ echo_step "3" "Complete" "Setting up roles and perms" if [ "$SUPERSET_LOAD_EXAMPLES" = "yes" ]; then # Load some data to play with echo_step "4" "Starting" "Loading examples" - superset load_examples + # If Cypress run which consumes superset_test_config – load required data for tests + if [ "$CYPRESS_CONFIG" == "true" ]; then + superset load_test_users + superset load_examples --load-test-data + else + superset load_examples + fi echo_step "4" "Complete" "Loading examples" fi From d5b16bcd85a32e261b4027c6f54673de8f291f29 Mon Sep 17 00:00:00 2001 From: Lily Kuang Date: Mon, 14 Dec 2020 08:44:47 -0800 Subject: [PATCH 02/43] fix schema datasource modal (#12018) --- superset-frontend/src/datasource/DatasourceModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superset-frontend/src/datasource/DatasourceModal.tsx b/superset-frontend/src/datasource/DatasourceModal.tsx index daf47c25944e3..2ccb8fe1cc91f 100644 --- a/superset-frontend/src/datasource/DatasourceModal.tsx +++ b/superset-frontend/src/datasource/DatasourceModal.tsx @@ -88,9 +88,9 @@ const DatasourceModal: FunctionComponent = ({ const onConfirmSave = () => { // Pull out extra fields into the extra object const schema = - currentDatasource.schema || + currentDatasource.tableSelector?.schema || currentDatasource.databaseSelector?.schema || - currentDatasource.tableSelector?.schema; + currentDatasource.schema; setIsSaving(true); From 3d56f58ef53e21912dac425cb49ae7610915b608 Mon Sep 17 00:00:00 2001 From: "Hugh A. Miles II" Date: Mon, 14 Dec 2020 10:17:20 -0800 Subject: [PATCH 03/43] fix: Explore "Change Dataset" UX Enhancements (#12006) --- .../datasource/ChangeDatasourceModal_spec.jsx | 26 +- .../src/datasource/ChangeDatasourceModal.tsx | 268 +++++++++++------- superset-frontend/src/explore/exploreUtils.js | 16 ++ superset-frontend/src/types/Dataset.ts | 36 +++ 4 files changed, 246 insertions(+), 100 deletions(-) create mode 100644 superset-frontend/src/types/Dataset.ts diff --git a/superset-frontend/spec/javascripts/datasource/ChangeDatasourceModal_spec.jsx b/superset-frontend/spec/javascripts/datasource/ChangeDatasourceModal_spec.jsx index c058851a056c9..b803c4641cedb 100644 --- a/superset-frontend/spec/javascripts/datasource/ChangeDatasourceModal_spec.jsx +++ b/superset-frontend/spec/javascripts/datasource/ChangeDatasourceModal_spec.jsx @@ -21,9 +21,9 @@ import { mount } from 'enzyme'; import configureStore from 'redux-mock-store'; import fetchMock from 'fetch-mock'; import thunk from 'redux-thunk'; +import { act } from 'react-dom/test-utils'; import sinon from 'sinon'; import { supersetTheme, ThemeProvider } from '@superset-ui/core'; -import { act } from 'react-dom/test-utils'; import Modal from 'src/common/components/Modal'; import ChangeDatasourceModal from 'src/datasource/ChangeDatasourceModal'; import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; @@ -47,11 +47,12 @@ const datasourceData = { uid: datasource.id, }; -const DATASOURCES_ENDPOINT = 'glob:*/superset/datasources/'; +const DATASOURCES_ENDPOINT = + 'glob:*/api/v1/dataset/?q=(order_column:changed_on_delta_humanized,order_direction:asc,page:0,page_size:20)'; const DATASOURCE_ENDPOINT = `glob:*/datasource/get/${datasourceData.type}/${datasourceData.id}`; const DATASOURCE_PAYLOAD = { new: 'data' }; -fetchMock.get(DATASOURCES_ENDPOINT, [mockDatasource['7__table']]); +fetchMock.get(DATASOURCES_ENDPOINT, { result: [mockDatasource['7__table']] }); fetchMock.get(DATASOURCE_ENDPOINT, DATASOURCE_PAYLOAD); async function mountAndWait(props = mockedProps) { @@ -80,14 +81,29 @@ describe('ChangeDatasourceModal', () => { }); it('fetches datasources', async () => { - expect(fetchMock.calls(/superset\/datasources/)).toHaveLength(3); + expect(fetchMock.calls(/api\/v1\/dataset/)).toHaveLength(6); + }); + + it('renders confirmation message', async () => { + act(() => { + wrapper.find('.datasource-link').at(0).props().onClick(); + }); + await waitForComponentToPaint(wrapper); + + expect(wrapper.find('.proceed-btn')).toExist(); }); it('changes the datasource', async () => { act(() => { - wrapper.find('.datasource-link').at(0).props().onClick(datasourceData); + wrapper.find('.datasource-link').at(0).props().onClick(); + }); + await waitForComponentToPaint(wrapper); + + act(() => { + wrapper.find('.proceed-btn').at(0).props().onClick(datasourceData); }); await waitForComponentToPaint(wrapper); + expect(fetchMock.calls(/datasource\/get\/table\/7/)).toHaveLength(1); }); }); diff --git a/superset-frontend/src/datasource/ChangeDatasourceModal.tsx b/superset-frontend/src/datasource/ChangeDatasourceModal.tsx index 81b9b8cb8c943..77b3ad4627243 100644 --- a/superset-frontend/src/datasource/ChangeDatasourceModal.tsx +++ b/superset-frontend/src/datasource/ChangeDatasourceModal.tsx @@ -20,25 +20,53 @@ import React, { FunctionComponent, useState, useRef, - useMemo, useEffect, + useCallback, } from 'react'; import { Alert, FormControl, FormControlProps } from 'react-bootstrap'; -import { SupersetClient, t } from '@superset-ui/core'; +import { SupersetClient, t, styled } from '@superset-ui/core'; import TableView from 'src/components/TableView'; -import Modal from 'src/common/components/Modal'; +import StyledModal from 'src/common/components/Modal'; +import Button from 'src/components/Button'; +import { useListViewResource } from 'src/views/CRUD/hooks'; +import Dataset from 'src/types/Dataset'; +import { useDebouncedEffect } from 'src/explore/exploreUtils'; import { getClientErrorObject } from '../utils/getClientErrorObject'; import Loading from '../components/Loading'; import withToasts from '../messageToasts/enhancers/withToasts'; +const CONFIRM_WARNING_MESSAGE = t( + 'Warning! Changing the dataset may break the chart if the metadata (columns/metrics) does not exist in the target dataset', +); + +interface Datasource { + type: string; + id: number; + uid: string; +} + interface ChangeDatasourceModalProps { addDangerToast: (msg: string) => void; - onChange: (id: number) => void; + addSuccessToast: (msg: string) => void; + onChange: (uid: string) => void; onDatasourceSave: (datasource: object, errors?: Array) => {}; onHide: () => void; show: boolean; } +const ConfirmModalStyled = styled.div` + .btn-container { + display: flex; + justify-content: flex-end; + padding: 0px 15px; + margin: 10px 0 0 0; + } + + .confirm-modal-container { + margin: 9px; + } +`; + const TABLE_COLUMNS = [ 'name', 'type', @@ -47,86 +75,78 @@ const TABLE_COLUMNS = [ 'creator', ].map(col => ({ accessor: col, Header: col })); -const TABLE_FILTERABLE = ['rawName', 'type', 'schema', 'connection', 'creator']; const CHANGE_WARNING_MSG = t( 'Changing the dataset may break the chart if the chart relies ' + 'on columns or metadata that does not exist in the target dataset', ); +const emptyRequest = { + pageIndex: 0, + pageSize: 20, + filters: [], + sortBy: [{ id: 'changed_on_delta_humanized' }], +}; + const ChangeDatasourceModal: FunctionComponent = ({ addDangerToast, + addSuccessToast, onChange, onDatasourceSave, onHide, show, }) => { - const [datasources, setDatasources] = useState(null); const [filter, setFilter] = useState(undefined); - const [loading, setLoading] = useState(true); + const [confirmChange, setConfirmChange] = useState(false); + const [confirmedDataset, setConfirmedDataset] = useState(); let searchRef = useRef(null); - useEffect(() => { - const selectDatasource = (datasource: any) => { - SupersetClient.get({ - endpoint: `/datasource/get/${datasource.type}/${datasource.id}`, - }) - .then(({ json }) => { - onDatasourceSave(json); - onChange(datasource.uid); - }) - .catch(response => { - getClientErrorObject(response).then( - ({ error, message }: { error: any; message: string }) => { - const errorMessage = error - ? error.error || error.statusText || error - : message; - addDangerToast(errorMessage); - }, - ); - }); - onHide(); - }; + const { + state: { loading, resourceCollection }, + fetchData, + } = useListViewResource('dataset', t('dataset'), addDangerToast); + + const selectDatasource = useCallback((datasource: Datasource) => { + setConfirmChange(true); + setConfirmedDataset(datasource); + }, []); + + useDebouncedEffect(() => { + if (filter) { + fetchData({ + ...emptyRequest, + filters: [ + { + id: 'table_name', + operator: 'ct', + value: filter, + }, + ], + }); + } + }, 1000); - const onEnterModal = () => { + useEffect(() => { + const onEnterModal = async () => { if (searchRef && searchRef.current) { searchRef.current.focus(); } - if (!datasources) { - SupersetClient.get({ - endpoint: '/superset/datasources/', - }) - .then(({ json }) => { - const data = json.map((ds: any) => ({ - rawName: ds.name, - connection: ds.connection, - schema: ds.schema, - name: ( - selectDatasource(ds)} - className="datasource-link" - > - {ds.name} - - ), - type: ds.type, - })); - setLoading(false); - setDatasources(data); - }) - .catch(response => { - setLoading(false); - getClientErrorObject(response).then(({ error }: any) => { - addDangerToast(error.error || error.statusText || error); - }); - }); - } + + // Fetch initial datasets for tableview + await fetchData(emptyRequest); }; if (show) { onEnterModal(); } - }, [addDangerToast, datasources, onChange, onDatasourceSave, onHide, show]); + }, [ + addDangerToast, + fetchData, + onChange, + onDatasourceSave, + onHide, + selectDatasource, + show, + ]); const setSearchRef = (ref: any) => { searchRef = ref; @@ -135,21 +155,58 @@ const ChangeDatasourceModal: FunctionComponent = ({ const changeSearch = ( event: React.FormEvent, ) => { - setFilter((event.currentTarget?.value as string) ?? ''); + const searchValue = (event.currentTarget?.value as string) ?? ''; + setFilter(searchValue); }; - const data = useMemo( - () => - filter && datasources - ? datasources.filter((datasource: any) => - TABLE_FILTERABLE.some(field => datasource[field]?.includes(filter)), - ) - : datasources, - [datasources, filter], - ); + const handleChangeConfirm = () => { + SupersetClient.get({ + endpoint: `/datasource/get/${confirmedDataset?.type}/${confirmedDataset?.id}`, + }) + .then(({ json }) => { + onDatasourceSave(json); + onChange(`${confirmedDataset?.id}__table`); + }) + .catch(response => { + getClientErrorObject(response).then( + ({ error, message }: { error: any; message: string }) => { + const errorMessage = error + ? error.error || error.statusText || error + : message; + addDangerToast(errorMessage); + }, + ); + }); + onHide(); + addSuccessToast('Successfully changed datasource!'); + }; + + const handlerCancelConfirm = () => { + setConfirmChange(false); + }; + + const renderTableView = () => { + const data = resourceCollection.map((ds: any) => ({ + rawName: ds.table_name, + connection: ds.database.database_name, + schema: ds.schema, + name: ( + selectDatasource({ type: 'table', ...ds })} + className="datasource-link" + > + {ds.table_name} + + ), + type: ds.kind, + })); + + return data; + }; return ( - = ({ hideFooter > <> - - {t('Warning!')} {CHANGE_WARNING_MSG} - -
- { - setSearchRef(ref); - }} - type="text" - bsSize="sm" - value={filter} - placeholder={t('Search / Filter')} - onChange={changeSearch} - /> -
- {loading && } - {datasources && ( - + {!confirmChange && ( + <> + + {t('Warning!')} {CHANGE_WARNING_MSG} + +
+ { + setSearchRef(ref); + }} + type="text" + bsSize="sm" + value={filter} + placeholder={t('Search / Filter')} + onChange={changeSearch} + /> +
+ {loading && } + {!loading && ( + + )} + + )} + {confirmChange && ( + +
+ {CONFIRM_WARNING_MESSAGE} +
+ + +
+
+
)} -
+ ); }; diff --git a/superset-frontend/src/explore/exploreUtils.js b/superset-frontend/src/explore/exploreUtils.js index e69ce0bf712e3..3b70733820b9a 100644 --- a/superset-frontend/src/explore/exploreUtils.js +++ b/superset-frontend/src/explore/exploreUtils.js @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ + +import { useCallback, useEffect } from 'react'; /* eslint camelcase: 0 */ import URI from 'urijs'; import { @@ -284,3 +286,17 @@ export const exploreChart = formData => { }); postForm(url, formData); }; + +export const useDebouncedEffect = (effect, delay) => { + const callback = useCallback(effect, [effect]); + + useEffect(() => { + const handler = setTimeout(() => { + callback(); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [callback, delay]); +}; diff --git a/superset-frontend/src/types/Dataset.ts b/superset-frontend/src/types/Dataset.ts new file mode 100644 index 0000000000000..7d69932f6f0dd --- /dev/null +++ b/superset-frontend/src/types/Dataset.ts @@ -0,0 +1,36 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import Owner from './Owner'; + +export default interface Dataset { + changed_by_name: string; + changed_by_url: string; + changed_by: string; + changed_on_delta_humanized: string; + database: { + id: string; + database_name: string; + }; + kind: string; + explore_url: string; + id: number; + owners: Array; + schema: string; + table_name: string; +} From fda3a2fe7c679784ed6858b27763ae252bf73962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CA=88=E1=B5=83=E1=B5=A2?= Date: Mon, 14 Dec 2020 14:10:55 -0800 Subject: [PATCH 04/43] fix: disable browser autocomplete for DeleteModal (#12043) --- superset-frontend/src/components/DeleteModal.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/superset-frontend/src/components/DeleteModal.tsx b/superset-frontend/src/components/DeleteModal.tsx index 37a9ca580c548..59a97ebf6f75f 100644 --- a/superset-frontend/src/components/DeleteModal.tsx +++ b/superset-frontend/src/components/DeleteModal.tsx @@ -73,6 +73,7 @@ export default function DeleteModal({ id="delete" type="text" bsSize="sm" + autoComplete="off" onChange={( event: React.FormEvent, ) => { From 6fcda5dac149bfbf91f08d4105c867fb6d916db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CA=88=E1=B5=83=E1=B5=A2?= Date: Mon, 14 Dec 2020 22:29:31 -0800 Subject: [PATCH 05/43] feat: add cron picker to AlertReportModal (#12032) * humanize cron display in list view --- requirements/base.txt | 1 + setup.cfg | 2 +- setup.py | 1 + superset-frontend/package-lock.json | 5 + superset-frontend/package.json | 1 + .../src/common/components/CronPicker.tsx | 117 ++++++++++++++++++ .../src/common/components/Select.tsx | 2 +- .../src/common/components/common.stories.tsx | 45 ++++++- .../src/common/components/index.tsx | 1 + .../src/views/CRUD/alert/AlertList.tsx | 27 ++-- .../src/views/CRUD/alert/AlertReportModal.tsx | 62 +++------- .../AlertReportCronScheduler.test.tsx | 72 +++++++++++ .../components/AlertReportCronScheduler.tsx | 92 ++++++++++++++ superset/models/reports.py | 6 + superset/reports/api.py | 2 + tests/reports/api_tests.py | 1 + 16 files changed, 384 insertions(+), 53 deletions(-) create mode 100644 superset-frontend/src/common/components/CronPicker.tsx create mode 100644 superset-frontend/src/views/CRUD/alert/components/AlertReportCronScheduler.test.tsx create mode 100644 superset-frontend/src/views/CRUD/alert/components/AlertReportCronScheduler.tsx diff --git a/requirements/base.txt b/requirements/base.txt index 8f0234d1f271a..0aa3250aa078a 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -24,6 +24,7 @@ chardet==3.0.4 # via aiohttp click==7.1.2 # via apache-superset, flask, flask-appbuilder colorama==0.4.4 # via apache-superset, flask-appbuilder contextlib2==0.6.0.post1 # via apache-superset +cron-descriptor==1.2.24 # via apache-superset croniter==0.3.36 # via apache-superset cryptography==3.2.1 # via apache-superset decorator==4.4.2 # via retry diff --git a/setup.cfg b/setup.cfg index 38856a350421e..3acb2257a25e3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,7 @@ combine_as_imports = true include_trailing_comma = true line_length = 88 known_first_party = superset -known_third_party =alembic,apispec,backoff,bleach,cachelib,celery,click,colorama,contextlib2,croniter,cryptography,dateutil,flask,flask_appbuilder,flask_babel,flask_caching,flask_compress,flask_login,flask_migrate,flask_sqlalchemy,flask_talisman,flask_testing,flask_wtf,freezegun,geohash,geopy,humanize,isodate,jinja2,jwt,markdown,markupsafe,marshmallow,msgpack,numpy,pandas,parameterized,parsedatetime,pathlib2,pgsanity,polyline,prison,pyarrow,pyhive,pytest,pytz,redis,retry,selenium,setuptools,simplejson,slack,sqlalchemy,sqlalchemy_utils,sqlparse,werkzeug,wtforms,wtforms_json,yaml +known_third_party =alembic,apispec,backoff,bleach,cachelib,celery,click,colorama,contextlib2,cron_descriptor,croniter,cryptography,dateutil,flask,flask_appbuilder,flask_babel,flask_caching,flask_compress,flask_login,flask_migrate,flask_sqlalchemy,flask_talisman,flask_testing,flask_wtf,freezegun,geohash,geopy,humanize,isodate,jinja2,jwt,markdown,markupsafe,marshmallow,msgpack,numpy,pandas,parameterized,parsedatetime,pathlib2,pgsanity,polyline,prison,pyarrow,pyhive,pytest,pytz,redis,retry,selenium,setuptools,simplejson,slack,sqlalchemy,sqlalchemy_utils,sqlparse,werkzeug,wtforms,wtforms_json,yaml multi_line_output = 3 order_by_type = false diff --git a/setup.py b/setup.py index bc30115604718..aebcb409970c7 100644 --- a/setup.py +++ b/setup.py @@ -72,6 +72,7 @@ def get_git_sha(): "colorama", "contextlib2", "croniter>=0.3.28", + "cron-descriptor", "cryptography>=3.2.1", "flask>=1.1.0, <2.0.0", "flask-appbuilder>=3.1.1, <4.0.0", diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index c0f0f138f1195..dc230f1721eae 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -43514,6 +43514,11 @@ "resolved": "https://registry.npmjs.org/react-is-mounted-hook/-/react-is-mounted-hook-1.0.3.tgz", "integrity": "sha512-YCCYcTVYMPfTi6WhWIwM9EYBcpHoivjjkE90O5ScsE9wXSbeXGZvLDMGt4mdSNcWshhc8JD0AzgBmsleCSdSFA==" }, + "react-js-cron": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-js-cron/-/react-js-cron-1.2.0.tgz", + "integrity": "sha512-mWxTmXkqP58ughdziS3qjEUVl1O03XEo8WDvr45/kTQfbd0C6ITniAsG5wZzwmTOgmrOKQheHog7L0TP683WUA==" + }, "react-json-tree": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.11.2.tgz", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index b4585c7721388..17c582eb5b728 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -141,6 +141,7 @@ "react-dom": "^16.13.0", "react-gravatar": "^2.6.1", "react-hot-loader": "^4.12.20", + "react-js-cron": "^1.2.0", "react-json-tree": "^0.11.2", "react-jsonschema-form": "^1.2.0", "react-loadable": "^5.5.0", diff --git a/superset-frontend/src/common/components/CronPicker.tsx b/superset-frontend/src/common/components/CronPicker.tsx new file mode 100644 index 0000000000000..b5825ec56b49f --- /dev/null +++ b/superset-frontend/src/common/components/CronPicker.tsx @@ -0,0 +1,117 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { styled, t } from '@superset-ui/core'; +import ReactCronPicker, { Locale, CronProps } from 'react-js-cron'; + +export * from 'react-js-cron'; + +export const LOCALE: Locale = { + everyText: t('every'), + emptyMonths: t('every month'), + emptyMonthDays: t('every day of the month'), + emptyMonthDaysShort: t('day of the month'), + emptyWeekDays: t('every day of the week'), + emptyWeekDaysShort: t('day of the week'), + emptyHours: t('every hour'), + emptyMinutes: t('every minute UTC'), + emptyMinutesForHourPeriod: t('every'), + yearOption: t('year'), + monthOption: t('month'), + weekOption: t('week'), + dayOption: t('day'), + hourOption: t('hour'), + minuteOption: t('minute'), + rebootOption: t('reboot'), + prefixPeriod: t('Every'), + prefixMonths: t('in'), + prefixMonthDays: t('on'), + prefixWeekDays: t('on'), + prefixWeekDaysForMonthAndYearPeriod: t('and'), + prefixHours: t('at'), + prefixMinutes: t(':'), + prefixMinutesForHourPeriod: t('at'), + suffixMinutesForHourPeriod: t('minute(s) UTC'), + errorInvalidCron: t('Invalid cron expression'), + clearButtonText: t('Clear'), + weekDays: [ + // Order is important, the index will be used as value + t('Sunday'), // Sunday must always be first, it's "0" + t('Monday'), + t('Tuesday'), + t('Wednesday'), + t('Thursday'), + t('Friday'), + t('Saturday'), + ], + months: [ + // Order is important, the index will be used as value + t('January'), + t('February'), + t('March'), + t('April'), + t('May'), + t('June'), + t('July'), + t('August'), + t('September'), + t('October'), + t('November'), + t('December'), + ], + // Order is important, the index will be used as value + altWeekDays: [ + t('SUN'), // Sunday must always be first, it's "0" + t('MON'), + t('TUE'), + t('WED'), + t('THU'), + t('FRI'), + t('SAT'), + ], + // Order is important, the index will be used as value + altMonths: [ + t('JAN'), + t('FEB'), + t('MAR'), + t('APR'), + t('MAY'), + t('JUN'), + t('JUL'), + t('AUG'), + t('SEP'), + t('OCT'), + t('NOV'), + t('DEC'), + ], +}; + +export const CronPicker = styled((props: CronProps) => ( + +))` + .react-js-cron-select:not(.react-js-cron-custom-select) > div:first-of-type, + .react-js-cron-custom-select { + border-radius: ${({ theme }) => theme.gridUnit}px; + background-color: ${({ theme }) => + theme.colors.grayscale.light4} !important; + } + .react-js-cron-custom-select > div:first-of-type { + border-radius: ${({ theme }) => theme.gridUnit}px; + } +`; diff --git a/superset-frontend/src/common/components/Select.tsx b/superset-frontend/src/common/components/Select.tsx index b3a49d483b3ee..ca2262197e701 100644 --- a/superset-frontend/src/common/components/Select.tsx +++ b/superset-frontend/src/common/components/Select.tsx @@ -39,7 +39,7 @@ const StyledSelect = styled(BaseSelect)` } } `; -const StyledOption = styled(BaseSelect.Option)``; +const StyledOption = BaseSelect.Option; export const Select = Object.assign(StyledSelect, { Option: StyledOption, diff --git a/superset-frontend/src/common/components/common.stories.tsx b/superset-frontend/src/common/components/common.stories.tsx index 5c73f87b3ad05..969d437a9375b 100644 --- a/superset-frontend/src/common/components/common.stories.tsx +++ b/superset-frontend/src/common/components/common.stories.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; +import React, { useState, useRef, useCallback } from 'react'; import { action } from '@storybook/addon-actions'; import { withKnobs, boolean, select } from '@storybook/addon-knobs'; import Button from 'src/components/Button'; @@ -24,8 +24,8 @@ import Modal from './Modal'; import Tabs, { EditableTabs } from './Tabs'; import AntdPopover from './Popover'; import { Tooltip as AntdTooltip } from './Tooltip'; -import { Menu } from '.'; import { Switch as AntdSwitch } from './Switch'; +import { Menu, Input, Divider } from '.'; import { Dropdown } from './Dropdown'; import InfoTooltip from './InfoTooltip'; import { @@ -35,6 +35,7 @@ import { import Badge from './Badge'; import ProgressBar from './ProgressBar'; import Collapse from './Collapse'; +import { CronPicker, CronError } from './CronPicker'; export default { title: 'Common Components', @@ -321,3 +322,43 @@ export const CollapseTextLight = () => ( ); +export function StyledCronPicker() { + const inputRef = useRef(null); + const defaultValue = '30 5 * * 1,6'; + const [value, setValue] = useState(defaultValue); + const customSetValue = useCallback( + (newValue: string) => { + setValue(newValue); + inputRef.current?.setValue(newValue); + }, + [inputRef], + ); + const [error, onError] = useState(); + + return ( +
+ { + setValue(event.target.value); + }} + onPressEnter={() => { + setValue(inputRef.current?.input.value || ''); + }} + /> + + + + + +

+ Error: {error ? error.description : 'undefined'} +

+
+ ); +} diff --git a/superset-frontend/src/common/components/index.tsx b/superset-frontend/src/common/components/index.tsx index 2bdcfb47e2065..4b1fa15531c2e 100644 --- a/superset-frontend/src/common/components/index.tsx +++ b/superset-frontend/src/common/components/index.tsx @@ -33,6 +33,7 @@ export { Button, Card, DatePicker, + Divider, Dropdown, Empty, Input, diff --git a/superset-frontend/src/views/CRUD/alert/AlertList.tsx b/superset-frontend/src/views/CRUD/alert/AlertList.tsx index 9b46377415612..3ecc034cac040 100644 --- a/superset-frontend/src/views/CRUD/alert/AlertList.tsx +++ b/superset-frontend/src/views/CRUD/alert/AlertList.tsx @@ -24,6 +24,7 @@ import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar'; import Button from 'src/components/Button'; import FacePile from 'src/components/FacePile'; import { IconName } from 'src/components/Icon'; +import { Tooltip } from 'src/common/components/Tooltip'; import ListView, { FilterOperators, Filters } from 'src/components/ListView'; import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu'; import { Switch } from 'src/common/components/Switch'; @@ -55,7 +56,7 @@ function AlertList({ isReportEnabled = false, user, }: AlertListProps) { - const title = isReportEnabled ? 'report' : 'alert'; + const title = isReportEnabled ? t('report') : t('alert'); const pathName = isReportEnabled ? 'Reports' : 'Alerts'; const initalFilters = useMemo( () => [ @@ -139,20 +140,31 @@ function AlertList({ }: any) => recipients.map((r: any) => ( - // )), accessor: 'recipients', Header: t('Notification Method'), disableSortBy: true, + size: 'xl', }, { Header: t('Schedule'), - accessor: 'crontab', + accessor: 'crontab_humanized', + size: 'xl', + Cell: ({ + row: { + original: { crontab_humanized = '' }, + }, + }: any) => ( + + {crontab_humanized}, + + ), }, { accessor: 'created_by', disableSortBy: true, hidden: true, + size: 'xl', }, { Cell: ({ @@ -163,7 +175,7 @@ function AlertList({ Header: t('Owners'), id: 'owners', disableSortBy: true, - size: 'lg', + size: 'xl', }, { Cell: ({ row: { original } }: any) => ( @@ -177,6 +189,7 @@ function AlertList({ Header: t('Active'), accessor: 'active', id: 'active', + size: 'xl', }, { Cell: ({ row: { original } }: any) => { @@ -234,7 +247,7 @@ function AlertList({ subMenuButtons.push({ name: ( <> - {t(`${title}`)} + {title} ), buttonStyle: 'primary', @@ -245,8 +258,8 @@ function AlertList({ } const EmptyStateButton = ( - ); diff --git a/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx b/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx index 1ebc5cd6cf3de..402e3f154c49f 100644 --- a/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx +++ b/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx @@ -28,8 +28,9 @@ import { Select } from 'src/common/components/Select'; import { Radio } from 'src/common/components/Radio'; import { AsyncSelect } from 'src/components/Select'; import withToasts from 'src/messageToasts/enhancers/withToasts'; - import Owner from 'src/types/Owner'; + +import { AlertReportCronScheduler } from './components/AlertReportCronScheduler'; import { AlertObject, Operator, Recipient, MetaObject } from './types'; type SelectValue = { @@ -121,7 +122,7 @@ const StyledSectionContainer = styled.div` .column { flex: 1 1 auto; - min-width: 33.33%; + min-width: calc(33.33% - ${({ theme }) => theme.gridUnit * 8}px); padding: ${({ theme }) => theme.gridUnit * 4}px; .async-select { @@ -142,6 +143,9 @@ const StyledSectionContainer = styled.div` display: flex; flex-direction: row; align-items: center; + &.wrap { + flex-wrap: wrap; + } > div { flex: 1 1 auto; @@ -180,7 +184,7 @@ const StyledSwitchContainer = styled.div` } `; -const StyledInputContainer = styled.div` +export const StyledInputContainer = styled.div` flex: 1 1 auto; margin: ${({ theme }) => theme.gridUnit * 2}px; margin-top: 0; @@ -449,10 +453,6 @@ const AlertReportModal: FunctionComponent = ({ const [currentAlert, setCurrentAlert] = useState(); const [isHidden, setIsHidden] = useState(true); const [contentType, setContentType] = useState('dashboard'); - const [scheduleFormat, setScheduleFormat] = useState( - 'dropdown-format', - ); - // Dropdown options const [sourceOptions, setSourceOptions] = useState([]); const [dashboardOptions, setDashboardOptions] = useState([]); @@ -806,12 +806,6 @@ const AlertReportModal: FunctionComponent = ({ updateAlertState('validator_config_json', config); }; - const onScheduleFormatChange = (event: any) => { - const { target } = event; - - setScheduleFormat(target.value); - }; - const onLogRetentionChange = (retention: number) => { updateAlertState('log_retention', retention); }; @@ -976,12 +970,18 @@ const AlertReportModal: FunctionComponent = ({ // Dropdown options const conditionOptions = CONDITIONS.map(condition => { return ( - {condition.label} + + {condition.label} + ); }); const retentionOptions = RETENTION_OPTIONS.map(option => { - return {option.label}; + return ( + + {option.label} + + ); }); return ( @@ -1102,7 +1102,7 @@ const AlertReportModal: FunctionComponent = ({ /> -
+
{t('Alert If...')} @@ -1153,32 +1153,10 @@ const AlertReportModal: FunctionComponent = ({

{t('Alert Condition Schedule')}

- -
- - - Every x Minutes (should be set of dropdown options) - -
-
- - CRON Schedule - -
- -
-
-
-
+ updateAlertState('crontab', newVal)} + />

{t('Schedule Settings')}

diff --git a/superset-frontend/src/views/CRUD/alert/components/AlertReportCronScheduler.test.tsx b/superset-frontend/src/views/CRUD/alert/components/AlertReportCronScheduler.test.tsx new file mode 100644 index 0000000000000..3f7e5bce9e4e9 --- /dev/null +++ b/superset-frontend/src/views/CRUD/alert/components/AlertReportCronScheduler.test.tsx @@ -0,0 +1,72 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { ReactWrapper } from 'enzyme'; +import { styledMount as mount } from 'spec/helpers/theming'; +import { CronPicker } from 'src/common/components/CronPicker'; +import { Input } from 'src/common/components'; + +import { AlertReportCronScheduler } from './AlertReportCronScheduler'; + +describe('AlertReportCronScheduler', () => { + let wrapper: ReactWrapper; + + it('calls onChnage when value chnages', () => { + const onChangeMock = jest.fn(); + wrapper = mount( + , + ); + + const changeValue = '1,7 * * * *'; + + wrapper.find(CronPicker).props().setValue(changeValue); + expect(onChangeMock).toHaveBeenLastCalledWith(changeValue); + }); + + it('sets input value when cron picker changes', () => { + const onChangeMock = jest.fn(); + wrapper = mount( + , + ); + + const changeValue = '1,7 * * * *'; + + wrapper.find(CronPicker).props().setValue(changeValue); + expect(wrapper.find(Input).state().value).toEqual(changeValue); + }); + + it('calls onChange when input value changes', () => { + const onChangeMock = jest.fn(); + wrapper = mount( + , + ); + + const changeValue = '1,7 * * * *'; + const event = { + target: { value: changeValue }, + } as React.FocusEvent; + + const inputProps = wrapper.find(Input).props(); + if (inputProps.onBlur) { + inputProps.onBlur(event); + } + expect(onChangeMock).toHaveBeenLastCalledWith(changeValue); + }); +}); diff --git a/superset-frontend/src/views/CRUD/alert/components/AlertReportCronScheduler.tsx b/superset-frontend/src/views/CRUD/alert/components/AlertReportCronScheduler.tsx new file mode 100644 index 0000000000000..f3e874faea8fe --- /dev/null +++ b/superset-frontend/src/views/CRUD/alert/components/AlertReportCronScheduler.tsx @@ -0,0 +1,92 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useState, useCallback, useRef, FunctionComponent } from 'react'; +import { t, useTheme } from '@superset-ui/core'; + +import { Input } from 'src/common/components'; +import { Radio } from 'src/common/components/Radio'; +import { CronPicker, CronError } from 'src/common/components/CronPicker'; +import { StyledInputContainer } from '../AlertReportModal'; + +interface AlertReportCronSchedulerProps { + value?: string; + onChange: (change: string) => any; +} + +export const AlertReportCronScheduler: FunctionComponent = ({ + value = '* * * * *', + onChange, +}) => { + const theme = useTheme(); + const inputRef = useRef(null); + const [scheduleFormat, setScheduleFormat] = useState<'picker' | 'input'>( + 'picker', + ); + const customSetValue = useCallback( + (newValue: string) => { + onChange(newValue); + inputRef.current?.setValue(newValue); + }, + [inputRef, onChange], + ); + const [error, onError] = useState(); + + return ( + <> + setScheduleFormat(e.target.value)} + value={scheduleFormat} + > +
+ + +
+
+ + CRON Schedule + +
+ { + onChange(event.target.value); + }} + onPressEnter={() => { + onChange(inputRef.current?.input.value || ''); + }} + /> +
+
+
+
+ + ); +}; diff --git a/superset/models/reports.py b/superset/models/reports.py index 0cecaa8a76c65..0c2816b457ac3 100644 --- a/superset/models/reports.py +++ b/superset/models/reports.py @@ -17,7 +17,9 @@ """A collection of ORM sqlalchemy models for Superset""" import enum +from cron_descriptor import get_description from flask_appbuilder import Model +from flask_appbuilder.models.decorators import renders from sqlalchemy import ( Boolean, Column, @@ -131,6 +133,10 @@ class ReportSchedule(Model, AuditMixinNullable): def __repr__(self) -> str: return str(self.name) + @renders("crontab") + def crontab_humanized(self) -> str: + return get_description(self.crontab) + class ReportRecipients( Model, AuditMixinNullable diff --git a/superset/reports/api.py b/superset/reports/api.py index 583001aa0c69b..5bdcccab878b0 100644 --- a/superset/reports/api.py +++ b/superset/reports/api.py @@ -104,6 +104,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi): "created_by.last_name", "created_on", "crontab", + "crontab_humanized", "id", "last_eval_dttm", "last_state", @@ -147,6 +148,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi): "created_on", "name", "type", + "crontab_humanized", ] search_columns = ["name", "active", "created_by", "type", "last_state"] search_filters = {"name": [ReportScheduleAllTextFilter]} diff --git a/tests/reports/api_tests.py b/tests/reports/api_tests.py index b868ab4c8a592..6f374e6cd8a47 100644 --- a/tests/reports/api_tests.py +++ b/tests/reports/api_tests.py @@ -187,6 +187,7 @@ def test_get_list_report_schedule(self): "created_by", "created_on", "crontab", + "crontab_humanized", "id", "last_eval_dttm", "last_state", From 7a7da27253ff0cf63288a9c0da1d9f2ad1f23ce8 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Mon, 14 Dec 2020 23:19:35 -0800 Subject: [PATCH 06/43] chore: bumping plugin packages to latest (#11957) * chore: bumping plugin packages to latest * new package-lock.json --- superset-frontend/package-lock.json | 3184 ++++++++++++++++++++++----- superset-frontend/package.json | 52 +- 2 files changed, 2671 insertions(+), 565 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index dc230f1721eae..76cd94656ad44 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -2900,9 +2900,9 @@ }, "dependencies": { "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" } } }, @@ -4306,9 +4306,9 @@ } }, "d3-array": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.8.0.tgz", - "integrity": "sha512-6V272gsOeg7+9pTW1jSYOR1QE37g95I3my1hBmY+vOUNHRrk9yt4OTz/gK7PMkVAVDrYYq4mq3grTiZ8iJdNIw==" + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" }, "d3-interpolate": { "version": "2.0.1", @@ -16598,431 +16598,32 @@ } }, "@superset-ui/legacy-plugin-chart-calendar": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-calendar/-/legacy-plugin-chart-calendar-0.15.13.tgz", - "integrity": "sha512-FV5MAMDQFvhSAl280KuE5J54l/UBH9NaC7G3GfNgQkyjGPabQjJTlzaZtRMU+6IFlsYqlA9xr5vzUanTT2Gxsw==", + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-calendar/-/legacy-plugin-chart-calendar-0.15.17.tgz", + "integrity": "sha512-8fKJWASWwieHIGZlTpozsOrRM8p8cTnC6JwdMlvjiJ1xXcwrsjQKXi1FJmbI7mWuJ5+9gdeuBlgX/QW/dLEWpQ==", "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", "d3-array": "^2.0.3", "d3-selection": "^1.4.0", "d3-tip": "^0.9.1", "prop-types": "^15.6.2" }, - "dependencies": { - "d3-array": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.8.0.tgz", - "integrity": "sha512-6V272gsOeg7+9pTW1jSYOR1QE37g95I3my1hBmY+vOUNHRrk9yt4OTz/gK7PMkVAVDrYYq4mq3grTiZ8iJdNIw==" - } - } - }, - "@superset-ui/legacy-plugin-chart-chord": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-chord/-/legacy-plugin-chart-chord-0.15.13.tgz", - "integrity": "sha512-si6yLHCcThncSBUDlASkak2euMr/XO52ZMaMBLBB6DpWoD0wWnbVUnWnBioUCQDF/s64sWHMkOtBDmPu8nA/yQ==", - "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "d3": "^3.5.17", - "prop-types": "^15.6.2", - "react": "^16.13.1" - } - }, - "@superset-ui/legacy-plugin-chart-country-map": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-country-map/-/legacy-plugin-chart-country-map-0.15.13.tgz", - "integrity": "sha512-m62G+mJU+oqjrFScNrvneaBswjH3yINm5bX12TIokauR5pEINP46PFAJgsGwlM4GVnqqSE/rYHe8QbEJTnje8A==", - "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "d3": "^3.5.17", - "d3-array": "^2.0.3", - "prop-types": "^15.6.2" - }, - "dependencies": { - "d3-array": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.8.0.tgz", - "integrity": "sha512-6V272gsOeg7+9pTW1jSYOR1QE37g95I3my1hBmY+vOUNHRrk9yt4OTz/gK7PMkVAVDrYYq4mq3grTiZ8iJdNIw==" - } - } - }, - "@superset-ui/legacy-plugin-chart-event-flow": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-event-flow/-/legacy-plugin-chart-event-flow-0.15.13.tgz", - "integrity": "sha512-97KvEZQdXwqYZJLqVMWXi5wlFNElCOmnJNoAnkhC/Az8r6PJWpBefgR7qYu8SxOc8fXAUHTtC9F7KHhzgXjdrQ==", - "requires": { - "@data-ui/event-flow": "^0.0.84", - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "prop-types": "^15.6.2" - } - }, - "@superset-ui/legacy-plugin-chart-force-directed": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-force-directed/-/legacy-plugin-chart-force-directed-0.15.13.tgz", - "integrity": "sha512-nrZtvmiXdL+bRa4PU4vSPy+ZkHnFN/2nSpId8OQhTNtCuo718A8X45gm0M1mhxtoX1H69dVit0O0xKQPtoAa8A==", - "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "d3": "^3.5.17", - "prop-types": "^15.7.2" - } - }, - "@superset-ui/legacy-plugin-chart-heatmap": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-heatmap/-/legacy-plugin-chart-heatmap-0.15.13.tgz", - "integrity": "sha512-4WWJ+X0jVB9HMlnWQ9NTTR+83HJomZRTzbs2KJy8wDmsTEVlzmpWwsPiY7yOMaH17x9YgSWfoSdUjr3tsXaf1w==", - "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "d3": "^3.5.17", - "d3-svg-legend": "^1.x", - "d3-tip": "^0.9.1", - "prop-types": "^15.6.2" - } - }, - "@superset-ui/legacy-plugin-chart-histogram": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-histogram/-/legacy-plugin-chart-histogram-0.15.13.tgz", - "integrity": "sha512-ahxVjClc8nC7B7dOnF4KP2FPWFbFvtOiyHjuNwPIT5NfE3jyREFdhkm40aH9KmarujEJNDBRVaDzBkB4rSvXZQ==", - "requires": { - "@data-ui/histogram": "^0.0.84", - "@data-ui/theme": "^0.0.84", - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "@vx/legend": "^0.0.198", - "@vx/responsive": "^0.0.197", - "@vx/scale": "^0.0.197", - "prop-types": "^15.6.2" - }, - "dependencies": { - "@vx/group": { - "version": "0.0.198", - "resolved": "https://registry.npmjs.org/@vx/group/-/group-0.0.198.tgz", - "integrity": "sha512-0PivE+fWZlPkSzFO/is5m4VSSv3pg+sS1yxYAZHbNffUvn472WDWptriHvoUIPQe0lOXhTSrc73UQzew9GtW/g==", - "requires": { - "@types/classnames": "^2.2.9", - "@types/react": "*", - "classnames": "^2.2.5", - "prop-types": "^15.6.2" - } - }, - "@vx/legend": { - "version": "0.0.198", - "resolved": "https://registry.npmjs.org/@vx/legend/-/legend-0.0.198.tgz", - "integrity": "sha512-3S2/yP6IvkkhUlTj6In5M1OrzY1OaT1D06hRxuiOLAbaXTerhbUGwIjGSNoovQM6JebFlbWnnA5xH1SKgw5GGA==", - "requires": { - "@types/classnames": "^2.2.9", - "@types/d3-scale": "^2.1.1", - "@types/react": "*", - "@vx/group": "0.0.198", - "classnames": "^2.2.5", - "prop-types": "^15.5.10" - } - }, - "@vx/responsive": { - "version": "0.0.197", - "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", - "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", - "requires": { - "@types/lodash": "^4.14.146", - "@types/react": "*", - "lodash": "^4.17.10", - "prop-types": "^15.6.1", - "resize-observer-polyfill": "1.5.1" - } - }, - "@vx/scale": { - "version": "0.0.197", - "resolved": "https://registry.npmjs.org/@vx/scale/-/scale-0.0.197.tgz", - "integrity": "sha512-FF0POm9rh66I3Om5DsuxynwWU+Q645aTF47vgP2dVDeOOq3Oet7CZzmXLDh3W6nVcxvzq1UdPwu94dto2PUfhg==", - "requires": { - "@types/d3-scale": "^2.1.1", - "d3-scale": "^2.2.2" - } - }, - "d3-scale": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", - "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", - "requires": { - "d3-array": "^1.2.0", - "d3-collection": "1", - "d3-format": "1", - "d3-interpolate": "1", - "d3-time": "1", - "d3-time-format": "2" - } - } - } - }, - "@superset-ui/legacy-plugin-chart-horizon": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-horizon/-/legacy-plugin-chart-horizon-0.15.13.tgz", - "integrity": "sha512-XjoCmjajkaLA9Grgo+6cODmr5v0e1lKPSxDGuyNwg3c5gIu2Tq8BdJqV/mKGyDXAdbsKiIJGUIq/baUDeYi62Q==", - "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "d3-array": "^2.0.3", - "d3-scale": "^3.0.1", - "prop-types": "^15.6.2" - }, - "dependencies": { - "d3-array": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.8.0.tgz", - "integrity": "sha512-6V272gsOeg7+9pTW1jSYOR1QE37g95I3my1hBmY+vOUNHRrk9yt4OTz/gK7PMkVAVDrYYq4mq3grTiZ8iJdNIw==" - }, - "d3-scale": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", - "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", - "requires": { - "d3-array": "^2.3.0", - "d3-format": "1 - 2", - "d3-interpolate": "1.2.0 - 2", - "d3-time": "1 - 2", - "d3-time-format": "2 - 3" - } - } - } - }, - "@superset-ui/legacy-plugin-chart-map-box": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-map-box/-/legacy-plugin-chart-map-box-0.15.13.tgz", - "integrity": "sha512-/lsjIZ4gGqhlBFzOWm8sjFG7IEYnSj6B49aVrEuTLR4HOtEgW8ilK/RaVg1R2pPLU4WKxV3/2kpBYT4mUv5JgQ==", - "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "immutable": "^3.8.2", - "mapbox-gl": "^0.53.0", - "prop-types": "^15.6.2", - "react-map-gl": "^4.0.10", - "supercluster": "^4.1.1", - "viewport-mercator-project": "^6.1.1" - }, - "dependencies": { - "immutable": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", - "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" - } - } - }, - "@superset-ui/legacy-plugin-chart-paired-t-test": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-paired-t-test/-/legacy-plugin-chart-paired-t-test-0.15.13.tgz", - "integrity": "sha512-7GHMxYIX6iCJuFUCtA0DZsKVgZPzjPhwF+cKUfp4DLDjGW0T54bP1Kvq7MJIdn9YKEPD2bEl9dA49bjpaQeuZw==", - "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "distributions": "^1.0.0", - "prop-types": "^15.6.2", - "reactable-arc": "0.15.0" - }, - "dependencies": { - "reactable-arc": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/reactable-arc/-/reactable-arc-0.15.0.tgz", - "integrity": "sha512-XH1mryI/xvbYb3lCVOU3rx/KRacDE0PDa45KazL/PPTM0AgPZ/awVmCAxRi179BpjbStk7cgCyFjI2oYJ28E8A==" - } - } - }, - "@superset-ui/legacy-plugin-chart-parallel-coordinates": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-parallel-coordinates/-/legacy-plugin-chart-parallel-coordinates-0.15.13.tgz", - "integrity": "sha512-W/OgD6P79wLPm9g+A+aMq/VYkJ1sHJPeKjIev2DSr2sVo/CNmryq/fyQxgipdEUhJQrOe7ke1n5nBBKDz7wg/w==", - "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "d3": "^3.5.17", - "prop-types": "^15.7.2" - } - }, - "@superset-ui/legacy-plugin-chart-partition": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-partition/-/legacy-plugin-chart-partition-0.15.13.tgz", - "integrity": "sha512-NCmueZbjIC0gIUlWXdARagvxDuM+zLG5EUqw9x5JHvQctIj6D0aQ170F/1JWx48jnRD3Rp33/CKFSJ+j+KSyMg==", - "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "d3": "^3.5.17", - "d3-hierarchy": "^1.1.8", - "prop-types": "^15.6.2" - } - }, - "@superset-ui/legacy-plugin-chart-pivot-table": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-pivot-table/-/legacy-plugin-chart-pivot-table-0.15.13.tgz", - "integrity": "sha512-SmIae3YAy5mXcI/zlEnRIun5ztgY9kTXEkdC61QqGiQ7d1tenzxneoNIeifkUh20L/QnJqP1lnPJlgB4jD33lw==", - "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "d3": "^3.5.17", - "datatables.net-bs": "^1.10.15", - "prop-types": "^15.6.2" - } - }, - "@superset-ui/legacy-plugin-chart-rose": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-rose/-/legacy-plugin-chart-rose-0.15.13.tgz", - "integrity": "sha512-C5/Llb+eJBsrVPm/KJbfMPyvMovA0njYw75+YKnE08XlZDZ6nFwMbhb6MV79kqZgVmL++inJ1I5YSxzVrQ2w3A==", - "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "d3": "^3.5.17", - "nvd3": "1.8.6", - "prop-types": "^15.6.2" - } - }, - "@superset-ui/legacy-plugin-chart-sankey": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sankey/-/legacy-plugin-chart-sankey-0.15.13.tgz", - "integrity": "sha512-1wPA4+En339Ex5t/YNR1f+4VwCXmtyFMaTBVeMlUZ09ggiULPZqEqX9GLVYa+j1VPCiW3DKM+wv1nZPo8Pj9lQ==", - "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "d3": "^3.5.17", - "d3-sankey": "^0.4.2", - "prop-types": "^15.6.2" - } - }, - "@superset-ui/legacy-plugin-chart-sankey-loop": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sankey-loop/-/legacy-plugin-chart-sankey-loop-0.15.13.tgz", - "integrity": "sha512-/7Kag4GEq00tQpV0V/H7VRoPXm+w6q4Rz8iVu7/fJyKM8Nls25yCSescpr3usPP+PX1947jXpM+OyxABUhzocQ==", - "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "d3-sankey-diagram": "^0.7.3", - "d3-selection": "^1.4.0", - "prop-types": "^15.6.2" - } - }, - "@superset-ui/legacy-plugin-chart-sunburst": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sunburst/-/legacy-plugin-chart-sunburst-0.15.13.tgz", - "integrity": "sha512-C4e2YylIkbu1j1HU6Ci+ioYSBVhQ0WTN6yeI2H+yGjvVZO7vuUC1zVipklU90Ofaa1Zy3JuwdhUIiaZOyh9J4g==", - "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "d3": "^3.5.17", - "prop-types": "^15.6.2" - } - }, - "@superset-ui/legacy-plugin-chart-treemap": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-treemap/-/legacy-plugin-chart-treemap-0.15.13.tgz", - "integrity": "sha512-Owq0n7NDiWzVELMo/ko5Io0J9smIono61N6LYXUEqD+h2OEc37rHfLOWTHdGjPQKCfOn3/rOF/YEvslh5+gpPw==", - "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "d3-hierarchy": "^1.1.8", - "d3-selection": "^1.4.0", - "prop-types": "^15.6.2" - } - }, - "@superset-ui/legacy-plugin-chart-world-map": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-world-map/-/legacy-plugin-chart-world-map-0.15.13.tgz", - "integrity": "sha512-UDk0gWWE9Hpu1ETOcNa0eox0mfje8opHuhotXZeqpTLNws7lIwej5th+BhuFy9Yc/gKt2c6ABUjFfagyqAdslQ==", - "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "d3": "^3.5.17", - "d3-array": "^2.4.0", - "d3-color": "^1.4.1", - "datamaps": "^0.5.8", - "prop-types": "^15.6.2" - }, - "dependencies": { - "d3-array": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.8.0.tgz", - "integrity": "sha512-6V272gsOeg7+9pTW1jSYOR1QE37g95I3my1hBmY+vOUNHRrk9yt4OTz/gK7PMkVAVDrYYq4mq3grTiZ8iJdNIw==" - }, - "d3-color": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", - "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" - } - } - }, - "@superset-ui/legacy-preset-chart-big-number": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-big-number/-/legacy-preset-chart-big-number-0.15.13.tgz", - "integrity": "sha512-1Skbacxo0vswa6YG3qmql5fxG5XTm4kRBryH4sWKlFdw46AkONYci0khP/7Ql1mjOxmbt6hjbUkaC6mbs84BLw==", - "requires": { - "@data-ui/xy-chart": "^0.0.84", - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "@types/d3-color": "^1.2.2", - "@types/shortid": "^0.0.29", - "d3-color": "^1.2.3", - "shortid": "^2.2.14" - } - }, - "@superset-ui/legacy-preset-chart-deckgl": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-deckgl/-/legacy-preset-chart-deckgl-0.3.1.tgz", - "integrity": "sha512-mHX2vWJH+1Hq5QuQu2wlKstLgql3/ukNU1SVoLyjDX/BdTqeZiISMCumj2UUMbf0cK1zjHPHrVUCLzJwfsQbUQ==", - "requires": { - "@math.gl/web-mercator": "^3.2.2", - "@types/d3-array": "^2.0.0", - "bootstrap-slider": "^10.0.0", - "d3-array": "^1.2.4", - "d3-color": "^1.2.0", - "d3-scale": "^2.1.2", - "deck.gl": "7.1.11", - "jquery": "^3.4.1", - "lodash": "^4.17.15", - "mapbox-gl": "^0.53.0", - "moment": "^2.20.1", - "mousetrap": "^1.6.1", - "prop-types": "^15.6.0", - "react-bootstrap-slider": "2.1.5", - "react-map-gl": "^4.0.10", - "underscore": "^1.8.3", - "urijs": "^1.18.10", - "xss": "^1.0.6" - } - }, - "@superset-ui/legacy-preset-chart-nvd3": { - "version": "0.15.16", - "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-nvd3/-/legacy-preset-chart-nvd3-0.15.16.tgz", - "integrity": "sha512-BwTslkj/0dp+YK88AvMlnz24mNz5kpAyLloFD3Bjjl0BzqSR/KhhYaXn5n2XNWAzJnhtGMtRxDShI/p4GeMCAw==", - "requires": { - "@data-ui/xy-chart": "^0.0.84", - "@superset-ui/chart-controls": "0.15.15", - "@superset-ui/core": "0.15.15", - "d3": "^3.5.17", - "d3-tip": "^0.9.1", - "dompurify": "^2.0.6", - "fast-safe-stringify": "^2.0.6", - "lodash": "^4.17.11", - "mathjs": "^8.0.1", - "moment": "^2.20.1", - "nvd3-fork": "2.0.3", - "prop-types": "^15.6.2", - "urijs": "^1.18.10" - }, "dependencies": { "@superset-ui/chart-controls": { - "version": "0.15.15", - "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.15.tgz", - "integrity": "sha512-DrvkHLztMyjEqvosu4op+CpxMfPVEHfz5MFYc1jPlREItwHEdCYUAfPk3G29a+pr82MzKQTepvVGWE/5l7C1gQ==", + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", "requires": { - "@superset-ui/core": "0.15.15", + "@superset-ui/core": "0.15.17", "lodash": "^4.17.15", "prop-types": "^15.7.2" } }, "@superset-ui/core": { - "version": "0.15.15", - "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.15.tgz", - "integrity": "sha512-lrdKncYwO6jebQ8C/jvhq7wOrg3fpC+xdnxvJpSeVaICvces8Iv6669t8fbQKBL/rdhQ4aCfg/A3mk/Pa5qQ5A==", + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", "requires": { "@babel/runtime": "^7.1.2", "@emotion/core": "^10.0.28", @@ -17065,9 +16666,2255 @@ } }, "d3-array": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.0.tgz", - "integrity": "sha512-yOokB8GozB6GAubW9n7phLdRugC8TgEjF6V1cX/q78L80d2tLirUnc0jvDSSF622JJJTmtnJOe9+WKs+yS5GFQ==" + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-plugin-chart-chord": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-chord/-/legacy-plugin-chart-chord-0.15.17.tgz", + "integrity": "sha512-VJuTPCg3ww7Q43Uv2q0w/2FVrzt+Jfu+BF7nyrE1NAwuPbyZ8xmUlU19QSevxr/l3ALuFbD4Nqg1zuz160RBTQ==", + "requires": { + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "d3": "^3.5.17", + "prop-types": "^15.6.2", + "react": "^16.13.1" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-plugin-chart-country-map": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-country-map/-/legacy-plugin-chart-country-map-0.15.17.tgz", + "integrity": "sha512-FOy4dMJosXBdpDUg26Bq/UKsS1qG3gfJISaW+q6M17+OEd/zyMjQjZ75NHN2vvCTlBCkYRsvhUV8pAH/YkGSqQ==", + "requires": { + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "d3": "^3.5.17", + "d3-array": "^2.0.3", + "prop-types": "^15.6.2" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-plugin-chart-event-flow": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-event-flow/-/legacy-plugin-chart-event-flow-0.15.17.tgz", + "integrity": "sha512-TnSxfg9aC4sYtgSWMS+P2ju91Nn8Wfc5O8OZ4ZIyeOrBoz9RtPdQOQEIaDE9+NIJZs0WSbkbiNVnfsg6aYnksA==", + "requires": { + "@data-ui/event-flow": "^0.0.84", + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "prop-types": "^15.6.2" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-plugin-chart-force-directed": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-force-directed/-/legacy-plugin-chart-force-directed-0.15.17.tgz", + "integrity": "sha512-gJgKp/4X/B09RS5o4/PlCw1WCByeDbOG0gbKjkRJ4Gm4ldbhocvHvkUEkIA3meu3vgpel4jFyCX1aNKZXIxuQg==", + "requires": { + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "d3": "^3.5.17", + "prop-types": "^15.7.2" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-plugin-chart-heatmap": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-heatmap/-/legacy-plugin-chart-heatmap-0.15.17.tgz", + "integrity": "sha512-BFf4l+/YmIIMRruAapDX7owrwXPktF5ZizND2fYQM3FQ4FBLWRXvmeGLSMKo0bwUnWUNmoQSDjJKYhTrij1Hdg==", + "requires": { + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "d3": "^3.5.17", + "d3-svg-legend": "^1.x", + "d3-tip": "^0.9.1", + "prop-types": "^15.6.2" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-plugin-chart-histogram": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-histogram/-/legacy-plugin-chart-histogram-0.15.17.tgz", + "integrity": "sha512-nFdajSp6fchB2laMYCFqQS2Ya+vIYc6gq49kRedP7vhxHPxMMx2Ksm89yXgLM4XRHHUkzMJX3995UGc61UkFuw==", + "requires": { + "@data-ui/histogram": "^0.0.84", + "@data-ui/theme": "^0.0.84", + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "@vx/legend": "^0.0.198", + "@vx/responsive": "^0.0.197", + "@vx/scale": "^0.0.197", + "prop-types": "^15.6.2" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/group": { + "version": "0.0.198", + "resolved": "https://registry.npmjs.org/@vx/group/-/group-0.0.198.tgz", + "integrity": "sha512-0PivE+fWZlPkSzFO/is5m4VSSv3pg+sS1yxYAZHbNffUvn472WDWptriHvoUIPQe0lOXhTSrc73UQzew9GtW/g==", + "requires": { + "@types/classnames": "^2.2.9", + "@types/react": "*", + "classnames": "^2.2.5", + "prop-types": "^15.6.2" + } + }, + "@vx/legend": { + "version": "0.0.198", + "resolved": "https://registry.npmjs.org/@vx/legend/-/legend-0.0.198.tgz", + "integrity": "sha512-3S2/yP6IvkkhUlTj6In5M1OrzY1OaT1D06hRxuiOLAbaXTerhbUGwIjGSNoovQM6JebFlbWnnA5xH1SKgw5GGA==", + "requires": { + "@types/classnames": "^2.2.9", + "@types/d3-scale": "^2.1.1", + "@types/react": "*", + "@vx/group": "0.0.198", + "classnames": "^2.2.5", + "prop-types": "^15.5.10" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "@vx/scale": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/scale/-/scale-0.0.197.tgz", + "integrity": "sha512-FF0POm9rh66I3Om5DsuxynwWU+Q645aTF47vgP2dVDeOOq3Oet7CZzmXLDh3W6nVcxvzq1UdPwu94dto2PUfhg==", + "requires": { + "@types/d3-scale": "^2.1.1", + "d3-scale": "^2.2.2" + }, + "dependencies": { + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + } + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-plugin-chart-horizon": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-horizon/-/legacy-plugin-chart-horizon-0.15.17.tgz", + "integrity": "sha512-BrqeaE97NwObv287lsvlFtrAkoaUxRCNkmjATcTEyFXvQGUsqb/3LJdqefEFxTtx0E+ivS7Py1vcxF3YSmPY7Q==", + "requires": { + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "d3-array": "^2.0.3", + "d3-scale": "^3.0.1", + "prop-types": "^15.6.2" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-plugin-chart-map-box": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-map-box/-/legacy-plugin-chart-map-box-0.15.17.tgz", + "integrity": "sha512-GFputmMt3NaMtHOJ+AmO/H/b5bNhhrtldWwNYS7ZpaV26KYEZ2njx7kHAowCgDAz4HluDW+qyEbuz8PZt9+WfA==", + "requires": { + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "immutable": "^3.8.2", + "mapbox-gl": "^0.53.0", + "prop-types": "^15.6.2", + "react-map-gl": "^4.0.10", + "supercluster": "^4.1.1", + "viewport-mercator-project": "^6.1.1" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + }, + "immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" + } + } + }, + "@superset-ui/legacy-plugin-chart-paired-t-test": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-paired-t-test/-/legacy-plugin-chart-paired-t-test-0.15.17.tgz", + "integrity": "sha512-GGVBkLElWkZ7fYAi++UBisC7s/WKqpZa8qUkNjY07YkJqx0LBa+eV+N/D4x+loTekRJY0pUt4ZnuI0R2va7AzA==", + "requires": { + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "distributions": "^1.0.0", + "prop-types": "^15.6.2", + "reactable-arc": "0.15.0" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-plugin-chart-parallel-coordinates": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-parallel-coordinates/-/legacy-plugin-chart-parallel-coordinates-0.15.17.tgz", + "integrity": "sha512-R4aZ8WS0P7D/VnP0eY5TMQyscdKE0NkufDaTYBcV9zLzfFibx9Nk560tzZoZcH2cdqw9eH1DjfFYDr8qRXPTCA==", + "requires": { + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "d3": "^3.5.17", + "prop-types": "^15.7.2" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-plugin-chart-partition": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-partition/-/legacy-plugin-chart-partition-0.15.17.tgz", + "integrity": "sha512-PRoefZD+9jtKZzNuIiXEH4DRuvxhtgTz2PCP4t13kOrs2KAMRs4aCLTURpIUVpt96QyCOE5d3MRyrwVWABf/Hw==", + "requires": { + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "d3": "^3.5.17", + "d3-hierarchy": "^1.1.8", + "prop-types": "^15.6.2" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-plugin-chart-pivot-table": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-pivot-table/-/legacy-plugin-chart-pivot-table-0.15.17.tgz", + "integrity": "sha512-/Hyx+TH93ZJFs/9XY0P+Km5dH57teO2kEh4+bWToI+Q4NRCg+GzAg+/oaXUnGrASiVxExfl+iWK6Wz6IRVFBOg==", + "requires": { + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "d3": "^3.5.17", + "datatables.net-bs": "^1.10.15", + "prop-types": "^15.6.2" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-plugin-chart-rose": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-rose/-/legacy-plugin-chart-rose-0.15.17.tgz", + "integrity": "sha512-F4HBioJAVv1FfctgQHZXDnlX3B1ThBDMyPOC2s+hNO/xmJ5ufKyBtoITdai0wZd0NDlZoN8SH3Rvnov2I4St3A==", + "requires": { + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "d3": "^3.5.17", + "nvd3": "1.8.6", + "prop-types": "^15.6.2" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-plugin-chart-sankey": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sankey/-/legacy-plugin-chart-sankey-0.15.17.tgz", + "integrity": "sha512-MTVAYUPKch562vdz2LlPUREkooCS3WIb+KX1AcrQau6SPqCfm54WgqqG35xEXcA8cy2SM2zxcLYf6dHnC5Ynmg==", + "requires": { + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "d3": "^3.5.17", + "d3-sankey": "^0.4.2", + "prop-types": "^15.6.2" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-plugin-chart-sankey-loop": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sankey-loop/-/legacy-plugin-chart-sankey-loop-0.15.17.tgz", + "integrity": "sha512-+OXQjFJs8N1aRXmf1MDb7ljoKo2PZSur1Ae/vu4zRJrcr+s9FaHSXiG9UEP3krLas4Bsm8le4jYatQNP4NtgaQ==", + "requires": { + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "d3-sankey-diagram": "^0.7.3", + "d3-selection": "^1.4.0", + "prop-types": "^15.6.2" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-plugin-chart-sunburst": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sunburst/-/legacy-plugin-chart-sunburst-0.15.17.tgz", + "integrity": "sha512-GrthNQKCgBZIm1Tu9xvGAaGXkrQUi8PRliApp022KDQ6drqfHZ08cur/MsuQOJb+g3vxEG3plw87Evdg1FU79g==", + "requires": { + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "d3": "^3.5.17", + "prop-types": "^15.6.2" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-plugin-chart-treemap": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-treemap/-/legacy-plugin-chart-treemap-0.15.17.tgz", + "integrity": "sha512-yzuqs4PPILbJfx+a7Syv+idMdv9UZpBUYsLXsijzWGFSUOE0Q724UKmEkLmDgsSfPPZbbJWvovHTwJiOY4ZCtw==", + "requires": { + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "d3-hierarchy": "^1.1.8", + "d3-selection": "^1.4.0", + "prop-types": "^15.6.2" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-plugin-chart-world-map": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-world-map/-/legacy-plugin-chart-world-map-0.15.17.tgz", + "integrity": "sha512-2vyNnLgFna6nJ0/jR+DYn02PMUm9G/2yL0wFzQ5COQ7s9RDICLqf6nXUljB6NXILLKD4L3pUj5h0Sx9t045nog==", + "requires": { + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "d3": "^3.5.17", + "d3-array": "^2.4.0", + "d3-color": "^1.4.1", + "datamaps": "^0.5.8", + "prop-types": "^15.6.2" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-color": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", + "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-preset-chart-big-number": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-big-number/-/legacy-preset-chart-big-number-0.15.17.tgz", + "integrity": "sha512-dJjA1OS7utHicUIyyi9eQbo3ziBaq4XuABFlkwDI/dv2plJEj5LkmefFjBicIQT9odptxUvn7D2haGOdCNL0ZQ==", + "requires": { + "@data-ui/xy-chart": "^0.0.84", + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "@types/d3-color": "^1.2.2", + "@types/shortid": "^0.0.29", + "d3-color": "^1.2.3", + "shortid": "^2.2.14" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/legacy-preset-chart-deckgl": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-deckgl/-/legacy-preset-chart-deckgl-0.3.1.tgz", + "integrity": "sha512-mHX2vWJH+1Hq5QuQu2wlKstLgql3/ukNU1SVoLyjDX/BdTqeZiISMCumj2UUMbf0cK1zjHPHrVUCLzJwfsQbUQ==", + "requires": { + "@math.gl/web-mercator": "^3.2.2", + "@types/d3-array": "^2.0.0", + "bootstrap-slider": "^10.0.0", + "d3-array": "^1.2.4", + "d3-color": "^1.2.0", + "d3-scale": "^2.1.2", + "deck.gl": "7.1.11", + "jquery": "^3.4.1", + "lodash": "^4.17.15", + "mapbox-gl": "^0.53.0", + "moment": "^2.20.1", + "mousetrap": "^1.6.1", + "prop-types": "^15.6.0", + "react-bootstrap-slider": "2.1.5", + "react-map-gl": "^4.0.10", + "underscore": "^1.8.3", + "urijs": "^1.18.10", + "xss": "^1.0.6" + } + }, + "@superset-ui/legacy-preset-chart-nvd3": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-nvd3/-/legacy-preset-chart-nvd3-0.15.17.tgz", + "integrity": "sha512-04wV1CIzRIglknR0YxwffiRC3OvF1jo2sr6I7iBamXUtD/HxMXr+vlkmrnqIjAKjQ/rKtyll0fjdlTCX9pWo0A==", + "requires": { + "@data-ui/xy-chart": "^0.0.84", + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "d3": "^3.5.17", + "d3-tip": "^0.9.1", + "dompurify": "^2.0.6", + "fast-safe-stringify": "^2.0.6", + "lodash": "^4.17.11", + "mathjs": "^8.0.1", + "moment": "^2.20.1", + "nvd3-fork": "2.0.3", + "prop-types": "^15.6.2", + "urijs": "^1.18.10" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + } + } + }, + "@superset-ui/plugin-chart-echarts": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-echarts/-/plugin-chart-echarts-0.15.17.tgz", + "integrity": "sha512-1EG07no/8+uCd6DDqkGQLoFqEou1JXee59lQeb2mujtB306P+JpT+Yf2L7Uo/5oPD5hAVSiZTHBKNdghDymnUQ==", + "requires": { + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", + "@types/echarts": "^4.6.3", + "@types/mathjs": "^6.0.7", + "echarts": "^5.0.0", + "mathjs": "^8.0.1" + }, + "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" }, "d3-interpolate": { "version": "1.4.0", @@ -17099,27 +18946,14 @@ } } }, - "@superset-ui/plugin-chart-echarts": { - "version": "0.15.14", - "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-echarts/-/plugin-chart-echarts-0.15.14.tgz", - "integrity": "sha512-8a08AGi+a2B6f6PaZNQTWcv645ub0zRyI/M7xD/7sDIsK7lxs0AktTpBo/6DzXdr5o6bY9TBewuTyad0SCv5qA==", - "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", - "@types/echarts": "^4.6.3", - "@types/mathjs": "^6.0.7", - "echarts": "^4.9.0", - "mathjs": "^8.0.1" - } - }, "@superset-ui/plugin-chart-table": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-table/-/plugin-chart-table-0.15.13.tgz", - "integrity": "sha512-Lu5r/3ydMXp3Ko/LBHqrxs4FN/ellJODx/4457sr0ggWeSUbCg6YMTAdpzOeuZFdgXvxV0TpY5ADYcCd9xdx6A==", + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-table/-/plugin-chart-table-0.15.17.tgz", + "integrity": "sha512-ZXVZtfwEpTzxepFnUk6kzJptQEgxmX/MsYVKYG8OjQb9n7s9PNvP2SKI7GFhpFXW2A9zlwcD7pCs3FMm2lPMIw==", "requires": { "@emotion/core": "^10.0.28", - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", "@types/d3-array": "^2.0.0", "@types/match-sorter": "^4.0.0", "@types/react-table": "^7.0.19", @@ -17132,20 +18966,103 @@ "xss": "^1.0.6" }, "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, "d3-array": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.8.0.tgz", - "integrity": "sha512-6V272gsOeg7+9pTW1jSYOR1QE37g95I3my1hBmY+vOUNHRrk9yt4OTz/gK7PMkVAVDrYYq4mq3grTiZ8iJdNIw==" + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-scale": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } } } }, "@superset-ui/plugin-chart-word-cloud": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-word-cloud/-/plugin-chart-word-cloud-0.15.13.tgz", - "integrity": "sha512-D1wbSN1ztYlrLvxdmVYyq3JAvWAJp0jVskrDFtLwyledL2NccAcQgI8UjkZNE946xi1u65CxvX7c++lqkVPFDA==", + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-word-cloud/-/plugin-chart-word-cloud-0.15.17.tgz", + "integrity": "sha512-GcVL6dEvlLrawPmlAB1toR/Drm9EKpu3olQw6WkgqV5Q21vieY3iOYXn0v2RxAkEteEnkPTcp2fABXzhLbQuSA==", "requires": { - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", "@types/d3-cloud": "^1.2.1", "@types/d3-scale": "^2.0.2", "d3-cloud": "^1.2.5", @@ -17154,10 +19071,73 @@ "encodable": "^0.7.6" }, "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, "d3-array": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.8.0.tgz", - "integrity": "sha512-6V272gsOeg7+9pTW1jSYOR1QE37g95I3my1hBmY+vOUNHRrk9yt4OTz/gK7PMkVAVDrYYq4mq3grTiZ8iJdNIw==" + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } }, "d3-scale": { "version": "3.2.3", @@ -17170,18 +19150,26 @@ "d3-time": "1 - 2", "d3-time-format": "2 - 3" } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } } } }, "@superset-ui/preset-chart-xy": { - "version": "0.15.13", - "resolved": "https://registry.npmjs.org/@superset-ui/preset-chart-xy/-/preset-chart-xy-0.15.13.tgz", - "integrity": "sha512-79Mm0y+QUVth8spNiR3b87P1bk56UVkweXwbAl49FqdB4FR92UhgmrtvVVb+k5bCgdBltrD0LyxcEl5mhMfPgg==", + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/preset-chart-xy/-/preset-chart-xy-0.15.17.tgz", + "integrity": "sha512-1rMeD4zcQBTj/wxZ8n1H0xDhAk8SJ7ATSeW8rvqxhrJYRqC8q7Q8Ep1GuxqLKmLdg7MwpNtX9KTuw6uz/DnSMw==", "requires": { "@data-ui/theme": "^0.0.84", "@data-ui/xy-chart": "^0.0.84", - "@superset-ui/chart-controls": "0.15.13", - "@superset-ui/core": "0.15.13", + "@superset-ui/chart-controls": "0.15.17", + "@superset-ui/core": "0.15.17", "@vx/axis": "^0.0.198", "@vx/legend": "^0.0.198", "@vx/scale": "^0.0.197", @@ -17191,6 +19179,49 @@ "reselect": "^4.0.0" }, "dependencies": { + "@superset-ui/chart-controls": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.15.17.tgz", + "integrity": "sha512-RjuA3mBZuNSOa0LJZ7M3rvNJT7DYXrKLYrKKV2e3sLAW4i3FuJdxGD4HD0+W6eRRHb0YybDMaysa31ulAOGrBg==", + "requires": { + "@superset-ui/core": "0.15.17", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + } + }, + "@superset-ui/core": { + "version": "0.15.17", + "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.15.17.tgz", + "integrity": "sha512-VjrWkxMvVSU/LB0AO6Qf/VHOdk3Jweug78FfnXlKmFK3Z4eRtfrCpKZOjEwMqKhhdppkU34MOfduZYq0uS6Ocw==", + "requires": { + "@babel/runtime": "^7.1.2", + "@emotion/core": "^10.0.28", + "@emotion/styled": "^10.0.27", + "@types/d3-format": "^1.3.0", + "@types/d3-interpolate": "^1.3.1", + "@types/d3-scale": "^2.1.1", + "@types/d3-time": "^1.0.9", + "@types/d3-time-format": "^2.1.0", + "@types/lodash": "^4.14.149", + "@types/rison": "0.0.6", + "@vx/responsive": "^0.0.197", + "csstype": "^2.6.4", + "d3-format": "^1.3.2", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.0", + "d3-time": "^1.0.10", + "d3-time-format": "^2.2.0", + "emotion-theming": "^10.0.27", + "fetch-retry": "^4.0.1", + "jed": "^1.1.1", + "lodash": "^4.17.11", + "pretty-ms": "^7.0.0", + "react-error-boundary": "^1.2.5", + "reselect": "^4.0.0", + "rison": "^0.1.1", + "whatwg-fetch": "^3.0.0" + } + }, "@vx/axis": { "version": "0.0.198", "resolved": "https://registry.npmjs.org/@vx/axis/-/axis-0.0.198.tgz", @@ -17244,6 +19275,18 @@ "resolved": "https://registry.npmjs.org/@vx/point/-/point-0.0.198.tgz", "integrity": "sha512-oFlw8uBLf4JDX7OJc+7eQXcnlLszdQgEs531u0t6HNpARQY/jTeeMLVUlp8sNF0XBOC+iVHU8Qe8TJdz/ONBAA==" }, + "@vx/responsive": { + "version": "0.0.197", + "resolved": "https://registry.npmjs.org/@vx/responsive/-/responsive-0.0.197.tgz", + "integrity": "sha512-Qv15PJ/Hy79LjyfJ/9E8z+zacKAnD43O2Jg9wvB6PFSNs73xPEDy/mHTYxH+FZv94ruAE3scBO0330W29sQpyg==", + "requires": { + "@types/lodash": "^4.14.146", + "@types/react": "*", + "lodash": "^4.17.10", + "prop-types": "^15.6.1", + "resize-observer-polyfill": "1.5.1" + } + }, "@vx/scale": { "version": "0.0.197", "resolved": "https://registry.npmjs.org/@vx/scale/-/scale-0.0.197.tgz", @@ -17251,6 +19294,26 @@ "requires": { "@types/d3-scale": "^2.1.1", "d3-scale": "^2.2.2" + }, + "dependencies": { + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + } } }, "@vx/shape": { @@ -17284,17 +19347,37 @@ "reduce-css-calc": "^1.3.0" } }, + "d3-array": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, "d3-scale": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", - "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz", + "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==", "requires": { - "d3-array": "^1.2.0", - "d3-collection": "1", - "d3-format": "1", - "d3-interpolate": "1", - "d3-time": "1", - "d3-time-format": "2" + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "1 - 2", + "d3-time-format": "2 - 3" + } + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" } } } @@ -18849,9 +20932,9 @@ } }, "@types/echarts": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@types/echarts/-/echarts-4.9.0.tgz", - "integrity": "sha512-9QIAUe6cxM5GyGNCIhlEwf7l5oclZDVM0HNRfehPx3dDUt1Jfhbvp/U2wfgwtL/IDqyASBVs1zu4qyaCsuJINA==", + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/@types/echarts/-/echarts-4.9.2.tgz", + "integrity": "sha512-ycAmdt/PbQEuj+9cI9O0mZV6Dd+R1+ngs9S8P7Cbxj8RWXbL8NOvnzXdJRvR/+9lx/hq/O1rdLxXYXXTVImujw==", "requires": { "@types/zrender": "*" } @@ -19039,9 +21122,9 @@ "integrity": "sha512-JK7HNHXZA7i/nEp6fbNAxoX/1j1ysZXmv2/nlkt2UpX1LiUWKLtyt/dMmDTlMPR6t6PkwMmIr2W2AAyu6oELNw==" }, "@types/mathjs": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/@types/mathjs/-/mathjs-6.0.7.tgz", - "integrity": "sha512-UPpG34wVjlr8uSijJ747q0SmC459t294xm/3Ed8GAnqM/I2K786WgCLQ4BO4lIsM07Gj1UhO7x0n0TSfqO0DNQ==", + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@types/mathjs/-/mathjs-6.0.8.tgz", + "integrity": "sha512-/oDt+o/q3cTHgOsYPgvHWw6xG8ZhIWMNnz8WJMogs2jeCEHz4PODOzrT3jzRzf7UeX7nPLXhqL+cV4hOfu1mWA==", "requires": { "decimal.js": "^10.0.0" } @@ -22888,28 +24971,28 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true, "optional": true }, "aproba": { "version": "1.2.0", - "resolved": false, + "resolved": "", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.5", - "resolved": false, + "resolved": "", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "dev": true, "optional": true, @@ -22920,14 +25003,14 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true, "optional": true }, "brace-expansion": { "version": "1.1.11", - "resolved": false, + "resolved": "", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "optional": true, @@ -22938,35 +25021,35 @@ }, "code-point-at": { "version": "1.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true, "optional": true }, "concat-map": { "version": "0.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true, "optional": true }, "console-control-strings": { "version": "1.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true, "optional": true }, "core-util-is": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true, "optional": true }, "debug": { "version": "4.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "optional": true, @@ -22976,35 +25059,35 @@ }, "deep-extend": { "version": "0.6.0", - "resolved": false, + "resolved": "", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "optional": true }, "delegates": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true, "optional": true }, "fs.realpath": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true, "optional": true }, "gauge": { "version": "2.7.4", - "resolved": false, + "resolved": "", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, "optional": true, @@ -23021,7 +25104,7 @@ }, "glob": { "version": "7.1.3", - "resolved": false, + "resolved": "", "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "optional": true, @@ -23036,14 +25119,14 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.24", - "resolved": false, + "resolved": "", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "optional": true, @@ -23053,7 +25136,7 @@ }, "ignore-walk": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "dev": true, "optional": true, @@ -23063,7 +25146,7 @@ }, "inflight": { "version": "1.0.6", - "resolved": false, + "resolved": "", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "optional": true, @@ -23074,21 +25157,21 @@ }, "inherits": { "version": "2.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true, "optional": true }, "ini": { "version": "1.3.5", - "resolved": false, + "resolved": "", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "optional": true, @@ -23098,14 +25181,14 @@ }, "isarray": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", - "resolved": false, + "resolved": "", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "optional": true, @@ -23122,14 +25205,14 @@ }, "ms": { "version": "2.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true, "optional": true }, "needle": { "version": "2.3.0", - "resolved": false, + "resolved": "", "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "dev": true, "optional": true, @@ -23141,7 +25224,7 @@ }, "node-pre-gyp": { "version": "0.12.0", - "resolved": false, + "resolved": "", "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "dev": true, "optional": true, @@ -23160,7 +25243,7 @@ }, "nopt": { "version": "4.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "dev": true, "optional": true, @@ -23171,14 +25254,14 @@ }, "npm-bundled": { "version": "1.0.6", - "resolved": false, + "resolved": "", "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "dev": true, "optional": true }, "npm-packlist": { "version": "1.4.1", - "resolved": false, + "resolved": "", "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "dev": true, "optional": true, @@ -23189,7 +25272,7 @@ }, "npmlog": { "version": "4.1.2", - "resolved": false, + "resolved": "", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, "optional": true, @@ -23202,21 +25285,21 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true, "optional": true }, "object-assign": { "version": "4.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true, "optional": true }, "once": { "version": "1.4.0", - "resolved": false, + "resolved": "", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "optional": true, @@ -23226,21 +25309,21 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true, "optional": true }, "osenv": { "version": "0.1.5", - "resolved": false, + "resolved": "", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "dev": true, "optional": true, @@ -23251,21 +25334,21 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true, "optional": true }, "rc": { "version": "1.2.8", - "resolved": false, + "resolved": "", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "optional": true, @@ -23278,7 +25361,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": false, + "resolved": "", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "optional": true, @@ -23294,7 +25377,7 @@ }, "rimraf": { "version": "2.6.3", - "resolved": false, + "resolved": "", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, "optional": true, @@ -23304,49 +25387,49 @@ }, "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, "optional": true }, "safer-buffer": { "version": "2.1.2", - "resolved": false, + "resolved": "", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "optional": true }, "sax": { "version": "1.2.4", - "resolved": false, + "resolved": "", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true, "optional": true }, "semver": { "version": "5.7.0", - "resolved": false, + "resolved": "", "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true, "optional": true }, "string-width": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "optional": true, @@ -23358,7 +25441,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "optional": true, @@ -23368,7 +25451,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "optional": true, @@ -23378,21 +25461,21 @@ }, "strip-json-comments": { "version": "2.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true, "optional": true }, "util-deprecate": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true, "optional": true }, "wide-align": { "version": "1.1.3", - "resolved": false, + "resolved": "", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, "optional": true, @@ -23402,7 +25485,7 @@ }, "wrappy": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true, "optional": true @@ -25996,9 +28079,9 @@ } }, "dompurify": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.2.2.tgz", - "integrity": "sha512-BsGR4nDLaC5CNBnyT5I+d5pOeaoWvgVeg6Gq/aqmKYWMPR07131u60I80BvExLAJ0FQEIBQ1BTicw+C5+jOyrg==" + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.2.3.tgz", + "integrity": "sha512-8Hv7Q0FuwD9rWoB6qI2eZsfKbGXfoUVuGHHrE15vgk4ReOKwOkSgbqb2OMFtc0d5besOEkoLkcyuV10zQ2X5gw==" }, "domutils": { "version": "1.5.1", @@ -26092,11 +28175,19 @@ } }, "echarts": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/echarts/-/echarts-4.9.0.tgz", - "integrity": "sha512-+ugizgtJ+KmsJyyDPxaw2Br5FqzuBnyOWwcxPKO6y0gc5caYcfnEUIlNStx02necw8jmKmTafmpHhGo4XDtEIA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.0.0.tgz", + "integrity": "sha512-6SDcJbLVOcfQyjPg+spNU1+JVrkU1B9fzUa5tpbP/mMNUPyigCOJwcEIQAJSbp9jt5UP3EXvQR0vtYXIo9AjyA==", "requires": { - "zrender": "4.3.2" + "tslib": "1.10.0", + "zrender": "5.0.1" + }, + "dependencies": { + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + } } }, "editorconfig": { @@ -26266,9 +28357,9 @@ }, "dependencies": { "d3-array": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.8.0.tgz", - "integrity": "sha512-6V272gsOeg7+9pTW1jSYOR1QE37g95I3my1hBmY+vOUNHRrk9yt4OTz/gK7PMkVAVDrYYq4mq3grTiZ8iJdNIw==" + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz", + "integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg==" }, "d3-interpolate": { "version": "2.0.1", @@ -44161,6 +46252,11 @@ "has": "^1.0.1" } }, + "reactable-arc": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/reactable-arc/-/reactable-arc-0.15.0.tgz", + "integrity": "sha512-XH1mryI/xvbYb3lCVOU3rx/KRacDE0PDa45KazL/PPTM0AgPZ/awVmCAxRi179BpjbStk7cgCyFjI2oYJ28E8A==" + }, "reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", @@ -51398,9 +53494,19 @@ } }, "zrender": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-4.3.2.tgz", - "integrity": "sha512-bIusJLS8c4DkIcdiK+s13HiQ/zjQQVgpNohtd8d94Y2DnJqgM1yjh/jpDb8DoL6hd7r8Awagw8e3qK/oLaWr3g==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.0.1.tgz", + "integrity": "sha512-i8FNCKAKfF0EfZFJ6w2p30umBrCyy481/PePFQqPdtNgCl5Hp5z7/dovqb7soEoFkhNvhjJ/J4W9zFALeae6yA==", + "requires": { + "tslib": "1.10.0" + }, + "dependencies": { + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + } + } }, "zwitch": { "version": "1.0.5", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 17c582eb5b728..297700d8d0f81 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -67,32 +67,32 @@ "@emotion/core": "^10.0.35", "@superset-ui/chart-controls": "^0.15.13", "@superset-ui/core": "^0.15.13", - "@superset-ui/legacy-plugin-chart-calendar": "^0.15.13", - "@superset-ui/legacy-plugin-chart-chord": "^0.15.13", - "@superset-ui/legacy-plugin-chart-country-map": "^0.15.13", - "@superset-ui/legacy-plugin-chart-event-flow": "^0.15.13", - "@superset-ui/legacy-plugin-chart-force-directed": "^0.15.13", - "@superset-ui/legacy-plugin-chart-heatmap": "^0.15.13", - "@superset-ui/legacy-plugin-chart-histogram": "^0.15.13", - "@superset-ui/legacy-plugin-chart-horizon": "^0.15.13", - "@superset-ui/legacy-plugin-chart-map-box": "^0.15.13", - "@superset-ui/legacy-plugin-chart-paired-t-test": "^0.15.13", - "@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.15.13", - "@superset-ui/legacy-plugin-chart-partition": "^0.15.13", - "@superset-ui/legacy-plugin-chart-pivot-table": "^0.15.13", - "@superset-ui/legacy-plugin-chart-rose": "^0.15.13", - "@superset-ui/legacy-plugin-chart-sankey": "^0.15.13", - "@superset-ui/legacy-plugin-chart-sankey-loop": "^0.15.13", - "@superset-ui/legacy-plugin-chart-sunburst": "^0.15.13", - "@superset-ui/legacy-plugin-chart-treemap": "^0.15.13", - "@superset-ui/legacy-plugin-chart-world-map": "^0.15.13", - "@superset-ui/legacy-preset-chart-big-number": "^0.15.13", + "@superset-ui/legacy-plugin-chart-calendar": "^0.15.17", + "@superset-ui/legacy-plugin-chart-chord": "^0.15.17", + "@superset-ui/legacy-plugin-chart-country-map": "^0.15.17", + "@superset-ui/legacy-plugin-chart-event-flow": "^0.15.17", + "@superset-ui/legacy-plugin-chart-force-directed": "^0.15.17", + "@superset-ui/legacy-plugin-chart-heatmap": "^0.15.17", + "@superset-ui/legacy-plugin-chart-histogram": "^0.15.17", + "@superset-ui/legacy-plugin-chart-horizon": "^0.15.17", + "@superset-ui/legacy-plugin-chart-map-box": "^0.15.17", + "@superset-ui/legacy-plugin-chart-paired-t-test": "^0.15.17", + "@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.15.17", + "@superset-ui/legacy-plugin-chart-partition": "^0.15.17", + "@superset-ui/legacy-plugin-chart-pivot-table": "^0.15.17", + "@superset-ui/legacy-plugin-chart-rose": "^0.15.17", + "@superset-ui/legacy-plugin-chart-sankey": "^0.15.17", + "@superset-ui/legacy-plugin-chart-sankey-loop": "^0.15.17", + "@superset-ui/legacy-plugin-chart-sunburst": "^0.15.17", + "@superset-ui/legacy-plugin-chart-treemap": "^0.15.17", + "@superset-ui/legacy-plugin-chart-world-map": "^0.15.17", + "@superset-ui/legacy-preset-chart-big-number": "^0.15.17", "@superset-ui/legacy-preset-chart-deckgl": "^0.3.1", - "@superset-ui/legacy-preset-chart-nvd3": "^0.15.16", - "@superset-ui/plugin-chart-echarts": "^0.15.14", - "@superset-ui/plugin-chart-table": "^0.15.13", - "@superset-ui/plugin-chart-word-cloud": "^0.15.13", - "@superset-ui/preset-chart-xy": "^0.15.13", + "@superset-ui/legacy-preset-chart-nvd3": "^0.15.17", + "@superset-ui/plugin-chart-echarts": "^0.15.17", + "@superset-ui/plugin-chart-table": "^0.15.17", + "@superset-ui/plugin-chart-word-cloud": "^0.15.17", + "@superset-ui/preset-chart-xy": "^0.15.17", "@vx/responsive": "^0.0.195", "abortcontroller-polyfill": "^1.1.9", "antd": "^4.8.2", @@ -152,7 +152,7 @@ "react-select": "^3.1.0", "react-select-async-paginate": "^0.4.1", "react-sortable-hoc": "^1.11.0", - "react-split": "^2.0.9", + "react-split": "^2.0.4", "react-sticky": "^6.0.3", "react-syntax-highlighter": "^15.3.0", "react-table": "^7.2.1", From 329dcc314ed4a7166388926541ad2c728d368321 Mon Sep 17 00:00:00 2001 From: Victor Malai Date: Tue, 15 Dec 2020 10:03:53 +0200 Subject: [PATCH 07/43] fix: Fix style for error modal (#11996) * Fix style for error alert * Revert test code --- .../src/components/ErrorMessage/ErrorAlert.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/superset-frontend/src/components/ErrorMessage/ErrorAlert.tsx b/superset-frontend/src/components/ErrorMessage/ErrorAlert.tsx index 74ef9ffc54ad8..e31dcd7ebbab9 100644 --- a/superset-frontend/src/components/ErrorMessage/ErrorAlert.tsx +++ b/superset-frontend/src/components/ErrorMessage/ErrorAlert.tsx @@ -59,6 +59,11 @@ const ErrorModal = styled(Modal)<{ level: ErrorLevel }>` color: ${({ level, theme }) => theme.colors[level].dark2}; overflow-wrap: break-word; + .ant-modal-header { + background-color: ${({ level, theme }) => theme.colors[level].light2}; + padding: ${({ theme }) => 4 * theme.gridUnit}px; + } + .icon { margin-right: ${({ theme }) => 2 * theme.gridUnit}px; } @@ -66,7 +71,6 @@ const ErrorModal = styled(Modal)<{ level: ErrorLevel }>` .header { display: flex; align-items: center; - background-color: ${({ level, theme }) => theme.colors[level].light2}; font-size: ${({ theme }) => theme.typography.sizes.l}px; } `; From 20b1aa7d6cf1d6de382e2b4ebe5a8c3d8ca75e77 Mon Sep 17 00:00:00 2001 From: Daniel Vaz Gaspar Date: Tue, 15 Dec 2020 08:43:31 +0000 Subject: [PATCH 08/43] fix(reports): apply owners security validation (#12035) * fix(reports): apply owners security validation * fix pylint --- superset/reports/api.py | 15 ++++- superset/reports/commands/bulk_delete.py | 10 ++++ superset/reports/commands/delete.py | 9 +++ superset/reports/commands/exceptions.py | 5 ++ superset/reports/commands/update.py | 9 +++ tests/reports/api_tests.py | 75 ++++++++++++++++++++++++ 6 files changed, 122 insertions(+), 1 deletion(-) diff --git a/superset/reports/api.py b/superset/reports/api.py index 5bdcccab878b0..6a2234f2aa45b 100644 --- a/superset/reports/api.py +++ b/superset/reports/api.py @@ -34,6 +34,7 @@ ReportScheduleBulkDeleteFailedError, ReportScheduleCreateFailedError, ReportScheduleDeleteFailedError, + ReportScheduleForbiddenError, ReportScheduleInvalidError, ReportScheduleNotFoundError, ReportScheduleUpdateFailedError, @@ -192,6 +193,8 @@ def delete(self, pk: int) -> Response: properties: message: type: string + 403: + $ref: '#/components/responses/403' 404: $ref: '#/components/responses/404' 422: @@ -204,6 +207,8 @@ def delete(self, pk: int) -> Response: return self.response(200, message="OK") except ReportScheduleNotFoundError as ex: return self.response_404() + except ReportScheduleForbiddenError: + return self.response_403() except ReportScheduleDeleteFailedError as ex: logger.error( "Error deleting report schedule %s: %s", @@ -278,7 +283,7 @@ def post(self) -> Response: @safe @statsd_metrics @permission_name("put") - def put(self, pk: int) -> Response: + def put(self, pk: int) -> Response: # pylint: disable=too-many-return-statements """Updates an Report Schedule --- put: @@ -313,6 +318,8 @@ def put(self, pk: int) -> Response: $ref: '#/components/responses/400' 401: $ref: '#/components/responses/401' + 403: + $ref: '#/components/responses/403' 404: $ref: '#/components/responses/404' 500: @@ -332,6 +339,8 @@ def put(self, pk: int) -> Response: return self.response_404() except ReportScheduleInvalidError as ex: return self.response_422(message=ex.normalized_messages()) + except ReportScheduleForbiddenError: + return self.response_403() except ReportScheduleUpdateFailedError as ex: logger.error( "Error updating report %s: %s", self.__class__.__name__, str(ex) @@ -368,6 +377,8 @@ def bulk_delete(self, **kwargs: Any) -> Response: type: string 401: $ref: '#/components/responses/401' + 403: + $ref: '#/components/responses/403' 404: $ref: '#/components/responses/404' 422: @@ -388,5 +399,7 @@ def bulk_delete(self, **kwargs: Any) -> Response: ) except ReportScheduleNotFoundError: return self.response_404() + except ReportScheduleForbiddenError: + return self.response_403() except ReportScheduleBulkDeleteFailedError as ex: return self.response_422(message=str(ex)) diff --git a/superset/reports/commands/bulk_delete.py b/superset/reports/commands/bulk_delete.py index b9dd57267555e..aa4898e9e55d3 100644 --- a/superset/reports/commands/bulk_delete.py +++ b/superset/reports/commands/bulk_delete.py @@ -21,12 +21,15 @@ from superset.commands.base import BaseCommand from superset.dao.exceptions import DAODeleteFailedError +from superset.exceptions import SupersetSecurityException from superset.models.reports import ReportSchedule from superset.reports.commands.exceptions import ( ReportScheduleBulkDeleteFailedError, + ReportScheduleForbiddenError, ReportScheduleNotFoundError, ) from superset.reports.dao import ReportScheduleDAO +from superset.views.base import check_ownership logger = logging.getLogger(__name__) @@ -51,3 +54,10 @@ def validate(self) -> None: self._models = ReportScheduleDAO.find_by_ids(self._model_ids) if not self._models or len(self._models) != len(self._model_ids): raise ReportScheduleNotFoundError() + + # Check ownership + for model in self._models: + try: + check_ownership(model) + except SupersetSecurityException: + raise ReportScheduleForbiddenError() diff --git a/superset/reports/commands/delete.py b/superset/reports/commands/delete.py index 79a0f4455b740..8375f52c30e34 100644 --- a/superset/reports/commands/delete.py +++ b/superset/reports/commands/delete.py @@ -22,12 +22,15 @@ from superset.commands.base import BaseCommand from superset.dao.exceptions import DAODeleteFailedError +from superset.exceptions import SupersetSecurityException from superset.models.reports import ReportSchedule from superset.reports.commands.exceptions import ( ReportScheduleDeleteFailedError, + ReportScheduleForbiddenError, ReportScheduleNotFoundError, ) from superset.reports.dao import ReportScheduleDAO +from superset.views.base import check_ownership logger = logging.getLogger(__name__) @@ -52,3 +55,9 @@ def validate(self) -> None: self._model = ReportScheduleDAO.find_by_id(self._model_id) if not self._model: raise ReportScheduleNotFoundError() + + # Check ownership + try: + check_ownership(self._model) + except SupersetSecurityException: + raise ReportScheduleForbiddenError() diff --git a/superset/reports/commands/exceptions.py b/superset/reports/commands/exceptions.py index b6fd375a91719..a4aa9d985439d 100644 --- a/superset/reports/commands/exceptions.py +++ b/superset/reports/commands/exceptions.py @@ -21,6 +21,7 @@ CommandInvalidError, CreateFailedError, DeleteFailedError, + ForbiddenError, ValidationError, ) @@ -172,3 +173,7 @@ class ReportScheduleStateNotFoundError(CommandException): class ReportScheduleUnexpectedError(CommandException): message = _("Report schedule unexpected error") + + +class ReportScheduleForbiddenError(ForbiddenError): + message = _("Changing this report is forbidden") diff --git a/superset/reports/commands/update.py b/superset/reports/commands/update.py index c8a9b6be370c0..d2cc9fd151048 100644 --- a/superset/reports/commands/update.py +++ b/superset/reports/commands/update.py @@ -25,16 +25,19 @@ from superset.commands.utils import populate_owners from superset.dao.exceptions import DAOUpdateFailedError from superset.databases.dao import DatabaseDAO +from superset.exceptions import SupersetSecurityException from superset.models.reports import ReportSchedule, ReportScheduleType from superset.reports.commands.base import BaseReportScheduleCommand from superset.reports.commands.exceptions import ( DatabaseNotFoundValidationError, + ReportScheduleForbiddenError, ReportScheduleInvalidError, ReportScheduleNameUniquenessValidationError, ReportScheduleNotFoundError, ReportScheduleUpdateFailedError, ) from superset.reports.dao import ReportScheduleDAO +from superset.views.base import check_ownership logger = logging.getLogger(__name__) @@ -93,6 +96,12 @@ def validate(self) -> None: self._properties["validator_config_json"] ) + # Check ownership + try: + check_ownership(self._model) + except SupersetSecurityException: + raise ReportScheduleForbiddenError() + # Validate/Populate owner if owner_ids is None: owner_ids = [owner.id for owner in self._model.owners] diff --git a/tests/reports/api_tests.py b/tests/reports/api_tests.py index 6f374e6cd8a47..23736ab2a0521 100644 --- a/tests/reports/api_tests.py +++ b/tests/reports/api_tests.py @@ -96,6 +96,26 @@ def create_report_schedules(self): db.session.delete(report_schedule) db.session.commit() + @pytest.fixture() + def create_alpha_users(self): + with self.create_app().app_context(): + + users = [ + self.create_user( + "alpha1", "password", "Alpha", email="alpha1@superset.org" + ), + self.create_user( + "alpha2", "password", "Alpha", email="alpha2@superset.org" + ), + ] + + yield users + + # rollback changes (assuming cascade delete) + for user in users: + db.session.delete(user) + db.session.commit() + @pytest.mark.usefixtures("create_report_schedules") def test_get_report_schedule(self): """ @@ -656,6 +676,26 @@ def test_update_report_schedule_relations_exist(self): data = json.loads(rv.data.decode("utf-8")) assert data == {"message": {"dashboard": "Dashboard does not exist"}} + @pytest.mark.usefixtures("create_report_schedules") + @pytest.mark.usefixtures("create_alpha_users") + def test_update_report_not_owned(self): + """ + ReportSchedule API: Test update report not owned + """ + report_schedule = ( + db.session.query(ReportSchedule) + .filter(ReportSchedule.name == "name2") + .one_or_none() + ) + + self.login(username="alpha2", password="password") + report_schedule_data = { + "active": False, + } + uri = f"api/v1/report/{report_schedule.id}" + rv = self.put_assert_metric(uri, report_schedule_data, "put") + self.assertEqual(rv.status_code, 403) + @pytest.mark.usefixtures("create_report_schedules") def test_delete_report_schedule(self): """ @@ -698,6 +738,23 @@ def test_delete_report_schedule_not_found(self): rv = self.client.delete(uri) assert rv.status_code == 404 + @pytest.mark.usefixtures("create_report_schedules") + @pytest.mark.usefixtures("create_alpha_users") + def test_delete_report_not_owned(self): + """ + ReportSchedule API: Test delete try not owned + """ + report_schedule = ( + db.session.query(ReportSchedule) + .filter(ReportSchedule.name == "name2") + .one_or_none() + ) + + self.login(username="alpha2", password="password") + uri = f"api/v1/report/{report_schedule.id}" + rv = self.client.delete(uri) + self.assertEqual(rv.status_code, 403) + @pytest.mark.usefixtures("create_report_schedules") def test_bulk_delete_report_schedule(self): """ @@ -737,6 +794,24 @@ def test_bulk_delete_report_schedule_not_found(self): rv = self.client.delete(uri) assert rv.status_code == 404 + @pytest.mark.usefixtures("create_report_schedules") + @pytest.mark.usefixtures("create_alpha_users") + def test_bulk_delete_report_not_owned(self): + """ + ReportSchedule API: Test bulk delete try not owned + """ + report_schedule = ( + db.session.query(ReportSchedule) + .filter(ReportSchedule.name == "name2") + .one_or_none() + ) + report_schedules_ids = [report_schedule.id] + + self.login(username="alpha2", password="password") + uri = f"api/v1/report/?q={prison.dumps(report_schedules_ids)}" + rv = self.client.delete(uri) + self.assertEqual(rv.status_code, 403) + @pytest.mark.usefixtures("create_report_schedules") def test_get_list_report_schedule_logs(self): """ From 832267c77650fad8a1f69666900ea37a2006ad5c Mon Sep 17 00:00:00 2001 From: Daniel Vaz Gaspar Date: Tue, 15 Dec 2020 08:44:30 +0000 Subject: [PATCH 09/43] fix(reports): log duration and sort column (#12039) --- superset-frontend/src/views/CRUD/alert/ExecutionLog.tsx | 3 ++- superset/reports/api.py | 3 ++- superset/reports/logs/api.py | 1 + superset/reports/schemas.py | 7 ++++++- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/superset-frontend/src/views/CRUD/alert/ExecutionLog.tsx b/superset-frontend/src/views/CRUD/alert/ExecutionLog.tsx index d5c950beffdd7..fe5966abd41d9 100644 --- a/superset-frontend/src/views/CRUD/alert/ExecutionLog.tsx +++ b/superset-frontend/src/views/CRUD/alert/ExecutionLog.tsx @@ -111,7 +111,8 @@ function ExecutionLog({ addDangerToast, isReportEnabled }: ExecutionLogProps) { row: { original: { start_dttm: startDttm, end_dttm: endDttm }, }, - }: any) => fDuration(endDttm - startDttm), + }: any) => + fDuration(new Date(startDttm).getTime(), new Date(endDttm).getTime()), Header: t('Duration'), disableSortBy: true, }, diff --git a/superset/reports/api.py b/superset/reports/api.py index 6a2234f2aa45b..6c5b8385e48c3 100644 --- a/superset/reports/api.py +++ b/superset/reports/api.py @@ -147,6 +147,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi): "changed_on", "changed_on_delta_humanized", "created_on", + "crontab", "name", "type", "crontab_humanized", @@ -205,7 +206,7 @@ def delete(self, pk: int) -> Response: try: DeleteReportScheduleCommand(g.user, pk).run() return self.response(200, message="OK") - except ReportScheduleNotFoundError as ex: + except ReportScheduleNotFoundError: return self.response_404() except ReportScheduleForbiddenError: return self.response_403() diff --git a/superset/reports/logs/api.py b/superset/reports/logs/api.py index 4c175b663576d..4b3177b73ca05 100644 --- a/superset/reports/logs/api.py +++ b/superset/reports/logs/api.py @@ -64,6 +64,7 @@ class ReportExecutionLogRestApi(BaseSupersetModelRestApi): "error_message", "end_dttm", "start_dttm", + "scheduled_dttm", ] openapi_spec_tag = "Report Schedules" openapi_spec_methods = openapi_spec_methods_override diff --git a/superset/reports/schemas.py b/superset/reports/schemas.py index 4acbdf53d3bb3..777a56a10454e 100644 --- a/superset/reports/schemas.py +++ b/superset/reports/schemas.py @@ -199,6 +199,7 @@ class ReportSchedulePutSchema(Schema): description=sql_description, example="SELECT value FROM time_series_table", required=False, + allow_none=True, ) chart = fields.Integer(required=False) dashboard = fields.Integer(required=False) @@ -209,6 +210,7 @@ class ReportSchedulePutSchema(Schema): validate=validate.OneOf( choices=tuple(key.value for key in ReportScheduleValidatorType) ), + allow_none=True, required=False, ) validator_config_json = fields.Nested(ValidatorConfigJSONSchema, required=False) @@ -219,6 +221,9 @@ class ReportSchedulePutSchema(Schema): description=grace_period_description, example=60 * 60 * 4, required=False ) working_timeout = fields.Integer( - description=working_timeout_description, example=60 * 60 * 1, required=False + description=working_timeout_description, + example=60 * 60 * 1, + allow_none=True, + required=False, ) recipients = fields.List(fields.Nested(ReportRecipientSchema), required=False) From 0f979dea064fe165dded3cabc81fe15c6cc35758 Mon Sep 17 00:00:00 2001 From: Daniel Vaz Gaspar Date: Tue, 15 Dec 2020 08:48:00 +0000 Subject: [PATCH 10/43] feat(reports): security perm simplification (#11853) * feat: security converge report * black * fix: comment * add frontend changes and rebase * fix multiple heads --- .../src/views/CRUD/alert/AlertList.tsx | 6 +- .../40f16acf1ba7_security_converge_reports.py | 78 +++++++++++++++++++ superset/reports/api.py | 3 +- tests/reports/api_tests.py | 14 ++++ 4 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 superset/migrations/versions/40f16acf1ba7_security_converge_reports.py diff --git a/superset-frontend/src/views/CRUD/alert/AlertList.tsx b/superset-frontend/src/views/CRUD/alert/AlertList.tsx index 3ecc034cac040..e8437b1b52fc6 100644 --- a/superset-frontend/src/views/CRUD/alert/AlertList.tsx +++ b/superset-frontend/src/views/CRUD/alert/AlertList.tsx @@ -97,9 +97,9 @@ function AlertList({ setAlertModalOpen(true); } - const canEdit = hasPerm('can_edit'); - const canDelete = hasPerm('can_delete'); - const canCreate = hasPerm('can_add'); + const canEdit = hasPerm('can_write'); + const canDelete = hasPerm('can_write'); + const canCreate = hasPerm('can_write'); const initialSort = [{ id: 'name', desc: true }]; diff --git a/superset/migrations/versions/40f16acf1ba7_security_converge_reports.py b/superset/migrations/versions/40f16acf1ba7_security_converge_reports.py new file mode 100644 index 0000000000000..227c421944a53 --- /dev/null +++ b/superset/migrations/versions/40f16acf1ba7_security_converge_reports.py @@ -0,0 +1,78 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""security converge reports + +Revision ID: 40f16acf1ba7 +Revises: e38177dbf641 +Create Date: 2020-11-30 15:25:47.489419 + +""" + +# revision identifiers, used by Alembic. +revision = "40f16acf1ba7" +down_revision = "5daced1f0e76" + + +from alembic import op +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session + +from superset.migrations.shared.security_converge import ( + add_pvms, + get_reversed_new_pvms, + get_reversed_pvm_map, + migrate_roles, + Pvm, +) + +NEW_PVMS = {"ReportSchedule": ("can_read", "can_write",)} +PVM_MAP = { + Pvm("ReportSchedule", "can_list"): (Pvm("ReportSchedule", "can_read"),), + Pvm("ReportSchedule", "can_show"): (Pvm("ReportSchedule", "can_read"),), + Pvm("ReportSchedule", "can_add",): (Pvm("ReportSchedule", "can_write"),), + Pvm("ReportSchedule", "can_edit",): (Pvm("ReportSchedule", "can_write"),), + Pvm("ReportSchedule", "can_delete",): (Pvm("ReportSchedule", "can_write"),), +} + + +def upgrade(): + bind = op.get_bind() + session = Session(bind=bind) + + # Add the new permissions on the migration itself + add_pvms(session, NEW_PVMS) + migrate_roles(session, PVM_MAP) + try: + session.commit() + except SQLAlchemyError as ex: + print(f"An error occurred while upgrading permissions: {ex}") + session.rollback() + + +def downgrade(): + bind = op.get_bind() + session = Session(bind=bind) + + # Add the old permissions on the migration itself + add_pvms(session, get_reversed_new_pvms(PVM_MAP)) + migrate_roles(session, get_reversed_pvm_map(PVM_MAP)) + try: + session.commit() + except SQLAlchemyError as ex: + print(f"An error occurred while downgrading permissions: {ex}") + session.rollback() + pass diff --git a/superset/reports/api.py b/superset/reports/api.py index 6c5b8385e48c3..6be4a12f35fba 100644 --- a/superset/reports/api.py +++ b/superset/reports/api.py @@ -24,7 +24,7 @@ from marshmallow import ValidationError from superset.charts.filters import ChartFilter -from superset.constants import RouteMethod +from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.dashboards.filters import DashboardFilter from superset.models.reports import ReportSchedule from superset.reports.commands.bulk_delete import BulkDeleteReportScheduleCommand @@ -60,6 +60,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi): "bulk_delete", # not using RouteMethod since locally defined } class_permission_name = "ReportSchedule" + method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP resource_name = "report" allow_browser_login = True diff --git a/tests/reports/api_tests.py b/tests/reports/api_tests.py index 23736ab2a0521..97aecea4b8ba5 100644 --- a/tests/reports/api_tests.py +++ b/tests/reports/api_tests.py @@ -179,6 +179,20 @@ def test_info_report_schedule(self): rv = self.get_assert_metric(uri, "info") assert rv.status_code == 200 + def test_info_security_report(self): + """ + ReportSchedule API: Test info security + """ + self.login(username="admin") + params = {"keys": ["permissions"]} + uri = f"api/v1/report/_info?q={prison.dumps(params)}" + rv = self.get_assert_metric(uri, "info") + data = json.loads(rv.data.decode("utf-8")) + assert rv.status_code == 200 + assert "can_read" in data["permissions"] + assert "can_write" in data["permissions"] + assert len(data["permissions"]) == 2 + @pytest.mark.usefixtures("create_report_schedules") def test_get_report_schedule_not_found(self): """ From 2df519eab0ee867f31c0ea7f957e43c54cf8b2ca Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Tue, 15 Dec 2020 00:54:35 -0800 Subject: [PATCH 11/43] upgrade react-split (#12054) --- superset-frontend/package-lock.json | 2 +- superset-frontend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 76cd94656ad44..b6a5773d0a4c7 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -30618,7 +30618,7 @@ "dependencies": { "core-js": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "resolved": "http://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" } } diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 297700d8d0f81..c77ea61c138a5 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -152,7 +152,7 @@ "react-select": "^3.1.0", "react-select-async-paginate": "^0.4.1", "react-sortable-hoc": "^1.11.0", - "react-split": "^2.0.4", + "react-split": "^2.0.9", "react-sticky": "^6.0.3", "react-syntax-highlighter": "^15.3.0", "react-table": "^7.2.1", From 821b01737d4181698e3fe95abbe47cdb20572567 Mon Sep 17 00:00:00 2001 From: Ville Brofeldt <33317356+villebro@users.noreply.github.com> Date: Tue, 15 Dec 2020 12:54:32 +0200 Subject: [PATCH 12/43] fix(viz): remove orderby from sample request (#12055) --- superset/viz.py | 1 + 1 file changed, 1 insertion(+) diff --git a/superset/viz.py b/superset/viz.py index 9b7d46ac40229..549445f7eb08e 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -246,6 +246,7 @@ def get_samples(self) -> List[Dict[str, Any]]: { "groupby": [], "metrics": [], + "orderby": [], "row_limit": config["SAMPLES_ROW_LIMIT"], "columns": [o.column_name for o in self.datasource.columns], } From f79e52f48eb462fc7327b05b9c52b8197b395dad Mon Sep 17 00:00:00 2001 From: Daniel Vaz Gaspar Date: Tue, 15 Dec 2020 11:27:06 +0000 Subject: [PATCH 13/43] feat(charts): security perm simplification (#11981) * feat(charts): security perm simplification * fix superset explore * fix JS test * fix cypress test * fix split heads * fix favorite permission * fix permission * update with new async permission * fix new permission coming from master * fix core permission assert * black * update alembic down revision --- .../views/CRUD/chart/ChartList_spec.jsx | 2 +- .../src/views/CRUD/chart/ChartCard.tsx | 6 +- .../src/views/CRUD/chart/ChartList.tsx | 8 +- superset/charts/api.py | 5 +- superset/constants.py | 7 ++ .../ccb74baaa89b_security_converge_charts.py | 87 +++++++++++++++++++ superset/views/chart/views.py | 4 +- superset/views/core.py | 6 +- tests/charts/api_tests.py | 14 +++ tests/security_tests.py | 32 ++----- 10 files changed, 131 insertions(+), 40 deletions(-) create mode 100644 superset/migrations/versions/ccb74baaa89b_security_converge_charts.py diff --git a/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx b/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx index 1ba734074689b..33f2385dabd51 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx +++ b/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx @@ -59,7 +59,7 @@ const mockUser = { }; fetchMock.get(chartsInfoEndpoint, { - permissions: ['can_list', 'can_edit', 'can_delete'], + permissions: ['can_read', 'can_write'], }); fetchMock.get(chartssOwnersEndpoint, { diff --git a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx index 8df4f02b8fbaf..6f7bc8127c187 100644 --- a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx +++ b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx @@ -59,10 +59,10 @@ export default function ChartCard({ chartFilter, userId, }: ChartCardProps) { - const canEdit = hasPerm('can_edit'); - const canDelete = hasPerm('can_delete'); + const canEdit = hasPerm('can_write'); + const canDelete = hasPerm('can_write'); const canExport = - hasPerm('can_mulexport') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT); + hasPerm('can_read') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT); const menu = ( diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx index 1e07c03d66a13..79bb22cccfd47 100644 --- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx +++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx @@ -152,11 +152,11 @@ function ChartList(props: ChartListProps) { refreshData(); }; - const canCreate = hasPerm('can_add'); - const canEdit = hasPerm('can_edit'); - const canDelete = hasPerm('can_delete'); + const canCreate = hasPerm('can_write'); + const canEdit = hasPerm('can_write'); + const canDelete = hasPerm('can_write'); const canExport = - hasPerm('can_mulexport') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT); + hasPerm('can_read') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT); const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }]; function handleBulkChartDelete(chartsToDelete: Chart[]) { diff --git a/superset/charts/api.py b/superset/charts/api.py index 50245be3a016f..9611a26fef219 100644 --- a/superset/charts/api.py +++ b/superset/charts/api.py @@ -64,7 +64,7 @@ ) from superset.commands.exceptions import CommandInvalidError from superset.commands.importers.v1.utils import remove_root -from superset.constants import RouteMethod +from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.exceptions import SupersetSecurityException from superset.extensions import event_logger from superset.models.slice import Slice @@ -104,7 +104,8 @@ class ChartRestApi(BaseSupersetModelRestApi): "viz_types", "favorite_status", } - class_permission_name = "SliceModelView" + class_permission_name = "Chart" + method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP show_columns = [ "cache_timeout", "dashboards.dashboard_title", diff --git a/superset/constants.py b/superset/constants.py index 167e128676177..16cd4e64ab829 100644 --- a/superset/constants.py +++ b/superset/constants.py @@ -95,4 +95,11 @@ class RouteMethod: # pylint: disable=too-few-public-methods "post": "write", "put": "write", "related": "read", + "favorite_status": "write", + "import_": "write", + "cache_screenshot": "read", + "screenshot": "read", + "data": "read", + "thumbnail": "read", + "data_from_cache": "read", } diff --git a/superset/migrations/versions/ccb74baaa89b_security_converge_charts.py b/superset/migrations/versions/ccb74baaa89b_security_converge_charts.py new file mode 100644 index 0000000000000..d025cd5762f98 --- /dev/null +++ b/superset/migrations/versions/ccb74baaa89b_security_converge_charts.py @@ -0,0 +1,87 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""security converge charts + +Revision ID: ccb74baaa89b +Revises: 811494c0cc23 +Create Date: 2020-12-09 14:13:48.058003 + +""" + +# revision identifiers, used by Alembic. +revision = "ccb74baaa89b" +down_revision = "40f16acf1ba7" + + +from alembic import op +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session + +from superset.migrations.shared.security_converge import ( + add_pvms, + get_reversed_new_pvms, + get_reversed_pvm_map, + migrate_roles, + Pvm, +) + +NEW_PVMS = {"Chart": ("can_read", "can_write",)} +PVM_MAP = { + Pvm("SliceModelView", "can_list"): (Pvm("Chart", "can_read"),), + Pvm("SliceModelView", "can_show"): (Pvm("Chart", "can_read"),), + Pvm("SliceModelView", "can_edit",): (Pvm("Chart", "can_write"),), + Pvm("SliceModelView", "can_delete",): (Pvm("Chart", "can_write"),), + Pvm("SliceModelView", "can_add",): (Pvm("Chart", "can_write"),), + Pvm("SliceModelView", "can_download",): (Pvm("Chart", "can_read"),), + Pvm("SliceModelView", "muldelete",): (Pvm("Chart", "can_write"),), + Pvm("SliceModelView", "can_mulexport",): (Pvm("Chart", "can_read"),), + Pvm("SliceModelView", "can_favorite_status",): (Pvm("Chart", "can_read"),), + Pvm("SliceModelView", "can_cache_screenshot",): (Pvm("Chart", "can_read"),), + Pvm("SliceModelView", "can_screenshot",): (Pvm("Chart", "can_read"),), + Pvm("SliceModelView", "can_data_from_cache",): (Pvm("Chart", "can_read"),), + Pvm("SliceAsync", "can_list",): (Pvm("Chart", "can_read"),), + Pvm("SliceAsync", "muldelete",): (Pvm("Chart", "can_write"),), +} + + +def upgrade(): + bind = op.get_bind() + session = Session(bind=bind) + + # Add the new permissions on the migration itself + add_pvms(session, NEW_PVMS) + migrate_roles(session, PVM_MAP) + try: + session.commit() + except SQLAlchemyError as ex: + print(f"An error occurred while upgrading permissions: {ex}") + session.rollback() + + +def downgrade(): + bind = op.get_bind() + session = Session(bind=bind) + + # Add the old permissions on the migration itself + add_pvms(session, get_reversed_new_pvms(PVM_MAP)) + migrate_roles(session, get_reversed_pvm_map(PVM_MAP)) + try: + session.commit() + except SQLAlchemyError as ex: + print(f"An error occurred while downgrading permissions: {ex}") + session.rollback() + pass diff --git a/superset/views/chart/views.py b/superset/views/chart/views.py index 301d89c87451b..b87a2d801f1a1 100644 --- a/superset/views/chart/views.py +++ b/superset/views/chart/views.py @@ -22,7 +22,7 @@ from superset import db, is_feature_enabled from superset.connectors.connector_registry import ConnectorRegistry -from superset.constants import RouteMethod +from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.models.slice import Slice from superset.typing import FlaskResponse from superset.utils import core as utils @@ -45,6 +45,8 @@ class SliceModelView( RouteMethod.API_READ, RouteMethod.API_DELETE, } + class_permission_name = "Chart" + method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP def pre_add(self, item: "SliceModelView") -> None: utils.validate_json(item.params) diff --git a/superset/views/core.py b/superset/views/core.py index 63591b73efb94..2ec0d3a104c47 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -722,11 +722,9 @@ def explore( # pylint: disable=too-many-locals,too-many-return-statements return redirect(datasource.default_endpoint) # slc perms - slice_add_perm = security_manager.can_access("can_add", "SliceModelView") + slice_add_perm = security_manager.can_access("can_write", "Chart") slice_overwrite_perm = is_owner(slc, g.user) if slc else False - slice_download_perm = security_manager.can_access( - "can_download", "SliceModelView" - ) + slice_download_perm = security_manager.can_access("can_read", "Chart") form_data["datasource"] = str(datasource_id) + "__" + cast(str, datasource_type) diff --git a/tests/charts/api_tests.py b/tests/charts/api_tests.py index 092c354199b3f..a374c47baf8cf 100644 --- a/tests/charts/api_tests.py +++ b/tests/charts/api_tests.py @@ -182,6 +182,20 @@ def add_dashboard_to_chart(self): db.session.delete(self.chart) db.session.commit() + def test_info_security_chart(self): + """ + Chart API: Test info security + """ + self.login(username="admin") + params = {"keys": ["permissions"]} + uri = f"api/v1/chart/_info?q={prison.dumps(params)}" + rv = self.get_assert_metric(uri, "info") + data = json.loads(rv.data.decode("utf-8")) + assert rv.status_code == 200 + assert "can_read" in data["permissions"] + assert "can_write" in data["permissions"] + assert len(data["permissions"]) == 2 + def create_chart_import(self): buf = BytesIO() with ZipFile(buf, "w") as bundle: diff --git a/tests/security_tests.py b/tests/security_tests.py index 13c80127583fb..4ef63922f7c80 100644 --- a/tests/security_tests.py +++ b/tests/security_tests.py @@ -48,7 +48,7 @@ from .fixtures.energy_dashboard import load_energy_table_with_slice from .fixtures.unicode_dashboard import load_unicode_dashboard_with_slice -NEW_SECURITY_CONVERGE_VIEWS = ("CssTemplate", "SavedQuery") +NEW_SECURITY_CONVERGE_VIEWS = ("CssTemplate", "SavedQuery", "Chart") def get_perm_tuples(role_name): @@ -647,7 +647,7 @@ def assert_can_gamma(self, perm_set): self.assert_can_read("TableModelView", perm_set) # make sure that user can create slices and dashboards - self.assert_can_all("SliceModelView", perm_set) + self.assert_can_all("Chart", perm_set) self.assert_can_all("DashboardModelView", perm_set) self.assertIn(("can_add_slices", "Superset"), perm_set) @@ -832,37 +832,19 @@ def test_granter_permissions(self): self.assert_cannot_alpha(granter_set) def test_gamma_permissions(self): - def assert_can_read(view_menu): - self.assertIn(("can_list", view_menu), gamma_perm_set) - - def assert_can_write(view_menu): - self.assertIn(("can_add", view_menu), gamma_perm_set) - self.assertIn(("can_delete", view_menu), gamma_perm_set) - self.assertIn(("can_edit", view_menu), gamma_perm_set) - - def assert_cannot_write(view_menu): - self.assertNotIn(("can_add", view_menu), gamma_perm_set) - self.assertNotIn(("can_delete", view_menu), gamma_perm_set) - self.assertNotIn(("can_edit", view_menu), gamma_perm_set) - self.assertNotIn(("can_save", view_menu), gamma_perm_set) - - def assert_can_all(view_menu): - assert_can_read(view_menu) - assert_can_write(view_menu) - gamma_perm_set = set() for perm in security_manager.find_role("Gamma").permissions: gamma_perm_set.add((perm.permission.name, perm.view_menu.name)) # check read only perms - assert_can_read("TableModelView") + self.assert_can_read("TableModelView", gamma_perm_set) # make sure that user can create slices and dashboards - assert_can_all("SliceModelView") - assert_can_all("DashboardModelView") + self.assert_can_all("Chart", gamma_perm_set) + self.assert_can_all("DashboardModelView", gamma_perm_set) - assert_cannot_write("UserDBModelView") - assert_cannot_write("RoleModelView") + self.assert_cannot_write("UserDBModelView", gamma_perm_set) + self.assert_cannot_write("RoleModelView", gamma_perm_set) self.assertIn(("can_add_slices", "Superset"), gamma_perm_set) self.assertIn(("can_copy_dash", "Superset"), gamma_perm_set) From 12e086dc616440b5edb96ccfec837a8f5eabcffb Mon Sep 17 00:00:00 2001 From: Victor Malai Date: Tue, 15 Dec 2020 18:12:00 +0200 Subject: [PATCH 14/43] refactor: Transform URLShortLinkModal to Typescript (#11971) * Transform jsx modal to tsx modal * Change required type Co-authored-by: Victor Malai --- ...ortLinkModal.jsx => URLShortLinkModal.tsx} | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) rename superset-frontend/src/components/{URLShortLinkModal.jsx => URLShortLinkModal.tsx} (78%) diff --git a/superset-frontend/src/components/URLShortLinkModal.jsx b/superset-frontend/src/components/URLShortLinkModal.tsx similarity index 78% rename from superset-frontend/src/components/URLShortLinkModal.jsx rename to superset-frontend/src/components/URLShortLinkModal.tsx index 2b5bbfca8bdd5..40e9b4521b817 100644 --- a/superset-frontend/src/components/URLShortLinkModal.jsx +++ b/superset-frontend/src/components/URLShortLinkModal.tsx @@ -17,24 +17,38 @@ * under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { t } from '@superset-ui/core'; import CopyToClipboard from './CopyToClipboard'; import { getShortUrl } from '../utils/common'; import withToasts from '../messageToasts/enhancers/withToasts'; import ModalTrigger from './ModalTrigger'; -const propTypes = { - url: PropTypes.string, - emailSubject: PropTypes.string, - emailContent: PropTypes.string, - addDangerToast: PropTypes.func.isRequired, - title: PropTypes.string, - triggerNode: PropTypes.node.isRequired, +type URLShortLinkModalProps = { + url: string; + emailSubject: string; + emailContent: string; + title?: string; + addDangerToast: (msg: string) => void; + triggerNode: JSX.Element; }; -class URLShortLinkModal extends React.Component { - constructor(props) { +type URLShortLinkModalState = { + shortUrl: string; +}; + +class URLShortLinkModal extends React.Component< + URLShortLinkModalProps, + URLShortLinkModalState +> { + static defaultProps = { + url: window.location.href.substring(window.location.origin.length), + emailSubject: '', + emailContent: '', + }; + + modal: ModalTrigger | null; + + constructor(props: URLShortLinkModalProps) { super(props); this.state = { shortUrl: '', @@ -45,11 +59,11 @@ class URLShortLinkModal extends React.Component { this.getCopyUrl = this.getCopyUrl.bind(this); } - onShortUrlSuccess(shortUrl) { + onShortUrlSuccess(shortUrl: string) { this.setState(() => ({ shortUrl })); } - setModalRef(ref) { + setModalRef(ref: ModalTrigger | null) { this.modal = ref; } @@ -88,12 +102,4 @@ class URLShortLinkModal extends React.Component { } } -URLShortLinkModal.defaultProps = { - url: window.location.href.substring(window.location.origin.length), - emailSubject: '', - emailContent: '', -}; - -URLShortLinkModal.propTypes = propTypes; - export default withToasts(URLShortLinkModal); From 7dac150dc6f9279c93a832e509aaa89bd35412bb Mon Sep 17 00:00:00 2001 From: adam-stasiak-polidea Date: Tue, 15 Dec 2020 18:07:34 +0100 Subject: [PATCH 15/43] fixed CONTRIBUTING typos (#12057) --- CONTRIBUTING.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 30da73333545b..6518b4378d0c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -682,20 +682,18 @@ If you already have launched Docker environment please use the following command Launch environment: -CYPRESS_CONFIG=true docker-compose up +```CYPRESS_CONFIG=true docker-compose up``` It will serve backend and frontend on port 8088. -Run Cypres tests: +Run Cypress tests: ```bash cd cypress-base npm install +npm run cypress open ``` -# run tests via headless Chrome browser (requires Chrome 64+) -npm run cypress-run-chrome - ### Storybook Superset includes a [Storybook](https://storybook.js.org/) to preview the layout/styling of various Superset components, and variations thereof. To open and view the Storybook: From 862c25192441535216b18b2f6f3f876b4eabc4e6 Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Tue, 15 Dec 2020 11:21:13 -0800 Subject: [PATCH 16/43] feat: handle new export in CLI (#11803) * feat: handle new export in CLI * Fix imports * Fix lint * Set options based on feature flag * Fix lint * Fix lint * Add better error messages --- superset/cli.py | 484 +++++++++++++++++++++++++++++++----------------- 1 file changed, 319 insertions(+), 165 deletions(-) diff --git a/superset/cli.py b/superset/cli.py index bc72427db4e68..8704151a55da2 100755 --- a/superset/cli.py +++ b/superset/cli.py @@ -16,10 +16,11 @@ # specific language governing permissions and limitations # under the License. import logging +import sys from datetime import datetime, timedelta from subprocess import Popen -from sys import stdout -from typing import Any, Dict, List, Type, Union +from typing import Any, Dict, List, Optional, Type, Union +from zipfile import ZipFile import click import yaml @@ -30,7 +31,7 @@ from flask_appbuilder import Model from pathlib2 import Path -from superset import app, appbuilder, security_manager +from superset import app, appbuilder, config, security_manager from superset.app import create_app from superset.extensions import celery_app, db from superset.utils import core as utils @@ -40,6 +41,14 @@ logger = logging.getLogger(__name__) +feature_flags = config.DEFAULT_FEATURE_FLAGS.copy() +feature_flags.update(config.FEATURE_FLAGS) +feature_flags_func = config.GET_FEATURE_FLAGS_FUNC +if feature_flags_func: + # pylint: disable=not-callable + feature_flags = feature_flags_func(feature_flags) + + def normalize_token(token_name: str) -> str: """ As of click>=7, underscores in function names are replaced by dashes. @@ -212,180 +221,325 @@ def refresh_druid(datasource: str, merge: bool) -> None: session.commit() -@superset.command() -@with_appcontext -@click.option( - "--path", - "-p", - help="Path to a single JSON file or path containing multiple JSON " - "files to import (*.json)", -) -@click.option( - "--recursive", - "-r", - is_flag=True, - default=False, - help="recursively search the path for json files", -) -@click.option( - "--username", - "-u", - default=None, - help="Specify the user name to assign dashboards to", -) -def import_dashboards(path: str, recursive: bool, username: str) -> None: - """Import dashboards from JSON""" - from superset.dashboards.commands.importers.dispatcher import ( - ImportDashboardsCommand, - ) +if feature_flags.get("VERSIONED_EXPORT"): - path_object = Path(path) - files: List[Path] = [] - if path_object.is_file(): - files.append(path_object) - elif path_object.exists() and not recursive: - files.extend(path_object.glob("*.json")) - elif path_object.exists() and recursive: - files.extend(path_object.rglob("*.json")) - if username is not None: - g.user = security_manager.find_user(username=username) - contents = {path.name: open(path).read() for path in files} - try: - ImportDashboardsCommand(contents).run() - except Exception: # pylint: disable=broad-except - logger.exception("Error when importing dashboard") + @superset.command() + @with_appcontext + @click.option( + "--dashboard-file", + "-f", + default="dashboard_export_YYYYMMDDTHHMMSS", + help="Specify the the file to export to", + ) + def export_dashboards(dashboard_file: Optional[str]) -> None: + """Export dashboards to ZIP file""" + from superset.dashboards.commands.export import ExportDashboardsCommand + from superset.models.dashboard import Dashboard + g.user = security_manager.find_user(username="admin") -@superset.command() -@with_appcontext -@click.option( - "--dashboard-file", "-f", default=None, help="Specify the the file to export to" -) -@click.option( - "--print_stdout", "-p", is_flag=True, default=False, help="Print JSON to stdout" -) -def export_dashboards(dashboard_file: str, print_stdout: bool) -> None: - """Export dashboards to JSON""" - from superset.utils import dashboard_import_export + dashboard_ids = [id_ for (id_,) in db.session.query(Dashboard.id).all()] + timestamp = datetime.now().strftime("%Y%m%dT%H%M%S") + root = f"dashboard_export_{timestamp}" + dashboard_file = dashboard_file or f"{root}.zip" - data = dashboard_import_export.export_dashboards(db.session) - if print_stdout or not dashboard_file: - print(data) - if dashboard_file: - logger.info("Exporting dashboards to %s", dashboard_file) - with open(dashboard_file, "w") as data_stream: - data_stream.write(data) + try: + with ZipFile(dashboard_file, "w") as bundle: + for file_name, file_content in ExportDashboardsCommand( + dashboard_ids + ).run(): + with bundle.open(f"{root}/{file_name}", "w") as fp: + fp.write(file_content.encode()) + except Exception: # pylint: disable=broad-except + logger.exception( + "There was an error when exporting the dashboards, please check " + "the exception traceback in the log" + ) + + # pylint: disable=too-many-locals + @superset.command() + @with_appcontext + @click.option( + "--datasource-file", + "-f", + default="dataset_export_YYYYMMDDTHHMMSS", + help="Specify the the file to export to", + ) + def export_datasources(datasource_file: Optional[str]) -> None: + """Export datasources to ZIP file""" + from superset.connectors.sqla.models import SqlaTable + from superset.datasets.commands.export import ExportDatasetsCommand + g.user = security_manager.find_user(username="admin") -@superset.command() -@with_appcontext -@click.option( - "--path", - "-p", - help="Path to a single YAML file or path containing multiple YAML " - "files to import (*.yaml or *.yml)", -) -@click.option( - "--sync", - "-s", - "sync", - default="", - help="comma seperated list of element types to synchronize " - 'e.g. "metrics,columns" deletes metrics and columns in the DB ' - "that are not specified in the YAML file", -) -@click.option( - "--recursive", - "-r", - is_flag=True, - default=False, - help="recursively search the path for yaml files", -) -def import_datasources(path: str, sync: str, recursive: bool) -> None: - """Import datasources from YAML""" - from superset.datasets.commands.importers.dispatcher import ImportDatasetsCommand - - sync_array = sync.split(",") - sync_columns = "columns" in sync_array - sync_metrics = "metrics" in sync_array - - path_object = Path(path) - files: List[Path] = [] - if path_object.is_file(): - files.append(path_object) - elif path_object.exists() and not recursive: - files.extend(path_object.glob("*.yaml")) - files.extend(path_object.glob("*.yml")) - elif path_object.exists() and recursive: - files.extend(path_object.rglob("*.yaml")) - files.extend(path_object.rglob("*.yml")) - contents = {path.name: open(path).read() for path in files} - try: - ImportDatasetsCommand(contents, sync_columns, sync_metrics).run() - except Exception: # pylint: disable=broad-except - logger.exception("Error when importing dataset") + dataset_ids = [id_ for (id_,) in db.session.query(SqlaTable.id).all()] + timestamp = datetime.now().strftime("%Y%m%dT%H%M%S") + root = f"dataset_export_{timestamp}" + datasource_file = datasource_file or f"{root}.zip" + try: + with ZipFile(datasource_file, "w") as bundle: + for file_name, file_content in ExportDatasetsCommand(dataset_ids).run(): + with bundle.open(f"{root}/{file_name}", "w") as fp: + fp.write(file_content.encode()) + except Exception: # pylint: disable=broad-except + logger.exception( + "There was an error when exporting the datasets, please check " + "the exception traceback in the log" + ) + + @superset.command() + @with_appcontext + @click.option( + "--path", "-p", help="Path to a single ZIP file", + ) + @click.option( + "--username", + "-u", + default=None, + help="Specify the user name to assign dashboards to", + ) + def import_dashboards(path: str, username: Optional[str]) -> None: + """Import dashboards from ZIP file""" + from superset.dashboards.commands.importers.dispatcher import ( + ImportDashboardsCommand, + ) -@superset.command() -@with_appcontext -@click.option( - "--datasource-file", "-f", default=None, help="Specify the the file to export to" -) -@click.option( - "--print_stdout", "-p", is_flag=True, default=False, help="Print YAML to stdout" -) -@click.option( - "--back-references", - "-b", - is_flag=True, - default=False, - help="Include parent back references", -) -@click.option( - "--include-defaults", - "-d", - is_flag=True, - default=False, - help="Include fields containing defaults", -) -def export_datasources( - print_stdout: bool, - datasource_file: str, - back_references: bool, - include_defaults: bool, -) -> None: - """Export datasources to YAML""" - from superset.utils import dict_import_export - - data = dict_import_export.export_to_dict( - session=db.session, - recursive=True, - back_references=back_references, - include_defaults=include_defaults, + if username is not None: + g.user = security_manager.find_user(username=username) + contents = {path: open(path).read()} + try: + ImportDashboardsCommand(contents).run() + except Exception: # pylint: disable=broad-except + logger.exception( + "There was an error when importing the dashboards(s), please check " + "the exception traceback in the log" + ) + + @superset.command() + @with_appcontext + @click.option( + "--path", + "-p", + help="Path to a single YAML file or path containing multiple YAML " + "files to import (*.yaml or *.yml)", ) - if print_stdout or not datasource_file: - yaml.safe_dump(data, stdout, default_flow_style=False) - if datasource_file: - logger.info("Exporting datasources to %s", datasource_file) - with open(datasource_file, "w") as data_stream: - yaml.safe_dump(data, data_stream, default_flow_style=False) + @click.option( + "--sync", + "-s", + "sync", + default="", + help="comma seperated list of element types to synchronize " + 'e.g. "metrics,columns" deletes metrics and columns in the DB ' + "that are not specified in the YAML file", + ) + @click.option( + "--recursive", + "-r", + is_flag=True, + default=False, + help="recursively search the path for yaml files", + ) + def import_datasources(path: str) -> None: + """Import datasources from ZIP file""" + from superset.datasets.commands.importers.dispatcher import ( + ImportDatasetsCommand, + ) + contents = {path: open(path).read()} + try: + ImportDatasetsCommand(contents).run() + except Exception: # pylint: disable=broad-except + logger.exception( + "There was an error when importing the dataset(s), please check the " + "exception traceback in the log" + ) -@superset.command() -@with_appcontext -@click.option( - "--back-references", - "-b", - is_flag=True, - default=False, - help="Include parent back references", -) -def export_datasource_schema(back_references: bool) -> None: - """Export datasource YAML schema to stdout""" - from superset.utils import dict_import_export - data = dict_import_export.export_schema_to_dict(back_references=back_references) - yaml.safe_dump(data, stdout, default_flow_style=False) +else: + + @superset.command() + @with_appcontext + @click.option( + "--dashboard-file", "-f", default=None, help="Specify the the file to export to" + ) + @click.option( + "--print_stdout", + "-p", + is_flag=True, + default=False, + help="Print JSON to stdout", + ) + def export_dashboards( + dashboard_file: Optional[str], print_stdout: bool = False + ) -> None: + """Export dashboards to JSON""" + from superset.utils import dashboard_import_export + + data = dashboard_import_export.export_dashboards(db.session) + if print_stdout or not dashboard_file: + print(data) + if dashboard_file: + logger.info("Exporting dashboards to %s", dashboard_file) + with open(dashboard_file, "w") as data_stream: + data_stream.write(data) + + # pylint: disable=too-many-locals + @superset.command() + @with_appcontext + @click.option( + "--datasource-file", + "-f", + default=None, + help="Specify the the file to export to", + ) + @click.option( + "--print_stdout", + "-p", + is_flag=True, + default=False, + help="Print YAML to stdout", + ) + @click.option( + "--back-references", + "-b", + is_flag=True, + default=False, + help="Include parent back references", + ) + @click.option( + "--include-defaults", + "-d", + is_flag=True, + default=False, + help="Include fields containing defaults", + ) + def export_datasources( + datasource_file: Optional[str], + print_stdout: bool = False, + back_references: bool = False, + include_defaults: bool = False, + ) -> None: + """Export datasources to YAML""" + from superset.utils import dict_import_export + + data = dict_import_export.export_to_dict( + session=db.session, + recursive=True, + back_references=back_references, + include_defaults=include_defaults, + ) + if print_stdout or not datasource_file: + yaml.safe_dump(data, sys.stdout, default_flow_style=False) + if datasource_file: + logger.info("Exporting datasources to %s", datasource_file) + with open(datasource_file, "w") as data_stream: + yaml.safe_dump(data, data_stream, default_flow_style=False) + + @superset.command() + @with_appcontext + @click.option( + "--path", + "-p", + help="Path to a single JSON file or path containing multiple JSON " + "files to import (*.json)", + ) + @click.option( + "--recursive", + "-r", + is_flag=True, + default=False, + help="recursively search the path for json files", + ) + @click.option( + "--username", + "-u", + default=None, + help="Specify the user name to assign dashboards to", + ) + def import_dashboards(path: str, recursive: bool, username: str) -> None: + """Import dashboards from ZIP file""" + from superset.dashboards.commands.importers.v0 import ImportDashboardsCommand + + path_object = Path(path) + files: List[Path] = [] + if path_object.is_file(): + files.append(path_object) + elif path_object.exists() and not recursive: + files.extend(path_object.glob("*.json")) + elif path_object.exists() and recursive: + files.extend(path_object.rglob("*.json")) + if username is not None: + g.user = security_manager.find_user(username=username) + contents = {path.name: open(path).read() for path in files} + try: + ImportDashboardsCommand(contents).run() + except Exception: # pylint: disable=broad-except + logger.exception("Error when importing dashboard") + + @superset.command() + @with_appcontext + @click.option( + "--path", + "-p", + help="Path to a single YAML file or path containing multiple YAML " + "files to import (*.yaml or *.yml)", + ) + @click.option( + "--sync", + "-s", + "sync", + default="", + help="comma seperated list of element types to synchronize " + 'e.g. "metrics,columns" deletes metrics and columns in the DB ' + "that are not specified in the YAML file", + ) + @click.option( + "--recursive", + "-r", + is_flag=True, + default=False, + help="recursively search the path for yaml files", + ) + def import_datasources(path: str, sync: str, recursive: bool) -> None: + """Import datasources from YAML""" + from superset.datasets.commands.importers.v0 import ImportDatasetsCommand + + sync_array = sync.split(",") + sync_columns = "columns" in sync_array + sync_metrics = "metrics" in sync_array + + path_object = Path(path) + files: List[Path] = [] + if path_object.is_file(): + files.append(path_object) + elif path_object.exists() and not recursive: + files.extend(path_object.glob("*.yaml")) + files.extend(path_object.glob("*.yml")) + elif path_object.exists() and recursive: + files.extend(path_object.rglob("*.yaml")) + files.extend(path_object.rglob("*.yml")) + contents = {path.name: open(path).read() for path in files} + try: + ImportDatasetsCommand(contents, sync_columns, sync_metrics).run() + except Exception: # pylint: disable=broad-except + logger.exception("Error when importing dataset") + + @superset.command() + @with_appcontext + @click.option( + "--back-references", + "-b", + is_flag=True, + default=False, + help="Include parent back references", + ) + def export_datasource_schema(back_references: bool) -> None: + """Export datasource YAML schema to stdout""" + from superset.utils import dict_import_export + + data = dict_import_export.export_schema_to_dict(back_references=back_references) + yaml.safe_dump(data, sys.stdout, default_flow_style=False) @superset.command() From 52e970a5200ac731016d4abc86febf73cef4337c Mon Sep 17 00:00:00 2001 From: Moriah Kreeger Date: Tue, 15 Dec 2020 12:21:10 -0800 Subject: [PATCH 17/43] fix(annotation layers): remove redirect on layer edit (#12063) --- .../src/views/CRUD/annotationlayers/AnnotationLayerModal.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayerModal.tsx b/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayerModal.tsx index 4196ecf30fe94..f5d19997d6a37 100644 --- a/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayerModal.tsx +++ b/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayerModal.tsx @@ -122,10 +122,6 @@ const AnnotationLayerModal: FunctionComponent = ({ delete currentLayer.id; delete currentLayer.created_by; updateResource(update_id, currentLayer).then(() => { - if (onLayerAdd) { - onLayerAdd(); - } - hide(); }); } From e0079bb5ae34a984c957e06d838efe48c8df637b Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Tue, 15 Dec 2020 12:24:24 -0800 Subject: [PATCH 18/43] fix: small fixes to the new import/export (#12064) --- superset/charts/commands/export.py | 2 +- superset/dashboards/commands/export.py | 2 +- .../dashboards/commands/importers/v1/utils.py | 4 ++-- superset/databases/commands/export.py | 2 +- superset/datasets/commands/export.py | 18 +++++++++++++++- .../datasets/commands/importers/v1/utils.py | 21 +++++++++++++++++++ superset/datasets/schemas.py | 8 +++---- 7 files changed, 47 insertions(+), 10 deletions(-) diff --git a/superset/charts/commands/export.py b/superset/charts/commands/export.py index 405a679fde0ac..88a87d9015ab6 100644 --- a/superset/charts/commands/export.py +++ b/superset/charts/commands/export.py @@ -57,7 +57,7 @@ def _export(model: Slice) -> Iterator[Tuple[str, str]]: # becomes the default export endpoint for key in REMOVE_KEYS: del payload[key] - if "params" in payload: + if payload.get("params"): try: payload["params"] = json.loads(payload["params"]) except json.decoder.JSONDecodeError: diff --git a/superset/dashboards/commands/export.py b/superset/dashboards/commands/export.py index 8f103d73c30af..9a1af4721fff3 100644 --- a/superset/dashboards/commands/export.py +++ b/superset/dashboards/commands/export.py @@ -108,7 +108,7 @@ def _export(model: Dashboard) -> Iterator[Tuple[str, str]]: # TODO (betodealmeida): move this logic to export_to_dict once this # becomes the default export endpoint for key, new_name in JSON_KEYS.items(): - if key in payload: + if payload.get(key): value = payload.pop(key) try: payload[new_name] = json.loads(value) diff --git a/superset/dashboards/commands/importers/v1/utils.py b/superset/dashboards/commands/importers/v1/utils.py index de3e4e84a6794..5052080957d4c 100644 --- a/superset/dashboards/commands/importers/v1/utils.py +++ b/superset/dashboards/commands/importers/v1/utils.py @@ -109,8 +109,8 @@ def import_dashboard( value = config.pop(key) try: config[new_name] = json.dumps(value) - except json.decoder.JSONDecodeError: - logger.info("Unable to decode `%s` field: %s", key, value) + except TypeError: + logger.info("Unable to encode `%s` field: %s", key, value) dashboard = Dashboard.import_from_dict(session, config, recursive=False) if dashboard.id is None: diff --git a/superset/databases/commands/export.py b/superset/databases/commands/export.py index e8937867d1e87..f373ce101a4db 100644 --- a/superset/databases/commands/export.py +++ b/superset/databases/commands/export.py @@ -50,7 +50,7 @@ def _export(model: Database) -> Iterator[Tuple[str, str]]: ) # TODO (betodealmeida): move this logic to export_to_dict once this # becomes the default export endpoint - if "extra" in payload: + if payload.get("extra"): try: payload["extra"] = json.loads(payload["extra"]) except json.decoder.JSONDecodeError: diff --git a/superset/datasets/commands/export.py b/superset/datasets/commands/export.py index e86b932295624..64946f48f2917 100644 --- a/superset/datasets/commands/export.py +++ b/superset/datasets/commands/export.py @@ -31,6 +31,8 @@ logger = logging.getLogger(__name__) +JSON_KEYS = {"params", "template_params", "extra"} + class ExportDatasetsCommand(ExportModelsCommand): @@ -49,6 +51,20 @@ def _export(model: SqlaTable) -> Iterator[Tuple[str, str]]: include_defaults=True, export_uuids=True, ) + # TODO (betodealmeida): move this logic to export_to_dict once this + # becomes the default export endpoint + for key in JSON_KEYS: + if payload.get(key): + try: + payload[key] = json.loads(payload[key]) + except json.decoder.JSONDecodeError: + logger.info("Unable to decode `%s` field: %s", key, payload[key]) + for metric in payload.get("metrics", []): + if metric.get("extra"): + try: + metric["extra"] = json.loads(metric["extra"]) + except json.decoder.JSONDecodeError: + logger.info("Unable to decode `extra` field: %s", metric["extra"]) payload["version"] = EXPORT_VERSION payload["database_uuid"] = str(model.database.uuid) @@ -67,7 +83,7 @@ def _export(model: SqlaTable) -> Iterator[Tuple[str, str]]: ) # TODO (betodealmeida): move this logic to export_to_dict once this # becomes the default export endpoint - if "extra" in payload: + if payload.get("extra"): try: payload["extra"] = json.loads(payload["extra"]) except json.decoder.JSONDecodeError: diff --git a/superset/datasets/commands/importers/v1/utils.py b/superset/datasets/commands/importers/v1/utils.py index 99326f3c3da28..1857e05245335 100644 --- a/superset/datasets/commands/importers/v1/utils.py +++ b/superset/datasets/commands/importers/v1/utils.py @@ -15,12 +15,18 @@ # specific language governing permissions and limitations # under the License. +import json +import logging from typing import Any, Dict from sqlalchemy.orm import Session from superset.connectors.sqla.models import SqlaTable +logger = logging.getLogger(__name__) + +JSON_KEYS = {"params", "template_params", "extra"} + def import_dataset( session: Session, config: Dict[str, Any], overwrite: bool = False @@ -31,6 +37,21 @@ def import_dataset( return existing config["id"] = existing.id + # TODO (betodealmeida): move this logic to import_from_dict + config = config.copy() + for key in JSON_KEYS: + if config.get(key): + try: + config[key] = json.dumps(config[key]) + except TypeError: + logger.info("Unable to encode `%s` field: %s", key, config[key]) + for metric in config.get("metrics", []): + if metric.get("extra"): + try: + metric["extra"] = json.dumps(metric["extra"]) + except TypeError: + logger.info("Unable to encode `extra` field: %s", metric["extra"]) + # should we delete columns and metrics not present in the current import? sync = ["columns", "metrics"] if overwrite else [] diff --git a/superset/datasets/schemas.py b/superset/datasets/schemas.py index 27032287393ac..4cbb2e05f4f0d 100644 --- a/superset/datasets/schemas.py +++ b/superset/datasets/schemas.py @@ -145,7 +145,7 @@ class ImportV1MetricSchema(Schema): expression = fields.String(required=True) description = fields.String(allow_none=True) d3format = fields.String(allow_none=True) - extra = fields.String(allow_none=True) + extra = fields.Dict(allow_none=True) warning_text = fields.String(allow_none=True) @@ -158,11 +158,11 @@ class ImportV1DatasetSchema(Schema): cache_timeout = fields.Integer(allow_none=True) schema = fields.String(allow_none=True) sql = fields.String(allow_none=True) - params = fields.String(allow_none=True) - template_params = fields.String(allow_none=True) + params = fields.Dict(allow_none=True) + template_params = fields.Dict(allow_none=True) filter_select_enabled = fields.Boolean() fetch_values_predicate = fields.String(allow_none=True) - extra = fields.String(allow_none=True) + extra = fields.Dict(allow_none=True) uuid = fields.UUID(required=True) columns = fields.List(fields.Nested(ImportV1ColumnSchema)) metrics = fields.List(fields.Nested(ImportV1MetricSchema)) From 5e811a14ef2e2463c8991a9388e6040acd7126f1 Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Tue, 15 Dec 2020 13:44:23 -0800 Subject: [PATCH 19/43] feat: load examples from config instead of code (#12026) * feat: load examples from config instead of code * Remove database * Update data URL --- setup.cfg | 2 +- superset/cli.py | 6 +- superset/commands/importers/v1/__init__.py | 19 +- superset/commands/importers/v1/examples.py | 119 +++++++++++++ superset/commands/importers/v1/utils.py | 2 +- .../commands/importers/v1/__init__.py | 14 +- .../dashboards/commands/importers/v1/utils.py | 9 +- .../datasets/commands/importers/v1/utils.py | 56 ++++++ superset/datasets/schemas.py | 1 + superset/examples/__init__.py | 2 +- .../configs/charts/Unicode_Cloud.yaml | 40 +++++ .../configs/dashboards/Unicode_Test.yaml | 52 ++++++ .../datasets/examples/unicode_test.yaml | 93 ++++++++++ superset/examples/configs/metadata.yaml | 18 ++ superset/examples/unicode_test_data.py | 167 ------------------ superset/examples/utils.py | 51 ++++++ 16 files changed, 444 insertions(+), 207 deletions(-) create mode 100644 superset/commands/importers/v1/examples.py create mode 100644 superset/examples/configs/charts/Unicode_Cloud.yaml create mode 100644 superset/examples/configs/dashboards/Unicode_Test.yaml create mode 100644 superset/examples/configs/datasets/examples/unicode_test.yaml create mode 100644 superset/examples/configs/metadata.yaml delete mode 100644 superset/examples/unicode_test_data.py create mode 100644 superset/examples/utils.py diff --git a/setup.cfg b/setup.cfg index 3acb2257a25e3..00a2062992e7e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,7 @@ combine_as_imports = true include_trailing_comma = true line_length = 88 known_first_party = superset -known_third_party =alembic,apispec,backoff,bleach,cachelib,celery,click,colorama,contextlib2,cron_descriptor,croniter,cryptography,dateutil,flask,flask_appbuilder,flask_babel,flask_caching,flask_compress,flask_login,flask_migrate,flask_sqlalchemy,flask_talisman,flask_testing,flask_wtf,freezegun,geohash,geopy,humanize,isodate,jinja2,jwt,markdown,markupsafe,marshmallow,msgpack,numpy,pandas,parameterized,parsedatetime,pathlib2,pgsanity,polyline,prison,pyarrow,pyhive,pytest,pytz,redis,retry,selenium,setuptools,simplejson,slack,sqlalchemy,sqlalchemy_utils,sqlparse,werkzeug,wtforms,wtforms_json,yaml +known_third_party =alembic,apispec,backoff,bleach,cachelib,celery,click,colorama,contextlib2,cron_descriptor,croniter,cryptography,dateutil,flask,flask_appbuilder,flask_babel,flask_caching,flask_compress,flask_login,flask_migrate,flask_sqlalchemy,flask_talisman,flask_testing,flask_wtf,freezegun,geohash,geopy,humanize,isodate,jinja2,jwt,markdown,markupsafe,marshmallow,msgpack,numpy,pandas,parameterized,parsedatetime,pathlib2,pgsanity,pkg_resources,polyline,prison,pyarrow,pyhive,pytest,pytz,redis,retry,selenium,setuptools,simplejson,slack,sqlalchemy,sqlalchemy_utils,sqlparse,werkzeug,wtforms,wtforms_json,yaml multi_line_output = 3 order_by_type = false diff --git a/superset/cli.py b/superset/cli.py index 8704151a55da2..a93dbed0c95da 100755 --- a/superset/cli.py +++ b/superset/cli.py @@ -124,9 +124,6 @@ def load_examples_run( print("Loading [Birth names]") examples.load_birth_names(only_metadata, force) - print("Loading [Unicode test data]") - examples.load_unicode_test_data(only_metadata, force) - if not load_test_data: print("Loading [Random time series data]") examples.load_random_time_series_data(only_metadata, force) @@ -164,6 +161,9 @@ def load_examples_run( print("Loading [Tabbed dashboard]") examples.load_tabbed_dashboard(only_metadata) + # load examples that are stored as YAML config files + examples.load_from_configs() + @with_appcontext @superset.command() diff --git a/superset/commands/importers/v1/__init__.py b/superset/commands/importers/v1/__init__.py index 9637ef94da7aa..09097394138a9 100644 --- a/superset/commands/importers/v1/__init__.py +++ b/superset/commands/importers/v1/__init__.py @@ -14,23 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - from typing import Any, Dict, List, Optional, Set from marshmallow import Schema, validate @@ -106,7 +89,7 @@ def validate(self) -> None: metadata = None # validate that the type declared in METADATA_FILE_NAME is correct - if metadata: + if metadata and "type" in metadata: type_validator = validate.Equal(self.dao.model_cls.__name__) # type: ignore try: type_validator(metadata["type"]) diff --git a/superset/commands/importers/v1/examples.py b/superset/commands/importers/v1/examples.py new file mode 100644 index 0000000000000..c5b2e6e023d4b --- /dev/null +++ b/superset/commands/importers/v1/examples.py @@ -0,0 +1,119 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from typing import Any, Dict, List, Tuple + +from marshmallow import Schema +from sqlalchemy.orm import Session +from sqlalchemy.sql import select + +from superset import db +from superset.charts.commands.importers.v1.utils import import_chart +from superset.charts.schemas import ImportV1ChartSchema +from superset.commands.exceptions import CommandException +from superset.commands.importers.v1 import ImportModelsCommand +from superset.dao.base import BaseDAO +from superset.dashboards.commands.importers.v1.utils import ( + find_chart_uuids, + import_dashboard, + update_id_refs, +) +from superset.dashboards.schemas import ImportV1DashboardSchema +from superset.databases.commands.importers.v1.utils import import_database +from superset.databases.schemas import ImportV1DatabaseSchema +from superset.datasets.commands.importers.v1.utils import import_dataset +from superset.datasets.schemas import ImportV1DatasetSchema +from superset.models.core import Database +from superset.models.dashboard import dashboard_slices + + +class ImportExamplesCommand(ImportModelsCommand): + + """Import examples""" + + dao = BaseDAO + model_name = "model" + schemas: Dict[str, Schema] = { + "charts/": ImportV1ChartSchema(), + "dashboards/": ImportV1DashboardSchema(), + "datasets/": ImportV1DatasetSchema(), + "databases/": ImportV1DatabaseSchema(), + } + import_error = CommandException + + # pylint: disable=too-many-locals + @staticmethod + def _import( + session: Session, configs: Dict[str, Any], overwrite: bool = False + ) -> None: + # import databases + database_ids: Dict[str, int] = {} + for file_name, config in configs.items(): + if file_name.startswith("databases/"): + database = import_database(session, config, overwrite=overwrite) + database_ids[str(database.uuid)] = database.id + + # import datasets + # TODO (betodealmeida): once we have all examples being imported we can + # have a stable UUID for the database stored in the dataset YAML; for + # now we need to fetch the current ID. + examples_id = ( + db.session.query(Database).filter_by(database_name="examples").one().id + ) + dataset_info: Dict[str, Dict[str, Any]] = {} + for file_name, config in configs.items(): + if file_name.startswith("datasets/"): + config["database_id"] = examples_id + dataset = import_dataset(session, config, overwrite=overwrite) + dataset_info[str(dataset.uuid)] = { + "datasource_id": dataset.id, + "datasource_type": "view" if dataset.is_sqllab_view else "table", + "datasource_name": dataset.table_name, + } + + # import charts + chart_ids: Dict[str, int] = {} + for file_name, config in configs.items(): + if file_name.startswith("charts/"): + # update datasource id, type, and name + config.update(dataset_info[config["dataset_uuid"]]) + chart = import_chart(session, config, overwrite=overwrite) + chart_ids[str(chart.uuid)] = chart.id + + # store the existing relationship between dashboards and charts + existing_relationships = session.execute( + select([dashboard_slices.c.dashboard_id, dashboard_slices.c.slice_id]) + ).fetchall() + + # import dashboards + dashboard_chart_ids: List[Tuple[int, int]] = [] + for file_name, config in configs.items(): + if file_name.startswith("dashboards/"): + config = update_id_refs(config, chart_ids) + dashboard = import_dashboard(session, config, overwrite=overwrite) + for uuid in find_chart_uuids(config["position"]): + chart_id = chart_ids[uuid] + if (dashboard.id, chart_id) not in existing_relationships: + dashboard_chart_ids.append((dashboard.id, chart_id)) + + # set ref in the dashboard_slices table + values = [ + {"dashboard_id": dashboard_id, "slice_id": chart_id} + for (dashboard_id, chart_id) in dashboard_chart_ids + ] + # pylint: disable=no-value-for-parameter (sqlalchemy/issues/4656) + session.execute(dashboard_slices.insert(), values) diff --git a/superset/commands/importers/v1/utils.py b/superset/commands/importers/v1/utils.py index 623911246a6eb..a94ae18124aa7 100644 --- a/superset/commands/importers/v1/utils.py +++ b/superset/commands/importers/v1/utils.py @@ -38,7 +38,7 @@ def remove_root(file_path: str) -> str: class MetadataSchema(Schema): version = fields.String(required=True, validate=validate.Equal(IMPORT_VERSION)) - type = fields.String(required=True) + type = fields.String(required=False) timestamp = fields.DateTime() diff --git a/superset/dashboards/commands/importers/v1/__init__.py b/superset/dashboards/commands/importers/v1/__init__.py index 00e01356c65bc..6bfb1c8bd27b2 100644 --- a/superset/dashboards/commands/importers/v1/__init__.py +++ b/superset/dashboards/commands/importers/v1/__init__.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -from typing import Any, Dict, Iterator, List, Set, Tuple +from typing import Any, Dict, List, Set, Tuple from marshmallow import Schema from sqlalchemy.orm import Session @@ -26,6 +26,7 @@ from superset.commands.importers.v1 import ImportModelsCommand from superset.dashboards.commands.exceptions import DashboardImportError from superset.dashboards.commands.importers.v1.utils import ( + find_chart_uuids, import_dashboard, update_id_refs, ) @@ -38,17 +39,6 @@ from superset.models.dashboard import dashboard_slices -def find_chart_uuids(position: Dict[str, Any]) -> Iterator[str]: - """Find all chart UUIDs in a dashboard""" - for child in position.values(): - if ( - isinstance(child, dict) - and child["type"] == "CHART" - and "uuid" in child["meta"] - ): - yield child["meta"]["uuid"] - - class ImportDashboardsCommand(ImportModelsCommand): """Import dashboards""" diff --git a/superset/dashboards/commands/importers/v1/utils.py b/superset/dashboards/commands/importers/v1/utils.py index 5052080957d4c..e9c584c057bb7 100644 --- a/superset/dashboards/commands/importers/v1/utils.py +++ b/superset/dashboards/commands/importers/v1/utils.py @@ -17,7 +17,7 @@ import json import logging -from typing import Any, Dict +from typing import Any, Dict, Set from sqlalchemy.orm import Session @@ -29,6 +29,10 @@ JSON_KEYS = {"position": "position_json", "metadata": "json_metadata"} +def find_chart_uuids(position: Dict[str, Any]) -> Set[str]: + return set(build_uuid_to_id_map(position)) + + def build_uuid_to_id_map(position: Dict[str, Any]) -> Dict[str, int]: return { child["meta"]["uuid"]: child["meta"]["chartId"] @@ -43,9 +47,6 @@ def build_uuid_to_id_map(position: Dict[str, Any]) -> Dict[str, int]: def update_id_refs(config: Dict[str, Any], chart_ids: Dict[str, int]) -> Dict[str, Any]: """Update dashboard metadata to use new IDs""" - if not config.get("metadata"): - return config - fixed = config.copy() # build map old_id => new_id diff --git a/superset/datasets/commands/importers/v1/utils.py b/superset/datasets/commands/importers/v1/utils.py index 1857e05245335..92b660d9e5c63 100644 --- a/superset/datasets/commands/importers/v1/utils.py +++ b/superset/datasets/commands/importers/v1/utils.py @@ -17,17 +17,48 @@ import json import logging +import re from typing import Any, Dict +from urllib import request +import pandas as pd +from sqlalchemy import Date, Float, String from sqlalchemy.orm import Session +from sqlalchemy.sql.visitors import VisitableType from superset.connectors.sqla.models import SqlaTable logger = logging.getLogger(__name__) +CHUNKSIZE = 512 +VARCHAR = re.compile(r"VARCHAR\((\d+)\)", re.IGNORECASE) + JSON_KEYS = {"params", "template_params", "extra"} +def get_sqla_type(native_type: str) -> VisitableType: + if native_type.upper() == "DATE": + return Date() + + if native_type.upper() == "FLOAT": + return Float() + + match = VARCHAR.match(native_type) + if match: + size = int(match.group(1)) + return String(size) + + raise Exception(f"Unknown type: {native_type}") + + +def get_dtype(df: pd.DataFrame, dataset: SqlaTable) -> Dict[str, VisitableType]: + return { + column.column_name: get_sqla_type(column.type) + for column in dataset.columns + if column.column_name in df.keys() + } + + def import_dataset( session: Session, config: Dict[str, Any], overwrite: bool = False ) -> SqlaTable: @@ -55,9 +86,34 @@ def import_dataset( # should we delete columns and metrics not present in the current import? sync = ["columns", "metrics"] if overwrite else [] + # should we also load data into the dataset? + data_uri = config.get("data") + # import recursively to include columns and metrics dataset = SqlaTable.import_from_dict(session, config, recursive=True, sync=sync) if dataset.id is None: session.flush() + # load data + if data_uri: + data = request.urlopen(data_uri) + df = pd.read_csv(data, encoding="utf-8") + dtype = get_dtype(df, dataset) + + # convert temporal columns + for column_name, sqla_type in dtype.items(): + if isinstance(sqla_type, Date): + df[column_name] = pd.to_datetime(df[column_name]) + + df.to_sql( + dataset.table_name, + con=session.connection(), + schema=dataset.schema, + if_exists="replace", + chunksize=CHUNKSIZE, + dtype=dtype, + index=False, + method="multi", + ) + return dataset diff --git a/superset/datasets/schemas.py b/superset/datasets/schemas.py index 4cbb2e05f4f0d..79a4c79706e19 100644 --- a/superset/datasets/schemas.py +++ b/superset/datasets/schemas.py @@ -168,3 +168,4 @@ class ImportV1DatasetSchema(Schema): metrics = fields.List(fields.Nested(ImportV1MetricSchema)) version = fields.String(required=True) database_uuid = fields.UUID(required=True) + data = fields.URL() diff --git a/superset/examples/__init__.py b/superset/examples/__init__.py index ff1b4e17a38ae..b8a844739b920 100644 --- a/superset/examples/__init__.py +++ b/superset/examples/__init__.py @@ -29,5 +29,5 @@ from .random_time_series import load_random_time_series_data from .sf_population_polygons import load_sf_population_polygons from .tabbed_dashboard import load_tabbed_dashboard -from .unicode_test_data import load_unicode_test_data +from .utils import load_from_configs from .world_bank import load_world_bank_health_n_pop diff --git a/superset/examples/configs/charts/Unicode_Cloud.yaml b/superset/examples/configs/charts/Unicode_Cloud.yaml new file mode 100644 index 0000000000000..5c60d8789b36a --- /dev/null +++ b/superset/examples/configs/charts/Unicode_Cloud.yaml @@ -0,0 +1,40 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +slice_name: Unicode Cloud +viz_type: word_cloud +params: + granularity_sqla: dttm + groupby: [] + limit: '100' + metric: + aggregate: SUM + column: + column_name: value + expressionType: SIMPLE + label: Value + rotation: square + row_limit: 50000 + series: short_phrase + since: 100 years ago + size_from: '10' + size_to: '70' + until: now + viz_type: word_cloud +cache_timeout: null +uuid: 609e26d8-8e1e-4097-9751-931708e24ee4 +version: 1.0.0 +dataset_uuid: a6771c73-96fc-44c6-8b6e-9d303955ea48 diff --git a/superset/examples/configs/dashboards/Unicode_Test.yaml b/superset/examples/configs/dashboards/Unicode_Test.yaml new file mode 100644 index 0000000000000..f14923a4d14eb --- /dev/null +++ b/superset/examples/configs/dashboards/Unicode_Test.yaml @@ -0,0 +1,52 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +dashboard_title: Unicode Test +description: null +css: null +slug: unicode-test +uuid: 6b2de44e-7db1-4264-bfc1-ac3c00d42fad +position: + CHART-Hkx6154FEm: + children: [] + id: CHART-Hkx6154FEm + meta: + chartId: 389 + height: 30 + sliceName: slice 1 + width: 4 + uuid: 609e26d8-8e1e-4097-9751-931708e24ee4 + type: CHART + GRID_ID: + children: + - ROW-SyT19EFEQ + id: GRID_ID + type: GRID + ROOT_ID: + children: + - GRID_ID + id: ROOT_ID + type: ROOT + ROW-SyT19EFEQ: + children: + - CHART-Hkx6154FEm + id: ROW-SyT19EFEQ + meta: + background: BACKGROUND_TRANSPARENT + type: ROW + DASHBOARD_VERSION_KEY: v2 +metadata: {} +version: 1.0.0 diff --git a/superset/examples/configs/datasets/examples/unicode_test.yaml b/superset/examples/configs/datasets/examples/unicode_test.yaml new file mode 100644 index 0000000000000..907eab5df32a7 --- /dev/null +++ b/superset/examples/configs/datasets/examples/unicode_test.yaml @@ -0,0 +1,93 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +table_name: unicode_test +main_dttm_col: dttm +description: null +default_endpoint: null +offset: 0 +cache_timeout: null +schema: null +sql: null +params: null +template_params: null +filter_select_enabled: false +fetch_values_predicate: null +extra: null +uuid: a6771c73-96fc-44c6-8b6e-9d303955ea48 +metrics: +- metric_name: count + verbose_name: COUNT(*) + metric_type: count + expression: COUNT(*) + description: null + d3format: null + extra: null + warning_text: null +columns: +- column_name: with_missing + verbose_name: null + is_dttm: false + is_active: true + type: VARCHAR(100) + groupby: true + filterable: true + expression: '' + description: null + python_date_format: null +- column_name: phrase + verbose_name: null + is_dttm: false + is_active: true + type: VARCHAR(500) + groupby: true + filterable: true + expression: '' + description: null + python_date_format: null +- column_name: short_phrase + verbose_name: null + is_dttm: false + is_active: true + type: VARCHAR(10) + groupby: true + filterable: true + expression: '' + description: null + python_date_format: null +- column_name: dttm + verbose_name: null + is_dttm: true + is_active: true + type: DATE + groupby: true + filterable: true + expression: '' + description: null + python_date_format: null +- column_name: value + verbose_name: null + is_dttm: false + is_active: true + type: FLOAT + groupby: true + filterable: true + expression: '' + description: null + python_date_format: null +version: 1.0.0 +database_uuid: a2dc77af-e654-49bb-b321-40f6b559a1ee +data: https://raw.githubusercontent.com/apache-superset/examples-data/master/datasets/examples/unicode_test.csv diff --git a/superset/examples/configs/metadata.yaml b/superset/examples/configs/metadata.yaml new file mode 100644 index 0000000000000..797d1f8764ba0 --- /dev/null +++ b/superset/examples/configs/metadata.yaml @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +version: 1.0.0 +timestamp: '2020-12-11T22:52:56.534241+00:00' diff --git a/superset/examples/unicode_test_data.py b/superset/examples/unicode_test_data.py deleted file mode 100644 index 15924b2e42d63..0000000000000 --- a/superset/examples/unicode_test_data.py +++ /dev/null @@ -1,167 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -import datetime -import json -import random - -import pandas as pd -from sqlalchemy import Date, Float, String - -from superset import db -from superset.models.dashboard import Dashboard -from superset.models.slice import Slice -from superset.utils import core as utils - -from .helpers import ( - config, - get_example_data, - get_slice_json, - merge_slice, - TBL, - update_slice_ids, -) - - -def load_unicode_test_data( - only_metadata: bool = False, force: bool = False, sample: bool = False -) -> None: - """Loading unicode test dataset from a csv file in the repo""" - tbl_name = "unicode_test" - database = utils.get_example_database() - table_exists = database.has_table_by_name(tbl_name) - - if not only_metadata and (not table_exists or force): - data = get_example_data( - "unicode_utf8_unixnl_test.csv", is_gzip=False, make_bytes=True - ) - df = pd.read_csv(data, encoding="utf-8") - # generate date/numeric data - df["dttm"] = datetime.datetime.now().date() - df["value"] = [random.randint(1, 100) for _ in range(len(df))] - df = df.head(100) if sample else df - df.to_sql( # pylint: disable=no-member - tbl_name, - database.get_sqla_engine(), - if_exists="replace", - chunksize=500, - dtype={ - "phrase": String(500), - "short_phrase": String(10), - "with_missing": String(100), - "dttm": Date(), - "value": Float(), - }, - index=False, - method="multi", - ) - print("Done loading table!") - print("-" * 80) - - print("Creating table [unicode_test] reference") - obj = db.session.query(TBL).filter_by(table_name=tbl_name).first() - if not obj: - obj = TBL(table_name=tbl_name) - obj.main_dttm_col = "dttm" - obj.database = database - db.session.merge(obj) - db.session.commit() - obj.fetch_metadata() - tbl = obj - - slice_data = { - "granularity_sqla": "dttm", - "groupby": [], - "metric": { - "aggregate": "SUM", - "column": {"column_name": "value"}, - "expressionType": "SIMPLE", - "label": "Value", - }, - "row_limit": config["ROW_LIMIT"], - "since": "100 years ago", - "until": "now", - "viz_type": "word_cloud", - "size_from": "10", - "series": "short_phrase", - "size_to": "70", - "rotation": "square", - "limit": "100", - } - - print("Creating a slice") - slc = Slice( - slice_name="Unicode Cloud", - viz_type="word_cloud", - datasource_type="table", - datasource_id=tbl.id, - params=get_slice_json(slice_data), - ) - merge_slice(slc) - - print("Creating a dashboard") - dash = db.session.query(Dashboard).filter_by(slug="unicode-test").first() - - if not dash: - dash = Dashboard() - js = """\ -{ - "CHART-Hkx6154FEm": { - "children": [], - "id": "CHART-Hkx6154FEm", - "meta": { - "chartId": 2225, - "height": 30, - "sliceName": "slice 1", - "width": 4 - }, - "type": "CHART" - }, - "GRID_ID": { - "children": [ - "ROW-SyT19EFEQ" - ], - "id": "GRID_ID", - "type": "GRID" - }, - "ROOT_ID": { - "children": [ - "GRID_ID" - ], - "id": "ROOT_ID", - "type": "ROOT" - }, - "ROW-SyT19EFEQ": { - "children": [ - "CHART-Hkx6154FEm" - ], - "id": "ROW-SyT19EFEQ", - "meta": { - "background": "BACKGROUND_TRANSPARENT" - }, - "type": "ROW" - }, - "DASHBOARD_VERSION_KEY": "v2" -} - """ - dash.dashboard_title = "Unicode Test" - pos = json.loads(js) - update_slice_ids(pos, [slc]) - dash.position_json = json.dumps(pos, indent=4) - dash.slug = "unicode-test" - dash.slices = [slc] - db.session.merge(dash) - db.session.commit() diff --git a/superset/examples/utils.py b/superset/examples/utils.py new file mode 100644 index 0000000000000..01e364d3fc119 --- /dev/null +++ b/superset/examples/utils.py @@ -0,0 +1,51 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from pathlib import Path +from typing import Any, Dict + +from pkg_resources import resource_isdir, resource_listdir, resource_stream + +from superset.commands.importers.v1.examples import ImportExamplesCommand + + +def load_from_configs() -> None: + contents = load_contents() + command = ImportExamplesCommand(contents, overwrite=True) + command.run() + + +def load_contents() -> Dict[str, Any]: + """Traverse configs directory and load contents""" + root = Path("examples/configs") + resource_names = resource_listdir("superset", str(root)) + queue = [root / resource_name for resource_name in resource_names] + + contents: Dict[Path, str] = {} + while queue: + path_name = queue.pop() + + if resource_isdir("superset", str(path_name)): + queue.extend( + path_name / child_name + for child_name in resource_listdir("superset", str(path_name)) + ) + else: + contents[path_name] = ( + resource_stream("superset", str(path_name)).read().decode("utf-8") + ) + + return {str(path.relative_to(root)): content for path, content in contents.items()} From 77cae64ccd89a451d89fdcf2b812c8dc061479bc Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Tue, 15 Dec 2020 14:21:51 -0800 Subject: [PATCH 20/43] feat: Add new default dashboard (#12044) * feat: Add new default dashboard * Fix license * Update data URL --- .../datasets/commands/importers/v1/utils.py | 22 +- superset/db_engine_specs/sqlite.py | 9 + superset/examples/configs/charts/Filter.yaml | 48 ++ .../Number_of_Deals_for_each_Combination.yaml | 56 +++ .../charts/Overall_Sales_By_Product_Line.yaml | 70 +++ ...Proportion_of_Revenue_by_Product_Line.yaml | 78 ++++ .../configs/charts/Quarterly_Sales.yaml | 89 ++++ .../Quarterly_Sales_By_Product_Line.yaml | 92 ++++ .../configs/charts/Revenue_by_Deal_Size.yaml | 79 ++++ ...asonality_of_Revenue_per_Product_Line.yaml | 62 +++ .../configs/charts/Total_Items_Sold.yaml | 57 +++ .../Total_Items_Sold_By_Product_Line.yaml | 68 +++ .../configs/charts/Total_Revenue.yaml | 58 +++ .../configs/dashboards/Sales_Dashboard.yaml | 415 ++++++++++++++++++ .../datasets/examples/Cleaned_Sales_Data.yaml | 293 +++++++++++++ 15 files changed, 1489 insertions(+), 7 deletions(-) create mode 100644 superset/examples/configs/charts/Filter.yaml create mode 100644 superset/examples/configs/charts/Number_of_Deals_for_each_Combination.yaml create mode 100644 superset/examples/configs/charts/Overall_Sales_By_Product_Line.yaml create mode 100644 superset/examples/configs/charts/Proportion_of_Revenue_by_Product_Line.yaml create mode 100644 superset/examples/configs/charts/Quarterly_Sales.yaml create mode 100644 superset/examples/configs/charts/Quarterly_Sales_By_Product_Line.yaml create mode 100644 superset/examples/configs/charts/Revenue_by_Deal_Size.yaml create mode 100644 superset/examples/configs/charts/Seasonality_of_Revenue_per_Product_Line.yaml create mode 100644 superset/examples/configs/charts/Total_Items_Sold.yaml create mode 100644 superset/examples/configs/charts/Total_Items_Sold_By_Product_Line.yaml create mode 100644 superset/examples/configs/charts/Total_Revenue.yaml create mode 100644 superset/examples/configs/dashboards/Sales_Dashboard.yaml create mode 100644 superset/examples/configs/datasets/examples/Cleaned_Sales_Data.yaml diff --git a/superset/datasets/commands/importers/v1/utils.py b/superset/datasets/commands/importers/v1/utils.py index 92b660d9e5c63..9941b1d2d3cac 100644 --- a/superset/datasets/commands/importers/v1/utils.py +++ b/superset/datasets/commands/importers/v1/utils.py @@ -22,7 +22,7 @@ from urllib import request import pandas as pd -from sqlalchemy import Date, Float, String +from sqlalchemy import BigInteger, Date, DateTime, Float, String, Text from sqlalchemy.orm import Session from sqlalchemy.sql.visitors import VisitableType @@ -36,12 +36,20 @@ JSON_KEYS = {"params", "template_params", "extra"} -def get_sqla_type(native_type: str) -> VisitableType: - if native_type.upper() == "DATE": - return Date() +type_map = { + "BIGINT": BigInteger(), + "FLOAT": Float(), + "DATE": Date(), + "DOUBLE PRECISION": Float(precision=32), + "TEXT": Text(), + "TIMESTAMP WITHOUT TIME ZONE": DateTime(timezone=False), + "TIMESTAMP WITH TIME ZONE": DateTime(timezone=True), +} + - if native_type.upper() == "FLOAT": - return Float() +def get_sqla_type(native_type: str) -> VisitableType: + if native_type.upper() in type_map: + return type_map[native_type.upper()] match = VARCHAR.match(native_type) if match: @@ -102,7 +110,7 @@ def import_dataset( # convert temporal columns for column_name, sqla_type in dtype.items(): - if isinstance(sqla_type, Date): + if isinstance(sqla_type, (Date, DateTime)): df[column_name] = pd.to_datetime(df[column_name]) df.to_sql( diff --git a/superset/db_engine_specs/sqlite.py b/superset/db_engine_specs/sqlite.py index d105d9b748a16..9b0aa9f33e0f5 100644 --- a/superset/db_engine_specs/sqlite.py +++ b/superset/db_engine_specs/sqlite.py @@ -31,6 +31,7 @@ class SqliteEngineSpec(BaseEngineSpec): engine = "sqlite" engine_name = "SQLite" + # pylint: disable=line-too-long _time_grain_expressions = { None: "{col}", "PT1S": "DATETIME(STRFTIME('%Y-%m-%dT%H:%M:%S', {col}))", @@ -39,6 +40,14 @@ class SqliteEngineSpec(BaseEngineSpec): "P1D": "DATE({col})", "P1W": "DATE({col}, -strftime('%W', {col}) || ' days')", "P1M": "DATE({col}, -strftime('%d', {col}) || ' days', '+1 day')", + "P0.25Y": ( + "DATETIME(STRFTIME('%Y-', {col}) || " # year + "SUBSTR('00' || " # pad with zeros to 2 chars + "((CAST(STRFTIME('%m', {col}) AS INTEGER)) - " # month as integer + "(((CAST(STRFTIME('%m', {col}) AS INTEGER)) - 1) % 3)), " # month in quarter + "-2) || " # close pad + "'-01T00:00:00')" + ), "P1Y": "DATETIME(STRFTIME('%Y-01-01T00:00:00', {col}))", "P1W/1970-01-03T00:00:00Z": "DATE({col}, 'weekday 6')", "1969-12-28T00:00:00Z/P1W": "DATE({col}, 'weekday 0', '-7 days')", diff --git a/superset/examples/configs/charts/Filter.yaml b/superset/examples/configs/charts/Filter.yaml new file mode 100644 index 0000000000000..1acb50663018c --- /dev/null +++ b/superset/examples/configs/charts/Filter.yaml @@ -0,0 +1,48 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +slice_name: Filter +viz_type: filter_box +params: + adhoc_filters: [] + datasource: 23__table + date_filter: true + filter_configs: + - asc: true + clearable: true + column: ProductLine + key: 7oUjq15eQ + multiple: true + searchAllOptions: false + - asc: true + clearable: true + column: DealSize + key: c3hO6Eub8 + multiple: true + searchAllOptions: false + granularity_sqla: OrderDate + queryFields: {} + slice_id: 671 + time_range: '2003-01-01T00:00:00 : 2005-06-01T00:00:00' + time_range_endpoints: + - inclusive + - exclusive + url_params: {} + viz_type: filter_box +cache_timeout: null +uuid: a5689df7-98fc-7c51-602c-ebd92dc3ec70 +version: 1.0.0 +dataset_uuid: e8623bb9-5e00-f531-506a-19607f5f8005 diff --git a/superset/examples/configs/charts/Number_of_Deals_for_each_Combination.yaml b/superset/examples/configs/charts/Number_of_Deals_for_each_Combination.yaml new file mode 100644 index 0000000000000..af9b4e84a7b6f --- /dev/null +++ b/superset/examples/configs/charts/Number_of_Deals_for_each_Combination.yaml @@ -0,0 +1,56 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +slice_name: Number of Deals (for each Combination) +viz_type: heatmap +params: + adhoc_filters: [] + all_columns_x: DealSize + all_columns_y: ProductLine + bottom_margin: 100 + canvas_image_rendering: pixelated + datasource: 23__table + granularity_sqla: OrderDate + left_margin: 75 + linear_color_scheme: schemePuBuGn + metric: count + normalize_across: heatmap + normalized: true + queryFields: + metric: metrics + row_limit: null + show_legend: true + show_perc: true + show_values: true + slice_id: 2810 + sort_x_axis: alpha_asc + sort_y_axis: alpha_asc + time_range: No filter + time_range_endpoints: + - inclusive + - exclusive + url_params: {} + viz_type: heatmap + xscale_interval: null + y_axis_bounds: + - null + - null + y_axis_format: SMART_NUMBER + yscale_interval: null +cache_timeout: null +uuid: bd20fc69-dd51-46c1-99b5-09e37a434bf1 +version: 1.0.0 +dataset_uuid: e8623bb9-5e00-f531-506a-19607f5f8005 diff --git a/superset/examples/configs/charts/Overall_Sales_By_Product_Line.yaml b/superset/examples/configs/charts/Overall_Sales_By_Product_Line.yaml new file mode 100644 index 0000000000000..26b395d47b1ad --- /dev/null +++ b/superset/examples/configs/charts/Overall_Sales_By_Product_Line.yaml @@ -0,0 +1,70 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +slice_name: Overall Sales (By Product Line) +viz_type: pie +params: + adhoc_filters: [] + color_scheme: supersetColors + datasource: 23__table + donut: true + granularity_sqla: OrderDate + groupby: + - ProductLine + innerRadius: 41 + label_line: true + labels_outside: true + metric: + aggregate: SUM + column: + column_name: Sales + description: null + expression: null + filterable: true + groupby: true + id: 917 + is_dttm: false + optionName: _col_Sales + python_date_format: null + type: DOUBLE PRECISION + verbose_name: null + expressionType: SIMPLE + hasCustomLabel: false + isNew: false + label: (Sales) + optionName: metric_3sk6pfj3m7i_64h77bs4sly + sqlExpression: null + number_format: SMART_NUMBER + outerRadius: 65 + pie_label_type: key + queryFields: + groupby: groupby + metric: metrics + row_limit: null + show_labels: true + show_labels_threshold: 2 + show_legend: false + slice_id: 670 + time_range: No filter + time_range_endpoints: + - inclusive + - exclusive + url_params: {} + viz_type: pie +cache_timeout: null +uuid: 09c497e0-f442-1121-c9e7-671e37750424 +version: 1.0.0 +dataset_uuid: e8623bb9-5e00-f531-506a-19607f5f8005 diff --git a/superset/examples/configs/charts/Proportion_of_Revenue_by_Product_Line.yaml b/superset/examples/configs/charts/Proportion_of_Revenue_by_Product_Line.yaml new file mode 100644 index 0000000000000..bc671e98f0a77 --- /dev/null +++ b/superset/examples/configs/charts/Proportion_of_Revenue_by_Product_Line.yaml @@ -0,0 +1,78 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +slice_name: Proportion of Revenue by Product Line +viz_type: area +params: + adhoc_filters: [] + annotation_layers: [] + bottom_margin: auto + color_scheme: supersetColors + comparison_type: values + contribution: true + datasource: 23__table + granularity_sqla: OrderDate + groupby: + - ProductLine + label_colors: {} + line_interpolation: linear + metrics: + - aggregate: SUM + column: + column_name: Sales + description: null + expression: null + filterable: true + groupby: true + id: 917 + is_dttm: false + optionName: _col_Sales + python_date_format: null + type: DOUBLE PRECISION + verbose_name: null + expressionType: SIMPLE + hasCustomLabel: false + isNew: false + label: (Sales) + optionName: metric_3is69ofceho_6d0ezok7ry6 + sqlExpression: null + order_desc: true + queryFields: + groupby: groupby + metrics: metrics + rich_tooltip: true + rolling_type: None + row_limit: null + show_brush: auto + show_legend: true + stacked_style: stack + time_grain_sqla: P1M + time_range: '2003-01-01T00:00:00 : 2005-06-01T00:00:00' + time_range_endpoints: + - inclusive + - exclusive + url_params: {} + viz_type: area + x_axis_format: smart_date + x_ticks_layout: auto + y_axis_bounds: + - null + - null + y_axis_format: SMART_NUMBER +cache_timeout: null +uuid: 08aff161-f60c-4cb3-a225-dc9b1140d2e3 +version: 1.0.0 +dataset_uuid: e8623bb9-5e00-f531-506a-19607f5f8005 diff --git a/superset/examples/configs/charts/Quarterly_Sales.yaml b/superset/examples/configs/charts/Quarterly_Sales.yaml new file mode 100644 index 0000000000000..acdd267310170 --- /dev/null +++ b/superset/examples/configs/charts/Quarterly_Sales.yaml @@ -0,0 +1,89 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +slice_name: Quarterly Sales +viz_type: bar +params: + adhoc_filters: [] + annotation_layers: [] + bottom_margin: auto + color_scheme: supersetColors + comparison_type: null + datasource: 23__table + granularity_sqla: OrderDate + groupby: [] + label_colors: + Classic Cars: '#5AC189' + Motorcycles: '#666666' + Planes: '#FCC700' + QuantityOrdered: '#454E7C' + SUM(Sales): '#1FA8C9' + Ships: '#A868B7' + Trains: '#3CCCCB' + Trucks and Buses: '#E04355' + Vintage Cars: '#FF7F44' + left_margin: auto + line_interpolation: linear + metrics: + - aggregate: SUM + column: + column_name: Sales + description: null + expression: null + filterable: true + groupby: true + id: 917 + is_dttm: false + optionName: _col_Sales + python_date_format: null + type: DOUBLE PRECISION + verbose_name: null + expressionType: SIMPLE + hasCustomLabel: false + isNew: false + label: SUM(Sales) + optionName: metric_tjn8bh6y44_7o4etwsqhal + sqlExpression: null + order_desc: true + queryFields: + groupby: groupby + metrics: metrics + rich_tooltip: true + rolling_type: null + row_limit: 10000 + show_brush: auto + show_legend: false + slice_id: 668 + time_compare: null + time_grain_sqla: P0.25Y + time_range: No filter + time_range_endpoints: + - inclusive + - exclusive + url_params: {} + viz_type: bar + x_axis_format: '%m/%d/%Y' + x_axis_label: Quarter starting + x_ticks_layout: auto + y_axis_bounds: + - null + - null + y_axis_format: null + y_axis_label: Total Sales +cache_timeout: null +uuid: 692aca26-a526-85db-c94c-411c91cc1077 +version: 1.0.0 +dataset_uuid: e8623bb9-5e00-f531-506a-19607f5f8005 diff --git a/superset/examples/configs/charts/Quarterly_Sales_By_Product_Line.yaml b/superset/examples/configs/charts/Quarterly_Sales_By_Product_Line.yaml new file mode 100644 index 0000000000000..85968bd81eebc --- /dev/null +++ b/superset/examples/configs/charts/Quarterly_Sales_By_Product_Line.yaml @@ -0,0 +1,92 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +slice_name: Quarterly Sales (By Product Line) +viz_type: bar +params: + adhoc_filters: [] + annotation_layers: [] + bar_stacked: true + bottom_margin: auto + color_scheme: supersetColors + comparison_type: null + datasource: 23__table + granularity_sqla: OrderDate + groupby: + - ProductLine + label_colors: + Classic Cars: '#5AC189' + Motorcycles: '#666666' + Planes: '#FCC700' + QuantityOrdered: '#454E7C' + SUM(Sales): '#1FA8C9' + Ships: '#A868B7' + Trains: '#3CCCCB' + Trucks and Buses: '#E04355' + Vintage Cars: '#FF7F44' + left_margin: auto + line_interpolation: linear + metrics: + - aggregate: SUM + column: + column_name: Sales + description: null + expression: null + filterable: true + groupby: true + id: 917 + is_dttm: false + optionName: _col_Sales + python_date_format: null + type: DOUBLE PRECISION + verbose_name: null + expressionType: SIMPLE + hasCustomLabel: false + isNew: false + label: SUM(Sales) + optionName: metric_tjn8bh6y44_7o4etwsqhal + sqlExpression: null + order_desc: true + queryFields: + groupby: groupby + metrics: metrics + rich_tooltip: true + rolling_type: null + row_limit: 10000 + show_brush: auto + show_controls: false + show_legend: true + slice_id: 2806 + time_compare: null + time_grain_sqla: P0.25Y + time_range: No filter + time_range_endpoints: + - inclusive + - exclusive + url_params: {} + viz_type: bar + x_axis_format: '%m/%d/%Y' + x_axis_label: Quarter starting + x_ticks_layout: "45\xB0" + y_axis_bounds: + - null + - null + y_axis_format: null + y_axis_label: Revenue ($) +cache_timeout: null +uuid: db9609e4-9b78-4a32-87a7-4d9e19d51cd8 +version: 1.0.0 +dataset_uuid: e8623bb9-5e00-f531-506a-19607f5f8005 diff --git a/superset/examples/configs/charts/Revenue_by_Deal_Size.yaml b/superset/examples/configs/charts/Revenue_by_Deal_Size.yaml new file mode 100644 index 0000000000000..e7bb9e160c442 --- /dev/null +++ b/superset/examples/configs/charts/Revenue_by_Deal_Size.yaml @@ -0,0 +1,79 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +slice_name: Revenue by Deal Size +viz_type: bar +params: + adhoc_filters: [] + annotation_layers: [] + bar_stacked: true + bottom_margin: auto + color_scheme: supersetColors + comparison_type: values + contribution: false + datasource: 23__table + granularity_sqla: OrderDate + groupby: + - DealSize + label_colors: {} + left_margin: auto + line_interpolation: linear + metrics: + - aggregate: SUM + column: + column_name: Sales + description: null + expression: null + filterable: true + groupby: true + id: 917 + is_dttm: false + optionName: _col_Sales + python_date_format: null + type: DOUBLE PRECISION + verbose_name: null + expressionType: SIMPLE + hasCustomLabel: false + isNew: false + label: (Sales) + optionName: metric_3is69ofceho_6d0ezok7ry6 + sqlExpression: null + order_desc: true + queryFields: + groupby: groupby + metrics: metrics + rich_tooltip: true + rolling_type: None + row_limit: null + show_brush: auto + show_legend: true + time_grain_sqla: P1M + time_range: '2003-01-01T00:00:00 : 2005-06-01T00:00:00' + time_range_endpoints: + - inclusive + - exclusive + url_params: {} + viz_type: bar + x_axis_format: smart_date + x_ticks_layout: auto + y_axis_bounds: + - null + - null + y_axis_format: SMART_NUMBER +cache_timeout: null +uuid: f065a533-2e13-42b9-bd19-801a21700dff +version: 1.0.0 +dataset_uuid: e8623bb9-5e00-f531-506a-19607f5f8005 diff --git a/superset/examples/configs/charts/Seasonality_of_Revenue_per_Product_Line.yaml b/superset/examples/configs/charts/Seasonality_of_Revenue_per_Product_Line.yaml new file mode 100644 index 0000000000000..3f9fcaa99f27a --- /dev/null +++ b/superset/examples/configs/charts/Seasonality_of_Revenue_per_Product_Line.yaml @@ -0,0 +1,62 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +slice_name: Seasonality of Revenue (per Product Line) +viz_type: horizon +params: + adhoc_filters: [] + datasource: 23__table + granularity_sqla: OrderDate + groupby: + - ProductLine + horizon_color_scale: series + metrics: + - aggregate: SUM + column: + column_name: Sales + description: null + expression: null + filterable: true + groupby: true + id: 917 + is_dttm: false + optionName: _col_Sales + python_date_format: null + type: DOUBLE PRECISION + verbose_name: null + expressionType: SIMPLE + hasCustomLabel: false + isNew: false + label: (Sales) + optionName: metric_e3kxby3hnjs_nfd4adbcnsn + sqlExpression: null + order_desc: true + queryFields: + groupby: groupby + metrics: metrics + row_limit: null + series_height: '25' + slice_id: 2811 + time_range: No filter + time_range_endpoints: + - inclusive + - exclusive + url_params: {} + viz_type: horizon +cache_timeout: null +uuid: cf0da099-b3ab-4d94-ab62-cf353ac3c611 +version: 1.0.0 +dataset_uuid: e8623bb9-5e00-f531-506a-19607f5f8005 diff --git a/superset/examples/configs/charts/Total_Items_Sold.yaml b/superset/examples/configs/charts/Total_Items_Sold.yaml new file mode 100644 index 0000000000000..63bd19a07afc8 --- /dev/null +++ b/superset/examples/configs/charts/Total_Items_Sold.yaml @@ -0,0 +1,57 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +slice_name: Total Items Sold +viz_type: big_number_total +params: + adhoc_filters: [] + datasource: 23__table + granularity_sqla: OrderDate + header_font_size: 0.4 + metric: + aggregate: SUM + column: + column_name: QuantityOrdered + description: null + expression: null + filterable: true + groupby: true + id: 914 + is_dttm: false + python_date_format: null + type: BIGINT + verbose_name: null + expressionType: SIMPLE + hasCustomLabel: false + isNew: false + label: SUM(Sales) + optionName: metric_twq59hf4ej_g70qjfmehsq + sqlExpression: null + queryFields: + metric: metrics + subheader: '' + subheader_font_size: 0.15 + time_range: No filter + time_range_endpoints: + - inclusive + - exclusive + url_params: {} + viz_type: big_number_total + y_axis_format: SMART_NUMBER +cache_timeout: null +uuid: c3d643cd-fd6f-4659-a5b7-59402487a8d0 +version: 1.0.0 +dataset_uuid: e8623bb9-5e00-f531-506a-19607f5f8005 diff --git a/superset/examples/configs/charts/Total_Items_Sold_By_Product_Line.yaml b/superset/examples/configs/charts/Total_Items_Sold_By_Product_Line.yaml new file mode 100644 index 0000000000000..3a4dec63f0384 --- /dev/null +++ b/superset/examples/configs/charts/Total_Items_Sold_By_Product_Line.yaml @@ -0,0 +1,68 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +slice_name: Total Items Sold (By Product Line) +viz_type: table +params: + adhoc_filters: [] + all_columns: [] + color_pn: true + datasource: 23__table + granularity_sqla: OrderDate + groupby: + - ProductLine + metrics: + - aggregate: SUM + column: + column_name: QuantityOrdered + description: null + expression: null + filterable: true + groupby: true + id: 914 + is_dttm: false + optionName: _col_QuantityOrdered + python_date_format: null + type: BIGINT + verbose_name: null + expressionType: SIMPLE + hasCustomLabel: true + isNew: false + label: '# of Products Sold' + optionName: metric_skdbciwba6g_z1r5w1pxlqj + sqlExpression: null + order_by_cols: [] + order_desc: true + percent_metrics: null + queryFields: + groupby: groupby + metrics: metrics + query_mode: aggregate + row_limit: null + show_cell_bars: true + slice_id: 2807 + table_timestamp_format: smart_date + time_grain_sqla: P1D + time_range: No filter + time_range_endpoints: + - inclusive + - exclusive + url_params: {} + viz_type: table +cache_timeout: null +uuid: b8b7ca30-6291-44b0-bc64-ba42e2892b86 +version: 1.0.0 +dataset_uuid: e8623bb9-5e00-f531-506a-19607f5f8005 diff --git a/superset/examples/configs/charts/Total_Revenue.yaml b/superset/examples/configs/charts/Total_Revenue.yaml new file mode 100644 index 0000000000000..c1e5de25630f2 --- /dev/null +++ b/superset/examples/configs/charts/Total_Revenue.yaml @@ -0,0 +1,58 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +slice_name: Total Revenue +viz_type: big_number_total +params: + adhoc_filters: [] + datasource: 23__table + granularity_sqla: OrderDate + header_font_size: 0.4 + metric: + aggregate: SUM + column: + column_name: Sales + description: null + expression: null + filterable: true + groupby: true + id: 917 + is_dttm: false + optionName: _col_Sales + python_date_format: null + type: DOUBLE PRECISION + verbose_name: null + expressionType: SIMPLE + hasCustomLabel: false + isNew: false + label: (Sales) + optionName: metric_twq59hf4ej_g70qjfmehsq + sqlExpression: null + queryFields: + metric: metrics + subheader: '' + subheader_font_size: 0.15 + time_range: No filter + time_range_endpoints: + - inclusive + - exclusive + url_params: {} + viz_type: big_number_total + y_axis_format: $,.2f +cache_timeout: null +uuid: 7b12a243-88e0-4dc5-ac33-9a840bb0ac5a +version: 1.0.0 +dataset_uuid: e8623bb9-5e00-f531-506a-19607f5f8005 diff --git a/superset/examples/configs/dashboards/Sales_Dashboard.yaml b/superset/examples/configs/dashboards/Sales_Dashboard.yaml new file mode 100644 index 0000000000000..a6ce2947b8214 --- /dev/null +++ b/superset/examples/configs/dashboards/Sales_Dashboard.yaml @@ -0,0 +1,415 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +dashboard_title: Sales Dashboard +description: null +css: '' +slug: null +uuid: 04f79081-fb49-7bac-7f14-cc76cd2ad93b +position: + CHART-1NOOLm5YPs: + children: [] + id: CHART-1NOOLm5YPs + meta: + chartId: 2805 + height: 25 + sliceName: Total Items Sold + sliceNameOverride: Total Products Sold + uuid: c3d643cd-fd6f-4659-a5b7-59402487a8d0 + width: 2 + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-d-E0Zc1cTH + - ROW-Tyv02UA_6W + - COLUMN-8Rp54B6ikC + type: CHART + CHART-AYpv8gFi_q: + children: [] + id: CHART-AYpv8gFi_q + meta: + chartId: 2810 + height: 91 + sliceName: Number of Deals (for each Combination) + uuid: bd20fc69-dd51-46c1-99b5-09e37a434bf1 + width: 3 + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-4fthLQmdX + - ROW-0l1WcDzW3 + type: CHART + CHART-KKT9BsnUst: + children: [] + id: CHART-KKT9BsnUst + meta: + chartId: 2806 + height: 59 + sliceName: Quarterly Sales (By Product Line) + sliceNameOverride: Quarterly Revenue (By Product Line) + uuid: db9609e4-9b78-4a32-87a7-4d9e19d51cd8 + width: 7 + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-d-E0Zc1cTH + - ROW-oAtmu5grZ + type: CHART + CHART-OJ9aWDmn1q: + children: [] + id: CHART-OJ9aWDmn1q + meta: + chartId: 2808 + height: 91 + sliceName: Proportion of Revenue by Product Line + sliceNameOverride: Proportion of Monthly Revenue by Product Line + uuid: 08aff161-f60c-4cb3-a225-dc9b1140d2e3 + width: 6 + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-4fthLQmdX + - ROW-0l1WcDzW3 + type: CHART + CHART-YFg-9wHE7s: + children: [] + id: CHART-YFg-9wHE7s + meta: + chartId: 2811 + height: 63 + sliceName: Seasonality of Revenue (per Product Line) + uuid: cf0da099-b3ab-4d94-ab62-cf353ac3c611 + width: 6 + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-4fthLQmdX + - ROW-E7MDSGfnm + type: CHART + CHART-_LMKI0D3tj: + children: [] + id: CHART-_LMKI0D3tj + meta: + chartId: 2809 + height: 62 + sliceName: Revenue by Deal SIze + sliceNameOverride: Monthly Revenue by Deal SIze + uuid: f065a533-2e13-42b9-bd19-801a21700dff + width: 6 + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-4fthLQmdX + - ROW-E7MDSGfnm + type: CHART + CHART-id4RGv80N-: + children: [] + id: CHART-id4RGv80N- + meta: + chartId: 2807 + height: 40 + sliceName: Total Items Sold (By Product Line) + sliceNameOverride: Total Products Sold (By Product Line) + uuid: b8b7ca30-6291-44b0-bc64-ba42e2892b86 + width: 2 + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-d-E0Zc1cTH + - ROW-oAtmu5grZ + - COLUMN-G6_2DvG8aK + type: CHART + CHART-iyvXMcqHt9: + children: [] + id: CHART-iyvXMcqHt9 + meta: + chartId: 671 + height: 39 + sliceName: Filter + uuid: a5689df7-98fc-7c51-602c-ebd92dc3ec70 + width: 2 + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-4fthLQmdX + - ROW-0l1WcDzW3 + - COLUMN-jlNWyWCfTC + type: CHART + CHART-j24u8ve41b: + children: [] + id: CHART-j24u8ve41b + meta: + chartId: 670 + height: 59 + sliceName: Overall Sales (By Product Line) + sliceNameOverride: Total Revenue (By Product Line) + uuid: 09c497e0-f442-1121-c9e7-671e37750424 + width: 3 + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-d-E0Zc1cTH + - ROW-oAtmu5grZ + type: CHART + CHART-lFanAaYKBK: + children: [] + id: CHART-lFanAaYKBK + meta: + chartId: 2804 + height: 26 + sliceName: Total Revenue + uuid: 7b12a243-88e0-4dc5-ac33-9a840bb0ac5a + width: 3 + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-d-E0Zc1cTH + - ROW-Tyv02UA_6W + - COLUMN-8Rp54B6ikC + type: CHART + CHART-vomBOiI7U9: + children: [] + id: CHART-vomBOiI7U9 + meta: + chartId: 668 + height: 53 + sliceName: Quarterly Sales + sliceNameOverride: Quarterly Revenue + uuid: 692aca26-a526-85db-c94c-411c91cc1077 + width: 7 + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-d-E0Zc1cTH + - ROW-Tyv02UA_6W + type: CHART + COLUMN-8Rp54B6ikC: + children: + - CHART-lFanAaYKBK + - CHART-1NOOLm5YPs + id: COLUMN-8Rp54B6ikC + meta: + background: BACKGROUND_TRANSPARENT + width: 2 + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-d-E0Zc1cTH + - ROW-Tyv02UA_6W + type: COLUMN + COLUMN-G6_2DvG8aK: + children: + - CHART-id4RGv80N- + id: COLUMN-G6_2DvG8aK + meta: + background: BACKGROUND_TRANSPARENT + width: 2 + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-d-E0Zc1cTH + - ROW-oAtmu5grZ + type: COLUMN + COLUMN-jlNWyWCfTC: + children: + - MARKDOWN-HrzsMmvGQo + - CHART-iyvXMcqHt9 + id: COLUMN-jlNWyWCfTC + meta: + background: BACKGROUND_TRANSPARENT + width: 3 + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-4fthLQmdX + - ROW-0l1WcDzW3 + type: COLUMN + DASHBOARD_VERSION_KEY: v2 + GRID_ID: + children: [] + id: GRID_ID + parents: + - ROOT_ID + type: GRID + HEADER_ID: + id: HEADER_ID + meta: + text: Sales Dashboard + type: HEADER + MARKDOWN--AtDSWnapE: + children: [] + id: MARKDOWN--AtDSWnapE + meta: + code: "# \U0001F697 Vehicle Sales Dashboard \U0001F3CD\n\nThis example dashboard\ + \ provides insight into the business operations of vehicle seller. The dataset\ + \ powering this dashboard can be found [here on Kaggle](https://www.kaggle.com/kyanyoga/sample-sales-data).\n\ + \n### Timeline\n\nThe dataset contains data on all orders from the 2003 and\ + \ 2004 fiscal years, and some orders from 2005.\n\n### Products Sold\n\nThis\ + \ shop mainly sells the following products:\n\n- \U0001F697 Classic Cars\n\ + - \U0001F3CE\uFE0F Vintage Cars\n- \U0001F3CD\uFE0F Motorcycles\n- \U0001F69A\ + \ Trucks & Buses \U0001F68C\n- \U0001F6E9\uFE0F Planes\n- \U0001F6A2 Ships\n\ + - \U0001F688 Trains" + height: 53 + width: 3 + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-d-E0Zc1cTH + - ROW-Tyv02UA_6W + type: MARKDOWN + MARKDOWN-HrzsMmvGQo: + children: [] + id: MARKDOWN-HrzsMmvGQo + meta: + code: "# \U0001F50D Filter Box\n\nDashboard filters are a powerful way to enable\ + \ teams to dive deeper into their business operations data. This filter box\ + \ helps focus the charts along the following variables:\n\n- Time Range: Focus\ + \ in on a specific time period (e.g. a holiday or quarter)\n- Product Line:\ + \ Choose 1 or more product lines to see relevant sales data\n- Deal Size:\ + \ Zoom in on small, medium, and / or large sales deals\n\nThe filter box below\ + \ \U0001F447 is configured to only apply to the charts in this tab (**Exploratory**).\ + \ You can customize the charts that this filter box applies to by:\n\n- entering\ + \ Edit mode in this dashboard\n- selecting the `...` in the top right corner\n\ + - selecting the **Set filter mapping** button" + height: 50 + width: 3 + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-4fthLQmdX + - ROW-0l1WcDzW3 + - COLUMN-jlNWyWCfTC + type: MARKDOWN + ROOT_ID: + children: + - TABS-e5Ruro0cjP + id: ROOT_ID + type: ROOT + ROW-0l1WcDzW3: + children: + - COLUMN-jlNWyWCfTC + - CHART-OJ9aWDmn1q + - CHART-AYpv8gFi_q + id: ROW-0l1WcDzW3 + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-4fthLQmdX + type: ROW + ROW-E7MDSGfnm: + children: + - CHART-YFg-9wHE7s + - CHART-_LMKI0D3tj + id: ROW-E7MDSGfnm + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-4fthLQmdX + type: ROW + ROW-Tyv02UA_6W: + children: + - COLUMN-8Rp54B6ikC + - CHART-vomBOiI7U9 + - MARKDOWN--AtDSWnapE + id: ROW-Tyv02UA_6W + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-d-E0Zc1cTH + type: ROW + ROW-oAtmu5grZ: + children: + - COLUMN-G6_2DvG8aK + - CHART-KKT9BsnUst + - CHART-j24u8ve41b + id: ROW-oAtmu5grZ + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + - TAB-d-E0Zc1cTH + type: ROW + TAB-4fthLQmdX: + children: + - ROW-0l1WcDzW3 + - ROW-E7MDSGfnm + id: TAB-4fthLQmdX + meta: + text: "\U0001F9ED Exploratory" + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + type: TAB + TAB-d-E0Zc1cTH: + children: + - ROW-Tyv02UA_6W + - ROW-oAtmu5grZ + id: TAB-d-E0Zc1cTH + meta: + text: "\U0001F3AF Sales Overview" + parents: + - ROOT_ID + - TABS-e5Ruro0cjP + type: TAB + TABS-e5Ruro0cjP: + children: + - TAB-d-E0Zc1cTH + - TAB-4fthLQmdX + id: TABS-e5Ruro0cjP + meta: {} + parents: + - ROOT_ID + type: TABS +metadata: + timed_refresh_immune_slices: [] + expanded_slices: {} + refresh_frequency: 0 + default_filters: '{"671": {"__time_range": "No filter"}}' + filter_scopes: + '671': + ProductLine: + scope: + - TAB-4fthLQmdX + immune: [] + DealSize: + scope: + - ROOT_ID + immune: [] + __time_range: + scope: + - ROOT_ID + immune: [] + color_scheme: supersetColors + label_colors: + Medium: '#1FA8C9' + Small: '#454E7C' + Large: '#5AC189' + SUM(SALES): '#1FA8C9' + Classic Cars: '#454E7C' + Vintage Cars: '#5AC189' + Motorcycles: '#FF7F44' + Trucks and Buses: '#666666' + Planes: '#E04355' + Ships: '#FCC700' + Trains: '#A868B7' +version: 1.0.0 diff --git a/superset/examples/configs/datasets/examples/Cleaned_Sales_Data.yaml b/superset/examples/configs/datasets/examples/Cleaned_Sales_Data.yaml new file mode 100644 index 0000000000000..f03cfff43acb7 --- /dev/null +++ b/superset/examples/configs/datasets/examples/Cleaned_Sales_Data.yaml @@ -0,0 +1,293 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +table_name: Cleaned Sales Data +main_dttm_col: OrderDate +description: null +default_endpoint: null +offset: 0 +cache_timeout: null +schema: null +sql: null +params: null +template_params: null +filter_select_enabled: false +fetch_values_predicate: null +extra: null +uuid: e8623bb9-5e00-f531-506a-19607f5f8005 +metrics: +- metric_name: count + verbose_name: COUNT(*) + metric_type: count + expression: COUNT(*) + description: null + d3format: null + extra: null + warning_text: null +columns: +- column_name: OrderDate + verbose_name: null + is_dttm: true + is_active: true + type: TIMESTAMP WITHOUT TIME ZONE + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: PriceEach + verbose_name: null + is_dttm: false + is_active: true + type: DOUBLE PRECISION + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: Sales + verbose_name: null + is_dttm: false + is_active: true + type: DOUBLE PRECISION + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: AddressLine1 + verbose_name: null + is_dttm: false + is_active: true + type: TEXT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: AddressLine2 + verbose_name: null + is_dttm: false + is_active: true + type: TEXT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: ContactLastName + verbose_name: null + is_dttm: false + is_active: true + type: TEXT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: ContactFirstName + verbose_name: null + is_dttm: false + is_active: true + type: TEXT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: QuantityOrdered + verbose_name: null + is_dttm: false + is_active: true + type: BIGINT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: Year + verbose_name: null + is_dttm: false + is_active: true + type: BIGINT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: PostalCode + verbose_name: null + is_dttm: false + is_active: true + type: TEXT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: CustomerName + verbose_name: null + is_dttm: false + is_active: true + type: TEXT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: DealSize + verbose_name: null + is_dttm: false + is_active: true + type: TEXT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: State + verbose_name: null + is_dttm: false + is_active: true + type: TEXT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: Status + verbose_name: null + is_dttm: false + is_active: true + type: TEXT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: OrderLineNumber + verbose_name: null + is_dttm: false + is_active: true + type: BIGINT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: OrderNumber + verbose_name: null + is_dttm: false + is_active: true + type: BIGINT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: Month + verbose_name: null + is_dttm: false + is_active: true + type: BIGINT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: Quarter + verbose_name: null + is_dttm: false + is_active: true + type: BIGINT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: MSRP + verbose_name: null + is_dttm: false + is_active: true + type: BIGINT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: ProductCode + verbose_name: null + is_dttm: false + is_active: true + type: TEXT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: ProductLine + verbose_name: null + is_dttm: false + is_active: true + type: TEXT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: City + verbose_name: null + is_dttm: false + is_active: true + type: TEXT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: Country + verbose_name: null + is_dttm: false + is_active: true + type: TEXT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: Phone + verbose_name: null + is_dttm: false + is_active: true + type: TEXT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +- column_name: Territory + verbose_name: null + is_dttm: false + is_active: true + type: TEXT + groupby: true + filterable: true + expression: null + description: null + python_date_format: null +version: 1.0.0 +database_uuid: a2dc77af-e654-49bb-b321-40f6b559a1ee +data: https://raw.githubusercontent.com/apache-superset/examples-data/master/datasets/examples/sales.csv From 76f9f185fb6afe04dfa2bdde190537d4007bade9 Mon Sep 17 00:00:00 2001 From: Jesse Yang Date: Tue, 15 Dec 2020 17:22:23 -0800 Subject: [PATCH 21/43] refactor: optimize backend log payload (#11927) --- setup.cfg | 2 +- superset/utils/log.py | 118 ++++++++++++++++++++++-------------- superset/views/base_api.py | 79 ++++++++++++++---------- superset/views/core.py | 8 +-- tests/event_logger_tests.py | 65 ++++++++++++-------- 5 files changed, 167 insertions(+), 105 deletions(-) diff --git a/setup.cfg b/setup.cfg index 00a2062992e7e..2f057d4b3af9d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,7 @@ combine_as_imports = true include_trailing_comma = true line_length = 88 known_first_party = superset -known_third_party =alembic,apispec,backoff,bleach,cachelib,celery,click,colorama,contextlib2,cron_descriptor,croniter,cryptography,dateutil,flask,flask_appbuilder,flask_babel,flask_caching,flask_compress,flask_login,flask_migrate,flask_sqlalchemy,flask_talisman,flask_testing,flask_wtf,freezegun,geohash,geopy,humanize,isodate,jinja2,jwt,markdown,markupsafe,marshmallow,msgpack,numpy,pandas,parameterized,parsedatetime,pathlib2,pgsanity,pkg_resources,polyline,prison,pyarrow,pyhive,pytest,pytz,redis,retry,selenium,setuptools,simplejson,slack,sqlalchemy,sqlalchemy_utils,sqlparse,werkzeug,wtforms,wtforms_json,yaml +known_third_party =alembic,apispec,backoff,bleach,cachelib,celery,click,colorama,contextlib2,cron_descriptor,croniter,cryptography,dateutil,flask,flask_appbuilder,flask_babel,flask_caching,flask_compress,flask_login,flask_migrate,flask_sqlalchemy,flask_talisman,flask_testing,flask_wtf,freezegun,geohash,geopy,humanize,isodate,jinja2,jwt,markdown,markupsafe,marshmallow,msgpack,numpy,pandas,parameterized,parsedatetime,pathlib2,pgsanity,pkg_resources,polyline,prison,pyarrow,pyhive,pytest,pytz,redis,retry,selenium,setuptools,simplejson,slack,sqlalchemy,sqlalchemy_utils,sqlparse,typing_extensions,werkzeug,wtforms,wtforms_json,yaml multi_line_output = 3 order_by_type = false diff --git a/superset/utils/log.py b/superset/utils/log.py index 1604739ef194f..824487e1643cf 100644 --- a/superset/utils/log.py +++ b/superset/utils/log.py @@ -22,19 +22,39 @@ import time from abc import ABC, abstractmethod from contextlib import contextmanager -from typing import Any, Callable, cast, Iterator, Optional, Type +from typing import Any, Callable, cast, Dict, Iterator, Optional, Type, Union from flask import current_app, g, request +from flask_appbuilder.const import API_URI_RIS_KEY from sqlalchemy.exc import SQLAlchemyError +from typing_extensions import Literal from superset.stats_logger import BaseStatsLogger -def strip_int_from_path(path: Optional[str]) -> str: - """Simple function to remove ints from '/' separated paths""" - if path: - return "/".join(["" if s.isdigit() else s for s in path.split("/")]) - return "" +def collect_request_payload() -> Dict[str, Any]: + """Collect log payload identifiable from request context""" + payload: Dict[str, Any] = { + "path": request.path, + **request.form.to_dict(), + # url search params can overwrite POST body + **request.args.to_dict(), + } + + # save URL match pattern in addition to the request path + url_rule = str(request.url_rule) + if url_rule != request.path: + payload["url_rule"] = url_rule + + # remove rison raw string (q=xxx in search params) in favor of + # rison object (could come from `payload_override`) + if "rison" in payload and API_URI_RIS_KEY in payload: + del payload[API_URI_RIS_KEY] + # delete empty rison object + if "rison" in payload and not payload["rison"]: + del payload["rison"] + + return payload class AbstractEventLogger(ABC): @@ -53,26 +73,37 @@ def log( # pylint: disable=too-many-arguments pass @contextmanager - def log_context( - self, action: str, ref: Optional[str] = None, log_to_statsd: bool = True, + def log_context( # pylint: disable=too-many-locals + self, action: str, object_ref: Optional[str] = None, log_to_statsd: bool = True, ) -> Iterator[Callable[..., None]]: """ - Log an event while reading information from the request context. - `kwargs` will be appended directly to the log payload. + Log an event with additional information from the request context. + + :param action: a name to identify the event + :param object_ref: reference to the Python object that triggered this action + :param log_to_statsd: whether to update statsd counter for the action """ from superset.views.core import get_form_data start_time = time.time() referrer = request.referrer[:1000] if request.referrer else None user_id = g.user.get_id() if hasattr(g, "user") and g.user else None - payload = request.form.to_dict() or {} - # request parameters can overwrite post body - payload.update(request.args.to_dict()) + payload_override = {} - # yield a helper to update additional kwargs - yield lambda **kwargs: payload.update(kwargs) + # yield a helper to add additional payload + yield lambda **kwargs: payload_override.update(kwargs) - dashboard_id = payload.get("dashboard_id") + payload = collect_request_payload() + if object_ref: + payload["object_ref"] = object_ref + # manual updates from context comes the last + payload.update(payload_override) + + dashboard_id: Optional[int] = None + try: + dashboard_id = int(payload.get("dashboard_id")) # type: ignore + except (TypeError, ValueError): + dashboard_id = None if "form_data" in payload: form_data, _ = get_form_data() @@ -89,15 +120,8 @@ def log_context( if log_to_statsd: self.stats_logger.incr(action) - payload.update( - { - "path": request.path, - "path_no_param": strip_int_from_path(request.path), - "ref": ref, - } - ) - # bulk insert try: + # bulk insert explode_by = payload.get("explode") records = json.loads(payload.get(explode_by)) # type: ignore except Exception: # pylint: disable=broad-except @@ -114,16 +138,30 @@ def log_context( ) def _wrapper( - self, f: Callable[..., Any], **wrapper_kwargs: Any + self, + f: Callable[..., Any], + action: Optional[Union[str, Callable[..., str]]] = None, + object_ref: Optional[Union[str, Callable[..., str], Literal[False]]] = None, + allow_extra_payload: Optional[bool] = False, + **wrapper_kwargs: Any, ) -> Callable[..., Any]: - action_str = wrapper_kwargs.get("action") or f.__name__ - ref = f.__qualname__ if hasattr(f, "__qualname__") else None - @functools.wraps(f) def wrapper(*args: Any, **kwargs: Any) -> Any: - with self.log_context(action_str, ref, **wrapper_kwargs) as log: - value = f(*args, **kwargs) + action_str = ( + action(*args, **kwargs) if callable(action) else action + ) or f.__name__ + object_ref_str = ( + object_ref(*args, **kwargs) if callable(object_ref) else object_ref + ) or (f.__qualname__ if object_ref is not False else None) + with self.log_context( + action=action_str, object_ref=object_ref_str, **wrapper_kwargs + ) as log: log(**kwargs) + if allow_extra_payload: + # add a payload updater to the decorated function + value = f(*args, add_extra_log_payload=log, **kwargs) + else: + value = f(*args, **kwargs) return value return wrapper @@ -140,18 +178,9 @@ def func(f: Callable[..., Any]) -> Callable[..., Any]: return func - def log_manually(self, f: Callable[..., Any]) -> Callable[..., Any]: - """Allow a function to manually update""" - - @functools.wraps(f) - def wrapper(*args: Any, **kwargs: Any) -> Any: - with self.log_context(f.__name__) as log: - # updated_log_payload should be either the last positional - # argument or one of the named arguments of the decorated function - value = f(*args, update_log_payload=log, **kwargs) - return value - - return wrapper + def log_this_with_extra_payload(self, f: Callable[..., Any]) -> Callable[..., Any]: + """Decorator that instrument `update_log_payload` to kwargs""" + return self._wrapper(f, allow_extra_payload=True) @property def stats_logger(self) -> BaseStatsLogger: @@ -217,9 +246,8 @@ def log( # pylint: disable=too-many-arguments,too-many-locals ) -> None: from superset.models.core import Log - records = kwargs.get("records", list()) - - logs = list() + records = kwargs.get("records", []) + logs = [] for record in records: json_string: Optional[str] try: diff --git a/superset/views/base_api.py b/superset/views/base_api.py index 2e0d3b23696e3..3e11f9230cdba 100644 --- a/superset/views/base_api.py +++ b/superset/views/base_api.py @@ -49,7 +49,6 @@ "filter": {"type": "string"}, }, } -log_context = event_logger.log_context class RelatedResultResponseSchema(Schema): @@ -310,65 +309,83 @@ def send_stats_metrics( if time_delta: self.timing_stats("time", key, time_delta) + @event_logger.log_this_with_context( + action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.info", + object_ref=False, + log_to_statsd=False, + ) def info_headless(self, **kwargs: Any) -> Response: """ Add statsd metrics to builtin FAB _info endpoint """ - ref = f"{self.__class__.__name__}.info" - with log_context(ref, ref, log_to_statsd=False): - duration, response = time_function(super().info_headless, **kwargs) - self.send_stats_metrics(response, self.info.__name__, duration) - return response + duration, response = time_function(super().info_headless, **kwargs) + self.send_stats_metrics(response, self.info.__name__, duration) + return response + @event_logger.log_this_with_context( + action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get", + object_ref=False, + log_to_statsd=False, + ) def get_headless(self, pk: int, **kwargs: Any) -> Response: """ Add statsd metrics to builtin FAB GET endpoint """ - ref = f"{self.__class__.__name__}.get" - with log_context(ref, ref, log_to_statsd=False): - duration, response = time_function(super().get_headless, pk, **kwargs) - self.send_stats_metrics(response, self.get.__name__, duration) - return response + duration, response = time_function(super().get_headless, pk, **kwargs) + self.send_stats_metrics(response, self.get.__name__, duration) + return response + @event_logger.log_this_with_context( + action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get_list", + object_ref=False, + log_to_statsd=False, + ) def get_list_headless(self, **kwargs: Any) -> Response: """ Add statsd metrics to builtin FAB GET list endpoint """ - ref = f"{self.__class__.__name__}.get_list" - with log_context(ref, ref, log_to_statsd=False): - duration, response = time_function(super().get_list_headless, **kwargs) - self.send_stats_metrics(response, self.get_list.__name__, duration) - return response + duration, response = time_function(super().get_list_headless, **kwargs) + self.send_stats_metrics(response, self.get_list.__name__, duration) + return response + @event_logger.log_this_with_context( + action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post", + object_ref=False, + log_to_statsd=False, + ) def post_headless(self) -> Response: """ Add statsd metrics to builtin FAB POST endpoint """ - ref = f"{self.__class__.__name__}.post" - with log_context(ref, ref, log_to_statsd=False): - duration, response = time_function(super().post_headless) - self.send_stats_metrics(response, self.post.__name__, duration) - return response + duration, response = time_function(super().post_headless) + self.send_stats_metrics(response, self.post.__name__, duration) + return response + @event_logger.log_this_with_context( + action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.put", + object_ref=False, + log_to_statsd=False, + ) def put_headless(self, pk: int) -> Response: """ Add statsd metrics to builtin FAB PUT endpoint """ - ref = f"{self.__class__.__name__}.put" - with log_context(ref, ref, log_to_statsd=False): - duration, response = time_function(super().put_headless, pk) - self.send_stats_metrics(response, self.put.__name__, duration) - return response + duration, response = time_function(super().put_headless, pk) + self.send_stats_metrics(response, self.put.__name__, duration) + return response + @event_logger.log_this_with_context( + action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.delete", + object_ref=False, + log_to_statsd=False, + ) def delete_headless(self, pk: int) -> Response: """ Add statsd metrics to builtin FAB DELETE endpoint """ - ref = f"{self.__class__.__name__}.delete" - with log_context(ref, ref, log_to_statsd=False): - duration, response = time_function(super().delete_headless, pk) - self.send_stats_metrics(response, self.delete.__name__, duration) - return response + duration, response = time_function(super().delete_headless, pk) + self.send_stats_metrics(response, self.delete.__name__, duration) + return response @expose("/related/", methods=["GET"]) @protect() diff --git a/superset/views/core.py b/superset/views/core.py index 2ec0d3a104c47..e3503533a3be5 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1752,13 +1752,13 @@ def publish( # pylint: disable=no-self-use @has_access @expose("/dashboard//") - @event_logger.log_manually + @event_logger.log_this_with_extra_payload def dashboard( # pylint: disable=too-many-locals self, dashboard_id_or_slug: str, - # this parameter is added by `log_manually`, + # this parameter is added by `log_this_with_manual_updates`, # set a default value to appease pylint - update_log_payload: Callable[..., None] = lambda **kwargs: None, + add_extra_log_payload: Callable[..., None] = lambda **kwargs: None, ) -> FlaskResponse: """Server side rendering for a dashboard""" session = db.session() @@ -1807,7 +1807,7 @@ def dashboard( # pylint: disable=too-many-locals request.args.get(utils.ReservedUrlParameters.EDIT_MODE.value) == "true" ) - update_log_payload( + add_extra_log_payload( dashboard_id=dash.id, dashboard_version="v2", dash_edit_perm=dash_edit_perm, diff --git a/tests/event_logger_tests.py b/tests/event_logger_tests.py index 9c6c5e621398d..599728ce3ed32 100644 --- a/tests/event_logger_tests.py +++ b/tests/event_logger_tests.py @@ -17,26 +17,23 @@ import logging import time import unittest -from datetime import datetime from unittest.mock import patch -from superset.utils.log import ( - AbstractEventLogger, - DBEventLogger, - get_event_logger_from_cfg_value, -) +from superset.utils.log import DBEventLogger, get_event_logger_from_cfg_value from tests.test_app import app class TestEventLogger(unittest.TestCase): - def test_returns_configured_object_if_correct(self): - # test that assignment of concrete AbstractBaseClass impl returns unmodified object + def test_correct_config_object(self): + # test that assignment of concrete AbstractBaseClass impl returns + # unmodified object obj = DBEventLogger() res = get_event_logger_from_cfg_value(obj) - self.assertTrue(obj is res) + self.assertIs(obj, res) - def test_event_logger_config_class_deprecation(self): - # test that assignment of a class object to EVENT_LOGGER is correctly deprecated + def test_config_class_deprecation(self): + # test that assignment of a class object to EVENT_LOGGER is correctly + # deprecated res = None # print warning if a class is assigned to EVENT_LOGGER @@ -46,13 +43,14 @@ def test_event_logger_config_class_deprecation(self): # class is instantiated and returned self.assertIsInstance(res, DBEventLogger) - def test_raises_typerror_if_not_abc_impl(self): - # test that assignment of non AbstractEventLogger derived type raises TypeError + def test_raises_typerror_if_not_abc(self): + # test that assignment of non AbstractEventLogger derived type raises + # TypeError with self.assertRaises(TypeError): get_event_logger_from_cfg_value(logging.getLogger()) @patch.object(DBEventLogger, "log") - def test_log_this_decorator(self, mock_log): + def test_log_this(self, mock_log): logger = DBEventLogger() @logger.log_this @@ -60,27 +58,46 @@ def test_func(): time.sleep(0.05) return 1 - with app.test_request_context(): + with app.test_request_context("/superset/dashboard/1/?myparam=foo"): result = test_func() + payload = mock_log.call_args[1] self.assertEqual(result, 1) - assert mock_log.call_args[1]["duration_ms"] >= 50 + self.assertEqual( + payload["records"], + [ + { + "myparam": "foo", + "path": "/superset/dashboard/1/", + "url_rule": "/superset/dashboard//", + "object_ref": test_func.__qualname__, + } + ], + ) + self.assertGreaterEqual(payload["duration_ms"], 50) @patch.object(DBEventLogger, "log") - def test_log_manually_decorator(self, mock_log): + def test_log_this_with_extra_payload(self, mock_log): logger = DBEventLogger() - @logger.log_manually - def test_func(arg1, update_log_payload, karg1=1): + @logger.log_this_with_extra_payload + def test_func(arg1, add_extra_log_payload, karg1=1): time.sleep(0.1) - update_log_payload(foo="bar") + add_extra_log_payload(foo="bar") return arg1 * karg1 with app.test_request_context(): result = test_func(1, karg1=2) # pylint: disable=no-value-for-parameter + payload = mock_log.call_args[1] self.assertEqual(result, 2) - # should contain only manual payload self.assertEqual( - mock_log.call_args[1]["records"], - [{"foo": "bar", "path": "/", "path_no_param": "/", "ref": None}], + payload["records"], + [ + { + "foo": "bar", + "path": "/", + "karg1": 2, + "object_ref": test_func.__qualname__, + } + ], ) - assert mock_log.call_args[1]["duration_ms"] >= 100 + self.assertGreaterEqual(payload["duration_ms"], 100) From 8da1900d8a5a686c599ac91a5d12abb8522135fa Mon Sep 17 00:00:00 2001 From: Grace Guo Date: Tue, 15 Dec 2020 18:12:06 -0800 Subject: [PATCH 22/43] feat: add hook for dataset health check (#11970) * feat: add hook for dataset health check * add event log * optimize datasource json data like certified data * add unit test * fix review comments * extra code review comments --- .../components/DatasourceControl_spec.jsx | 13 ++++++++++ .../components/controls/DatasourceControl.jsx | 19 ++++++++++++-- superset/config.py | 5 ++++ superset/connectors/sqla/models.py | 26 +++++++++++++++++++ superset/datasets/dao.py | 2 ++ superset/views/core.py | 4 +++ superset/views/datasource.py | 2 ++ tests/datasource_tests.py | 18 ++++++++++++- 8 files changed, 86 insertions(+), 3 deletions(-) diff --git a/superset-frontend/spec/javascripts/explore/components/DatasourceControl_spec.jsx b/superset-frontend/spec/javascripts/explore/components/DatasourceControl_spec.jsx index 8f6b8e9492c6a..1c91c1cd25256 100644 --- a/superset-frontend/spec/javascripts/explore/components/DatasourceControl_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/components/DatasourceControl_spec.jsx @@ -24,6 +24,8 @@ import { Menu } from 'src/common/components'; import DatasourceModal from 'src/datasource/DatasourceModal'; import ChangeDatasourceModal from 'src/datasource/ChangeDatasourceModal'; import DatasourceControl from 'src/explore/components/controls/DatasourceControl'; +import Icon from 'src/components/Icon'; +import { Tooltip } from 'src/common/components/Tooltip'; const defaultProps = { name: 'datasource', @@ -40,6 +42,7 @@ const defaultProps = { backend: 'mysql', name: 'main', }, + health_check_message: 'Warning message!', }, actions: { setDatasource: sinon.spy(), @@ -91,4 +94,14 @@ describe('DatasourceControl', () => { ); expect(menuWrapper.find(Menu.Item)).toHaveLength(2); }); + + it('should render health check message', () => { + const wrapper = setup(); + const alert = wrapper.find(Icon).first(); + expect(alert.prop('name')).toBe('alert-solid'); + const tooltip = wrapper.find(Tooltip).at(1); + expect(tooltip.prop('title')).toBe( + defaultProps.datasource.health_check_message, + ); + }); }); diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl.jsx index d7c2a5546a574..f8f79d36499ff 100644 --- a/superset-frontend/src/explore/components/controls/DatasourceControl.jsx +++ b/superset-frontend/src/explore/components/controls/DatasourceControl.jsx @@ -19,7 +19,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Col, Collapse, Row, Well } from 'react-bootstrap'; -import { t, styled } from '@superset-ui/core'; +import { t, styled, supersetTheme } from '@superset-ui/core'; import { ColumnOption, MetricOption } from '@superset-ui/chart-controls'; import { Dropdown, Menu } from 'src/common/components'; @@ -73,6 +73,10 @@ const Styles = styled.div` vertical-align: middle; cursor: pointer; } + + .datasource-controls { + display: flex; + } `; /** @@ -213,10 +217,13 @@ class DatasourceControl extends React.PureComponent { ); + // eslint-disable-next-line camelcase + const { health_check_message: healthCheckMessage } = datasource; + return ( -
+
+ {healthCheckMessage && ( + + + + )} Optional[str]: self.table_name, schema=self.schema, show_cols=False, latest_partition=False ) + @property + def health_check_message(self) -> Optional[str]: + return self.extra_dict.get("health_check", {}).get("message") + @property def data(self) -> Dict[str, Any]: data_ = super().data @@ -699,6 +704,7 @@ def data(self) -> Dict[str, Any]: data_["fetch_values_predicate"] = self.fetch_values_predicate data_["template_params"] = self.template_params data_["is_sqllab_view"] = self.is_sqllab_view + data_["health_check_message"] = self.health_check_message return data_ @property @@ -1468,6 +1474,26 @@ class and any keys added via `ExtraCache`. extra_cache_keys += sqla_query.extra_cache_keys return extra_cache_keys + def health_check(self, commit: bool = False, force: bool = False) -> None: + check = config.get("DATASET_HEALTH_CHECK") + if check is None: + return + + extra = self.extra_dict + # force re-run health check, or health check is updated + if force or extra.get("health_check", {}).get("version") != check.version: + with event_logger.log_context(action="dataset_health_check"): + message = check(self) + extra["health_check"] = { + "version": check.version, + "message": message, + } + self.extra = json.dumps(extra) + + db.session.merge(self) + if commit: + db.session.commit() + sa.event.listen(SqlaTable, "after_insert", security_manager.set_perm) sa.event.listen(SqlaTable, "after_update", security_manager.set_perm) diff --git a/superset/datasets/dao.py b/superset/datasets/dao.py index 284a43508068a..aaede30f26b4a 100644 --- a/superset/datasets/dao.py +++ b/superset/datasets/dao.py @@ -186,6 +186,8 @@ def update( # pylint: disable=W:279 super().update(model, properties, commit=commit) properties["columns"] = original_properties + super().update(model, properties, commit=False) + model.health_check(force=True, commit=False) return super().update(model, properties, commit=commit) @classmethod diff --git a/superset/views/core.py b/superset/views/core.py index e3503533a3be5..82bc2d128b7d0 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -717,6 +717,10 @@ def explore( # pylint: disable=too-many-locals,too-many-return-statements f"datasource_id={datasource_id}&" ) + # if feature enabled, run some health check rules for sqla datasource + if hasattr(datasource, "health_check"): + datasource.health_check() + viz_type = form_data.get("viz_type") if not viz_type and datasource.default_endpoint: return redirect(datasource.default_endpoint) diff --git a/superset/views/datasource.py b/superset/views/datasource.py index 92cf5c1540b35..7724d1e26c6b3 100644 --- a/superset/views/datasource.py +++ b/superset/views/datasource.py @@ -70,6 +70,8 @@ def save(self) -> FlaskResponse: f"Duplicate column name(s): {','.join(duplicates)}", status=409 ) orm_datasource.update_from_object(datasource_dict) + if hasattr(orm_datasource, "health_check"): + orm_datasource.health_check(force=True, commit=False) data = orm_datasource.data db.session.commit() diff --git a/tests/datasource_tests.py b/tests/datasource_tests.py index 31a4c96334188..890b4a63e42c6 100644 --- a/tests/datasource_tests.py +++ b/tests/datasource_tests.py @@ -18,7 +18,7 @@ import json from copy import deepcopy -from superset import db +from superset import app, db from superset.connectors.sqla.models import SqlaTable from superset.utils.core import get_example_database @@ -190,6 +190,22 @@ def test_get_datasource(self): }, ) + def test_get_datasource_with_health_check(self): + def my_check(datasource): + return "Warning message!" + + app.config["DATASET_HEALTH_CHECK"] = my_check + my_check.version = 0.1 + + self.login(username="admin") + tbl = self.get_table_by_name("birth_names") + url = f"/datasource/get/{tbl.type}/{tbl.id}/" + tbl.health_check(commit=True, force=True) + resp = self.get_json_resp(url) + self.assertEqual(resp["health_check_message"], "Warning message!") + + del app.config["DATASET_HEALTH_CHECK"] + def test_get_datasource_failed(self): self.login(username="admin") url = f"/datasource/get/druid/500000/" From 8bda6b0bd98bf6082fcf87b536f96a09050e06d8 Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Tue, 15 Dec 2020 18:47:40 -0800 Subject: [PATCH 23/43] feat: show missing parameters in query (#12049) * feat: show missing parameters in query * Fix lint * Address comments * Simplify error message * Use f-string in helper function --- .../pages/docs/Miscellaneous/issue_codes.mdx | 11 ++++++ .../src/SqlLab/components/ResultSet.tsx | 10 +++++- .../src/SqlLab/components/SqlEditor.jsx | 16 +++++---- .../src/components/ErrorMessage/types.ts | 3 ++ superset-frontend/src/featureFlags.ts | 1 + superset/errors.py | 13 +++++++ superset/jinja_context.py | 5 +-- superset/utils/core.py | 5 +++ superset/views/core.py | 34 +++++++++++++++++-- tests/base_tests.py | 2 ++ tests/sqllab_tests.py | 21 +++++++++++- 11 files changed, 108 insertions(+), 13 deletions(-) diff --git a/docs/src/pages/docs/Miscellaneous/issue_codes.mdx b/docs/src/pages/docs/Miscellaneous/issue_codes.mdx index b34209786b777..abf4886ce169d 100644 --- a/docs/src/pages/docs/Miscellaneous/issue_codes.mdx +++ b/docs/src/pages/docs/Miscellaneous/issue_codes.mdx @@ -73,3 +73,14 @@ The table was deleted or renamed in the database. Your query failed because it is referencing a table that no longer exists in the underlying database. You should modify your query to reference the correct table. + +## Issue 1006 + +``` +One or more parameters specified in the query are missing. +``` + +Your query was not submitted to the database because it's missing one or more +parameters. You should define all the parameters referenced in the query in a +valid JSON document. Check that the parameters are spelled correctly and that +the document has a valid syntax. diff --git a/superset-frontend/src/SqlLab/components/ResultSet.tsx b/superset-frontend/src/SqlLab/components/ResultSet.tsx index a5a1d5f842054..d421a43a1cabb 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet.tsx @@ -28,6 +28,7 @@ import { styled, t } from '@superset-ui/core'; import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace'; import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal'; import { getByUser, put as updateDatset } from 'src/api/dataset'; +import { ErrorTypeEnum } from 'src/components/ErrorMessage/types'; import Loading from '../../components/Loading'; import ExploreCtasResultsButton from './ExploreCtasResultsButton'; import ExploreResultsButton from './ExploreResultsButton'; @@ -489,10 +490,17 @@ export default class ResultSet extends React.PureComponent< return Query was stopped; } if (query.state === 'failed') { + // TODO (betodealmeida): handle this logic through the error component + // registry + const title = + query?.errors?.[0].error_type === + ErrorTypeEnum.MISSING_TEMPLATE_PARAMS_ERROR + ? t('Parameter Error') + : t('Database Error'); return (
{query.errorMessage}} copyText={query.errorMessage || undefined} diff --git a/superset-frontend/src/SqlLab/components/SqlEditor.jsx b/superset-frontend/src/SqlLab/components/SqlEditor.jsx index 044fee86ae579..4b4a1bc1acb4d 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor.jsx @@ -566,13 +566,15 @@ class SqlEditor extends React.PureComponent { {' '} {t('Autocomplete')} {' '} - { - this.props.actions.queryEditorSetTemplateParams(qe, params); - }} - code={qe.templateParams} - /> + {isFeatureEnabled(FeatureFlag.ENABLE_TEMPLATE_PROCESSING) && ( + { + this.props.actions.queryEditorSetTemplateParams(qe, params); + }} + code={qe.templateParams} + /> + )} {limitWarning} {this.props.latestQuery && ( = T[keyof T]; diff --git a/superset-frontend/src/featureFlags.ts b/superset-frontend/src/featureFlags.ts index 93b909ddcb3b1..75367d23a3f83 100644 --- a/superset-frontend/src/featureFlags.ts +++ b/superset-frontend/src/featureFlags.ts @@ -35,6 +35,7 @@ export enum FeatureFlag { ESCAPE_MARKDOWN_HTML = 'ESCAPE_MARKDOWN_HTML', VERSIONED_EXPORT = 'VERSIONED_EXPORT', GLOBAL_ASYNC_QUERIES = 'GLOBAL_ASYNC_QUERIES', + ENABLE_TEMPLATE_PROCESSING = 'ENABLE_TEMPLATE_PROCESSING', } export type FeatureFlagMap = { diff --git a/superset/errors.py b/superset/errors.py index 14d389fa3c607..416132b05b29d 100644 --- a/superset/errors.py +++ b/superset/errors.py @@ -27,6 +27,7 @@ class SupersetErrorType(str, Enum): Types of errors that can exist within Superset. Keep in sync with superset-frontend/src/components/ErrorMessage/types.ts + and docs/src/pages/docs/Miscellaneous/issue_codes.mdx """ # Frontend errors @@ -52,6 +53,9 @@ class SupersetErrorType(str, Enum): # Other errors BACKEND_TIMEOUT_ERROR = "BACKEND_TIMEOUT_ERROR" + # Sql Lab errors + MISSING_TEMPLATE_PARAMS_ERROR = "MISSING_TEMPLATE_PARAMS_ERROR" + ERROR_TYPES_TO_ISSUE_CODES_MAPPING = { SupersetErrorType.BACKEND_TIMEOUT_ERROR: [ @@ -100,6 +104,15 @@ class SupersetErrorType(str, Enum): ), }, ], + SupersetErrorType.MISSING_TEMPLATE_PARAMS_ERROR: [ + { + "code": 1006, + "message": _( + "Issue 1006 - One or more parameters specified in the query are " + "missing." + ), + }, + ], } diff --git a/superset/jinja_context.py b/superset/jinja_context.py index 400b9d60df7e0..b86b466e91c40 100644 --- a/superset/jinja_context.py +++ b/superset/jinja_context.py @@ -22,6 +22,7 @@ from flask import current_app, g, request from flask_babel import gettext as _ +from jinja2 import DebugUndefined from jinja2.sandbox import SandboxedEnvironment from superset.exceptions import SupersetTemplateException @@ -60,7 +61,7 @@ def context_addons() -> Dict[str, Any]: def filter_values(column: str, default: Optional[str] = None) -> List[str]: - """ Gets a values for a particular filter as a list + """Gets a values for a particular filter as a list This is useful if: - you want to use a filter box to filter a query where the name of filter box @@ -296,7 +297,7 @@ def __init__( self._schema = table.schema self._extra_cache_keys = extra_cache_keys self._context: Dict[str, Any] = {} - self._env = SandboxedEnvironment() + self._env = SandboxedEnvironment(undefined=DebugUndefined) self.set_context(**kwargs) def set_context(self, **kwargs: Any) -> None: diff --git a/superset/utils/core.py b/superset/utils/core.py index 52b0544cf5fa9..91de0c0d2238a 100644 --- a/superset/utils/core.py +++ b/superset/utils/core.py @@ -1669,3 +1669,8 @@ def get_time_filter_status( # pylint: disable=too-many-branches ) return applied, rejected + + +def format_list(items: Sequence[str], sep: str = ", ", quote: str = '"') -> str: + quote_escaped = "\\" + quote + return sep.join(f"{quote}{x.replace(quote, quote_escaped)}{quote}" for x in items) diff --git a/superset/views/core.py b/superset/views/core.py index 82bc2d128b7d0..e7706e0bff619 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -14,7 +14,8 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -# pylint: disable=comparison-with-callable +# pylint: disable=comparison-with-callable, line-too-long, too-many-branches +import dataclasses import logging import re from contextlib import closing @@ -34,8 +35,9 @@ permission_name, ) from flask_appbuilder.security.sqla import models as ab_models -from flask_babel import gettext as __, lazy_gettext as _ +from flask_babel import gettext as __, lazy_gettext as _, ngettext from jinja2.exceptions import TemplateError +from jinja2.meta import find_undeclared_variables from sqlalchemy import and_, or_ from sqlalchemy.engine.url import make_url from sqlalchemy.exc import ArgumentError, DBAPIError, NoSuchModuleError, SQLAlchemyError @@ -69,6 +71,7 @@ from superset.dashboards.dao import DashboardDAO from superset.databases.dao import DatabaseDAO from superset.databases.filters import DatabaseFilter +from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import ( CacheLoadError, CertificateException, @@ -157,6 +160,11 @@ DATASOURCE_MISSING_ERR = __("The data source seems to have been deleted") USER_MISSING_ERR = __("The user seems to have been deleted") +PARAMETER_MISSING_ERR = ( + "Please check your template parameters for syntax errors and make sure " + "they match across your SQL query and Set Parameters. Then, try running " + "your query again." +) class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods @@ -2508,6 +2516,28 @@ def sql_json_exec( # pylint: disable=too-many-statements,too-many-locals f"Query {query_id}: Template syntax error: {error_msg}" ) + # pylint: disable=protected-access + if is_feature_enabled("ENABLE_TEMPLATE_PROCESSING"): + ast = template_processor._env.parse(rendered_query) + undefined = find_undeclared_variables(ast) # type: ignore + if undefined: + error = SupersetError( + message=ngettext( + "There's an error with the parameter %(parameters)s.", + "There's an error with the parameters %(parameters)s.", + len(undefined), + parameters=utils.format_list(undefined), + ) + + " " + + PARAMETER_MISSING_ERR, + level=ErrorLevel.ERROR, + error_type=SupersetErrorType.MISSING_TEMPLATE_PARAMS_ERROR, + extra={"missing_parameters": list(undefined)}, + ) + return json_error_response( + payload={"errors": [dataclasses.asdict(error)]}, + ) + # Limit is not applied to the CTA queries if SQLLAB_CTAS_NO_LIMIT flag is set # to True. if not (config.get("SQLLAB_CTAS_NO_LIMIT") and select_as_cta): diff --git a/tests/base_tests.py b/tests/base_tests.py index 51bae4883bf31..6f3819996f52c 100644 --- a/tests/base_tests.py +++ b/tests/base_tests.py @@ -324,6 +324,7 @@ def run_sql( tmp_table_name=None, schema=None, ctas_method=CtasMethod.TABLE, + template_params="{}", ): if user_name: self.logout() @@ -336,6 +337,7 @@ def run_sql( "queryLimit": query_limit, "sql_editor_id": sql_editor_id, "ctas_method": ctas_method, + "templateParams": template_params, } if tmp_table_name: json_payload["tmp_table_name"] = tmp_table_name diff --git a/tests/sqllab_tests.py b/tests/sqllab_tests.py index 52fd32c488400..59ce8adeb03bf 100644 --- a/tests/sqllab_tests.py +++ b/tests/sqllab_tests.py @@ -552,7 +552,6 @@ def test_query_admin_can_access_all_queries(self) -> None: url = "/api/v1/query/" data = self.get_json_resp(url) - admin = security_manager.find_user("admin") self.assertEqual(3, len(data["result"])) def test_api_database(self): @@ -576,3 +575,23 @@ def test_api_database(self): {r.get("database_name") for r in self.get_json_resp(url)["result"]}, ) self.delete_fake_db() + + @mock.patch.dict( + "superset.extensions.feature_flag_manager._feature_flags", + {"ENABLE_TEMPLATE_PROCESSING": True}, + clear=True, + ) + def test_sql_json_parameter_error(self): + self.login("admin") + + data = self.run_sql( + "SELECT * FROM birth_names WHERE state = '{{ state }}' LIMIT 10", + "1", + template_params=json.dumps({"state": "CA"}), + ) + assert data["status"] == "success" + + data = self.run_sql( + "SELECT * FROM birth_names WHERE state = '{{ state }}' LIMIT 10", "2" + ) + assert data["errors"][0]["error_type"] == "MISSING_TEMPLATE_PARAMS_ERROR" From 794d31898943e54683af430aa999cd70b916aadd Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Wed, 16 Dec 2020 06:20:10 +0100 Subject: [PATCH 24/43] refactor: Migrate react-select to Antd Select in Metrics and Filters popovers (#12042) * Migrate react-select to Antd select in Metrics popover * Fix tests * Migrate react-select to Antd select in Filters popover * Migrate SelectControl to Antd Select * Add label with number of options left --- ...FilterEditPopoverSimpleTabContent_spec.jsx | 14 +- .../AdhocMetricEditPopover_spec.jsx | 12 +- .../src/common/components/Select.tsx | 8 ++ ...AdhocFilterEditPopoverSimpleTabContent.jsx | 120 ++++++++++++------ .../AdhocFilterEditPopoverSqlTabContent.jsx | 15 ++- .../components/AdhocMetricEditPopover.jsx | 63 ++++----- .../src/views/CRUD/alert/AlertReportModal.tsx | 2 +- 7 files changed, 139 insertions(+), 95 deletions(-) diff --git a/superset-frontend/spec/javascripts/explore/components/AdhocFilterEditPopoverSimpleTabContent_spec.jsx b/superset-frontend/spec/javascripts/explore/components/AdhocFilterEditPopoverSimpleTabContent_spec.jsx index 442527b77b4aa..fc13220940ce7 100644 --- a/superset-frontend/spec/javascripts/explore/components/AdhocFilterEditPopoverSimpleTabContent_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/components/AdhocFilterEditPopoverSimpleTabContent_spec.jsx @@ -59,10 +59,10 @@ const simpleCustomFilter = new AdhocFilter({ }); const options = [ - { type: 'VARCHAR(255)', column_name: 'source' }, - { type: 'VARCHAR(255)', column_name: 'target' }, - { type: 'DOUBLE', column_name: 'value' }, - { saved_metric_name: 'my_custom_metric' }, + { type: 'VARCHAR(255)', column_name: 'source', id: 1 }, + { type: 'VARCHAR(255)', column_name: 'target', id: 2 }, + { type: 'DOUBLE', column_name: 'value', id: 3 }, + { saved_metric_name: 'my_custom_metric', id: 4 }, sumValueAdhocMetric, ]; @@ -91,9 +91,7 @@ describe('AdhocFilterEditPopoverSimpleTabContent', () => { it('passes the new adhocFilter to onChange after onSubjectChange', () => { const { wrapper, onChange } = setup(); - wrapper - .instance() - .onSubjectChange({ type: 'VARCHAR(255)', column_name: 'source' }); + wrapper.instance().onSubjectChange(1); expect(onChange.calledOnce).toBe(true); expect(onChange.lastCall.args[0]).toEqual( simpleAdhocFilter.duplicateWith({ subject: 'source' }), @@ -102,7 +100,7 @@ describe('AdhocFilterEditPopoverSimpleTabContent', () => { it('may alter the clause in onSubjectChange if the old clause is not appropriate', () => { const { wrapper, onChange } = setup(); - wrapper.instance().onSubjectChange(sumValueAdhocMetric); + wrapper.instance().onSubjectChange(sumValueAdhocMetric.optionName); expect(onChange.calledOnce).toBe(true); expect(onChange.lastCall.args[0]).toEqual( simpleAdhocFilter.duplicateWith({ diff --git a/superset-frontend/spec/javascripts/explore/components/AdhocMetricEditPopover_spec.jsx b/superset-frontend/spec/javascripts/explore/components/AdhocMetricEditPopover_spec.jsx index 5aff466065f2e..98a62bd84abf7 100644 --- a/superset-frontend/spec/javascripts/explore/components/AdhocMetricEditPopover_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/components/AdhocMetricEditPopover_spec.jsx @@ -28,9 +28,9 @@ import AdhocMetricEditPopover from 'src/explore/components/AdhocMetricEditPopove import { AGGREGATES } from 'src/explore/constants'; const columns = [ - { type: 'VARCHAR(255)', column_name: 'source' }, - { type: 'VARCHAR(255)', column_name: 'target' }, - { type: 'DOUBLE', column_name: 'value' }, + { type: 'VARCHAR(255)', column_name: 'source', id: 1 }, + { type: 'VARCHAR(255)', column_name: 'target', id: 2 }, + { type: 'DOUBLE', column_name: 'value', id: 3 }, ]; const sumValueAdhocMetric = new AdhocMetric({ @@ -68,7 +68,7 @@ describe('AdhocMetricEditPopover', () => { it('overwrites the adhocMetric in state with onColumnChange', () => { const { wrapper } = setup(); - wrapper.instance().onColumnChange(columns[0]); + wrapper.instance().onColumnChange(columns[0].id); expect(wrapper.state('adhocMetric')).toEqual( sumValueAdhocMetric.duplicateWith({ column: columns[0] }), ); @@ -95,7 +95,7 @@ describe('AdhocMetricEditPopover', () => { expect(wrapper.find(Button).find({ disabled: true })).not.toExist(); wrapper.instance().onColumnChange(null); expect(wrapper.find(Button).find({ disabled: true })).toExist(); - wrapper.instance().onColumnChange({ column: columns[0] }); + wrapper.instance().onColumnChange(columns[0].id); expect(wrapper.find(Button).find({ disabled: true })).not.toExist(); wrapper.instance().onAggregateChange(null); expect(wrapper.find(Button).find({ disabled: true })).toExist(); @@ -104,7 +104,7 @@ describe('AdhocMetricEditPopover', () => { it('highlights save if changes are present', () => { const { wrapper } = setup(); expect(wrapper.find(Button).find({ buttonStyle: 'primary' })).not.toExist(); - wrapper.instance().onColumnChange({ column: columns[1] }); + wrapper.instance().onColumnChange(columns[1].id); expect(wrapper.find(Button).find({ buttonStyle: 'primary' })).toExist(); }); diff --git a/superset-frontend/src/common/components/Select.tsx b/superset-frontend/src/common/components/Select.tsx index ca2262197e701..75bd073912e6e 100644 --- a/superset-frontend/src/common/components/Select.tsx +++ b/superset-frontend/src/common/components/Select.tsx @@ -20,6 +20,10 @@ import { styled } from '@superset-ui/core'; import { Select as BaseSelect } from 'src/common/components'; const StyledSelect = styled(BaseSelect)` + display: block; +`; + +const StyledGraySelect = styled(StyledSelect)` &.ant-select-single { .ant-select-selector { height: 36px; @@ -44,3 +48,7 @@ const StyledOption = BaseSelect.Option; export const Select = Object.assign(StyledSelect, { Option: StyledOption, }); + +export const GraySelect = Object.assign(StyledGraySelect, { + Option: StyledOption, +}); diff --git a/superset-frontend/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx b/superset-frontend/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx index a0402193978d6..fa241914fc543 100644 --- a/superset-frontend/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx +++ b/superset-frontend/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx @@ -19,8 +19,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormGroup } from 'react-bootstrap'; -import { Select } from 'src/components/Select'; -import { t, SupersetClient } from '@superset-ui/core'; +import { Select } from 'src/common/components/Select'; +import { t, SupersetClient, styled } from '@superset-ui/core'; import AdhocFilter, { EXPRESSION_TYPES, CLAUSES } from '../AdhocFilter'; import adhocMetricType from '../propTypes/adhocMetricType'; @@ -36,7 +36,15 @@ import { DISABLE_INPUT_OPERATORS, } from '../constants'; import FilterDefinitionOption from './FilterDefinitionOption'; -import SelectControl from './controls/SelectControl'; + +const SelectWithLabel = styled(Select)` + .ant-select-selector::after { + content: '${({ labelText }) => labelText || '\\A0'}'; + display: inline-block; + white-space: nowrap; + color: ${({ theme }) => theme.colors.grayscale.light1}; + } +`; const propTypes = { adhocFilter: PropTypes.instanceOf(AdhocFilter).isRequired, @@ -92,11 +100,8 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon }; this.selectProps = { - isMulti: false, name: 'select-column', - labelKey: 'label', - autosize: false, - clearable: false, + showSearch: true, }; this.menuPortalProps = { @@ -116,7 +121,11 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon } } - onSubjectChange(option) { + onSubjectChange(id) { + const option = this.props.options.find( + option => option.id === id || option.optionName === id, + ); + let subject; let clause; // infer the new clause based on what subject was selected. @@ -247,27 +256,38 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon } } - renderSubjectOptionLabel(option) { - return ; + optionsRemaining() { + const { suggestions } = this.state; + const { comparator } = this.props.adhocFilter; + // if select is multi/value is array, we show the options not selected + const valuesFromSuggestionsLength = Array.isArray(comparator) + ? comparator.filter(v => suggestions.includes(v)).length + : 0; + return suggestions?.length - valuesFromSuggestionsLength ?? 0; } - renderSubjectOptionValue({ value }) { - return {value}; + createSuggestionsPlaceholder() { + const optionsRemaining = this.optionsRemaining(); + const placeholder = t('%s option(s)', optionsRemaining); + return optionsRemaining ? placeholder : ''; + } + + renderSubjectOptionLabel(option) { + return ; } render() { - const { adhocFilter, options: columns, datasource } = this.props; + const { adhocFilter, options, datasource } = this.props; + let columns = options; const { subject, operator, comparator } = adhocFilter; const subjectSelectProps = { - options: columns, - value: subject ? { value: subject } : undefined, + value: subject ?? undefined, onChange: this.onSubjectChange, - optionRenderer: this.renderSubjectOptionLabel, - valueRenderer: this.renderSubjectOptionValue, - valueKey: 'filterOptionName', - noResultsText: t( + notFoundContent: t( 'No such column found. To filter on a metric, try the Custom SQL tab.', ), + filterOption: (input, option) => + option.filterBy.toLowerCase().indexOf(input.toLowerCase()) >= 0, }; if (datasource.type === 'druid') { @@ -283,19 +303,16 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon adhocFilter.clause === CLAUSES.WHERE ? t('%s column(s)', columns.length) : t('To filter on a metric, use Custom SQL tab.'); - // make sure options have `column_name` - subjectSelectProps.options = columns.filter(option => option.column_name); + columns = options.filter(option => option.column_name); } const operatorSelectProps = { placeholder: t('%s operators(s)', OPERATORS_OPTIONS.length), // like AGGREGTES_OPTIONS, operator options are string - options: OPERATORS_OPTIONS.filter(op => - this.isOperatorRelevant(op, subject), - ), value: operator, onChange: this.onOperatorChange, - getOptionLabel: translateOperator, + filterOption: (input, option) => + option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0, }; return ( @@ -303,36 +320,63 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon {MULTI_OPERATORS.has(operator) || this.state.suggestions.length > 0 ? ( - + placeholder={this.createSuggestionsPlaceholder()} + labelText={ + comparator?.length > 0 && this.createSuggestionsPlaceholder() + } + > + {this.state.suggestions.map(suggestion => ( + + {suggestion} + + ))} + ) : ( + > + {Object.keys(CLAUSES).map(clause => ( + + {clause} + + ))} + WHERE {t('filters by columns')}
diff --git a/superset-frontend/src/explore/components/AdhocMetricEditPopover.jsx b/superset-frontend/src/explore/components/AdhocMetricEditPopover.jsx index da1cde11cca9a..a75406fcf183e 100644 --- a/superset-frontend/src/explore/components/AdhocMetricEditPopover.jsx +++ b/superset-frontend/src/explore/components/AdhocMetricEditPopover.jsx @@ -21,7 +21,7 @@ import PropTypes from 'prop-types'; import { FormGroup } from 'react-bootstrap'; import Tabs from 'src/common/components/Tabs'; import Button from 'src/components/Button'; -import Select from 'src/components/Select'; +import { Select } from 'src/common/components/Select'; import { styled, t } from '@superset-ui/core'; import { ColumnOption } from '@superset-ui/chart-controls'; @@ -70,27 +70,12 @@ export default class AdhocMetricEditPopover extends React.Component { this.handleAceEditorRef = this.handleAceEditorRef.bind(this); this.refreshAceEditor = this.refreshAceEditor.bind(this); - this.popoverRef = React.createRef(); - this.state = { adhocMetric: this.props.adhocMetric, width: startingWidth, height: startingHeight, }; - this.selectProps = { - labelKey: 'label', - isMulti: false, - autosize: false, - clearable: true, - }; - - this.menuPortalProps = { - menuPosition: 'fixed', - menuPlacement: 'bottom', - menuPortalTarget: this.popoverRef.current, - }; - document.addEventListener('mouseup', this.onMouseUp); } @@ -118,7 +103,8 @@ export default class AdhocMetricEditPopover extends React.Component { this.props.onClose(); } - onColumnChange(column) { + onColumnChange(columnId) { + const column = this.props.columns.find(column => column.id === columnId); this.setState(prevState => ({ adhocMetric: prevState.adhocMetric.duplicateWith({ column, @@ -213,20 +199,23 @@ export default class AdhocMetricEditPopover extends React.Component { const columnSelectProps = { placeholder: t('%s column(s)', columns.length), - options: columns, value: (adhocMetric.column && adhocMetric.column.column_name) || adhocMetric.inferSqlExpressionColumn(), onChange: this.onColumnChange, - optionRenderer: this.renderColumnOption, - valueKey: 'column_name', + allowClear: true, + showSearch: true, + filterOption: (input, option) => + option.filterBy.toLowerCase().indexOf(input.toLowerCase()) >= 0, }; const aggregateSelectProps = { placeholder: t('%s aggregates(s)', AGGREGATES_OPTIONS.length), - options: AGGREGATES_OPTIONS, value: adhocMetric.aggregate || adhocMetric.inferSqlExpressionAggregate(), onChange: this.onAggregateChange, + allowClear: true, + autoFocus: true, + showSearch: true, }; if (this.props.datasourceType === 'druid') { @@ -241,7 +230,6 @@ export default class AdhocMetricEditPopover extends React.Component {
column - + {columns.map(column => ( + + {this.renderColumnOption(column)} + + ))} + aggregate - + {AGGREGATES_OPTIONS.map(option => ( + + {option} + + ))} + Date: Wed, 16 Dec 2020 09:01:34 +0100 Subject: [PATCH 25/43] fix: Closes #11864 - Duplicate PropertiesModal (#12038) * Closes #11864 * Fix typo --- .../explore/components/DisplayQueryButton.jsx | 25 ++++--------------- .../components/ExploreActionButtons.jsx | 1 + .../explore/components/ExploreChartHeader.jsx | 11 +++++--- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/superset-frontend/src/explore/components/DisplayQueryButton.jsx b/superset-frontend/src/explore/components/DisplayQueryButton.jsx index dbb5d8441bda7..e292839248dda 100644 --- a/superset-frontend/src/explore/components/DisplayQueryButton.jsx +++ b/superset-frontend/src/explore/components/DisplayQueryButton.jsx @@ -36,7 +36,6 @@ import { getChartDataRequest } from '../../chart/chartAction'; import downloadAsImage from '../../utils/downloadAsImage'; import Loading from '../../components/Loading'; import ModalTrigger from '../../components/ModalTrigger'; -import PropertiesModal from './PropertiesModal'; import { sliceUpdated } from '../actions/exploreActions'; import { CopyButton } from './DataTableControl'; @@ -46,6 +45,7 @@ SyntaxHighlighter.registerLanguage('sql', sqlSyntax); SyntaxHighlighter.registerLanguage('json', jsonSyntax); const propTypes = { + onOpenPropertiesModal: PropTypes.func, onOpenInEditor: PropTypes.func, queryResponse: PropTypes.object, chartStatus: PropTypes.string, @@ -76,7 +76,6 @@ export const DisplayQueryButton = props => { const [sqlSupported] = useState( datasource && datasource.split('__')[1] === 'table', ); - const [isPropertiesModalOpen, setIsPropertiesModalOpen] = useState(false); const [menuVisible, setMenuVisible] = useState(false); const beforeOpen = resultType => { @@ -103,20 +102,12 @@ export const DisplayQueryButton = props => { }); }; - const openPropertiesModal = () => { - setIsPropertiesModalOpen(true); - }; - - const closePropertiesModal = () => { - setIsPropertiesModalOpen(false); - }; - const handleMenuClick = ({ key, domEvent }) => { const { chartHeight, slice, onOpenInEditor, latestQueryFormData } = props; setMenuVisible(false); switch (key) { case MENU_KEYS.EDIT_PROPERTIES: - openPropertiesModal(); + props.onOpenPropertiesModal(); break; case MENU_KEYS.RUN_IN_SQL_LAB: onOpenInEditor(latestQueryFormData); @@ -182,17 +173,11 @@ export const DisplayQueryButton = props => { onToggle={setMenuVisible} > - {slice && [ + {slice && ( {t('Edit properties')} - , - , - ]} + + )}
diff --git a/superset-frontend/src/explore/components/ExploreChartHeader.jsx b/superset-frontend/src/explore/components/ExploreChartHeader.jsx index 52af3df178779..88a54b3b18ff6 100644 --- a/superset-frontend/src/explore/components/ExploreChartHeader.jsx +++ b/superset-frontend/src/explore/components/ExploreChartHeader.jsx @@ -94,7 +94,7 @@ export class ExploreChartHeader extends React.PureComponent { this.state = { isPropertiesModalOpen: false, }; - this.openProperiesModal = this.openProperiesModal.bind(this); + this.openPropertiesModal = this.openPropertiesModal.bind(this); this.closePropertiesModal = this.closePropertiesModal.bind(this); } @@ -111,7 +111,7 @@ export class ExploreChartHeader extends React.PureComponent { ); } - openProperiesModal() { + openPropertiesModal() { this.setState({ isPropertiesModalOpen: true, }); @@ -167,7 +167,7 @@ export class ExploreChartHeader extends React.PureComponent { role="button" tabIndex={0} className="edit-desc-icon" - onClick={this.openProperiesModal} + onClick={this.openPropertiesModal} >
@@ -202,7 +202,10 @@ export class ExploreChartHeader extends React.PureComponent { status={CHART_STATUS_MAP[chartStatus]} /> Date: Wed, 16 Dec 2020 10:08:06 +0100 Subject: [PATCH 26/43] feat(annotations): security permissions simplification (#12014) * Changed security permissions for annotations and annotation layers * Updated permissions in annotation layers list * Created test for retrieving premissions info. Updated uris from f-strings to strings * Updated annotations in security_tests and added annotations to NEW_SECURITY_CONVERGE_VIEWS * Added migration for annotations security converge * Updated current revision after rebase master * Updated migration name to annotations security converge * Updated annotations permissions names in AnnotationLayersList and updated test since 'can_write' has wider permissions * Updated annotations migration to current --- .../AnnotationLayersList_spec.jsx | 4 +- .../annotationlayers/AnnotationLayersList.tsx | 6 +- superset/annotation_layers/annotations/api.py | 6 +- superset/annotation_layers/api.py | 6 +- ...cb2c78727_security_converge_annotations.py | 84 +++++++++++++++++++ superset/views/annotations.py | 9 +- tests/annotation_layers/api_tests.py | 20 ++++- tests/security_tests.py | 4 +- 8 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 superset/migrations/versions/c25cb2c78727_security_converge_annotations.py diff --git a/superset-frontend/spec/javascripts/views/CRUD/annotationlayers/AnnotationLayersList_spec.jsx b/superset-frontend/spec/javascripts/views/CRUD/annotationlayers/AnnotationLayersList_spec.jsx index 7a4847007e07e..fa6adddae69af 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/annotationlayers/AnnotationLayersList_spec.jsx +++ b/superset-frontend/spec/javascripts/views/CRUD/annotationlayers/AnnotationLayersList_spec.jsx @@ -61,7 +61,7 @@ const mockUser = { }; fetchMock.get(layersInfoEndpoint, { - permissions: ['can_delete'], + permissions: ['can_write'], }); fetchMock.get(layersEndpoint, { result: mocklayers, @@ -156,7 +156,7 @@ describe('AnnotationLayersList', () => { }); it('shows/hides bulk actions when bulk actions is clicked', async () => { - const button = wrapper.find(Button).at(0); + const button = wrapper.find(Button).at(1); act(() => { button.props().onClick(); }); diff --git a/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayersList.tsx b/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayersList.tsx index d066d4dd12779..92bf9bd586e07 100644 --- a/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayersList.tsx +++ b/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayersList.tsx @@ -114,9 +114,9 @@ function AnnotationLayersList({ ); }; - const canCreate = hasPerm('can_add'); - const canEdit = hasPerm('can_edit'); - const canDelete = hasPerm('can_delete'); + const canCreate = hasPerm('can_write'); + const canEdit = hasPerm('can_write'); + const canDelete = hasPerm('can_write'); function handleAnnotationLayerEdit(layer: AnnotationLayerObject | null) { setCurrentAnnotationLayer(layer); diff --git a/superset/annotation_layers/annotations/api.py b/superset/annotation_layers/annotations/api.py index d7241b650e696..1c63f699d51c6 100644 --- a/superset/annotation_layers/annotations/api.py +++ b/superset/annotation_layers/annotations/api.py @@ -52,7 +52,7 @@ openapi_spec_methods_override, ) from superset.annotation_layers.commands.exceptions import AnnotationLayerNotFoundError -from superset.constants import RouteMethod +from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.models.annotations import Annotation from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics @@ -65,7 +65,9 @@ class AnnotationRestApi(BaseSupersetModelRestApi): include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET | { "bulk_delete", # not using RouteMethod since locally defined } - class_permission_name = "AnnotationLayerModelView" + class_permission_name = "Annotation" + method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP + resource_name = "annotation_layer" allow_browser_login = True diff --git a/superset/annotation_layers/api.py b/superset/annotation_layers/api.py index c608e30157d56..b47bf87ecb891 100644 --- a/superset/annotation_layers/api.py +++ b/superset/annotation_layers/api.py @@ -46,7 +46,7 @@ get_delete_ids_schema, openapi_spec_methods_override, ) -from superset.constants import RouteMethod +from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.extensions import event_logger from superset.models.annotations import AnnotationLayer from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics @@ -61,7 +61,9 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi): RouteMethod.RELATED, "bulk_delete", # not using RouteMethod since locally defined } - class_permission_name = "AnnotationLayerModelView" + class_permission_name = "Annotation" + method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP + resource_name = "annotation_layer" allow_browser_login = True diff --git a/superset/migrations/versions/c25cb2c78727_security_converge_annotations.py b/superset/migrations/versions/c25cb2c78727_security_converge_annotations.py new file mode 100644 index 0000000000000..33099dd2e74b2 --- /dev/null +++ b/superset/migrations/versions/c25cb2c78727_security_converge_annotations.py @@ -0,0 +1,84 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""security converge annotations + +Revision ID: c25cb2c78727 +Revises: ccb74baaa89b +Create Date: 2020-12-11 17:02:21.213046 + +""" + +from alembic import op +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session + +# revision identifiers, used by Alembic. +from superset.migrations.shared.security_converge import ( + add_pvms, + get_reversed_new_pvms, + get_reversed_pvm_map, + migrate_roles, + Pvm, +) + +revision = "c25cb2c78727" +down_revision = "ccb74baaa89b" + + +NEW_PVMS = {"Annotation": ("can_read", "can_write",)} +PVM_MAP = { + Pvm("AnnotationLayerModelView", "can_delete"): (Pvm("Annotation", "can_write"),), + Pvm("AnnotationLayerModelView", "can_list"): (Pvm("Annotation", "can_read"),), + Pvm("AnnotationLayerModelView", "can_show",): (Pvm("Annotation", "can_read"),), + Pvm("AnnotationLayerModelView", "can_add",): (Pvm("Annotation", "can_write"),), + Pvm("AnnotationLayerModelView", "can_edit",): (Pvm("Annotation", "can_write"),), + Pvm("AnnotationModelView", "can_annotation",): (Pvm("Annotation", "can_read"),), + Pvm("AnnotationModelView", "can_show",): (Pvm("Annotation", "can_read"),), + Pvm("AnnotationModelView", "can_add",): (Pvm("Annotation", "can_write"),), + Pvm("AnnotationModelView", "can_delete",): (Pvm("Annotation", "can_write"),), + Pvm("AnnotationModelView", "can_edit",): (Pvm("Annotation", "can_write"),), + Pvm("AnnotationModelView", "can_list",): (Pvm("Annotation", "can_read"),), +} + + +def upgrade(): + bind = op.get_bind() + session = Session(bind=bind) + + # Add the new permissions on the migration itself + add_pvms(session, NEW_PVMS) + migrate_roles(session, PVM_MAP) + try: + session.commit() + except SQLAlchemyError as ex: + print(f"An error occurred while upgrading annotation permissions: {ex}") + session.rollback() + + +def downgrade(): + bind = op.get_bind() + session = Session(bind=bind) + + # Add the old permissions on the migration itself + add_pvms(session, get_reversed_new_pvms(PVM_MAP)) + migrate_roles(session, get_reversed_pvm_map(PVM_MAP)) + try: + session.commit() + except SQLAlchemyError as ex: + print(f"An error occurred while downgrading annotation permissions: {ex}") + session.rollback() + pass diff --git a/superset/views/annotations.py b/superset/views/annotations.py index 03cc26006ff35..345fd2c15a1f0 100644 --- a/superset/views/annotations.py +++ b/superset/views/annotations.py @@ -24,7 +24,7 @@ from wtforms.validators import StopValidation from superset import is_feature_enabled -from superset.constants import RouteMethod +from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.models.annotations import Annotation, AnnotationLayer from superset.typing import FlaskResponse from superset.views.base import SupersetModelView @@ -54,6 +54,9 @@ class AnnotationModelView( datamodel = SQLAInterface(Annotation) include_route_methods = RouteMethod.CRUD_SET | {"annotation"} + class_permission_name = "Annotation" + method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP + list_title = _("Annotations") show_title = _("Show Annotation") add_title = _("Add Annotation") @@ -109,6 +112,10 @@ class AnnotationLayerModelView(SupersetModelView): # pylint: disable=too-many-a datamodel = SQLAInterface(AnnotationLayer) include_route_methods = RouteMethod.CRUD_SET | {RouteMethod.API_READ} related_views = [AnnotationModelView] + + class_permission_name = "Annotation" + method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP + list_title = _("Annotation Layers") show_title = _("Show Annotation Layer") add_title = _("Add Annotation Layer") diff --git a/tests/annotation_layers/api_tests.py b/tests/annotation_layers/api_tests.py index 0ee361bcfbd1b..b53624394ef46 100644 --- a/tests/annotation_layers/api_tests.py +++ b/tests/annotation_layers/api_tests.py @@ -75,10 +75,24 @@ def test_info_annotation(self): Annotation API: Test info """ self.login(username="admin") - uri = f"api/v1/annotation_layer/_info" + uri = "api/v1/annotation_layer/_info" rv = self.get_assert_metric(uri, "info") assert rv.status_code == 200 + def test_info_security_query(self): + """ + Annotation API: Test info security + """ + self.login(username="admin") + params = {"keys": ["permissions"]} + uri = f"api/v1/annotation_layer/_info?q={prison.dumps(params)}" + rv = self.get_assert_metric(uri, "info") + data = json.loads(rv.data.decode("utf-8")) + assert rv.status_code == 200 + assert "can_read" in data["permissions"] + assert "can_write" in data["permissions"] + assert len(data["permissions"]) == 2 + @pytest.mark.usefixtures("create_annotation_layers") def test_get_annotation_layer_not_found(self): """ @@ -96,7 +110,7 @@ def test_get_list_annotation_layer(self): Annotation Api: Test get list annotation layers """ self.login(username="admin") - uri = f"api/v1/annotation_layer/" + uri = "api/v1/annotation_layer/" rv = self.get_assert_metric(uri, "get_list") expected_fields = [ @@ -120,7 +134,7 @@ def test_get_list_annotation_layer_sorting(self): Annotation Api: Test sorting on get list annotation layers """ self.login(username="admin") - uri = f"api/v1/annotation_layer/" + uri = "api/v1/annotation_layer/" order_columns = [ "name", diff --git a/tests/security_tests.py b/tests/security_tests.py index 4ef63922f7c80..2836b48501266 100644 --- a/tests/security_tests.py +++ b/tests/security_tests.py @@ -48,7 +48,7 @@ from .fixtures.energy_dashboard import load_energy_table_with_slice from .fixtures.unicode_dashboard import load_unicode_dashboard_with_slice -NEW_SECURITY_CONVERGE_VIEWS = ("CssTemplate", "SavedQuery", "Chart") +NEW_SECURITY_CONVERGE_VIEWS = ("CssTemplate", "SavedQuery", "Chart", "Annotation") def get_perm_tuples(role_name): @@ -672,7 +672,7 @@ def assert_can_gamma(self, perm_set): self.assert_can_menu("Dashboards", perm_set) def assert_can_alpha(self, perm_set): - self.assert_can_all("AnnotationLayerModelView", perm_set) + self.assert_can_all("Annotation", perm_set) self.assert_can_all("CssTemplate", perm_set) self.assert_can_all("TableModelView", perm_set) self.assert_can_read("QueryView", perm_set) From 2302adb61ac5bf969f5687b894d39cff67f79c79 Mon Sep 17 00:00:00 2001 From: Daniel Vaz Gaspar Date: Wed, 16 Dec 2020 11:49:03 +0000 Subject: [PATCH 27/43] feat(datasets): security perm simplification (#12000) * feat(datasets): security perm simplification * feat(datasets): security perm simplification * fix tests * fix tests * fix tests * fix tests * fix tests * include SqlMetricInlineView converge and fix JS tests * update to current alembic revision --- .../CRUD/data/dataset/DatasetList_spec.jsx | 2 +- .../views/CRUD/data/dataset/DatasetList.tsx | 8 +- superset/connectors/sqla/views.py | 8 +- superset/constants.py | 5 +- superset/datasets/api.py | 5 +- ...45731db65d9c_security_converge_datasets.py | 91 +++++++++++++++++++ superset/security/manager.py | 12 ++- tests/datasets/api_tests.py | 30 ++++-- tests/security_tests.py | 24 +++-- 9 files changed, 154 insertions(+), 31 deletions(-) create mode 100644 superset/migrations/versions/45731db65d9c_security_converge_datasets.py diff --git a/superset-frontend/spec/javascripts/views/CRUD/data/dataset/DatasetList_spec.jsx b/superset-frontend/spec/javascripts/views/CRUD/data/dataset/DatasetList_spec.jsx index d919bc033b48a..a816a60910f8d 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/data/dataset/DatasetList_spec.jsx +++ b/superset-frontend/spec/javascripts/views/CRUD/data/dataset/DatasetList_spec.jsx @@ -58,7 +58,7 @@ const mockUser = { }; fetchMock.get(datasetsInfoEndpoint, { - permissions: ['can_list', 'can_edit', 'can_add', 'can_delete'], + permissions: ['can_read', 'can_write'], }); fetchMock.get(datasetsOwnersEndpoint, { result: [], diff --git a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx index 565541eff78e6..752fb44cb1fd5 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx @@ -141,10 +141,10 @@ const DatasetList: FunctionComponent = ({ refreshData(); }; - const canEdit = hasPerm('can_edit'); - const canDelete = hasPerm('can_delete'); - const canCreate = hasPerm('can_add'); - const canExport = hasPerm('can_mulexport'); + const canEdit = hasPerm('can_write'); + const canDelete = hasPerm('can_write'); + const canCreate = hasPerm('can_write'); + const canExport = hasPerm('can_read'); const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }]; diff --git a/superset/connectors/sqla/views.py b/superset/connectors/sqla/views.py index 23efa86e1f3af..a9ae564e34d4e 100644 --- a/superset/connectors/sqla/views.py +++ b/superset/connectors/sqla/views.py @@ -33,7 +33,7 @@ from superset import app, db, is_feature_enabled from superset.connectors.base.views import DatasourceModelView from superset.connectors.sqla import models -from superset.constants import RouteMethod +from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.typing import FlaskResponse from superset.utils import core as utils from superset.views.base import ( @@ -55,6 +55,8 @@ class TableColumnInlineView( # pylint: disable=too-many-ancestors ): datamodel = SQLAInterface(models.TableColumn) # TODO TODO, review need for this on related_views + class_permission_name = "Dataset" + method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP include_route_methods = RouteMethod.RELATED_VIEW_SET | RouteMethod.API_SET list_title = _("Columns") @@ -174,6 +176,8 @@ class SqlMetricInlineView( # pylint: disable=too-many-ancestors CompactCRUDMixin, SupersetModelView ): datamodel = SQLAInterface(models.SqlMetric) + class_permission_name = "Dataset" + method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP include_route_methods = RouteMethod.RELATED_VIEW_SET | RouteMethod.API_SET list_title = _("Metrics") @@ -327,6 +331,8 @@ class TableModelView( # pylint: disable=too-many-ancestors DatasourceModelView, DeleteMixin, YamlExportMixin ): datamodel = SQLAInterface(models.SqlaTable) + class_permission_name = "Dataset" + method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP include_route_methods = RouteMethod.CRUD_SET list_title = _("Tables") diff --git a/superset/constants.py b/superset/constants.py index 16cd4e64ab829..dc56939dddda8 100644 --- a/superset/constants.py +++ b/superset/constants.py @@ -82,6 +82,7 @@ class RouteMethod: # pylint: disable=too-few-public-methods "list": "read", "muldelete": "write", "show": "read", + "yaml_export": "read", } MODEL_API_RW_METHOD_PERMISSION_MAP = { @@ -95,8 +96,10 @@ class RouteMethod: # pylint: disable=too-few-public-methods "post": "write", "put": "write", "related": "read", - "favorite_status": "write", + "refresh": "read", + "related_objects": "read", "import_": "write", + "favorite_status": "write", "cache_screenshot": "read", "screenshot": "read", "data": "read", diff --git a/superset/datasets/api.py b/superset/datasets/api.py index a9a210e6a86c3..68cfdbb85556d 100644 --- a/superset/datasets/api.py +++ b/superset/datasets/api.py @@ -33,7 +33,7 @@ from superset.commands.exceptions import CommandInvalidError from superset.commands.importers.v1.utils import remove_root from superset.connectors.sqla.models import SqlaTable -from superset.constants import RouteMethod +from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.databases.filters import DatabaseFilter from superset.datasets.commands.bulk_delete import BulkDeleteDatasetCommand from superset.datasets.commands.create import CreateDatasetCommand @@ -79,7 +79,8 @@ class DatasetRestApi(BaseSupersetModelRestApi): resource_name = "dataset" allow_browser_login = True - class_permission_name = "TableModelView" + class_permission_name = "Dataset" + method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET | { RouteMethod.EXPORT, RouteMethod.IMPORT, diff --git a/superset/migrations/versions/45731db65d9c_security_converge_datasets.py b/superset/migrations/versions/45731db65d9c_security_converge_datasets.py new file mode 100644 index 0000000000000..5b4670857faf4 --- /dev/null +++ b/superset/migrations/versions/45731db65d9c_security_converge_datasets.py @@ -0,0 +1,91 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""security converge datasets + +Revision ID: 45731db65d9c +Revises: ccb74baaa89b +Create Date: 2020-12-10 15:05:44.928020 + +""" + +# revision identifiers, used by Alembic. +revision = "45731db65d9c" +down_revision = "c25cb2c78727" + +from alembic import op +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session + +from superset.migrations.shared.security_converge import ( + add_pvms, + get_reversed_new_pvms, + get_reversed_pvm_map, + migrate_roles, + Pvm, +) + +NEW_PVMS = {"Dataset": ("can_read", "can_write",)} +PVM_MAP = { + Pvm("SqlMetricInlineView", "can_add"): (Pvm("Dataset", "can_write"),), + Pvm("SqlMetricInlineView", "can_delete"): (Pvm("Dataset", "can_write"),), + Pvm("SqlMetricInlineView", "can_edit"): (Pvm("Dataset", "can_write"),), + Pvm("SqlMetricInlineView", "can_list"): (Pvm("Dataset", "can_read"),), + Pvm("SqlMetricInlineView", "can_show"): (Pvm("Dataset", "can_read"),), + Pvm("TableColumnInlineView", "can_add"): (Pvm("Dataset", "can_write"),), + Pvm("TableColumnInlineView", "can_delete"): (Pvm("Dataset", "can_write"),), + Pvm("TableColumnInlineView", "can_edit"): (Pvm("Dataset", "can_write"),), + Pvm("TableColumnInlineView", "can_list"): (Pvm("Dataset", "can_read"),), + Pvm("TableColumnInlineView", "can_show"): (Pvm("Dataset", "can_read"),), + Pvm("TableModelView", "can_add",): (Pvm("Dataset", "can_write"),), + Pvm("TableModelView", "can_delete",): (Pvm("Dataset", "can_write"),), + Pvm("TableModelView", "can_edit",): (Pvm("Dataset", "can_write"),), + Pvm("TableModelView", "can_list"): (Pvm("Dataset", "can_read"),), + Pvm("TableModelView", "can_mulexport"): (Pvm("Dataset", "can_read"),), + Pvm("TableModelView", "can_show"): (Pvm("Dataset", "can_read"),), + Pvm("TableModelView", "muldelete",): (Pvm("Dataset", "can_write"),), + Pvm("TableModelView", "refresh",): (Pvm("Dataset", "can_write"),), + Pvm("TableModelView", "yaml_export",): (Pvm("Dataset", "can_read"),), +} + + +def upgrade(): + bind = op.get_bind() + session = Session(bind=bind) + + # Add the new permissions on the migration itself + add_pvms(session, NEW_PVMS) + migrate_roles(session, PVM_MAP) + try: + session.commit() + except SQLAlchemyError as ex: + print(f"An error occurred while upgrading permissions: {ex}") + session.rollback() + + +def downgrade(): + bind = op.get_bind() + session = Session(bind=bind) + + # Add the old permissions on the migration itself + add_pvms(session, get_reversed_new_pvms(PVM_MAP)) + migrate_roles(session, get_reversed_pvm_map(PVM_MAP)) + try: + session.commit() + except SQLAlchemyError as ex: + print(f"An error occurred while downgrading permissions: {ex}") + session.rollback() + pass diff --git a/superset/security/manager.py b/superset/security/manager.py index 3bc0974332193..04744879972c3 100644 --- a/superset/security/manager.py +++ b/superset/security/manager.py @@ -120,9 +120,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods } GAMMA_READ_ONLY_MODEL_VIEWS = { - "SqlMetricInlineView", - "TableColumnInlineView", - "TableModelView", + "Dataset", "DruidColumnInlineView", "DruidDatasourceModelView", "DruidMetricInlineView", @@ -160,7 +158,13 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods "all_query_access", } - READ_ONLY_PERMISSION = {"can_show", "can_list", "can_get", "can_external_metadata"} + READ_ONLY_PERMISSION = { + "can_show", + "can_list", + "can_get", + "can_external_metadata", + "can_read", + } ALPHA_ONLY_PERMISSIONS = { "muldelete", diff --git a/tests/datasets/api_tests.py b/tests/datasets/api_tests.py index 65ea2c3274d5d..02e2266b768a7 100644 --- a/tests/datasets/api_tests.py +++ b/tests/datasets/api_tests.py @@ -353,6 +353,20 @@ def test_get_dataset_info(self): rv = self.get_assert_metric(uri, "info") assert rv.status_code == 200 + def test_info_security_dataset(self): + """ + Dataset API: Test info security + """ + self.login(username="admin") + params = {"keys": ["permissions"]} + uri = f"api/v1/dataset/_info?q={prison.dumps(params)}" + rv = self.get_assert_metric(uri, "info") + data = json.loads(rv.data.decode("utf-8")) + assert rv.status_code == 200 + assert "can_read" in data["permissions"] + assert "can_write" in data["permissions"] + assert len(data["permissions"]) == 2 + def test_create_dataset_item(self): """ Dataset API: Test create dataset item @@ -455,23 +469,23 @@ def test_create_dataset_item_owners_invalid(self): expected_result = {"message": {"owners": ["Owners are invalid"]}} assert data == expected_result + @pytest.mark.usefixtures("load_energy_table_with_slice") def test_create_dataset_validate_uniqueness(self): """ Dataset API: Test create dataset validate table uniqueness """ - example_db = get_example_database() + energy_usage_ds = self.get_energy_usage_dataset() self.login(username="admin") table_data = { - "database": example_db.id, - "schema": "", - "table_name": "birth_names", + "database": energy_usage_ds.database_id, + "table_name": energy_usage_ds.table_name, } uri = "api/v1/dataset/" rv = self.post_assert_metric(uri, table_data, "post") assert rv.status_code == 422 data = json.loads(rv.data.decode("utf-8")) assert data == { - "message": {"table_name": ["Datasource birth_names already exists"]} + "message": {"table_name": ["Datasource energy_usage already exists"]} } def test_create_dataset_same_name_different_schema(self): @@ -1104,7 +1118,7 @@ def test_export_dataset_gamma(self): self.login(username="gamma") rv = self.client.get(uri) - assert rv.status_code == 401 + assert rv.status_code == 404 @patch.dict( "superset.extensions.feature_flag_manager._feature_flags", @@ -1165,8 +1179,8 @@ def test_export_dataset_bundle_gamma(self): self.login(username="gamma") rv = self.client.get(uri) - - assert rv.status_code == 401 + # gamma users by default do not have access to this dataset + assert rv.status_code == 404 def test_get_dataset_related_objects(self): """ diff --git a/tests/security_tests.py b/tests/security_tests.py index 2836b48501266..5625fd9636b1c 100644 --- a/tests/security_tests.py +++ b/tests/security_tests.py @@ -48,7 +48,13 @@ from .fixtures.energy_dashboard import load_energy_table_with_slice from .fixtures.unicode_dashboard import load_unicode_dashboard_with_slice -NEW_SECURITY_CONVERGE_VIEWS = ("CssTemplate", "SavedQuery", "Chart", "Annotation") +NEW_SECURITY_CONVERGE_VIEWS = ( + "Annotation", + "Dataset", + "CssTemplate", + "Chart", + "SavedQuery", +) def get_perm_tuples(role_name): @@ -644,7 +650,7 @@ def assert_can_menu(self, view_menu, permissions_set): self.assertIn(("menu_access", view_menu), permissions_set) def assert_can_gamma(self, perm_set): - self.assert_can_read("TableModelView", perm_set) + self.assert_can_read("Dataset", perm_set) # make sure that user can create slices and dashboards self.assert_can_all("Chart", perm_set) @@ -674,7 +680,7 @@ def assert_can_gamma(self, perm_set): def assert_can_alpha(self, perm_set): self.assert_can_all("Annotation", perm_set) self.assert_can_all("CssTemplate", perm_set) - self.assert_can_all("TableModelView", perm_set) + self.assert_can_all("Dataset", perm_set) self.assert_can_read("QueryView", perm_set) self.assertIn(("can_import_dashboards", "Superset"), perm_set) self.assertIn(("can_this_form_post", "CsvToDatabaseView"), perm_set) @@ -711,7 +717,7 @@ def assert_can_admin(self, perm_set): def test_is_admin_only(self): self.assertFalse( security_manager._is_admin_only( - security_manager.find_permission_view_menu("can_list", "TableModelView") + security_manager.find_permission_view_menu("can_read", "Dataset") ) ) self.assertFalse( @@ -759,15 +765,13 @@ def test_is_admin_only(self): def test_is_alpha_only(self): self.assertFalse( security_manager._is_alpha_only( - security_manager.find_permission_view_menu("can_list", "TableModelView") + security_manager.find_permission_view_menu("can_read", "Dataset") ) ) self.assertTrue( security_manager._is_alpha_only( - security_manager.find_permission_view_menu( - "muldelete", "TableModelView" - ) + security_manager.find_permission_view_menu("can_write", "Dataset") ) ) self.assertTrue( @@ -788,7 +792,7 @@ def test_is_alpha_only(self): def test_is_gamma_pvm(self): self.assertTrue( security_manager._is_gamma_pvm( - security_manager.find_permission_view_menu("can_list", "TableModelView") + security_manager.find_permission_view_menu("can_read", "Dataset") ) ) @@ -837,7 +841,7 @@ def test_gamma_permissions(self): gamma_perm_set.add((perm.permission.name, perm.view_menu.name)) # check read only perms - self.assert_can_read("TableModelView", gamma_perm_set) + self.assert_can_read("Dataset", gamma_perm_set) # make sure that user can create slices and dashboards self.assert_can_all("Chart", gamma_perm_set) From 48fb8c0b7793e6b618db8c229101075c44c2bac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Gigi=C4=87?= Date: Wed, 16 Dec 2020 15:35:12 +0100 Subject: [PATCH 28/43] fix(dataset): Page blanks on large data load (#11979) * Implement pagination in for Samples preview * Increase page size * Fix lint * Render cells based on width * Fix lint errors * Additional tests and changes * Search bar fix * Additional fixes * Suggested changes --- .../spec/javascripts/explore/utils_spec.jsx | 15 +++++++++++++++ .../src/explore/components/DataTableControl.tsx | 2 +- .../src/explore/components/DataTablesPane.tsx | 7 ++++++- superset-frontend/src/explore/exploreUtils.js | 8 ++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/superset-frontend/spec/javascripts/explore/utils_spec.jsx b/superset-frontend/spec/javascripts/explore/utils_spec.jsx index 8e419d1350d92..11a40b23762ae 100644 --- a/superset-frontend/spec/javascripts/explore/utils_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/utils_spec.jsx @@ -23,6 +23,7 @@ import { buildV1ChartDataPayload, getExploreUrl, getExploreLongUrl, + getDataTablePageSize, shouldUseLegacyApi, } from 'src/explore/exploreUtils'; import { @@ -200,6 +201,20 @@ describe('exploreUtils', () => { }); }); + describe('getDataTablePageSize', () => { + it('divides samples data into pages dynamically', () => { + let pageSize; + pageSize = getDataTablePageSize(500); + expect(pageSize).toEqual(20); + pageSize = getDataTablePageSize(0); + expect(pageSize).toEqual(50); + pageSize = getDataTablePageSize(1); + expect(pageSize).toEqual(10000); + pageSize = getDataTablePageSize(1000000); + expect(pageSize).toEqual(5); + }); + }); + describe('buildV1ChartDataPayload', () => { it('generate valid request payload despite no registered buildQuery', () => { const v1RequestPayload = buildV1ChartDataPayload({ diff --git a/superset-frontend/src/explore/components/DataTableControl.tsx b/superset-frontend/src/explore/components/DataTableControl.tsx index 8768e31bfd627..52619f37d7dcc 100644 --- a/superset-frontend/src/explore/components/DataTableControl.tsx +++ b/superset-frontend/src/explore/components/DataTableControl.tsx @@ -90,7 +90,7 @@ export const useFilteredTableData = ( const formattedData = applyFormattingToTabularData(data); return formattedData.filter((row: Record) => Object.values(row).some(value => - value.toString().toLowerCase().includes(filterText.toLowerCase()), + value?.toString().toLowerCase().includes(filterText.toLowerCase()), ), ); }, [data, filterText]); diff --git a/superset-frontend/src/explore/components/DataTablesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane.tsx index 9336051e7d8af..d9b3493c67567 100644 --- a/superset-frontend/src/explore/components/DataTablesPane.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane.tsx @@ -24,6 +24,7 @@ import Loading from 'src/components/Loading'; import TableView, { EmptyWrapperType } from 'src/components/TableView'; import { getChartDataRequest } from 'src/chart/chartAction'; import { getClientErrorObject } from 'src/utils/getClientErrorObject'; +import { getDataTablePageSize } from 'src/explore/exploreUtils'; import { CopyToClipboardButton, FilterInput, @@ -181,6 +182,9 @@ export const DataTablesPane = ({ }; const renderDataTable = (type: string) => { + // restrict cell count to 10000 or min 5 rows to avoid crashing browser + const columnsLength = columns[type].length; + const pageSize = getDataTablePageSize(columnsLength); if (isLoading[type]) { return ; } @@ -195,7 +199,8 @@ export const DataTablesPane = ({ Date: Wed, 16 Dec 2020 10:45:21 -0800 Subject: [PATCH 29/43] build(cypress): Use pull_request_target event to run cypress (#11750) * Use pull_request_target event to run cypress in order to access repo secrets * Summary job for branch protection requirements * Restore pull_request trigger * Use merge base for e2e tests * Restore push trigger for all branches --- .github/workflows/superset-e2e.yml | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/workflows/superset-e2e.yml b/.github/workflows/superset-e2e.yml index 578a7d2d69ed4..5499d39fb2948 100644 --- a/.github/workflows/superset-e2e.yml +++ b/.github/workflows/superset-e2e.yml @@ -1,13 +1,18 @@ name: E2E -on: [push, pull_request] +on: [push, pull_request, pull_request_target] jobs: - Cypress: + cypress-matrix: runs-on: ubuntu-18.04 strategy: - fail-fast: true + # when one test fails, DO NOT cancel the other + # containers, because this will kill Cypress processes + # leaving the Dashboard hanging ... + # https://github.com/cypress-io/github-action/issues/48 + fail-fast: false matrix: + containers: [1, 2, 3] browser: ["chrome"] env: FLASK_ENV: development @@ -34,8 +39,14 @@ jobs: uses: styfle/cancel-workflow-action@0.6.0 with: access_token: ${{ github.token }} - - name: Checkout code + - name: Checkout code (push) + if: github.event_name == 'push' + uses: actions/checkout@v2 + - name: Checkout code (pull_request) + if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target' uses: actions/checkout@v2 + with: + ref: 'refs/pull/${{ github.event.number }}/merge' - name: Setup Python uses: actions/setup-python@v2 with: @@ -89,3 +100,12 @@ jobs: with: name: screenshots path: ${{ github.workspace }}/superset-frontend/cypress-base/cypress/screenshots + Cypress: + if: ${{ always() }} + name: Cypress (chrome) + runs-on: ubuntu-18.04 + needs: cypress-matrix + steps: + - name: Check build matrix status + if: ${{ needs.cypress-matrix.result != 'success' }} + run: exit 1 From 148a0017b79ae05597eb9bc87f6159243b896cec Mon Sep 17 00:00:00 2001 From: Rob DiCiuccio Date: Wed, 16 Dec 2020 11:38:55 -0800 Subject: [PATCH 30/43] Remove e2e pull_request event trigger (#12076) --- .github/workflows/superset-e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/superset-e2e.yml b/.github/workflows/superset-e2e.yml index 5499d39fb2948..f080452868ba6 100644 --- a/.github/workflows/superset-e2e.yml +++ b/.github/workflows/superset-e2e.yml @@ -1,6 +1,6 @@ name: E2E -on: [push, pull_request, pull_request_target] +on: [push, pull_request_target] jobs: cypress-matrix: From 9be9034f1a8083f390923ef183bf4fd02d84f761 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Wed, 16 Dec 2020 11:55:38 -0800 Subject: [PATCH 31/43] feat: Global nav menus open on hover (#12025) * hover opens menus * hover opens menus * linting, removing some styles I added * moving useState up (non-conditional) * just a tweak to prevent a conflict. --- .../src/components/Menu/LanguagePicker.tsx | 7 ++++++- superset-frontend/src/components/Menu/Menu.tsx | 12 ++++++++++-- superset-frontend/src/components/Menu/MenuObject.tsx | 12 ++++++++++-- superset-frontend/src/components/Menu/NewMenu.tsx | 12 ++++++++++-- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/superset-frontend/src/components/Menu/LanguagePicker.tsx b/superset-frontend/src/components/Menu/LanguagePicker.tsx index 51b88be89fbe1..c2af6cef3af62 100644 --- a/superset-frontend/src/components/Menu/LanguagePicker.tsx +++ b/superset-frontend/src/components/Menu/LanguagePicker.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { Menu } from 'src/common/components'; import NavDropdown from 'src/components/NavDropdown'; @@ -37,8 +37,13 @@ export default function LanguagePicker({ locale, languages, }: LanguagePickerProps) { + const [dropdownOpen, setDropdownOpen] = useState(false); + return ( setDropdownOpen(true)} + onMouseLeave={() => setDropdownOpen(false)} + open={dropdownOpen} id="locale-dropdown" title={ diff --git a/superset-frontend/src/components/Menu/Menu.tsx b/superset-frontend/src/components/Menu/Menu.tsx index 173de84be8c19..9216e11ebed7e 100644 --- a/superset-frontend/src/components/Menu/Menu.tsx +++ b/superset-frontend/src/components/Menu/Menu.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { t, styled } from '@superset-ui/core'; import { Nav, Navbar, NavItem } from 'react-bootstrap'; import NavDropdown from 'src/components/NavDropdown'; @@ -154,6 +154,8 @@ const StyledHeader = styled.header` export function Menu({ data: { menu, brand, navbar_right: navbarRight, settings }, }: MenuProps) { + const [dropdownOpen, setDropdownOpen] = useState(false); + return ( @@ -173,7 +175,13 @@ export function Menu({