From c3f7a011a518dedc147909602c5aa13b57f01e48 Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Thu, 13 Apr 2023 11:50:50 -0700 Subject: [PATCH 1/4] Fix datetime_eval holiday multiple result issue - Bump holidays version family from ">=0.17.2, <0.18" to ">=0.22, <0.23" - Change holiday search result evaluation logic for multiple date cases - Change holiday lookup from `icontains` (default) to `istartswith`. - Add tests for described cases --- setup.py | 2 +- superset/utils/date_parser.py | 4 ++-- tests/unit_tests/utils/date_parser_tests.py | 22 +++++++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index c6850070a0a71..b36ab37c9700f 100644 --- a/setup.py +++ b/setup.py @@ -95,7 +95,7 @@ def get_git_sha() -> str: "graphlib-backport", "gunicorn>=20.1.0; sys_platform != 'win32'", "hashids>=1.3.1, <2", - "holidays>=0.17.2, <0.18", + "holidays>=0.22, <0.23", "humanize", "isodate", "markdown>=3.0", diff --git a/superset/utils/date_parser.py b/superset/utils/date_parser.py index d23cd01de8664..7cdc23784a737 100644 --- a/superset/utils/date_parser.py +++ b/superset/utils/date_parser.py @@ -386,8 +386,8 @@ def eval(self) -> datetime: country = country.eval() if country else "US" holiday_lookup = country_holidays(country, years=[holiday_year], observed=False) - searched_result = holiday_lookup.get_named(holiday) - if len(searched_result) == 1: + searched_result = holiday_lookup.get_named(holiday, lookup="istartswith") + if len(searched_result) > 0: return dttm_from_timetuple(searched_result[0].timetuple()) raise ValueError( _("Unable to find such a holiday: [%(holiday)s]", holiday=holiday) diff --git a/tests/unit_tests/utils/date_parser_tests.py b/tests/unit_tests/utils/date_parser_tests.py index f3c8b6968077b..fb0fe07d29902 100644 --- a/tests/unit_tests/utils/date_parser_tests.py +++ b/tests/unit_tests/utils/date_parser_tests.py @@ -60,10 +60,14 @@ def mock_parse_human_datetime(s: str) -> Optional[datetime]: return datetime(2017, 4, 7) elif s in ["5 days", "5 days ago"]: return datetime(2016, 11, 2) + elif s == "2000-01-01T00:00:00": + return datetime(2000, 1, 1) elif s == "2018-01-01T00:00:00": return datetime(2018, 1, 1) elif s == "2018-12-31T23:59:59": return datetime(2018, 12, 31, 23, 59, 59) + elif s == "2022-01-01T00:00:00": + return datetime(2022, 1, 1) else: return None @@ -260,12 +264,30 @@ def test_datetime_eval() -> None: expected = datetime(2018, 9, 3, 0, 0, 0) assert result == expected + result = datetime_eval( + "holiday('Eid al-Fitr', datetime('2000-01-01T00:00:00'), 'SA')" + ) + expected = datetime(2000, 1, 8, 0, 0, 0) + assert result == expected + result = datetime_eval( "holiday('Boxing day', datetime('2018-01-01T00:00:00'), 'UK')" ) expected = datetime(2018, 12, 26, 0, 0, 0) assert result == expected + result = datetime_eval( + "holiday('Juneteenth', datetime('2022-01-01T00:00:00'), 'US')" + ) + expected = datetime(2022, 6, 19, 0, 0, 0) + assert result == expected + + result = datetime_eval( + "holiday('Independence Day', datetime('2022-01-01T00:00:00'), 'US')" + ) + expected = datetime(2022, 7, 4, 0, 0, 0) + assert result == expected + result = datetime_eval( "lastday(dateadd(datetime('2018-01-01T00:00:00'), 1, month), month)" ) From d9b7c17c2d97b3f711954aafe7162e20666e20af Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Fri, 21 Apr 2023 10:04:12 -0700 Subject: [PATCH 2/4] Update dependencies. --- requirements/base.txt | 35 +++++++++++++++++++++++++++++++++-- setup.py | 2 +- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 3a5ec607fe9f3..7a1d9453a49d3 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -64,6 +64,8 @@ cryptography==39.0.1 # via # apache-superset # paramiko +deprecated==1.2.13 + # via limits deprecation==2.1.0 # via apache-superset dnspython==2.1.0 @@ -78,6 +80,7 @@ flask==2.1.3 # flask-caching # flask-compress # flask-jwt-extended + # flask-limiter # flask-login # flask-migrate # flask-sqlalchemy @@ -92,6 +95,8 @@ flask-compress==1.13 # via apache-superset flask-jwt-extended==4.3.1 # via flask-appbuilder +flask-limiter==3.3.0 + # via flask-appbuilder flask-login==0.6.0 # via # apache-superset @@ -122,12 +127,16 @@ hashids==1.3.1 # via apache-superset hijri-converter==2.2.4 # via holidays -holidays==0.17.2 +holidays==0.23 # via apache-superset humanize==3.11.0 # via apache-superset idna==3.2 # via email-validator +importlib-metadata==6.5.0 + # via flask +importlib-resources==5.12.0 + # via limits isodate==0.6.0 # via apache-superset itsdangerous==2.1.1 @@ -144,10 +153,14 @@ kombu==5.2.4 # via celery korean-lunar-calendar==0.2.1 # via holidays +limits==3.4.0 + # via flask-limiter mako==1.1.4 # via alembic markdown==3.3.4 # via apache-superset +markdown-it-py==2.2.0 + # via rich markupsafe==2.1.1 # via # jinja2 @@ -162,6 +175,8 @@ marshmallow-enum==1.5.1 # via flask-appbuilder marshmallow-sqlalchemy==0.23.1 # via flask-appbuilder +mdurl==0.1.2 + # via markdown-it-py msgpack==1.0.2 # via apache-superset numpy==1.23.5 @@ -169,10 +184,13 @@ numpy==1.23.5 # apache-superset # pandas # pyarrow +ordered-set==4.1.0 + # via flask-limiter packaging==21.3 # via # bleach # deprecation + # limits pandas==1.5.3 # via apache-superset paramiko==2.11.0 @@ -191,6 +209,8 @@ pyarrow==10.0.1 # via apache-superset pycparser==2.20 # via cffi +pygments==2.15.1 + # via rich pyjwt==2.4.0 # via # apache-superset @@ -232,6 +252,8 @@ pyyaml==5.4.1 # apispec redis==3.5.3 # via apache-superset +rich==13.3.4 + # via flask-limiter selenium==3.141.0 # via apache-superset simplejson==3.17.3 @@ -269,7 +291,10 @@ sshtunnel==0.4.0 tabulate==0.8.9 # via apache-superset typing-extensions==4.4.0 - # via apache-superset + # via + # apache-superset + # flask-limiter + # limits urllib3==1.26.6 # via selenium vine==5.0.0 @@ -286,6 +311,8 @@ werkzeug==2.1.2 # flask # flask-jwt-extended # flask-login +wrapt==1.15.0 + # via deprecated wtforms==2.3.3 # via # apache-superset @@ -296,6 +323,10 @@ wtforms-json==0.3.3 # via apache-superset xlsxwriter==3.0.7 # via apache-superset +zipp==3.15.0 + # via + # importlib-metadata + # importlib-resources # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/setup.py b/setup.py index b36ab37c9700f..0b21bca1dd958 100644 --- a/setup.py +++ b/setup.py @@ -95,7 +95,7 @@ def get_git_sha() -> str: "graphlib-backport", "gunicorn>=20.1.0; sys_platform != 'win32'", "hashids>=1.3.1, <2", - "holidays>=0.22, <0.23", + "holidays>=0.23, <0.24", "humanize", "isodate", "markdown>=3.0", From 8b1e3a85e4e620e0afe082819832f00eb993a12f Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Fri, 21 Apr 2023 10:11:59 -0700 Subject: [PATCH 3/4] Sync with the master. --- .github/workflows/cancel_duplicates.yml | 9 +- .../workflows/check_db_migration_confict.yml | 7 +- .github/workflows/chromatic-master.yml | 19 +- .github/workflows/docker-ephemeral-env.yml | 22 +- .github/workflows/docker-release.yml | 18 +- .github/workflows/docker.yml | 19 +- .github/workflows/embedded-sdk-release.yml | 17 +- .github/workflows/embedded-sdk-test.yml | 2 +- .github/workflows/ephemeral-env-pr-close.yml | 19 +- .github/workflows/ephemeral-env.yml | 32 +- .github/workflows/latest-release-tag.yml | 6 +- .github/workflows/license-check.yml | 17 +- .github/workflows/pr-lint.yml | 7 +- .github/workflows/prefer-typescript.yml | 9 +- .github/workflows/release.yml | 37 +- .../workflows/superset-applitool-cypress.yml | 17 +- .../superset-applitools-storybook.yml | 15 + .github/workflows/superset-cli.yml | 2 +- .github/workflows/superset-docs.yml | 17 +- .github/workflows/superset-e2e.yml | 9 +- .github/workflows/superset-frontend.yml | 2 +- .github/workflows/superset-helm-lint.yml | 2 +- .github/workflows/superset-helm-release.yml | 7 +- .../superset-python-integrationtest.yml | 6 +- .github/workflows/superset-python-misc.yml | 6 +- .../workflows/superset-python-presto-hive.yml | 4 +- .../workflows/superset-python-unittest.yml | 2 +- .github/workflows/superset-translations.yml | 4 +- .github/workflows/superset-websocket.yml | 2 +- .github/workflows/welcome-new-users.yml | 2 +- CONTRIBUTING.md | 5 + Dockerfile | 68 +- RESOURCES/FEATURE_FLAGS.md | 17 +- UPDATING.md | 3 + docs/docs/contributing/translations.mdx | 2 +- docs/docs/frequently-asked-questions.mdx | 4 +- .../miscellaneous/native-filter-migration.mdx | 103 ++ helm/superset/Chart.lock | 6 +- helm/superset/Chart.yaml | 4 +- helm/superset/README.md | 4 +- helm/superset/templates/NOTES.txt | 4 +- helm/superset/templates/_helpers.tpl | 45 +- .../templates/configmap-superset.yaml | 8 +- helm/superset/templates/deployment-beat.yaml | 75 +- .../superset/templates/deployment-flower.yaml | 76 +- .../superset/templates/deployment-worker.yaml | 95 +- helm/superset/templates/deployment-ws.yaml | 68 +- helm/superset/templates/deployment.yaml | 96 +- helm/superset/templates/ingress.yaml | 21 +- helm/superset/templates/init-job.yaml | 48 +- helm/superset/templates/secret-env.yaml | 2 +- .../templates/secret-superset-config.yaml | 18 +- helm/superset/templates/secret-ws.yaml | 6 +- helm/superset/templates/service-account.yaml | 2 +- helm/superset/templates/service.yaml | 19 +- helm/superset/values.yaml | 51 +- requirements/base.txt | 21 +- requirements/development.txt | 2 - requirements/integration.txt | 8 +- setup.py | 2 + .../cypress/integration/dashboard/utils.ts | 4 - .../integration/dataset/dataset_list.test.ts | 2 +- .../cypress/integration/explore/utils.ts | 7 +- .../explore/visualizations/table.test.ts | 4 +- superset-frontend/package-lock.json | 12 +- .../src/fixtures.ts | 2 +- .../src/shared-controls/customControls.tsx | 30 +- .../superset-ui-chart-controls/src/types.ts | 2 +- .../test/utils/columnChoices.test.tsx | 2 +- .../test/utils/defineSavedMetrics.test.tsx | 2 +- .../src/components/SafeMarkdown.tsx | 7 +- .../src/utils/featureFlags.ts | 4 - .../test/utils/featureFlag.test.ts | 10 +- .../legacy-preset-chart-nvd3/src/NVD3Vis.js | 8 +- .../src/MixedTimeseries/transformProps.ts | 4 +- .../src/Timeseries/transformProps.ts | 11 +- .../src/Timeseries/types.ts | 2 + .../plugin-chart-echarts/src/utils/series.ts | 35 +- .../test/utils/series.test.ts | 282 ++++-- .../src/PivotTableChart.tsx | 19 +- .../src/react-pivottable/TableRenderers.jsx | 18 +- .../src/SqlLab/actions/sqlLab.js | 10 +- .../components/AceEditorWrapper/index.tsx | 19 +- .../src/SqlLab/components/App/index.jsx | 3 +- .../QueryAutoRefresh.test.tsx | 127 ++- .../components/QueryAutoRefresh/index.tsx | 46 +- .../components/SaveDatasetModal/index.tsx | 8 + .../src/SqlLab/components/SaveQuery/index.tsx | 1 - .../components/SqlEditor/SqlEditor.test.jsx | 58 +- .../src/SqlLab/components/SqlEditor/index.jsx | 5 +- .../components/SqlEditorLeftBar/index.tsx | 11 - superset-frontend/src/SqlLab/fixtures.ts | 1 - .../src/SqlLab/reducers/sqlLab.js | 27 +- superset-frontend/src/SqlLab/types.ts | 1 - .../src/components/AsyncAceEditor/index.tsx | 3 + .../ChartContextMenu/ChartContextMenu.tsx | 3 +- .../components/Chart/DrillBy/DrillByChart.tsx | 1 + .../Chart/DrillBy/DrillByMenuItems.test.tsx | 9 +- .../Chart/DrillBy/DrillByMenuItems.tsx | 55 +- .../Chart/DrillBy/DrillByModal.test.tsx | 79 +- .../components/Chart/DrillBy/DrillByModal.tsx | 316 ++++-- .../Chart/DrillBy/useDisplayModeToggle.tsx | 64 ++ .../DrillBy/useDrillByBreadcrumbs.test.ts | 72 ++ .../Chart/DrillBy/useDrillByBreadcrumbs.tsx | 93 ++ .../Chart/DrillBy/useResultsTableView.test.ts | 108 ++ .../Chart/DrillBy/useResultsTableView.tsx | 73 ++ .../Datasource/ChangeDatasourceModal.test.jsx | 4 +- .../Datasource/ChangeDatasourceModal.tsx | 6 +- .../Datasource/DatasourceModal.test.jsx | 12 +- .../components/Datasource/DatasourceModal.tsx | 98 +- .../DeprecatedSelect/DeprecatedSelect.tsx | 4 +- .../TableSelector/TableSelector.test.tsx | 47 - .../src/components/TableSelector/index.tsx | 10 - superset-frontend/src/components/index.ts | 1 + .../components/SliceHeader/index.tsx | 2 +- .../FiltersConfigForm/FiltersConfigForm.tsx | 2 +- superset-frontend/src/dashboard/constants.ts | 2 +- .../actions/datasourcesActions.test.ts | 4 +- .../src/explore/actions/saveModalActions.js | 37 - .../explore/actions/saveModalActions.test.js | 68 -- .../test/DataTablesPane.test.tsx | 2 +- .../src/explore/components/SaveModal.test.jsx | 24 +- .../src/explore/components/SaveModal.tsx | 294 +++--- .../DatasourceControl.test.tsx | 23 +- .../ColumnSelectPopover.tsx | 15 +- .../DndFilterSelect.tsx | 2 +- .../DndMetricSelect.tsx | 18 +- .../useResizeButton.tsx | 139 +++ .../AdhocFilterEditPopover/index.jsx | 2 +- .../AdhocMetricEditPopover/index.jsx | 2 +- superset-frontend/src/explore/constants.ts | 2 - .../controlUtils/controlUtils.test.tsx | 2 +- ...trolValuesCompatibleWithDatasource.test.ts | 2 +- superset-frontend/src/explore/fixtures.tsx | 4 +- .../databases/DatabaseModal/ExtraOptions.tsx | 4 +- .../databases/DatabaseModal/index.tsx | 5 +- superset-frontend/src/utils/common.js | 10 +- superset-frontend/src/utils/common.test.jsx | 24 +- .../src/utils/getDatasourceUid.test.ts | 2 +- .../src/utils/hostNamesConfig.js | 4 +- superset-websocket/package-lock.json | 178 ++-- superset-websocket/package.json | 6 +- superset/advanced_data_type/schemas.py | 6 +- .../annotation_layers/annotations/schemas.py | 28 +- superset/annotation_layers/schemas.py | 16 +- superset/cachekeys/schemas.py | 12 +- superset/charts/data/api.py | 5 +- superset/charts/schemas.py | 929 +++++++++++------- superset/cli/main.py | 5 +- superset/cli/native_filters.py | 398 ++++++++ superset/common/query_context_processor.py | 2 +- superset/config.py | 13 +- superset/connectors/base/models.py | 44 +- superset/connectors/sqla/models.py | 14 +- superset/dashboards/permalink/schemas.py | 18 +- superset/dashboards/schemas.py | 101 +- superset/databases/api.py | 2 +- superset/databases/schemas.py | 227 +++-- superset/datasets/api.py | 8 + superset/datasets/commands/exceptions.py | 17 + superset/datasets/commands/update.py | 11 + superset/datasets/schemas.py | 38 +- superset/db_engine_specs/base.py | 37 +- superset/db_engine_specs/bigquery.py | 2 +- superset/db_engine_specs/clickhouse.py | 21 +- superset/db_engine_specs/databricks.py | 5 +- superset/db_engine_specs/gsheets.py | 6 +- superset/db_engine_specs/hive.py | 2 + superset/db_engine_specs/mysql.py | 6 +- superset/db_engine_specs/presto.py | 28 +- superset/db_engine_specs/trino.py | 2 + superset/explore/form_data/schemas.py | 20 +- superset/explore/permalink/schemas.py | 14 +- superset/explore/schemas.py | 174 ++-- ..._12-30_7e67aecbf3f1_chart_ds_constraint.py | 79 ++ superset/models/core.py | 5 +- superset/reports/schemas.py | 107 +- superset/security/manager.py | 1 - superset/sql_parse.py | 4 +- superset/sqllab/schemas.py | 14 +- superset/temporary_cache/schemas.py | 4 +- .../dashboard_filter_scopes_converter.py | 256 ++++- superset/views/base_api.py | 18 +- superset/views/core.py | 5 +- superset/views/datasource/schemas.py | 2 +- superset/views/datasource/views.py | 3 + superset/views/log/schemas.py | 23 +- .../charts/data/api_tests.py | 27 + tests/integration_tests/datasets/api_tests.py | 52 +- .../datasets/commands_tests.py | 2 +- ...7e67aecbf3f1_chart_ds_constraint__tests.py | 56 ++ tests/integration_tests/model_tests.py | 15 + tests/integration_tests/utils_tests.py | 7 + .../unit_tests/db_engine_specs/test_mssql.py | 1 + .../unit_tests/db_engine_specs/test_mysql.py | 34 +- tests/unit_tests/sql_parse_tests.py | 22 + 196 files changed, 4740 insertions(+), 2149 deletions(-) create mode 100644 docs/docs/miscellaneous/native-filter-migration.mdx create mode 100644 superset-frontend/src/components/Chart/DrillBy/useDisplayModeToggle.tsx create mode 100644 superset-frontend/src/components/Chart/DrillBy/useDrillByBreadcrumbs.test.ts create mode 100644 superset-frontend/src/components/Chart/DrillBy/useDrillByBreadcrumbs.tsx create mode 100644 superset-frontend/src/components/Chart/DrillBy/useResultsTableView.test.ts create mode 100644 superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx create mode 100644 superset-frontend/src/explore/components/controls/DndColumnSelectControl/useResizeButton.tsx create mode 100644 superset/cli/native_filters.py create mode 100644 superset/migrations/versions/2023-03-27_12-30_7e67aecbf3f1_chart_ds_constraint.py create mode 100644 tests/integration_tests/migrations/7e67aecbf3f1_chart_ds_constraint__tests.py diff --git a/.github/workflows/cancel_duplicates.yml b/.github/workflows/cancel_duplicates.yml index b3457c7e75819..a78ebc07fdbe7 100644 --- a/.github/workflows/cancel_duplicates.yml +++ b/.github/workflows/cancel_duplicates.yml @@ -10,11 +10,14 @@ jobs: cancel-duplicate-runs: name: Cancel duplicate workflow runs runs-on: ubuntu-20.04 + permissions: + actions: write + contents: read steps: - name: Check number of queued tasks id: check_queued env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} GITHUB_REPO: ${{ github.repository }} run: | get_count() { @@ -28,12 +31,12 @@ jobs: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" if: steps.check_queued.outputs.count >= 20 - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Cancel duplicate workflow runs if: steps.check_queued.outputs.count >= 20 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} GITHUB_REPOSITORY: ${{ github.repository }} run: | pip install click requests typing_extensions python-dateutil diff --git a/.github/workflows/check_db_migration_confict.yml b/.github/workflows/check_db_migration_confict.yml index 0f6c26d90a6fd..8dc7ab08824e9 100644 --- a/.github/workflows/check_db_migration_confict.yml +++ b/.github/workflows/check_db_migration_confict.yml @@ -8,13 +8,16 @@ jobs: check_db_migration_conflict: name: Check DB migration conflict runs-on: ubuntu-20.04 + permissions: + contents: read + pull-requests: write steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Check and notify uses: actions/github-script@v3 with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ github.token }} script: | // API reference: https://octokit.github.io/rest.js const currentBranch = context.ref.replace('refs/heads/', ''); diff --git a/.github/workflows/chromatic-master.yml b/.github/workflows/chromatic-master.yml index 6cdf10506f00f..11e07e6c5ab3e 100644 --- a/.github/workflows/chromatic-master.yml +++ b/.github/workflows/chromatic-master.yml @@ -32,12 +32,29 @@ on: # List of jobs jobs: + config: + runs-on: "ubuntu-latest" + outputs: + has-secrets: ${{ steps.check.outputs.has-secrets }} + steps: + - name: "Check for secrets" + id: check + shell: bash + run: | + if [ -n "${{ (secrets.CHROMATIC_PROJECT_TOKEN != '') || '' }}" ]; then + echo "has-secrets=1" >> "$GITHUB_OUTPUT" + fi + chromatic-deployment: + needs: config + if: needs.config.outputs.has-secrets # Operating System runs-on: ubuntu-latest # Job steps steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # 👈 Required to retrieve git history - name: Install dependencies run: npm ci working-directory: superset-frontend diff --git a/.github/workflows/docker-ephemeral-env.yml b/.github/workflows/docker-ephemeral-env.yml index bfa2542687657..544c1c8b1f421 100644 --- a/.github/workflows/docker-ephemeral-env.yml +++ b/.github/workflows/docker-ephemeral-env.yml @@ -7,9 +7,29 @@ on: - completed jobs: + config: + runs-on: "ubuntu-latest" + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' + outputs: + has-secrets: ${{ steps.check.outputs.has-secrets }} + steps: + - name: "Check for secrets" + id: check + shell: bash + run: | + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + if [ -n "${{ (secrets.AWS_ACCESS_KEY_ID != '' && + secrets.AWS_ACCESS_KEY_ID != '' && + secrets.AWS_SECRET_ACCESS_KEY != '' && + secrets.AWS_SECRET_ACCESS_KEY != '') || '' }}" ]; then + echo "has-secrets=1" >> "$GITHUB_OUTPUT" + fi + docker_ephemeral_env: + needs: config + if: needs.config.outputs.has-secrets name: Push ephemeral env Docker image to ECR - if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest steps: diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index d082603be9ba9..cc47c996d30ce 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -4,12 +4,28 @@ on: release: types: [published] jobs: + config: + runs-on: "ubuntu-latest" + if: github.event.pull_request.draft == false + outputs: + has-secrets: ${{ steps.check.outputs.has-secrets }} + steps: + - name: "Check for secrets" + id: check + shell: bash + run: | + if [ -n "${{ (secrets.DOCKERHUB_USER != '' && secrets.DOCKERHUB_TOKEN != '') || '' }}" ]; then + echo "has-secrets=1" >> "$GITHUB_OUTPUT" + fi + docker-release: + needs: config + if: needs.config.outputs.has-secrets name: docker-release runs-on: ubuntu-latest steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index cbbb9a8379877..5bdb74eb92721 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -8,13 +8,28 @@ on: types: [synchronize, opened, reopened, ready_for_review] jobs: - docker-build: + config: + runs-on: "ubuntu-latest" if: github.event.pull_request.draft == false + outputs: + has-secrets: ${{ steps.check.outputs.has-secrets }} + steps: + - name: "Check for secrets" + id: check + shell: bash + run: | + if [ -n "${{ (secrets.DOCKERHUB_USER != '' && secrets.DOCKERHUB_TOKEN != '') || '' }}" ]; then + echo "has-secrets=1" >> "$GITHUB_OUTPUT" + fi + + docker-build: + needs: config + if: needs.config.outputs.has-secrets name: docker-build runs-on: ubuntu-latest steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false diff --git a/.github/workflows/embedded-sdk-release.yml b/.github/workflows/embedded-sdk-release.yml index be130759f94cf..60a2819e89479 100644 --- a/.github/workflows/embedded-sdk-release.yml +++ b/.github/workflows/embedded-sdk-release.yml @@ -6,13 +6,28 @@ on: - 'master' jobs: + config: + runs-on: "ubuntu-latest" + outputs: + has-secrets: ${{ steps.check.outputs.has-secrets }} + steps: + - name: "Check for secrets" + id: check + shell: bash + run: | + if [ -n "${{ (secrets.NPM_TOKEN != '') || '' }}" ]; then + echo "has-secrets=1" >> "$GITHUB_OUTPUT" + fi + build: + needs: config + if: needs.config.outputs.has-secrets runs-on: ubuntu-20.04 defaults: run: working-directory: superset-embedded-sdk steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-node@v2 with: node-version: "16" diff --git a/.github/workflows/embedded-sdk-test.yml b/.github/workflows/embedded-sdk-test.yml index cb312907b781f..f849ae637aae4 100644 --- a/.github/workflows/embedded-sdk-test.yml +++ b/.github/workflows/embedded-sdk-test.yml @@ -14,7 +14,7 @@ jobs: run: working-directory: superset-embedded-sdk steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-node@v2 with: node-version: "16" diff --git a/.github/workflows/ephemeral-env-pr-close.yml b/.github/workflows/ephemeral-env-pr-close.yml index 3c5209fca8c09..7430950b453f6 100644 --- a/.github/workflows/ephemeral-env-pr-close.yml +++ b/.github/workflows/ephemeral-env-pr-close.yml @@ -5,9 +5,26 @@ on: types: [closed] jobs: + config: + runs-on: "ubuntu-latest" + outputs: + has-secrets: ${{ steps.check.outputs.has-secrets }} + steps: + - name: "Check for secrets" + id: check + shell: bash + run: | + if [ -n "${{ (secrets.AWS_ACCESS_KEY_ID != '' && secrets.AWS_SECRET_ACCESS_KEY != '') || '' }}" ]; then + echo "has-secrets=1" >> "$GITHUB_OUTPUT" + fi + ephemeral-env-cleanup: + needs: config + if: needs.config.outputs.has-secrets name: Cleanup ephemeral envs runs-on: ubuntu-latest + permissions: + pull-requests: write steps: - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 @@ -48,7 +65,7 @@ jobs: if: steps.describe-services.outputs.active == 'true' uses: actions/github-script@v3 with: - github-token: ${{secrets.GITHUB_TOKEN}} + github-token: ${{github.token}} script: | github.issues.createComment({ issue_number: ${{ github.event.number }}, diff --git a/.github/workflows/ephemeral-env.yml b/.github/workflows/ephemeral-env.yml index c1945b3c20912..08e3a998c3cfa 100644 --- a/.github/workflows/ephemeral-env.yml +++ b/.github/workflows/ephemeral-env.yml @@ -5,10 +5,27 @@ on: types: [created] jobs: - ephemeral_env_comment: + config: + runs-on: "ubuntu-latest" if: github.event.issue.pull_request + outputs: + has-secrets: ${{ steps.check.outputs.has-secrets }} + steps: + - name: "Check for secrets" + id: check + shell: bash + run: | + if [ -n "${{ (secrets.AWS_ACCESS_KEY_ID != '' && secrets.AWS_SECRET_ACCESS_KEY != '') || '' }}" ]; then + echo "has-secrets=1" >> "$GITHUB_OUTPUT" + fi + + ephemeral_env_comment: + needs: config + if: needs.config.outputs.has-secrets name: Evaluate ephemeral env comment trigger (/testenv) runs-on: ubuntu-latest + permissions: + pull-requests: write outputs: slash-command: ${{ steps.eval-body.outputs.result }} feature-flags: ${{ steps.eval-feature-flags.outputs.result }} @@ -51,7 +68,7 @@ jobs: github.event.comment.author_association != 'OWNER' uses: actions/github-script@v3 with: - github-token: ${{secrets.GITHUB_TOKEN}} + github-token: ${{github.token}} script: | const errMsg = '@${{ github.event.comment.user.login }} Ephemeral environment creation is currently limited to committers.' github.issues.createComment({ @@ -67,9 +84,12 @@ jobs: if: needs.ephemeral_env_comment.outputs.slash-command == 'up' name: Spin up an ephemeral environment runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: persist-credentials: false @@ -97,7 +117,7 @@ jobs: if: steps.check-image.outcome == 'failure' uses: actions/github-script@v3 with: - github-token: ${{secrets.GITHUB_TOKEN}} + github-token: ${{github.token}} script: | const errMsg = '@${{ github.event.comment.user.login }} Container image not yet published for this PR. Please try again when build is complete.' github.issues.createComment({ @@ -171,7 +191,7 @@ jobs: if: ${{ success() }} uses: actions/github-script@v3 with: - github-token: ${{secrets.GITHUB_TOKEN}} + github-token: ${{github.token}} script: | github.issues.createComment({ issue_number: ${{ github.event.issue.number }}, @@ -184,7 +204,7 @@ jobs: if: ${{ failure() }} uses: actions/github-script@v3 with: - github-token: ${{secrets.GITHUB_TOKEN}} + github-token: ${{github.token}} script: | github.issues.createComment({ issue_number: ${{ github.event.issue.number }}, diff --git a/.github/workflows/latest-release-tag.yml b/.github/workflows/latest-release-tag.yml index ae3703af749f5..ccb941178a119 100644 --- a/.github/workflows/latest-release-tag.yml +++ b/.github/workflows/latest-release-tag.yml @@ -7,10 +7,12 @@ jobs: latest-release: name: Add/update tag to new release runs-on: ubuntu-latest + permissions: + contents: write steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive @@ -27,4 +29,4 @@ jobs: description: Superset latest release tag-name: latest env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/license-check.yml b/.github/workflows/license-check.yml index 9ae633bdc4920..8e2b9bbed3096 100644 --- a/.github/workflows/license-check.yml +++ b/.github/workflows/license-check.yml @@ -7,12 +7,27 @@ on: pull_request: jobs: + config: + runs-on: "ubuntu-latest" + outputs: + has-secrets: ${{ steps.check.outputs.has-secrets }} + steps: + - name: "Check for secrets" + id: check + shell: bash + run: | + if [ -n "${{ (secrets.FOSSA_API_KEY != '' ) || '' }}" ]; then + echo "has-secrets=1" >> "$GITHUB_OUTPUT" + fi + license_check: + needs: config + if: needs.config.outputs.has-secrets name: License Check runs-on: ubuntu-20.04 steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 5f91522cfa449..5283e138c1a0a 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -11,9 +11,12 @@ on: jobs: check: runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive @@ -25,4 +28,4 @@ jobs: on-failed-regex-create-review: false on-failed-regex-comment: "Please format your PR title to match: `%regex%`!" - repo-token: "${{ secrets.GITHUB_TOKEN }}" + repo-token: "${{ github.token }}" diff --git a/.github/workflows/prefer-typescript.yml b/.github/workflows/prefer-typescript.yml index 8005cf36a3555..49ab90fb6f92a 100644 --- a/.github/workflows/prefer-typescript.yml +++ b/.github/workflows/prefer-typescript.yml @@ -11,9 +11,12 @@ jobs: if: github.ref == 'ref/heads/master' && github.event_name == 'pull_request' name: Prefer Typescript runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive @@ -21,7 +24,7 @@ jobs: id: changed uses: ./.github/actions/file-changes-action with: - githubToken: ${{ secrets.GITHUB_TOKEN }} + githubToken: ${{ github.token }} - name: Determine if a .js or .jsx file was added id: check @@ -42,7 +45,7 @@ jobs: uses: ./.github/actions/comment-on-pr continue-on-error: true env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} with: msg: | ### WARNING: Prefer TypeScript diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d716fc2d4206..ea3a75a42e19d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,22 @@ on: - 'master' jobs: + config: + runs-on: "ubuntu-latest" + outputs: + has-secrets: ${{ steps.check.outputs.has-secrets }} + steps: + - name: "Check for secrets" + id: check + shell: bash + run: | + if [ -n "${{ (secrets.NPM_TOKEN != '' && secrets.GH_PERSONAL_ACCESS_TOKEN != '') || '' }}" ]; then + echo "has-secrets=1" >> "$GITHUB_OUTPUT" + fi + build: + needs: config + if: needs.config.outputs.has-secrets name: Bump version and publish package(s) runs-on: ubuntu-20.04 @@ -16,22 +31,28 @@ jobs: node-version: [16] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: # pulls all commits (needed for lerna / semantic release to correctly version) fetch-depth: 0 - name: Get tags and filter trigger tags run: | - git fetch --depth=1 origin "+refs/tags/*:refs/tags/*" + if ! git fetch --depth=1 origin "+refs/tags/*:refs/tags/*"; then + echo "::notice title=Workflow skipped::No tags present in repository" + exit + fi + echo "HAS_TAGS=1" >> $GITHUB_ENV" git fetch --prune --unshallow git tag -d `git tag | grep -E '^trigger-'` - name: Use Node.js ${{ matrix.node-version }} + if: env.HAS_TAGS uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - name: Cache npm + if: env.HAS_TAGS uses: actions/cache@v1 with: path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS @@ -41,9 +62,11 @@ jobs: ${{ runner.OS }}- - name: Get npm cache directory path + if: env.HAS_TAGS id: npm-cache-dir-path run: echo "::set-output name=dir::$(npm config get cache)" - name: Cache npm + if: env.HAS_TAGS uses: actions/cache@v1 id: npm-cache # use this to check for `cache-hit` (`steps.npm-cache.outputs.cache-hit != 'true'`) with: @@ -53,16 +76,20 @@ jobs: ${{ runner.os }}-npm- - name: Install dependencies + if: env.HAS_TAGS working-directory: ./superset-frontend run: npm ci - name: Run unit tests + if: env.HAS_TAGS working-directory: ./superset-frontend run: npm run test -- plugins packages - name: Build packages + if: env.HAS_TAGS working-directory: ./superset-frontend run: npm run plugins:build - name: Configure npm and git + if: env.HAS_TAGS run: | echo "@superset-ui:registry=https://registry.npmjs.org/" > .npmrc echo "registry=https://registry.npmjs.org/" >> .npmrc @@ -70,17 +97,17 @@ jobs: npm whoami git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - git remote set-url origin "https://${GITHUB_TOKEN}@github.com/apache-superset/superset-ui.git" > /dev/null 2>&1 env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} - name: Bump version and publish package(s) + if: env.HAS_TAGS working-directory: ./superset-frontend run: | git tag -d `git tag | grep -E '^trigger-'` npm run plugins:release-from-tag env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} GH_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} diff --git a/.github/workflows/superset-applitool-cypress.yml b/.github/workflows/superset-applitool-cypress.yml index 47fc1a24e4c26..8485dfb2012e2 100644 --- a/.github/workflows/superset-applitool-cypress.yml +++ b/.github/workflows/superset-applitool-cypress.yml @@ -5,7 +5,22 @@ on: - cron: "0 1 * * *" jobs: + config: + runs-on: "ubuntu-latest" + outputs: + has-secrets: ${{ steps.check.outputs.has-secrets }} + steps: + - name: "Check for secrets" + id: check + shell: bash + run: | + if [ -n "${{ (secrets.APPLITOOLS_API_KEY != '' && secrets.APPLITOOLS_API_KEY != '') || '' }}" ]; then + echo "has-secrets=1" >> "$GITHUB_OUTPUT" + fi + cypress-applitools: + needs: config + if: needs.config.outputs.has-secrets runs-on: ubuntu-20.04 strategy: fail-fast: false @@ -18,7 +33,7 @@ jobs: SUPERSET__SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://superset:superset@127.0.0.1:15432/superset PYTHONPATH: ${{ github.workspace }} REDIS_PORT: 16379 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} APPLITOOLS_APP_NAME: Superset APPLITOOLS_API_KEY: ${{ secrets.APPLITOOLS_API_KEY }} APPLITOOLS_BATCH_ID: ${{ github.sha }} diff --git a/.github/workflows/superset-applitools-storybook.yml b/.github/workflows/superset-applitools-storybook.yml index 5e50c6fd8821a..4225509e3a1a0 100644 --- a/.github/workflows/superset-applitools-storybook.yml +++ b/.github/workflows/superset-applitools-storybook.yml @@ -11,7 +11,22 @@ env: APPLITOOLS_BATCH_NAME: Superset Storybook jobs: + config: + runs-on: "ubuntu-latest" + outputs: + has-secrets: ${{ steps.check.outputs.has-secrets }} + steps: + - name: "Check for secrets" + id: check + shell: bash + run: | + if [ -n "${{ (secrets.APPLITOOLS_API_KEY != '' && secrets.APPLITOOLS_API_KEY != '') || '' }}" ]; then + echo "has-secrets=1" >> "$GITHUB_OUTPUT" + fi + cron: + needs: config + if: needs.config.outputs.has-secrets runs-on: ubuntu-20.04 strategy: matrix: diff --git a/.github/workflows/superset-cli.yml b/.github/workflows/superset-cli.yml index 65ec8b018f217..14810f33177ce 100644 --- a/.github/workflows/superset-cli.yml +++ b/.github/workflows/superset-cli.yml @@ -35,7 +35,7 @@ jobs: - 16379:6379 steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/superset-docs.yml b/.github/workflows/superset-docs.yml index f1cc08f9f1001..6a47df4400dd8 100644 --- a/.github/workflows/superset-docs.yml +++ b/.github/workflows/superset-docs.yml @@ -9,7 +9,22 @@ on: - "docs/**" jobs: + config: + runs-on: "ubuntu-latest" + outputs: + has-secrets: ${{ steps.check.outputs.has-secrets }} + steps: + - name: "Check for secrets" + id: check + shell: bash + run: | + if [ -n "${{ (secrets.SUPERSET_SITE_BUILD != '' && secrets.SUPERSET_SITE_BUILD != '') || '' }}" ]; then + echo "has-secrets=1" >> "$GITHUB_OUTPUT" + fi + build-deploy: + needs: config + if: needs.config.outputs.has-secrets name: Build & Deploy runs-on: ubuntu-20.04 defaults: @@ -17,7 +32,7 @@ jobs: working-directory: docs steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/superset-e2e.yml b/.github/workflows/superset-e2e.yml index ab82731ac481b..b49622c85fe45 100644 --- a/.github/workflows/superset-e2e.yml +++ b/.github/workflows/superset-e2e.yml @@ -13,6 +13,9 @@ jobs: cypress-matrix: if: github.event.pull_request.draft == false runs-on: ubuntu-20.04 + permissions: + contents: read + pull-requests: read strategy: # when one test fails, DO NOT cancel the other # containers, because this will kill Cypress processes @@ -28,7 +31,7 @@ jobs: SUPERSET__SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://superset:superset@127.0.0.1:15432/superset PYTHONPATH: ${{ github.workspace }} REDIS_PORT: 16379 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} services: postgres: image: postgres:14-alpine @@ -43,13 +46,13 @@ jobs: - 16379:6379 steps: - name: "Checkout (pull) ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 if: github.event_name == 'push' with: persist-credentials: false submodules: recursive - name: "Checkout (pull_request) ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target' with: ref: "refs/pull/${{ github.event.number }}/merge" diff --git a/.github/workflows/superset-frontend.yml b/.github/workflows/superset-frontend.yml index bf09d293c6e3a..63b16cd3af114 100644 --- a/.github/workflows/superset-frontend.yml +++ b/.github/workflows/superset-frontend.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/superset-helm-lint.yml b/.github/workflows/superset-helm-lint.yml index d0e650839f9a4..5f8051c22a854 100644 --- a/.github/workflows/superset-helm-lint.yml +++ b/.github/workflows/superset-helm-lint.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/superset-helm-release.yml b/.github/workflows/superset-helm-release.yml index 1559432eb247a..e75186609c4dc 100644 --- a/.github/workflows/superset-helm-release.yml +++ b/.github/workflows/superset-helm-release.yml @@ -10,9 +10,12 @@ on: jobs: release: runs-on: ubuntu-latest + permissions: + contents: write + steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive @@ -36,5 +39,5 @@ jobs: with: charts_dir: helm env: - CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + CR_TOKEN: "${{ github.token }}" CR_RELEASE_NAME_TEMPLATE: "superset-helm-chart-{{ .Version }}" diff --git a/.github/workflows/superset-python-integrationtest.yml b/.github/workflows/superset-python-integrationtest.yml index eae19b234cf34..aa61a1c9ac086 100644 --- a/.github/workflows/superset-python-integrationtest.yml +++ b/.github/workflows/superset-python-integrationtest.yml @@ -35,7 +35,7 @@ jobs: - 16379:6379 steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive @@ -102,7 +102,7 @@ jobs: - 16379:6379 steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive @@ -161,7 +161,7 @@ jobs: - 16379:6379 steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/superset-python-misc.yml b/.github/workflows/superset-python-misc.yml index 739869a7bb915..dfd6ce7b72d5b 100644 --- a/.github/workflows/superset-python-misc.yml +++ b/.github/workflows/superset-python-misc.yml @@ -17,7 +17,7 @@ jobs: python-version: [3.8] steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive @@ -57,7 +57,7 @@ jobs: python-version: [3.8] steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive @@ -98,7 +98,7 @@ jobs: python-version: [3.8] steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/superset-python-presto-hive.yml b/.github/workflows/superset-python-presto-hive.yml index 875901b1ec6d7..6798842c1e66a 100644 --- a/.github/workflows/superset-python-presto-hive.yml +++ b/.github/workflows/superset-python-presto-hive.yml @@ -46,7 +46,7 @@ jobs: - 16379:6379 steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive @@ -115,7 +115,7 @@ jobs: - 16379:6379 steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/superset-python-unittest.yml b/.github/workflows/superset-python-unittest.yml index 8c94d0f4582cd..1ba1ee258955c 100644 --- a/.github/workflows/superset-python-unittest.yml +++ b/.github/workflows/superset-python-unittest.yml @@ -19,7 +19,7 @@ jobs: PYTHONPATH: ${{ github.workspace }} steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/superset-translations.yml b/.github/workflows/superset-translations.yml index 6b2a6aa3c6f77..9f735518fda53 100644 --- a/.github/workflows/superset-translations.yml +++ b/.github/workflows/superset-translations.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive @@ -38,7 +38,7 @@ jobs: python-version: [3.8] steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/superset-websocket.yml b/.github/workflows/superset-websocket.yml index 2f4b0aea04a06..770ec54df429f 100644 --- a/.github/workflows/superset-websocket.yml +++ b/.github/workflows/superset-websocket.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false - name: Install dependencies diff --git a/.github/workflows/welcome-new-users.yml b/.github/workflows/welcome-new-users.yml index ae16bf49c6490..0144e20892d44 100644 --- a/.github/workflows/welcome-new-users.yml +++ b/.github/workflows/welcome-new-users.yml @@ -15,7 +15,7 @@ jobs: uses: actions/first-interaction@v1 continue-on-error: true with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + repo-token: ${{ github.token }} pr-message: |- Congrats on making your first PR and thank you for contributing to Superset! :tada: :heart: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b5d42fb1954bc..1c13006aa5bbb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -547,6 +547,11 @@ We recommend using [nvm](https://github.com/nvm-sh/nvm) to manage your node envi ```bash curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.0/install.sh | bash +incase it shows '-bash: nvm: command not found' +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm +[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion + cd superset-frontend nvm install --lts nvm use --lts diff --git a/Dockerfile b/Dockerfile index 9089fc8f28035..aa83ff5fdb9dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,52 +58,48 @@ ENV LANG=C.UTF-8 \ SUPERSET_PORT=8088 RUN mkdir -p ${PYTHONPATH} \ - && useradd --user-group -d ${SUPERSET_HOME} -m --no-log-init --shell /bin/bash superset \ - && apt-get update -y \ - && apt-get install -y --no-install-recommends \ - build-essential \ - curl \ - default-libmysqlclient-dev \ - libsasl2-dev \ - libsasl2-modules-gssapi-mit \ - libpq-dev \ - libecpg-dev \ - && rm -rf /var/lib/apt/lists/* - -COPY ./requirements/*.txt /app/requirements/ -COPY setup.py MANIFEST.in README.md /app/ + && useradd --user-group -d ${SUPERSET_HOME} -m --no-log-init --shell /bin/bash superset \ + && apt-get update -y \ + && apt-get install -y --no-install-recommends \ + build-essential \ + curl \ + default-libmysqlclient-dev \ + libsasl2-dev \ + libsasl2-modules-gssapi-mit \ + libpq-dev \ + libecpg-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY --chown=superset:superset ./requirements/*.txt requirements/ +COPY --chown=superset:superset setup.py MANIFEST.in README.md ./ # setup.py uses the version information in package.json -COPY superset-frontend/package.json /app/superset-frontend/ +COPY --chown=superset:superset superset-frontend/package.json superset-frontend/ -RUN cd /app \ - && mkdir -p superset/static \ +RUN mkdir -p superset/static \ && touch superset/static/version_info.json \ - && pip install --no-cache -r requirements/local.txt + && pip install --no-cache-dir -r requirements/local.txt -COPY --from=superset-node /app/superset/static/assets /app/superset/static/assets +COPY --chown=superset:superset --from=superset-node /app/superset/static/assets superset/static/assets ## Lastly, let's install superset itself -COPY superset /app/superset -COPY setup.py MANIFEST.in README.md /app/ -RUN cd /app \ - && chown -R superset:superset * \ - && pip install -e . \ - && flask fab babel-compile --target superset/translations +COPY --chown=superset:superset superset superset +RUN chown -R superset:superset ./* \ + && pip install --no-cache-dir -e . \ + && flask fab babel-compile --target superset/translations COPY ./docker/run-server.sh /usr/bin/ - RUN chmod a+x /usr/bin/run-server.sh -WORKDIR /app - USER superset HEALTHCHECK CMD curl -f "http://localhost:$SUPERSET_PORT/health" EXPOSE ${SUPERSET_PORT} -CMD /usr/bin/run-server.sh +CMD ["/usr/bin/run-server.sh"] ###################################################################### # Dev image... @@ -118,13 +114,13 @@ USER root RUN apt-get update -y \ && apt-get install -y --no-install-recommends \ - libnss3 \ - libdbus-glib-1-2 \ - libgtk-3-0 \ - libx11-xcb1 \ - libasound2 \ - libxtst6 \ - wget + libnss3 \ + libdbus-glib-1-2 \ + libgtk-3-0 \ + libx11-xcb1 \ + libasound2 \ + libxtst6 \ + wget # Install GeckoDriver WebDriver RUN wget https://github.com/mozilla/geckodriver/releases/download/${GECKODRIVER_VERSION}/geckodriver-${GECKODRIVER_VERSION}-linux64.tar.gz -O /tmp/geckodriver.tar.gz && \ diff --git a/RESOURCES/FEATURE_FLAGS.md b/RESOURCES/FEATURE_FLAGS.md index 3374669870bce..49fac479cb922 100644 --- a/RESOURCES/FEATURE_FLAGS.md +++ b/RESOURCES/FEATURE_FLAGS.md @@ -31,11 +31,10 @@ These features are considered **unfinished** and should only be used on developm - DASHBOARD_CACHE - DASHBOARD_NATIVE_FILTERS_SET - DISABLE_DATASET_SOURCE_EDIT -- DRILL_TO_DETAIL +- DRILL_BY - ENABLE_ADVANCED_DATA_TYPES - ENABLE_EXPLORE_JSON_CSRF_PROTECTION - ENABLE_TEMPLATE_REMOVE_FILTERS -- HORIZONTAL_FILTER_BAR - KV_STORE - PRESTO_EXPAND_DATA - REMOVE_SLICE_LEVEL_LABEL_COLORS @@ -54,20 +53,22 @@ These features are **finished** but currently being tested. They are usable, but - CONFIRM_DASHBOARD_DIFF - DASHBOARD_EDIT_CHART_IN_NEW_TAB - DASHBOARD_FILTERS_EXPERIMENTAL -- DASHBOARD_NATIVE_FILTERS +- DASHBOARD_VIRTUALIZATION +- DRILL_TO_DETAIL - DYNAMIC_PLUGINS: [(docs)](https://superset.apache.org/docs/installation/running-on-kubernetes) - ENABLE_JAVASCRIPT_CONTROLS +- ESTIMATE_QUERY_COST - GENERIC_CHART_AXES - GLOBAL_ASYNC_QUERIES [(docs)](https://github.com/apache/superset/blob/master/CONTRIBUTING.md#async-chart-queries) +- HORIZONTAL_FILTER_BAR - RLS_IN_SQLLAB - SSH_TUNNELING [(docs)](https://superset.apache.org/docs/installation/setup-ssh-tunneling) - USE_ANALAGOUS_COLORS -- UX_BETA - VERSIONED_EXPORT ## Stable -These features flags are **safe for production** and have been tested. +These features flags are **safe for production**. They have been tested and will be supported for the foreseeable future. [//]: # "PLEASE KEEP THE LIST SORTED ALPHABETICALLY" @@ -75,6 +76,8 @@ These features flags are **safe for production** and have been tested. - ALLOW_ADHOC_SUBQUERY - DASHBOARD_CROSS_FILTERS - DASHBOARD_RBAC [(docs)](https://superset.apache.org/docs/creating-charts-dashboards/first-dashboard#manage-access-to-dashboards) +- DASHBOARD_NATIVE_FILTERS +- DATAPANEL_CLOSED_BY_DEFAULT - DISABLE_LEGACY_DATASOURCE_EDITOR - DRUID_JOINS - EMBEDDABLE_CHARTS @@ -95,6 +98,4 @@ These features flags currently default to True and **will be removed in a future [//]: # "PLEASE KEEP THE LIST SORTED ALPHABETICALLY" -- ALLOW_DASHBOARD_DOMAIN_SHARDING -- DISPLAY_MARKDOWN_HTML -- FORCE_DATABASE_CONNECTIONS_SSL +- GENERIC_CHART_AXES diff --git a/UPDATING.md b/UPDATING.md index 3496742bee131..9afd8d353fd10 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -24,6 +24,7 @@ assists people when migrating to a new version. ## Next +- [23652](https://github.com/apache/superset/pull/23652) Enables GENERIC_CHART_AXES feature flag by default. - [23226](https://github.com/apache/superset/pull/23226) Migrated endpoint `/estimate_query_cost/` to `/api/v1/sqllab/estimate/`. Corresponding permissions are can estimate query cost on SQLLab. Make sure you add/replace the necessary permissions on any custom roles you may have. - [22809](https://github.com/apache/superset/pull/22809): Migrated endpoint `/superset/sql_json` and `/superset/results/` to `/api/v1/sqllab/execute/` and `/api/v1/sqllab/results/` respectively. Corresponding permissions are `can sql_json on Superset` to `can execute on SQLLab`, `can results on Superset` to `can results on SQLLab`. Make sure you add/replace the necessary permissions on any custom roles you may have. - [22931](https://github.com/apache/superset/pull/22931): Migrated endpoint `/superset/get_or_create_table/` to `/api/v1/dataset/get_or_create/`. Corresponding permissions are `can get or create table on Superset` to `can get or create dataset on Dataset`. Make sure you add/replace the necessary permissions on any custom roles you may have. @@ -47,6 +48,8 @@ assists people when migrating to a new version. ### Breaking Changes +- [23651](https://github.com/apache/superset/pull/23651) Removes UX_BETA feature flag. +- [23663](https://github.com/apache/superset/pull/23663) Removes deprecated feature flags `ALLOW_DASHBOARD_DOMAIN_SHARDING`, `DISPLAY_MARKDOWN_HTML`, and `FORCE_DATABASE_CONNECTIONS_SSL`. - [22798](https://github.com/apache/superset/pull/22798): To make the welcome page more relevant in production environments, the last tab on the welcome page has been changed from to feature all charts/dashboards the user has access to (previously only examples were shown). To keep current behavior unchanged, add the following to your `superset_config.py`: `WELCOME_PAGE_LAST_TAB = "examples"` - [22328](https://github.com/apache/superset/pull/22328): For deployments that have enabled the "THUMBNAILS" feature flag, the function that calculates dashboard digests has been updated to consider additional properties to more accurately identify changes in the dashboard metadata. This change will invalidate all currently cached dashboard thumbnails. - [21765](https://github.com/apache/superset/pull/21765): For deployments that have enabled the "ALERT_REPORTS" feature flag, Gamma users will no longer have read and write access to Alerts & Reports by default. To give Gamma users the ability to schedule reports from the Dashboard and Explore view like before, create an additional role with "can read on ReportSchedule" and "can write on ReportSchedule" permissions. To further give Gamma users access to the "Alerts & Reports" menu and CRUD view, add "menu access on Manage" and "menu access on Alerts & Report" permissions to the role. diff --git a/docs/docs/contributing/translations.mdx b/docs/docs/contributing/translations.mdx index 13572f801e2d3..e8c656b026354 100644 --- a/docs/docs/contributing/translations.mdx +++ b/docs/docs/contributing/translations.mdx @@ -107,7 +107,7 @@ Run the following command to update the language files with the new extracted st You can then translate the strings gathered in files located under `superset/translation`, where there's one folder per language. You can use [Poedit](https://poedit.net/features) to translate the `po` file more conveniently. -There are some [tutorials in the wiki](https://wiki.lxde.org/en/Translate_*.po_files_with_Poedit). +Here is [a tutorial](https://web.archive.org/web/20220517065036/https://wiki.lxde.org/en/Translate_*.po_files_with_Poedit). To perform the translation on MacOS, you can install `poedit` via Homebrew: diff --git a/docs/docs/frequently-asked-questions.mdx b/docs/docs/frequently-asked-questions.mdx index 779f6c8c8dc77..78fd2a0b8d8a8 100644 --- a/docs/docs/frequently-asked-questions.mdx +++ b/docs/docs/frequently-asked-questions.mdx @@ -24,8 +24,8 @@ will do its own _GROUP BY_ and doing the work twice might slow down performance. Whether you use a table or a view, the important factor is whether your database is fast enough to serve it in an interactive fashion to provide a good user experience in Superset. -However, if you are using the SQL Lab, there is no such limitation, you can write sql query to join -multiple tables as long as your db account has access to the tables. +However, if you are using SQL Lab, there is no such limitation. You can write SQL queries to join +multiple tables as long as your database account has access to the tables. ### How BIG can my datasource be? diff --git a/docs/docs/miscellaneous/native-filter-migration.mdx b/docs/docs/miscellaneous/native-filter-migration.mdx new file mode 100644 index 0000000000000..b231c049b2378 --- /dev/null +++ b/docs/docs/miscellaneous/native-filter-migration.mdx @@ -0,0 +1,103 @@ +--- +title: Migrating from Legacy to Native Filters +sidebar_position: 5 +version: 1 +--- + +## + +The `superset native-filters` CLI command group—somewhat akin to an Alembic migration— +comprises of a number of sub-commands which allows administrators to upgrade/downgrade +existing dashboards which use the legacy filter-box charts—in combination with the +filter scopes/filter mapping—to use the native filter dashboard component. + +Even though both legacy and native filters can coexist the overall user experience (UX) +is substandard as the already convoluted filter space becomes overly complex. After +enabling the `DASHBOARD_NATIVE_FILTERS` it is strongly advised to run the migration ASAP to +ensure users are not exposed to the hybrid state. + +### Upgrading + +The + +``` +superset native-filters upgrade +``` + +command—which provides the option to target either specific dashboard(s) or all +dashboards—migrates the legacy filters to native filters. + +Specifically, the command performs the following: + +- Replaces every filter-box chart within the dashboard with a markdown element which +provides a link to the deprecated chart. This preserves the layout whilst simultaneously +providing context to help owners review/verify said change. +- Migrates the filter scopes/filter mappings to the native filter configuration. + +#### Quality Control + +Dashboard owners should: + +- Verify that the filter behavior is correct. +- Consolidate any conflicting/redundant filters—this previously may not have been +obvious given the embedded nature of the legacy filters and/or the non-optimal UX of the +legacy filter mapping (scopes and immunity). +- Rename the filters—which may not be uniquely named—to provide the necessary context +which previously was likely provided by both the location of the filter-box and the +corresponding filter-box title. + +Dashboard owners may: + +- Remove† the markdown elements from their dashboards and adjust the layout accordingly. + +† Note removing the markdown elements—which contain metadata relating to the replaced +chart—prevents the dashboard from being fully restored and thus this operation should +only be performed if it is evident that a downgrade is not necessary. + +### Downgrading + +Similarly the + +``` +superset native-filters downgrade +``` + +command reverses said migration, i.e., restores the dashboard to the previous state. + + +### Cleanup + +The ability to downgrade/reverse the migration requires temporary storage of the +dashboard metadata—relating to both positional composition and filter configuration. + +Once the upgrade has been verified it is recommended to run the + +``` +superset native-filters cleanup +``` + +command—which provides the option to target either specific dashboard(s) or all +dashboards. Note this operation is irreversible. + +Specifically, the command performs the following: + +- Removes the temporary dashboard metadata. +- Deletes the filter-box charts associated with the dashboard†. + +† Note the markdown elements will still remain however the link to the referenced filter-box +chart will no longer be valid. + +Finally, the + +``` +superset native-filers cleanup --all +``` + +command will additionally delete all filter-box charts, irrespective of whether they +were ever associated with a dashboard. + +#### Quality Control + +Dashboard owners should: + +- Remove the markdown elements from their dashboards and adjust the layout accordingly. diff --git a/helm/superset/Chart.lock b/helm/superset/Chart.lock index d1bdc5115b957..432dd2a9873ae 100644 --- a/helm/superset/Chart.lock +++ b/helm/superset/Chart.lock @@ -4,6 +4,6 @@ dependencies: version: 12.1.6 - name: redis repository: https://charts.bitnami.com/bitnami - version: 17.3.17 -digest: sha256:3bfff146fa89077705c0bedea59bbe2c9f15715220f9ea84f493335f0413af6e -generated: "2023-01-04T12:50:49.567524-08:00" + version: 17.9.4 +digest: sha256:98b6c107066652e40c242a6b0e3c573a4eedf9f64bae0d60f884f0f5cfa1c01a +generated: "2023-04-17T10:38:41.966779+03:00" diff --git a/helm/superset/Chart.yaml b/helm/superset/Chart.yaml index b94fe6efc70b5..b7c5f6c08c422 100644 --- a/helm/superset/Chart.yaml +++ b/helm/superset/Chart.yaml @@ -29,13 +29,13 @@ maintainers: - name: craig-rueda email: craig@craigrueda.com url: https://github.com/craig-rueda -version: 0.9.1 +version: 0.9.2 dependencies: - name: postgresql version: 12.1.6 repository: https://charts.bitnami.com/bitnami condition: postgresql.enabled - name: redis - version: 17.3.17 + version: 17.9.4 repository: https://charts.bitnami.com/bitnami condition: redis.enabled diff --git a/helm/superset/README.md b/helm/superset/README.md index a4ed197f6315f..016bff992ef91 100644 --- a/helm/superset/README.md +++ b/helm/superset/README.md @@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs # superset -![Version: 0.9.1](https://img.shields.io/badge/Version-0.9.1-informational?style=flat-square) +![Version: 0.9.2](https://img.shields.io/badge/Version-0.9.2-informational?style=flat-square) Apache Superset is a modern, enterprise-ready business intelligence web application @@ -45,7 +45,7 @@ helm install my-superset superset/superset | Repository | Name | Version | |------------|------|---------| | https://charts.bitnami.com/bitnami | postgresql | 12.1.6 | -| https://charts.bitnami.com/bitnami | redis | 17.3.17 | +| https://charts.bitnami.com/bitnami | redis | 17.9.4 | ## Values diff --git a/helm/superset/templates/NOTES.txt b/helm/superset/templates/NOTES.txt index 07fcba38102a8..6a686d2c40439 100644 --- a/helm/superset/templates/NOTES.txt +++ b/helm/superset/templates/NOTES.txt @@ -18,9 +18,9 @@ */}} 1. Get the application URL by running these commands: {{- if .Values.ingress.enabled }} -{{- range .Values.ingress.hosts }} + {{- range .Values.ingress.hosts }} http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} -{{- end }} + {{- end }} {{- else if contains "NodePort" .Values.service.type }} export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "superset.fullname" . }}) export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") diff --git a/helm/superset/templates/_helpers.tpl b/helm/superset/templates/_helpers.tpl index d551fcf6e82c7..67f018095d8ee 100644 --- a/helm/superset/templates/_helpers.tpl +++ b/helm/superset/templates/_helpers.tpl @@ -21,7 +21,7 @@ Expand the name of the chart. */}} {{- define "superset.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} + {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} {{- end -}} {{/* @@ -30,34 +30,34 @@ We truncate at 63 chars because some Kubernetes name fields are limited to this If release name contains chart name it will be used as a full name. */}} {{- define "superset.fullname" -}} -{{- if .Values.fullnameOverride -}} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- $name := default .Chart.Name .Values.nameOverride -}} -{{- if contains $name .Release.Name -}} -{{- .Release.Name | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} -{{- end -}} -{{- end -}} + {{- if .Values.fullnameOverride -}} + {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} + {{- else -}} + {{- $name := default .Chart.Name .Values.nameOverride -}} + {{- if contains $name .Release.Name -}} + {{- .Release.Name | trunc 63 | trimSuffix "-" -}} + {{- else -}} + {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} + {{- end -}} + {{- end -}} {{- end -}} {{/* Create the name of the service account to use */}} {{- define "superset.serviceAccountName" -}} -{{- if .Values.serviceAccount.create -}} -{{- default (include "superset.fullname" .) .Values.serviceAccountName -}} -{{- else -}} -{{- default "default" .Values.serviceAccountName -}} -{{- end -}} + {{- if .Values.serviceAccount.create -}} + {{- default (include "superset.fullname" .) .Values.serviceAccountName -}} + {{- else -}} + {{- default "default" .Values.serviceAccountName -}} + {{- end -}} {{- end -}} {{/* Create chart name and version as used by the chart label. */}} {{- define "superset.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} + {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} {{- end -}} {{- define "superset-config" }} @@ -86,20 +86,20 @@ SECRET_KEY = env('SECRET_KEY', 'thisISaSECRET_1234') class CeleryConfig(object): CELERY_IMPORTS = ('superset.sql_lab', ) CELERY_ANNOTATIONS = {'tasks.add': {'rate_limit': '10/s'}} -{{- if .Values.supersetNode.connections.redis_password }} + {{- if .Values.supersetNode.connections.redis_password }} BROKER_URL = f"redis://:{env('REDIS_PASSWORD')}@{env('REDIS_HOST')}:{env('REDIS_PORT')}/0" CELERY_RESULT_BACKEND = f"redis://:{env('REDIS_PASSWORD')}@{env('REDIS_HOST')}:{env('REDIS_PORT')}/0" -{{- else }} + {{- else }} BROKER_URL = f"redis://{env('REDIS_HOST')}:{env('REDIS_PORT')}/0" CELERY_RESULT_BACKEND = f"redis://{env('REDIS_HOST')}:{env('REDIS_PORT')}/0" -{{- end }} + {{- end }} CELERY_CONFIG = CeleryConfig RESULTS_BACKEND = RedisCache( host=env('REDIS_HOST'), -{{- if .Values.supersetNode.connections.redis_password }} + {{- if .Values.supersetNode.connections.redis_password }} password=env('REDIS_PASSWORD'), -{{- end }} + {{- end }} port=env('REDIS_PORT'), key_prefix='superset_results' ) @@ -111,6 +111,7 @@ RESULTS_BACKEND = RedisCache( {{ tpl $value $ }} {{- end }} {{- end }} + {{ if .Values.configOverridesFiles }} # Overrides from files {{- $files := .Files }} diff --git a/helm/superset/templates/configmap-superset.yaml b/helm/superset/templates/configmap-superset.yaml index eb8564619b187..fee59cbbd895e 100644 --- a/helm/superset/templates/configmap-superset.yaml +++ b/helm/superset/templates/configmap-superset.yaml @@ -19,15 +19,15 @@ apiVersion: v1 kind: ConfigMap metadata: name: {{ template "superset.fullname" . }}-extra-config + namespace: {{ .Release.Namespace }} labels: app: {{ template "superset.name" . }} chart: {{ template "superset.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} - namespace: {{ .Release.Namespace }} data: -{{- range $path, $config := .Values.extraConfigs }} + {{- range $path, $config := .Values.extraConfigs }} {{ $path }}: | -{{- tpl $config $ | nindent 4 -}} -{{- end -}} + {{- tpl $config $ | nindent 4 -}} + {{- end -}} {{- end -}} diff --git a/helm/superset/templates/deployment-beat.yaml b/helm/superset/templates/deployment-beat.yaml index 72de7ab204784..628ba6195a9df 100644 --- a/helm/superset/templates/deployment-beat.yaml +++ b/helm/superset/templates/deployment-beat.yaml @@ -19,16 +19,15 @@ apiVersion: apps/v1 kind: Deployment metadata: name: {{ template "superset.fullname" . }}-celerybeat + namespace: {{ .Release.Namespace }} labels: app: {{ template "superset.name" . }}-celerybeat chart: {{ template "superset.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} -{{- if .Values.supersetCeleryBeat.deploymentAnnotations }} - annotations: - {{- toYaml .Values.supersetCeleryBeat.deploymentAnnotations | nindent 4 }} -{{- end }} - namespace: {{ .Release.Namespace }} + {{- if .Values.supersetCeleryBeat.deploymentAnnotations }} + annotations: {{- toYaml .Values.supersetCeleryBeat.deploymentAnnotations | nindent 4 }} + {{- end }} spec: # This must be a singleton replicas: 1 @@ -46,31 +45,30 @@ spec: checksum/extraSecretEnv: {{ .Values.extraSecretEnv | toYaml | sha256sum }} checksum/configOverrides: {{ .Values.configOverrides | toYaml | sha256sum }} checksum/configOverridesFiles: {{ .Values.configOverridesFiles | toYaml | sha256sum }} - {{ if .Values.supersetCeleryBeat.forceReload }} + {{- if .Values.supersetCeleryBeat.forceReload }} # Optionally force the thing to reload force-reload: {{ randAlphaNum 5 | quote }} - {{ end }} - {{- if .Values.supersetCeleryBeat.podAnnotations }} - {{- toYaml .Values.supersetCeleryBeat.podAnnotations | nindent 8 }} - {{- end }} + {{- end }} + {{- if .Values.supersetCeleryBeat.podAnnotations }} + {{- toYaml .Values.supersetCeleryBeat.podAnnotations | nindent 8 }} + {{- end }} labels: app: "{{ template "superset.name" . }}-celerybeat" release: {{ .Release.Name }} - {{- if .Values.supersetCeleryBeat.podLabels }} - {{- toYaml .Values.supersetCeleryBeat.podLabels | nindent 8 }} - {{- end }} + {{- if .Values.supersetCeleryBeat.podLabels }} + {{- toYaml .Values.supersetCeleryBeat.podLabels | nindent 8 }} + {{- end }} spec: {{- if or (.Values.serviceAccount.create) (.Values.serviceAccountName) }} serviceAccountName: {{ template "superset.serviceAccountName" . }} {{- end }} securityContext: runAsUser: {{ .Values.runAsUser }} - {{- if .Values.supersetCeleryBeat.podSecurityContext }} - {{- toYaml .Values.supersetCeleryBeat.podSecurityContext | nindent 8 }} - {{- end }} + {{- if .Values.supersetCeleryBeat.podSecurityContext }} + {{- toYaml .Values.supersetCeleryBeat.podSecurityContext | nindent 8 }} + {{- end }} {{- if .Values.supersetCeleryBeat.initContainers }} - initContainers: - {{- tpl (toYaml .Values.supersetCeleryBeat.initContainers) . | nindent 6 }} + initContainers: {{- tpl (toYaml .Values.supersetCeleryBeat.initContainers) . | nindent 6 }} {{- end }} {{- with .Values.hostAliases }} hostAliases: {{- toYaml . | nindent 6 }} @@ -91,7 +89,7 @@ spec: value: {{ $value | quote }} {{- end }} {{- if .Values.extraEnvRaw }} - {{- toYaml .Values.extraEnvRaw | nindent 12 }} + {{- toYaml .Values.extraEnvRaw | nindent 12 }} {{- end }} envFrom: - secretRef: @@ -109,44 +107,41 @@ spec: mountPath: {{ .Values.extraConfigMountPath | quote }} readOnly: true {{- end }} - {{- with .Values.extraVolumeMounts }} - {{- tpl (toYaml .) $ | nindent 12 -}} - {{- end }} + {{- with .Values.extraVolumeMounts }} + {{- tpl (toYaml .) $ | nindent 12 }} + {{- end }} resources: - {{- if .Values.supersetCeleryBeat.resources }} - {{- toYaml .Values.supersetCeleryBeat.resources | nindent 12 }} - {{- else }} - {{- toYaml .Values.resources | nindent 12 }} - {{- end }} + {{- if .Values.supersetCeleryBeat.resources }} + {{- toYaml .Values.supersetCeleryBeat.resources | nindent 12 }} + {{- else }} + {{- toYaml .Values.resources | nindent 12 }} + {{- end }} {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} + nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- if or .Values.affinity .Values.supersetCeleryBeat.affinity }} affinity: {{- with .Values.affinity }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.supersetCeleryBeat.affinity }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- end }} {{- if or .Values.topologySpreadConstraints .Values.supersetCeleryBeat.topologySpreadConstraints }} topologySpreadConstraints: {{- with .Values.topologySpreadConstraints }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.supersetCeleryBeat.topologySpreadConstraints }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- end }} {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} + tolerations: {{- toYaml . | nindent 8 }} {{- end }} {{- if .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml .Values.imagePullSecrets | nindent 8 }} + imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }} {{- end }} volumes: - name: superset-config @@ -157,7 +152,7 @@ spec: configMap: name: {{ template "superset.fullname" . }}-extra-config {{- end }} - {{- with .Values.extraVolumes }} - {{- tpl (toYaml .) $ | nindent 8 -}} - {{- end }} + {{- with .Values.extraVolumes }} + {{- tpl (toYaml .) $ | nindent 8 -}} + {{- end }} {{- end -}} diff --git a/helm/superset/templates/deployment-flower.yaml b/helm/superset/templates/deployment-flower.yaml index aefdf0f7dec61..3b3091c819508 100644 --- a/helm/superset/templates/deployment-flower.yaml +++ b/helm/superset/templates/deployment-flower.yaml @@ -19,16 +19,15 @@ apiVersion: apps/v1 kind: Deployment metadata: name: {{ template "superset.fullname" . }}-flower + namespace: {{ .Release.Namespace }} labels: app: {{ template "superset.name" . }}-flower chart: {{ template "superset.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} {{- if .Values.supersetCeleryFlower.deploymentAnnotations }} - annotations: - {{- toYaml .Values.supersetCeleryFlower.deploymentAnnotations | nindent 4 }} + annotations: {{- toYaml .Values.supersetCeleryFlower.deploymentAnnotations | nindent 4 }} {{- end }} - namespace: {{ .Release.Namespace }} spec: replicas: {{ .Values.supersetCeleryFlower.replicaCount }} selector: @@ -40,27 +39,26 @@ spec: annotations: checksum/config: {{ include "superset-config" . | sha256sum }} checksum/secrets: {{ tpl (toJson .Values.extraSecretEnv) . | sha256sum }} - {{- if .Values.supersetCeleryFlower.podAnnotations }} - {{- toYaml .Values.supersetCeleryFlower.podAnnotations | nindent 8 }} - {{- end }} + {{- if .Values.supersetCeleryFlower.podAnnotations }} + {{- toYaml .Values.supersetCeleryFlower.podAnnotations | nindent 8 }} + {{- end }} labels: app: "{{ template "superset.name" . }}-flower" release: {{ .Release.Name }} - {{- if .Values.supersetCeleryFlower.podLabels }} - {{- toYaml .Values.supersetCeleryFlower.podLabels | nindent 8 }} - {{- end }} + {{- if .Values.supersetCeleryFlower.podLabels }} + {{- toYaml .Values.supersetCeleryFlower.podLabels | nindent 8 }} + {{- end }} spec: {{- if or (.Values.serviceAccount.create) (.Values.serviceAccountName) }} serviceAccountName: {{ template "superset.serviceAccountName" . }} {{- end }} securityContext: runAsUser: {{ .Values.runAsUser }} - {{- if .Values.supersetCeleryFlower.podSecurityContext }} - {{- toYaml .Values.supersetCeleryFlower.podSecurityContext | nindent 8 }} - {{- end }} + {{- if .Values.supersetCeleryFlower.podSecurityContext }} + {{- toYaml .Values.supersetCeleryFlower.podSecurityContext | nindent 8 }} + {{- end }} {{- if .Values.supersetCeleryFlower.initContainers }} - initContainers: - {{- tpl (toYaml .Values.supersetCeleryFlower.initContainers) . | nindent 6 }} + initContainers: {{- tpl (toYaml .Values.supersetCeleryFlower.initContainers) . | nindent 6 }} {{- end }} {{- with .Values.hostAliases }} hostAliases: {{- toYaml . | nindent 6 }} @@ -79,7 +77,7 @@ spec: value: {{ $value | quote }} {{- end }} {{- if .Values.extraEnvRaw }} - {{- toYaml .Values.extraEnvRaw | nindent 12 }} + {{- toYaml .Values.extraEnvRaw | nindent 12 }} {{- end }} envFrom: - secretRef: @@ -96,62 +94,56 @@ spec: - name: superset-config mountPath: {{ .Values.configMountPath | quote }} readOnly: true - {{- with .Values.extraVolumeMounts }} - {{- tpl (toYaml .) $ | nindent 12 -}} - {{- end }} + {{- with .Values.extraVolumeMounts }} + {{- tpl (toYaml .) $ | nindent 12 -}} + {{- end }} {{- if .Values.supersetCeleryFlower.startupProbe }} - startupProbe: - {{- .Values.supersetCeleryFlower.startupProbe | toYaml | nindent 12 }} + startupProbe: {{- .Values.supersetCeleryFlower.startupProbe | toYaml | nindent 12 }} {{- end }} {{- if .Values.supersetCeleryFlower.readinessProbe }} - readinessProbe: - {{- .Values.supersetCeleryFlower.readinessProbe | toYaml | nindent 12 }} + readinessProbe: {{- .Values.supersetCeleryFlower.readinessProbe | toYaml | nindent 12 }} {{- end }} {{- if .Values.supersetCeleryFlower.livenessProbe }} - livenessProbe: - {{- .Values.supersetCeleryFlower.livenessProbe | toYaml | nindent 12 }} + livenessProbe: {{- .Values.supersetCeleryFlower.livenessProbe | toYaml | nindent 12 }} {{- end }} resources: - {{- if .Values.supersetCeleryFlower.resources }} - {{- toYaml .Values.supersetCeleryFlower.resources | nindent 12 }} - {{- else }} - {{- toYaml .Values.resources | nindent 12 }} - {{- end }} + {{- if .Values.supersetCeleryFlower.resources }} + {{- toYaml .Values.supersetCeleryFlower.resources | nindent 12 }} + {{- else }} + {{- toYaml .Values.resources | nindent 12 }} + {{- end }} {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} + nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- if or .Values.affinity .Values.supersetCeleryFlower.affinity }} affinity: {{- with .Values.affinity }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.supersetCeleryFlower.affinity }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- end }} {{- if or .Values.topologySpreadConstraints .Values.supersetCeleryFlower.topologySpreadConstraints }} topologySpreadConstraints: {{- with .Values.topologySpreadConstraints }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.supersetCeleryFlower.topologySpreadConstraints }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- end }} {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} + tolerations: {{- toYaml . | nindent 8 }} {{- end }} {{- if .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml .Values.imagePullSecrets | nindent 8 }} + imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }} {{- end }} volumes: - name: superset-config secret: secretName: {{ tpl .Values.configFromSecret . }} - {{- with .Values.extraVolumes }} - {{- tpl (toYaml .) $ | nindent 8 -}} - {{- end }} + {{- with .Values.extraVolumes }} + {{- tpl (toYaml .) $ | nindent 8 -}} + {{- end }} {{- end -}} diff --git a/helm/superset/templates/deployment-worker.yaml b/helm/superset/templates/deployment-worker.yaml index 543fa19acb6b8..610d2fe5049a3 100644 --- a/helm/superset/templates/deployment-worker.yaml +++ b/helm/superset/templates/deployment-worker.yaml @@ -18,19 +18,18 @@ apiVersion: apps/v1 kind: Deployment metadata: name: {{ template "superset.fullname" . }}-worker + namespace: {{ .Release.Namespace }} labels: app: {{ template "superset.name" . }}-worker chart: {{ template "superset.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} -{{- if .Values.supersetWorker.deploymentLabels }} - {{- toYaml .Values.supersetWorker.deploymentLabels | nindent 4 }} -{{- end }} -{{- if .Values.supersetWorker.deploymentAnnotations }} - annotations: - {{- toYaml .Values.supersetWorker.deploymentAnnotations | nindent 4 }} -{{- end }} - namespace: {{ .Release.Namespace }} + {{- if .Values.supersetWorker.deploymentLabels }} + {{- toYaml .Values.supersetWorker.deploymentLabels | nindent 4 }} + {{- end }} + {{- if .Values.supersetWorker.deploymentAnnotations }} + annotations: {{- toYaml .Values.supersetWorker.deploymentAnnotations | nindent 4 }} + {{- end }} spec: replicas: {{ .Values.supersetWorker.replicaCount }} selector: @@ -38,8 +37,7 @@ spec: app: {{ template "superset.name" . }}-worker release: {{ .Release.Name }} {{- if .Values.supersetWorker.strategy }} - strategy: - {{- toYaml .Values.supersetWorker.strategy | nindent 4 }} + strategy: {{- toYaml .Values.supersetWorker.strategy | nindent 4 }} {{- end }} template: metadata: @@ -51,31 +49,30 @@ spec: checksum/extraSecretEnv: {{ .Values.extraSecretEnv | toYaml | sha256sum }} checksum/configOverrides: {{ .Values.configOverrides | toYaml | sha256sum }} checksum/configOverridesFiles: {{ .Values.configOverridesFiles | toYaml | sha256sum }} - {{ if .Values.supersetWorker.forceReload }} + {{- if .Values.supersetWorker.forceReload }} # Optionally force the thing to reload force-reload: {{ randAlphaNum 5 | quote }} - {{ end }} - {{- if .Values.supersetWorker.podAnnotations }} - {{- toYaml .Values.supersetWorker.podAnnotations | nindent 8 }} - {{- end }} + {{- end }} + {{- if .Values.supersetWorker.podAnnotations }} + {{- toYaml .Values.supersetWorker.podAnnotations | nindent 8 }} + {{- end }} labels: app: {{ template "superset.name" . }}-worker release: {{ .Release.Name }} - {{- if .Values.supersetWorker.podLabels }} - {{- toYaml .Values.supersetWorker.podLabels | nindent 8 }} - {{- end }} + {{- if .Values.supersetWorker.podLabels }} + {{- toYaml .Values.supersetWorker.podLabels | nindent 8 }} + {{- end }} spec: {{- if or (.Values.serviceAccount.create) (.Values.serviceAccountName) }} serviceAccountName: {{ template "superset.serviceAccountName" . }} {{- end }} securityContext: runAsUser: {{ .Values.runAsUser }} - {{- if .Values.supersetWorker.podSecurityContext }} - {{- toYaml .Values.supersetWorker.podSecurityContext | nindent 8 }} - {{- end }} + {{- if .Values.supersetWorker.podSecurityContext }} + {{- toYaml .Values.supersetWorker.podSecurityContext | nindent 8 }} + {{- end }} {{- if .Values.supersetWorker.initContainers }} - initContainers: - {{- tpl (toYaml .Values.supersetWorker.initContainers) . | nindent 6 }} + initContainers: {{- tpl (toYaml .Values.supersetWorker.initContainers) . | nindent 6 }} {{- end }} {{- with .Values.hostAliases }} hostAliases: {{- toYaml . | nindent 6 }} @@ -109,64 +106,58 @@ spec: - name: superset-config mountPath: {{ .Values.configMountPath | quote }} readOnly: true - {{- if .Values.extraConfigs }} + {{- if .Values.extraConfigs }} - name: superset-extra-config mountPath: {{ .Values.extraConfigMountPath | quote }} readOnly: true - {{- end }} - {{- with .Values.extraVolumeMounts }} - {{- tpl (toYaml .) $ | nindent 12 -}} - {{- end }} + {{- end }} + {{- with .Values.extraVolumeMounts }} + {{- tpl (toYaml .) $ | nindent 12 -}} + {{- end }} {{- if .Values.supersetWorker.startupProbe }} - startupProbe: - {{- .Values.supersetWorker.startupProbe | toYaml | nindent 12 }} + startupProbe: {{- .Values.supersetWorker.startupProbe | toYaml | nindent 12 }} {{- end }} {{- if .Values.supersetWorker.readinessProbe }} - readinessProbe: - {{- .Values.supersetWorker.readinessProbe | toYaml | nindent 12 }} + readinessProbe: {{- .Values.supersetWorker.readinessProbe | toYaml | nindent 12 }} {{- end }} {{- if .Values.supersetWorker.livenessProbe }} - livenessProbe: - {{- .Values.supersetWorker.livenessProbe | toYaml | nindent 12 }} + livenessProbe: {{- .Values.supersetWorker.livenessProbe | toYaml | nindent 12 }} {{- end }} resources: - {{- if .Values.supersetWorker.resources }} + {{- if .Values.supersetWorker.resources }} {{- toYaml .Values.supersetWorker.resources | nindent 12 }} - {{- else }} - {{- toYaml .Values.resources | nindent 12 }} - {{- end }} -{{- if .Values.supersetWorker.extraContainers }} -{{- toYaml .Values.supersetWorker.extraContainers | nindent 8 }} -{{- end }} + {{- else }} + {{- toYaml .Values.resources | nindent 12 }} + {{- end }} + {{- if .Values.supersetWorker.extraContainers }} + {{- toYaml .Values.supersetWorker.extraContainers | nindent 8 }} + {{- end }} {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} + nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- if or .Values.affinity .Values.supersetWorker.affinity }} affinity: {{- with .Values.affinity }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.supersetWorker.affinity }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- end }} {{- if or .Values.topologySpreadConstraints .Values.supersetWorker.topologySpreadConstraints }} topologySpreadConstraints: {{- with .Values.topologySpreadConstraints }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.supersetWorker.topologySpreadConstraints }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- end }} {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} + tolerations: {{- toYaml . | nindent 8 }} {{- end }} {{- if .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml .Values.imagePullSecrets | nindent 8 }} + imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }} {{- end }} volumes: - name: superset-config @@ -178,5 +169,5 @@ spec: name: {{ template "superset.fullname" . }}-extra-config {{- end }} {{- with .Values.extraVolumes }} - {{- tpl (toYaml .) $ | nindent 8 -}} + {{- tpl (toYaml .) $ | nindent 8 -}} {{- end }} diff --git a/helm/superset/templates/deployment-ws.yaml b/helm/superset/templates/deployment-ws.yaml index 0bbc822ef9150..4d684e83e6069 100644 --- a/helm/superset/templates/deployment-ws.yaml +++ b/helm/superset/templates/deployment-ws.yaml @@ -19,16 +19,15 @@ apiVersion: apps/v1 kind: Deployment metadata: name: "{{ template "superset.fullname" . }}-ws" + namespace: {{ .Release.Namespace }} labels: app: "{{ template "superset.name" . }}-ws" chart: {{ template "superset.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} -{{- if .Values.supersetWebsockets.deploymentAnnotations }} - annotations: - {{- toYaml .Values.supersetWebsockets.deploymentAnnotations | nindent 4 }} -{{- end }} - namespace: {{ .Release.Namespace }} + {{- if .Values.supersetWebsockets.deploymentAnnotations }} + annotations: {{- toYaml .Values.supersetWebsockets.deploymentAnnotations | nindent 4 }} + {{- end }} spec: replicas: {{ .Values.supersetWebsockets.replicaCount }} selector: @@ -36,32 +35,31 @@ spec: app: "{{ template "superset.name" . }}-ws" release: {{ .Release.Name }} {{- if .Values.supersetWebsockets.strategy }} - strategy: - {{- toYaml .Values.supersetWebsockets.strategy | nindent 4 }} + strategy: {{- toYaml .Values.supersetWebsockets.strategy | nindent 4 }} {{- end }} template: metadata: annotations: checksum/wsconfig: {{ tpl (toJson .Values.supersetWebsockets.config) . | sha256sum }} checksum/secrets: {{ tpl (toJson .Values.extraSecretEnv) . | sha256sum }} - {{- if .Values.supersetWebsockets.podAnnotations }} - {{- toYaml .Values.supersetWebsockets.podAnnotations | nindent 8 }} - {{- end }} + {{- if .Values.supersetWebsockets.podAnnotations }} + {{- toYaml .Values.supersetWebsockets.podAnnotations | nindent 8 }} + {{- end }} labels: app: "{{ template "superset.name" . }}-ws" release: {{ .Release.Name }} - {{- if .Values.supersetWebsockets.podLabels }} - {{- toYaml .Values.supersetWebsockets.podLabels | nindent 8 }} - {{- end }} + {{- if .Values.supersetWebsockets.podLabels }} + {{- toYaml .Values.supersetWebsockets.podLabels | nindent 8 }} + {{- end }} spec: {{- if or (.Values.serviceAccount.create) (.Values.serviceAccountName) }} serviceAccountName: {{ template "superset.serviceAccountName" . }} {{- end }} securityContext: runAsUser: {{ .Values.runAsUser }} - {{- if .Values.supersetWebsockets.podSecurityContext }} - {{- toYaml .Values.supersetWebsockets.podSecurityContext | nindent 8 }} - {{- end }} + {{- if .Values.supersetWebsockets.podSecurityContext }} + {{- toYaml .Values.supersetWebsockets.podSecurityContext | nindent 8 }} + {{- end }} {{- with .Values.hostAliases }} hostAliases: {{- toYaml . | nindent 6 }} {{- end }} @@ -80,7 +78,7 @@ spec: value: {{ $value | quote }} {{- end }} {{- if .Values.extraEnvRaw }} - {{- toYaml .Values.extraEnvRaw | nindent 12 }} + {{- toYaml .Values.extraEnvRaw | nindent 12 }} {{- end }} envFrom: - secretRef: @@ -99,52 +97,46 @@ spec: subPath: config.json readOnly: true resources: - {{- if .Values.supersetWebsockets.resources }} - {{- toYaml .Values.supersetWebsockets.resources | nindent 12 }} - {{- else }} - {{- toYaml .Values.resources | nindent 12 }} - {{- end }} + {{- if .Values.supersetWebsockets.resources }} + {{- toYaml .Values.supersetWebsockets.resources | nindent 12 }} + {{- else }} + {{- toYaml .Values.resources | nindent 12 }} + {{- end }} {{- if .Values.supersetWebsockets.startupProbe }} - startupProbe: - {{- .Values.supersetWebsockets.startupProbe | toYaml | nindent 12 }} + startupProbe: {{- .Values.supersetWebsockets.startupProbe | toYaml | nindent 12 }} {{- end }} {{- if .Values.supersetWebsockets.readinessProbe }} - readinessProbe: - {{- .Values.supersetWebsockets.readinessProbe | toYaml | nindent 12 }} + readinessProbe: {{- .Values.supersetWebsockets.readinessProbe | toYaml | nindent 12 }} {{- end }} {{- if .Values.supersetWebsockets.livenessProbe }} - livenessProbe: - {{- .Values.supersetWebsockets.livenessProbe | toYaml | nindent 12 }} + livenessProbe: {{- .Values.supersetWebsockets.livenessProbe | toYaml | nindent 12 }} {{- end }} {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} + nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- if or .Values.affinity .Values.supersetWebsockets.affinity }} affinity: {{- with .Values.affinity }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.supersetWebsockets.affinity }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- end }} {{- if or .Values.topologySpreadConstraints .Values.supersetWebsockets.topologySpreadConstraints }} topologySpreadConstraints: {{- with .Values.topologySpreadConstraints }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.supersetWebsockets.topologySpreadConstraints }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- end }} {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} + tolerations: {{- toYaml . | nindent 8 }} {{- end }} {{- if .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml .Values.imagePullSecrets | nindent 8 }} + imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }} {{- end }} volumes: - name: superset-ws-config diff --git a/helm/superset/templates/deployment.yaml b/helm/superset/templates/deployment.yaml index d993b934ce104..e989b896d46aa 100644 --- a/helm/superset/templates/deployment.yaml +++ b/helm/superset/templates/deployment.yaml @@ -18,24 +18,22 @@ apiVersion: apps/v1 kind: Deployment metadata: name: {{ template "superset.fullname" . }} + namespace: {{ .Release.Namespace }} labels: app: {{ template "superset.name" . }} chart: {{ template "superset.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} -{{- if .Values.supersetNode.deploymentLabels }} - {{- toYaml .Values.supersetNode.deploymentLabels | nindent 4 }} -{{- end }} -{{- if .Values.supersetNode.deploymentAnnotations }} - annotations: - {{- toYaml .Values.supersetNode.deploymentAnnotations | nindent 4 }} -{{- end }} - namespace: {{ .Release.Namespace }} + {{- if .Values.supersetNode.deploymentLabels }} + {{- toYaml .Values.supersetNode.deploymentLabels | nindent 4 }} + {{- end }} + {{- if .Values.supersetNode.deploymentAnnotations }} + annotations: {{- toYaml .Values.supersetNode.deploymentAnnotations | nindent 4 }} + {{- end }} spec: replicas: {{ .Values.supersetNode.replicaCount }} {{- if .Values.supersetNode.strategy }} - strategy: - {{- toYaml .Values.supersetNode.strategy | nindent 4 }} + strategy: {{- toYaml .Values.supersetNode.strategy | nindent 4 }} {{- end }} selector: matchLabels: @@ -58,27 +56,26 @@ spec: # Optionally force the thing to reload force-reload: {{ randAlphaNum 5 | quote }} {{- end }} - {{- if .Values.supersetNode.podAnnotations }} - {{- toYaml .Values.supersetNode.podAnnotations | nindent 8 }} - {{- end }} + {{- if .Values.supersetNode.podAnnotations }} + {{- toYaml .Values.supersetNode.podAnnotations | nindent 8 }} + {{- end }} labels: app: {{ template "superset.name" . }} release: {{ .Release.Name }} - {{- if .Values.supersetNode.podLabels }} - {{- toYaml .Values.supersetNode.podLabels | nindent 8 }} - {{- end }} + {{- if .Values.supersetNode.podLabels }} + {{- toYaml .Values.supersetNode.podLabels | nindent 8 }} + {{- end }} spec: {{- if or (.Values.serviceAccount.create) (.Values.serviceAccountName) }} serviceAccountName: {{ template "superset.serviceAccountName" . }} {{- end }} securityContext: runAsUser: {{ .Values.runAsUser }} - {{- if .Values.supersetNode.podSecurityContext }} - {{- toYaml .Values.supersetNode.podSecurityContext | nindent 8 }} - {{- end }} + {{- if .Values.supersetNode.podSecurityContext }} + {{- toYaml .Values.supersetNode.podSecurityContext | nindent 8 }} + {{- end }} {{- if .Values.supersetNode.initContainers }} - initContainers: - {{- tpl (toYaml .Values.supersetNode.initContainers) . | nindent 6 }} + initContainers: {{- tpl (toYaml .Values.supersetNode.initContainers) . | nindent 6 }} {{- end }} {{- with .Values.hostAliases }} hostAliases: {{- toYaml . | nindent 6 }} @@ -103,7 +100,7 @@ spec: value: {{ $value | quote }} {{- end }} {{- if .Values.extraEnvRaw }} - {{- toYaml .Values.extraEnvRaw | nindent 12 }} + {{- toYaml .Values.extraEnvRaw | nindent 12 }} {{- end }} envFrom: - secretRef: @@ -116,70 +113,63 @@ spec: - name: superset-config mountPath: {{ .Values.configMountPath | quote }} readOnly: true - {{- if .Values.extraConfigs }} + {{- if .Values.extraConfigs }} - name: superset-extra-config mountPath: {{ .Values.extraConfigMountPath | quote }} readOnly: true - {{- end }} - {{- with .Values.extraVolumeMounts }} - {{- tpl (toYaml .) $ | nindent 12 -}} - {{- end }} + {{- end }} + {{- with .Values.extraVolumeMounts }} + {{- tpl (toYaml .) $ | nindent 12 -}} + {{- end }} ports: - name: http containerPort: {{ .Values.service.port }} protocol: TCP {{- if .Values.supersetNode.startupProbe }} - startupProbe: - {{- .Values.supersetNode.startupProbe | toYaml | nindent 12 }} + startupProbe: {{- .Values.supersetNode.startupProbe | toYaml | nindent 12 }} {{- end }} {{- if .Values.supersetNode.readinessProbe }} - readinessProbe: - {{- .Values.supersetNode.readinessProbe | toYaml | nindent 12 }} + readinessProbe: {{- .Values.supersetNode.readinessProbe | toYaml | nindent 12 }} {{- end }} {{- if .Values.supersetNode.livenessProbe }} - livenessProbe: - {{- .Values.supersetNode.livenessProbe | toYaml | nindent 12 }} + livenessProbe: {{- .Values.supersetNode.livenessProbe | toYaml | nindent 12 }} {{- end }} resources: - {{- if .Values.supersetNode.resources }} - {{- toYaml .Values.supersetNode.resources | nindent 12 }} - {{- else }} - {{- toYaml .Values.resources | nindent 12 }} - {{- end }} -{{- if .Values.supersetNode.extraContainers }} -{{- toYaml .Values.supersetNode.extraContainers | nindent 8 }} -{{- end }} + {{- if .Values.supersetNode.resources }} + {{- toYaml .Values.supersetNode.resources | nindent 12 }} + {{- else }} + {{- toYaml .Values.resources | nindent 12 }} + {{- end }} + {{- if .Values.supersetNode.extraContainers }} + {{- toYaml .Values.supersetNode.extraContainers | nindent 8 }} + {{- end }} {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} + nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- if or .Values.affinity .Values.supersetNode.affinity }} affinity: {{- with .Values.affinity }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.supersetNode.affinity }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- end }} {{- if or .Values.topologySpreadConstraints .Values.supersetNode.topologySpreadConstraints }} topologySpreadConstraints: {{- with .Values.topologySpreadConstraints }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.supersetNode.topologySpreadConstraints }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- end }} {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} + tolerations: {{- toYaml . | nindent 8 }} {{- end }} {{- if .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml .Values.imagePullSecrets | nindent 8 }} + imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }} {{- end }} - volumes: - name: superset-config secret: @@ -190,5 +180,5 @@ spec: name: {{ template "superset.fullname" . }}-extra-config {{- end }} {{- with .Values.extraVolumes }} - {{- tpl (toYaml .) $ | nindent 8 -}} + {{- tpl (toYaml .) $ | nindent 8 -}} {{- end }} diff --git a/helm/superset/templates/ingress.yaml b/helm/superset/templates/ingress.yaml index 44e1e48374d72..cec2562f7859e 100644 --- a/helm/superset/templates/ingress.yaml +++ b/helm/superset/templates/ingress.yaml @@ -20,32 +20,31 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: {{ $fullName }} + namespace: {{ .Release.Namespace }} labels: app: {{ template "superset.name" . }} chart: {{ template "superset.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} + annotations: {{- toYaml . | nindent 4 }} {{- end }} - namespace: {{ .Release.Namespace }} spec: -{{- if .Values.ingress.ingressClassName }} + {{- if .Values.ingress.ingressClassName }} ingressClassName: {{ .Values.ingress.ingressClassName }} -{{- end }} -{{- if .Values.ingress.tls }} + {{- end }} + {{- if .Values.ingress.tls }} tls: - {{- range .Values.ingress.tls }} + {{- range .Values.ingress.tls }} - hosts: {{- range .hosts }} - {{ . }} {{- end }} secretName: {{ .secretName }} + {{- end }} {{- end }} -{{- end }} rules: - {{- range .Values.ingress.hosts }} + {{- range .Values.ingress.hosts }} - host: {{ . }} http: paths: @@ -65,8 +64,8 @@ spec: port: name: ws {{- end }} - {{- end }} + {{- end }} {{- if .Values.ingress.extraHostsRaw }} - {{- toYaml .Values.ingress.extraHostsRaw | nindent 4 }} + {{- toYaml .Values.ingress.extraHostsRaw | nindent 4 }} {{- end }} {{- end }} diff --git a/helm/superset/templates/init-job.yaml b/helm/superset/templates/init-job.yaml index 1f4ba57ae54ea..e225600fefc7c 100644 --- a/helm/superset/templates/init-job.yaml +++ b/helm/superset/templates/init-job.yaml @@ -19,19 +19,18 @@ apiVersion: batch/v1 kind: Job metadata: name: {{ template "superset.name" . }}-init-db + namespace: {{ .Release.Namespace }} {{- if .Values.init.helmHook }} annotations: "helm.sh/hook": post-install,post-upgrade "helm.sh/hook-delete-policy": "before-hook-creation" {{- end }} - namespace: {{ .Release.Namespace }} spec: template: metadata: name: {{ template "superset.name" . }}-init-db {{- if .Values.init.podAnnotations }} - annotations: - {{- toYaml .Values.init.podAnnotations | nindent 8 }} + annotations: {{- toYaml .Values.init.podAnnotations | nindent 8 }} {{- end }} spec: {{- if or (.Values.serviceAccount.create) (.Values.serviceAccountName) }} @@ -39,12 +38,11 @@ spec: {{- end }} securityContext: runAsUser: {{ .Values.runAsUser }} - {{- if .Values.init.podSecurityContext }} - {{- toYaml .Values.init.podSecurityContext | nindent 8 }} - {{- end }} + {{- if .Values.init.podSecurityContext }} + {{- toYaml .Values.init.podSecurityContext | nindent 8 }} + {{- end }} {{- if .Values.init.initContainers }} - initContainers: - {{- tpl (toYaml .Values.init.initContainers) . | nindent 6 }} + initContainers: {{- tpl (toYaml .Values.init.initContainers) . | nindent 6 }} {{- end }} containers: - name: {{ template "superset.name" . }}-init-db @@ -56,7 +54,7 @@ spec: value: {{ $value | quote }} {{- end }} {{- if .Values.extraEnvRaw }} - {{- toYaml .Values.extraEnvRaw | nindent 10 }} + {{- toYaml .Values.extraEnvRaw | nindent 10 }} {{- end }} {{- end }} envFrom: @@ -74,46 +72,42 @@ spec: - name: superset-config mountPath: {{ .Values.configMountPath | quote }} readOnly: true - {{- if .Values.extraConfigs }} + {{- if .Values.extraConfigs }} - name: superset-extra-config mountPath: {{ .Values.extraConfigMountPath | quote }} readOnly: true - {{- end }} - {{- with .Values.extraVolumeMounts }} - {{- tpl (toYaml .) $ | nindent 10 -}} - {{- end }} + {{- end }} + {{- with .Values.extraVolumeMounts }} + {{- tpl (toYaml .) $ | nindent 10 -}} + {{- end }} command: {{ tpl (toJson .Values.init.command) . }} - resources: - {{- toYaml .Values.init.resources | nindent 10 }} + resources: {{- toYaml .Values.init.resources | nindent 10 }} {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} + nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- if or .Values.affinity .Values.init.affinity }} affinity: {{- with .Values.affinity }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.init.affinity }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- end }} {{- if or .Values.topologySpreadConstraints .Values.init.topologySpreadConstraints }} topologySpreadConstraints: {{- with .Values.topologySpreadConstraints }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.init.topologySpreadConstraints }} - {{- toYaml . | nindent 8 }} + {{- toYaml . | nindent 8 }} {{- end }} {{- end }} {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} + tolerations: {{- toYaml . | nindent 8 }} {{- end }} {{- if .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml .Values.imagePullSecrets | nindent 8 }} + imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }} {{- end }} volumes: - name: superset-config @@ -125,7 +119,7 @@ spec: name: {{ template "superset.fullname" . }}-extra-config {{- end }} {{- with .Values.extraVolumes }} - {{- tpl (toYaml .) $ | nindent 8 -}} + {{- tpl (toYaml .) $ | nindent 8 -}} {{- end }} restartPolicy: Never {{- end }} diff --git a/helm/superset/templates/secret-env.yaml b/helm/superset/templates/secret-env.yaml index 0164d96a8c129..41fac42bdaf1a 100644 --- a/helm/superset/templates/secret-env.yaml +++ b/helm/superset/templates/secret-env.yaml @@ -18,12 +18,12 @@ apiVersion: v1 kind: Secret metadata: name: {{ template "superset.fullname" . }}-env + namespace: {{ .Release.Namespace }} labels: app: {{ template "superset.fullname" . }} chart: {{ template "superset.chart" . }} release: "{{ .Release.Name }}" heritage: "{{ .Release.Service }}" - namespace: {{ .Release.Namespace }} type: Opaque stringData: REDIS_HOST: {{ tpl .Values.supersetNode.connections.redis_host . | quote }} diff --git a/helm/superset/templates/secret-superset-config.yaml b/helm/superset/templates/secret-superset-config.yaml index c1f4102858d93..165d00356b6c7 100644 --- a/helm/superset/templates/secret-superset-config.yaml +++ b/helm/superset/templates/secret-superset-config.yaml @@ -18,24 +18,24 @@ apiVersion: v1 kind: Secret metadata: name: {{ template "superset.fullname" . }}-config + namespace: {{ .Release.Namespace }} labels: app: {{ template "superset.fullname" . }} chart: {{ template "superset.chart" . }} release: "{{ .Release.Name }}" heritage: "{{ .Release.Service }}" - namespace: {{ .Release.Namespace }} type: Opaque stringData: superset_config.py: | -{{- include "superset-config" . | nindent 4 }} + {{- include "superset-config" . | nindent 4 }} superset_init.sh: | -{{- tpl .Values.init.initscript . | nindent 4 }} + {{- tpl .Values.init.initscript . | nindent 4 }} superset_bootstrap.sh: | -{{- tpl .Values.bootstrapScript . | nindent 4 }} + {{- tpl .Values.bootstrapScript . | nindent 4 }} -{{- if .Values.extraSecrets }} -{{- range $path, $config := .Values.extraSecrets }} + {{- if .Values.extraSecrets }} + {{- range $path, $config := .Values.extraSecrets }} {{ $path }}: | -{{- tpl $config $ | nindent 4 -}} -{{- end -}} -{{- end -}} + {{- tpl $config $ | nindent 4 -}} + {{- end -}} + {{- end -}} diff --git a/helm/superset/templates/secret-ws.yaml b/helm/superset/templates/secret-ws.yaml index c3ac55d96cb07..27f15b784118f 100644 --- a/helm/superset/templates/secret-ws.yaml +++ b/helm/superset/templates/secret-ws.yaml @@ -19,14 +19,14 @@ apiVersion: v1 kind: Secret metadata: name: "{{ template "superset.fullname" . }}-ws-config" + namespace: {{ .Release.Namespace }} labels: app: {{ template "superset.fullname" . }} chart: {{ template "superset.chart" . }} release: "{{ .Release.Name }}" heritage: "{{ .Release.Service }}" - namespace: {{ .Release.Namespace }} type: Opaque stringData: - config.json: | - {{- tpl (toJson .Values.supersetWebsockets.config) . | nindent 6 }} + config.json: | + {{- tpl (toJson .Values.supersetWebsockets.config) . | nindent 4 }} {{- end }} diff --git a/helm/superset/templates/service-account.yaml b/helm/superset/templates/service-account.yaml index 994ad8333afd8..fb8a16d4d8bc0 100755 --- a/helm/superset/templates/service-account.yaml +++ b/helm/superset/templates/service-account.yaml @@ -19,6 +19,7 @@ apiVersion: v1 kind: ServiceAccount metadata: name: {{ include "superset.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} labels: app.kubernetes.io/name: {{ include "superset.name" . }} helm.sh/chart: {{ include "superset.chart" . }} @@ -31,5 +32,4 @@ metadata: {{- if .Values.serviceAccount.annotations }} annotations: {{- toYaml .Values.serviceAccount.annotations | nindent 4 }} {{- end }} - namespace: {{ .Release.Namespace }} {{- end -}} diff --git a/helm/superset/templates/service.yaml b/helm/superset/templates/service.yaml index 97db594a23958..5850e5732b57b 100644 --- a/helm/superset/templates/service.yaml +++ b/helm/superset/templates/service.yaml @@ -18,16 +18,15 @@ apiVersion: v1 kind: Service metadata: name: {{ template "superset.fullname" . }} + namespace: {{ .Release.Namespace }} labels: app: {{ template "superset.name" . }} chart: {{ template "superset.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} -{{- with .Values.service.annotations }} - annotations: -{{- toYaml . | nindent 4 }} -{{- end }} - namespace: {{ .Release.Namespace }} + {{- with .Values.service.annotations }} + annotations: {{- toYaml . | nindent 4 }} + {{- end }} spec: type: {{ .Values.service.type }} ports: @@ -50,16 +49,15 @@ apiVersion: v1 kind: Service metadata: name: "{{ template "superset.fullname" . }}-flower" + namespace: {{ .Release.Namespace }} labels: app: {{ template "superset.name" . }} chart: {{ template "superset.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} {{- with .Values.supersetCeleryFlower.service.annotations }} - annotations: - {{- toYaml . | nindent 4 }} + annotations: {{- toYaml . | nindent 4 }} {{- end }} - namespace: {{ .Release.Namespace }} spec: type: {{ .Values.supersetCeleryFlower.service.type }} ports: @@ -83,16 +81,15 @@ apiVersion: v1 kind: Service metadata: name: "{{ template "superset.fullname" . }}-ws" + namespace: {{ .Release.Namespace }} labels: app: {{ template "superset.name" . }} chart: {{ template "superset.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} {{- with .Values.supersetWebsockets.service.annotations }} - annotations: - {{- toYaml . | nindent 4 }} + annotations: {{- toYaml . | nindent 4 }} {{- end }} - namespace: {{ .Release.Namespace }} spec: type: {{ .Values.supersetWebsockets.service.type }} ports: diff --git a/helm/superset/values.yaml b/helm/superset/values.yaml index b04dc6693497b..339833237939d 100644 --- a/helm/superset/values.yaml +++ b/helm/superset/values.yaml @@ -50,8 +50,7 @@ envFromSecret: '{{ template "superset.fullname" . }}-env' envFromSecrets: [] # -- Extra environment variables that will be passed into pods -extraEnv: - {} +extraEnv: {} # Different gunicorn settings, refer to the gunicorn documentation # https://docs.gunicorn.org/en/stable/settings.html# # These variables are used as Flags at the gunicorn startup @@ -74,8 +73,7 @@ extraEnv: # OAUTH_WHITELIST_REGEX: ... # -- Extra environment variables in RAW format that will be passed into pods -extraEnvRaw: - [] +extraEnvRaw: [] # Load DB password from other secret (e.g. for zalando operator) # - name: DB_PASS # valueFrom: @@ -84,16 +82,14 @@ extraEnvRaw: # key: password # -- Extra environment variables to pass as secrets -extraSecretEnv: - {} +extraSecretEnv: {} # MAPBOX_API_KEY: ... # # Google API Keys: https://console.cloud.google.com/apis/credentials # GOOGLE_KEY: ... # GOOGLE_SECRET: ... # -- Extra files to mount on `/app/pythonpath` -extraConfigs: - {} +extraConfigs: {} # import_datasources.yaml: | # databases: # - allow_file_upload: true @@ -109,8 +105,7 @@ extraConfigs: # -- Extra files to mount on `/app/pythonpath` as secrets extraSecrets: {} -extraVolumes: - [] +extraVolumes: [] # - name: customConfig # configMap: # name: '{{ template "superset.fullname" . }}-custom-config' @@ -119,8 +114,7 @@ extraVolumes: # secretName: my-secret # defaultMode: 0600 -extraVolumeMounts: - [] +extraVolumeMounts: [] # - name: customConfig # mountPath: /mnt/config # readOnly: true @@ -130,8 +124,7 @@ extraVolumeMounts: # -- A dictionary of overrides to append at the end of superset_config.py - the name does not matter # WARNING: the order is not guaranteed # Files can be passed as helm --set-file configOverrides.my-override=my-file.py -configOverrides: - {} +configOverrides: {} # extend_timeout: | # # Extend timeout to allow long running queries. # SUPERSET_WEBSERVER_TIMEOUT = ... @@ -168,8 +161,7 @@ configOverrides: # SECRET_KEY = 'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY' # -- Same as above but the values are files -configOverridesFiles: - {} +configOverridesFiles: {} # extend_timeout: extend_timeout.py # enable_oauth: enable_oauth.py @@ -203,8 +195,7 @@ service: ingress: enabled: false # ingressClassName: nginx - annotations: - {} + annotations: {} # kubernetes.io/tls-acme: "true" ## Extend timeout to allow long running queries. # nginx.ingress.kubernetes.io/proxy-connect-timeout: "300" @@ -220,8 +211,7 @@ ingress: # hosts: # - chart-example.local -resources: - {} +resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little # resources, such as Minikube. If you do want to specify resources, uncomment the following @@ -321,8 +311,7 @@ supersetNode: periodSeconds: 15 successThreshold: 1 # -- Resource settings for the supersetNode pods - these settings overwrite might existing values from the global resources object defined above. - resources: - {} + resources: {} # limits: # cpu: 100m # memory: 128Mi @@ -331,8 +320,7 @@ supersetNode: # memory: 128Mi podSecurityContext: {} containerSecurityContext: {} - strategy: - {} + strategy: {} # type: RollingUpdate # rollingUpdate: # maxSurge: 25% @@ -373,8 +361,7 @@ supersetWorker: # -- Labels to be added to supersetWorker pods podLabels: {} # -- Resource settings for the supersetWorker pods - these settings overwrite might existing values from the global resources object defined above. - resources: - {} + resources: {} # limits: # cpu: 100m # memory: 128Mi @@ -383,8 +370,7 @@ supersetWorker: # memory: 128Mi podSecurityContext: {} containerSecurityContext: {} - strategy: - {} + strategy: {} # type: RollingUpdate # rollingUpdate: # maxSurge: 25% @@ -443,8 +429,7 @@ supersetCeleryBeat: # -- Labels to be added to supersetCeleryBeat pods podLabels: {} # -- Resource settings for the CeleryBeat pods - these settings overwrite might existing values from the global resources object defined above. - resources: - {} + resources: {} # limits: # cpu: 100m # memory: 128Mi @@ -524,8 +509,7 @@ supersetCeleryFlower: # -- Labels to be added to supersetCeleryFlower pods podLabels: {} # -- Resource settings for the CeleryBeat pods - these settings overwrite might existing values from the global resources object defined above. - resources: - {} + resources: {} # limits: # cpu: 100m # memory: 128Mi @@ -623,8 +607,7 @@ init: # cause the process to be killed due to OOM if it exceeds limit # Make sure you are giving a strong password for the admin user creation( else make sure you are changing after setup) # Also change the admin email to your own custom email. - resources: - {} + resources: {} # limits: # cpu: # memory: diff --git a/requirements/base.txt b/requirements/base.txt index 7a1d9453a49d3..a8a61ef6e6e4d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -40,12 +40,15 @@ click==8.0.4 # apache-superset # celery # click-didyoumean + # click-option-group # click-plugins # click-repl # flask # flask-appbuilder click-didyoumean==0.3.0 # via celery +click-option-group==0.5.5 + # via apache-superset click-plugins==1.1.1 # via celery click-repl==0.2.0 @@ -133,10 +136,8 @@ humanize==3.11.0 # via apache-superset idna==3.2 # via email-validator -importlib-metadata==6.5.0 +importlib-metadata==6.0.0 # via flask -importlib-resources==5.12.0 - # via limits isodate==0.6.0 # via apache-superset itsdangerous==2.1.1 @@ -153,7 +154,7 @@ kombu==5.2.4 # via celery korean-lunar-calendar==0.2.1 # via holidays -limits==3.4.0 +limits==3.2.0 # via flask-limiter mako==1.1.4 # via alembic @@ -209,7 +210,7 @@ pyarrow==10.0.1 # via apache-superset pycparser==2.20 # via cffi -pygments==2.15.1 +pygments==2.14.0 # via rich pyjwt==2.4.0 # via @@ -252,10 +253,12 @@ pyyaml==5.4.1 # apispec redis==3.5.3 # via apache-superset -rich==13.3.4 +rich==13.3.1 # via flask-limiter selenium==3.141.0 # via apache-superset +shortid==0.1.2 + # via apache-superset simplejson==3.17.3 # via apache-superset six==1.16.0 @@ -311,7 +314,7 @@ werkzeug==2.1.2 # flask # flask-jwt-extended # flask-login -wrapt==1.15.0 +wrapt==1.12.1 # via deprecated wtforms==2.3.3 # via @@ -324,9 +327,7 @@ wtforms-json==0.3.3 xlsxwriter==3.0.7 # via apache-superset zipp==3.15.0 - # via - # importlib-metadata - # importlib-resources + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/requirements/development.txt b/requirements/development.txt index 47fe7a17372dd..aa92fcfda4d89 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -80,8 +80,6 @@ pure-sasl==0.6.2 # via thrift-sasl pydruid==0.6.5 # via apache-superset -pygments==2.12.0 - # via ipython pyhive[hive]==0.6.5 # via apache-superset pyinstrument==4.0.2 diff --git a/requirements/integration.txt b/requirements/integration.txt index 59c619a38602d..c11f956c68d03 100644 --- a/requirements/integration.txt +++ b/requirements/integration.txt @@ -30,7 +30,7 @@ packaging==21.3 pep517==0.11.0 # via build pip-compile-multi==2.6.2 - # via -r integration.in + # via -r requirements/integration.in pip-tools==6.8.0 # via pip-compile-multi platformdirs==2.6.2 @@ -38,7 +38,7 @@ platformdirs==2.6.2 pluggy==0.13.1 # via tox pre-commit==3.2.2 - # via -r integration.in + # via -r requirements/integration.in py==1.10.0 # via tox pyparsing==3.0.6 @@ -50,11 +50,11 @@ six==1.16.0 toml==0.10.2 # via tox tomli==1.2.1 - # via pep517 + # via build toposort==1.6 # via pip-compile-multi tox==3.25.1 - # via -r integration.in + # via -r requirements/integration.in virtualenv==20.17.1 # via # pre-commit diff --git a/setup.py b/setup.py index 0b21bca1dd958..52f9761acb37c 100644 --- a/setup.py +++ b/setup.py @@ -77,6 +77,7 @@ def get_git_sha() -> str: "cachelib>=0.4.1,<0.5", "celery>=5.2.2, <6.0.0", "click>=8.0.3", + "click-option-group", "colorama", "croniter>=0.3.28", "cron-descriptor", @@ -114,6 +115,7 @@ def get_git_sha() -> str: "PyJWT>=2.4.0, <3.0", "redis", "selenium>=3.141.0", + "shortid", "sshtunnel>=0.4.0, <0.5", "simplejson>=3.15.0", "slack_sdk>=3.1.1, <4", diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/utils.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/utils.ts index 0b8776b06c1a5..44322e1c42ceb 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/utils.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/utils.ts @@ -163,10 +163,6 @@ export function interceptDatasets() { cy.intercept('GET', `/api/v1/dashboard/*/datasets`).as('getDatasets'); } -export function interceptDashboardasync() { - cy.intercept('GET', `/dashboardasync/api/read*`).as('getDashboardasync'); -} - export function interceptFilterState() { cy.intercept('POST', `/api/v1/dashboard/*/filter_state*`).as( 'postFilterState', diff --git a/superset-frontend/cypress-base/cypress/integration/dataset/dataset_list.test.ts b/superset-frontend/cypress-base/cypress/integration/dataset/dataset_list.test.ts index e78c328ec5104..6bf0419cdda22 100644 --- a/superset-frontend/cypress-base/cypress/integration/dataset/dataset_list.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dataset/dataset_list.test.ts @@ -24,7 +24,7 @@ describe('Dataset list', () => { cy.visit(DATASET_LIST_PATH); }); - it('should open Explore on dataset name click', () => { + xit('should open Explore on dataset name click', () => { cy.intercept('**/api/v1/explore/**').as('explore'); cy.get('[data-test="listview-table"] [data-test="internal-link"]') .contains('birth_names') diff --git a/superset-frontend/cypress-base/cypress/integration/explore/utils.ts b/superset-frontend/cypress-base/cypress/integration/explore/utils.ts index 15e7dcba1b6f5..04cf1f1819986 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/utils.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/utils.ts @@ -17,10 +17,7 @@ * under the License. */ -import { - interceptGet as interceptDashboardGet, - interceptDashboardasync, -} from '../dashboard/utils'; +import { interceptGet as interceptDashboardGet } from '../dashboard/utils'; export function interceptFiltering() { cy.intercept('GET', `/api/v1/chart/?q=*`).as('filtering'); @@ -61,12 +58,10 @@ export function setFilter(filter: string, option: string) { export function saveChartToDashboard(dashboardName: string) { interceptDashboardGet(); - interceptDashboardasync(); interceptUpdate(); interceptExploreGet(); cy.getBySel('query-save-button').click(); - cy.wait('@getDashboardasync'); cy.getBySelLike('chart-modal').should('be.visible'); cy.get( '[data-test="save-chart-modal-select-dashboard-form"] [aria-label="Select a dashboard"]', diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.ts index 46030bfb35949..7db1dbe8ef95a 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.ts @@ -53,7 +53,7 @@ describe('Visualization > Table', () => { granularity_sqla: undefined, metrics: ['count'], }); - cy.get('[data-test=granularity_sqla] .column-option-label').contains('ds'); + cy.get('[data-test=adhoc_filters]').contains('ds'); }); it('Format non-numeric metrics correctly', () => { @@ -126,7 +126,7 @@ describe('Visualization > Table', () => { // should handle frontend sorting correctly cy.get('.chart-container th').contains('name').click(); cy.get('.chart-container td:nth-child(2):eq(0)').contains('Adam'); - cy.get('.chart-container th').contains('Time').click().click(); + cy.get('.chart-container th').contains('ds').click().click(); cy.get('.chart-container td:nth-child(1):eq(0)').contains('2008'); }); diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 1e5a73b39343b..2d40096fa8a8b 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -57203,9 +57203,9 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, "node_modules/vm2": { - "version": "3.9.15", - "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.15.tgz", - "integrity": "sha512-XqNqknHGw2avJo13gbIwLNZUumvrSHc9mLqoadFZTpo3KaNEJoe1I0lqTFhRXmXD7WkLyG01aaraXdXT0pa4ag==", + "version": "3.9.17", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.17.tgz", + "integrity": "sha512-AqwtCnZ/ERcX+AVj9vUsphY56YANXxRuqMb7GsDtAr0m0PcQX3u0Aj3KWiXM0YAHy7i6JEeHrwOnwXbGYgRpAw==", "dev": true, "dependencies": { "acorn": "^8.7.0", @@ -106734,9 +106734,9 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, "vm2": { - "version": "3.9.15", - "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.15.tgz", - "integrity": "sha512-XqNqknHGw2avJo13gbIwLNZUumvrSHc9mLqoadFZTpo3KaNEJoe1I0lqTFhRXmXD7WkLyG01aaraXdXT0pa4ag==", + "version": "3.9.17", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.17.tgz", + "integrity": "sha512-AqwtCnZ/ERcX+AVj9vUsphY56YANXxRuqMb7GsDtAr0m0PcQX3u0Aj3KWiXM0YAHy7i6JEeHrwOnwXbGYgRpAw==", "dev": true, "requires": { "acorn": "^8.7.0", diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts b/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts index 71c4dd31189ba..1e5152b2bd904 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts @@ -20,7 +20,7 @@ import { DatasourceType } from '@superset-ui/core'; import { Dataset } from './types'; export const TestDataset: Dataset = { - column_format: {}, + column_formats: {}, columns: [ { advanced_data_type: undefined, diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx index 28fbfb876cb95..82ba6dfeebe45 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx @@ -54,27 +54,29 @@ export const contributionModeControl = { }, }; +function isTemporal(controls: ControlStateMapping): boolean { + return !( + isDefined(controls?.x_axis?.value) && + !isTemporalColumn( + getColumnLabel(controls?.x_axis?.value as QueryFormColumn), + controls?.datasource?.datasource, + ) + ); +} + const xAxisSortVisibility = ({ controls }: { controls: ControlStateMapping }) => - isDefined(controls?.x_axis?.value) && - !isTemporalColumn( - getColumnLabel(controls?.x_axis?.value as QueryFormColumn), - controls?.datasource?.datasource, - ) && - Array.isArray(controls?.groupby?.value) && - controls.groupby.value.length === 0; + !isTemporal(controls) && + ensureIsArray(controls?.groupby?.value).length === 0 && + ensureIsArray(controls?.metrics?.value).length === 1; const xAxisMultiSortVisibility = ({ controls, }: { controls: ControlStateMapping; }) => - isDefined(controls?.x_axis?.value) && - !isTemporalColumn( - getColumnLabel(controls?.x_axis?.value as QueryFormColumn), - controls?.datasource?.datasource, - ) && - Array.isArray(controls?.groupby?.value) && - !!controls.groupby.value.length; + !isTemporal(controls) && + (!!ensureIsArray(controls?.groupby?.value).length || + ensureIsArray(controls?.metrics?.value).length > 1); export const xAxisSortControl = { name: 'x_axis_sort', diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts index 67582523bc312..c26f53b6a2097 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts @@ -67,7 +67,7 @@ export interface Dataset { type: DatasourceType; columns: ColumnMeta[]; metrics: Metric[]; - column_format: Record; + column_formats: Record; verbose_map: Record; main_dttm_col: string; // eg. ['["ds", true]', 'ds [asc]'] diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx b/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx index 59f4796a44d21..e27fa95120848 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx @@ -42,7 +42,7 @@ describe('columnChoices()', () => { }, ], verbose_map: {}, - column_format: { fiz: 'NUMERIC', about: 'STRING', foo: 'DATE' }, + column_formats: { fiz: 'NUMERIC', about: 'STRING', foo: 'DATE' }, datasource_name: 'my_datasource', description: 'this is my datasource', }), diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx b/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx index 48b000ed17ffa..765412d592c24 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx @@ -39,7 +39,7 @@ describe('defineSavedMetrics', () => { time_grain_sqla: 'P1D', columns: [], verbose_map: {}, - column_format: {}, + column_formats: {}, datasource_name: 'my_datasource', description: 'this is my datasource', }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/SafeMarkdown.tsx b/superset-frontend/packages/superset-ui-core/src/components/SafeMarkdown.tsx index 929db60b0c7e2..b0826ce2eda54 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/SafeMarkdown.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/SafeMarkdown.tsx @@ -44,12 +44,11 @@ function SafeMarkdown({ htmlSanitization = true, htmlSchemaOverrides = {}, }: SafeMarkdownProps) { - const displayHtml = isFeatureEnabled(FeatureFlag.DISPLAY_MARKDOWN_HTML); const escapeHtml = isFeatureEnabled(FeatureFlag.ESCAPE_MARKDOWN_HTML); const rehypePlugins = useMemo(() => { const rehypePlugins: any = []; - if (displayHtml && !escapeHtml) { + if (!escapeHtml) { rehypePlugins.push(rehypeRaw); if (htmlSanitization) { const schema = getOverrideHtmlSchema( @@ -60,14 +59,14 @@ function SafeMarkdown({ } } return rehypePlugins; - }, [displayHtml, escapeHtml, htmlSanitization, htmlSchemaOverrides]); + }, [escapeHtml, htmlSanitization, htmlSchemaOverrides]); // React Markdown escapes HTML by default return ( {source} diff --git a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts index 132d48c7a6767..2627a42369fd2 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts @@ -22,7 +22,6 @@ export enum FeatureFlag { // PLEASE KEEP THE LIST SORTED ALPHABETICALLY ALERTS_ATTACH_REPORTS = 'ALERTS_ATTACH_REPORTS', ALERT_REPORTS = 'ALERT_REPORTS', - ALLOW_DASHBOARD_DOMAIN_SHARDING = 'ALLOW_DASHBOARD_DOMAIN_SHARDING', ALLOW_FULL_CSV_EXPORT = 'ALLOW_FULL_CSV_EXPORT', CLIENT_CACHE = 'CLIENT_CACHE', DASHBOARD_CROSS_FILTERS = 'DASHBOARD_CROSS_FILTERS', @@ -36,7 +35,6 @@ export enum FeatureFlag { DATAPANEL_CLOSED_BY_DEFAULT = 'DATAPANEL_CLOSED_BY_DEFAULT', DISABLE_DATASET_SOURCE_EDIT = 'DISABLE_DATASET_SOURCE_EDIT', DISABLE_LEGACY_DATASOURCE_EDITOR = 'DISABLE_LEGACY_DATASOURCE_EDITOR', - DISPLAY_MARKDOWN_HTML = 'DISPLAY_MARKDOWN_HTML', DRILL_TO_DETAIL = 'DRILL_TO_DETAIL', DRILL_BY = 'DRILL_BY', DYNAMIC_PLUGINS = 'DYNAMIC_PLUGINS', @@ -50,7 +48,6 @@ export enum FeatureFlag { ENABLE_TEMPLATE_REMOVE_FILTERS = 'ENABLE_TEMPLATE_REMOVE_FILTERS', ESCAPE_MARKDOWN_HTML = 'ESCAPE_MARKDOWN_HTML', ESTIMATE_QUERY_COST = 'ESTIMATE_QUERY_COST', - FORCE_DATABASE_CONNECTIONS_SSL = 'FORCE_DATABASE_CONNECTIONS_SSL', GENERIC_CHART_AXES = 'GENERIC_CHART_AXES', GLOBAL_ASYNC_QUERIES = 'GLOBAL_ASYNC_QUERIES', HORIZONTAL_FILTER_BAR = 'HORIZONTAL_FILTER_BAR', @@ -62,7 +59,6 @@ export enum FeatureFlag { THUMBNAILS = 'THUMBNAILS', USE_ANALAGOUS_COLORS = 'USE_ANALAGOUS_COLORS', TAGGING_SYSTEM = 'TAGGING_SYSTEM', - UX_BETA = 'UX_BETA', VERSIONED_EXPORT = 'VERSIONED_EXPORT', SSH_TUNNELING = 'SSH_TUNNELING', } diff --git a/superset-frontend/packages/superset-ui-core/test/utils/featureFlag.test.ts b/superset-frontend/packages/superset-ui-core/test/utils/featureFlag.test.ts index 66c58e79af567..ef6e0c3729cf9 100644 --- a/superset-frontend/packages/superset-ui-core/test/utils/featureFlag.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/utils/featureFlag.test.ts @@ -25,13 +25,11 @@ it('returns false and raises console error if feature flags have not been initia value: undefined, }); - expect(isFeatureEnabled(FeatureFlag.ALLOW_DASHBOARD_DOMAIN_SHARDING)).toEqual( - false, - ); + expect(isFeatureEnabled(FeatureFlag.DRILL_BY)).toEqual(false); expect(console.error).toHaveBeenCalled(); // @ts-expect-error expect(console.error.mock.calls[0][0]).toEqual( - 'Failed to query feature flag ALLOW_DASHBOARD_DOMAIN_SHARDING', + 'Failed to query feature flag DRILL_BY', ); }); @@ -40,9 +38,7 @@ it('returns false for unset feature flag', () => { value: {}, }); - expect(isFeatureEnabled(FeatureFlag.ALLOW_DASHBOARD_DOMAIN_SHARDING)).toEqual( - false, - ); + expect(isFeatureEnabled(FeatureFlag.DRILL_BY)).toEqual(false); }); it('returns true for set feature flag', () => { diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js index 727954b2d0267..6e2725ad1cbc6 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js @@ -844,12 +844,12 @@ function nvd3Vis(element, props) { } chart.yDomain1([ - yAxisBounds[0] || ticks1[0], - yAxisBounds[1] || ticks1[ticks1.length - 1], + yAxisBounds[0] ?? ticks1[0], + yAxisBounds[1] ?? ticks1[ticks1.length - 1], ]); chart.yDomain2([ - yAxis2Bounds[0] || ticks2[0], - yAxis2Bounds[1] || ticks2[ticks2.length - 1], + yAxis2Bounds[0] ?? ticks2[0], + yAxis2Bounds[1] ?? ticks2[ticks2.length - 1], ]); } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts index 883b4daf9df49..fee6183a2c71f 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts @@ -170,12 +170,12 @@ export default function transformProps( } const rebasedDataA = rebaseForecastDatum(data1, verboseMap); - const rawSeriesA = extractSeries(rebasedDataA, { + const [rawSeriesA] = extractSeries(rebasedDataA, { fillNeighborValue: stack ? 0 : undefined, xAxis: xAxisLabel, }); const rebasedDataB = rebaseForecastDatum(data2, verboseMap); - const rawSeriesB = extractSeries(rebasedDataB, { + const [rawSeriesB] = extractSeries(rebasedDataB, { fillNeighborValue: stackB ? 0 : undefined, xAxis: xAxisLabel, }); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts index 5565fee6a26ea..84b97fae52bdc 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -126,6 +126,7 @@ export default function transformProps( logAxis, markerEnabled, markerSize, + metrics, minorSplitLine, onlyTotal, opacity, @@ -193,7 +194,9 @@ export default function transformProps( getMetricLabel, ); - const rawSeries = extractSeries(rebasedData, { + const isMultiSeries = groupby.length || metrics.length > 1; + + const [rawSeries, sortedTotalValues] = extractSeries(rebasedData, { fillNeighborValue: stack && !forecastEnabled ? 0 : undefined, xAxis: xAxisLabel, extraMetricLabels, @@ -202,8 +205,8 @@ export default function transformProps( isHorizontal, sortSeriesType, sortSeriesAscending, - xAxisSortSeries: groupby.length ? xAxisSortSeries : undefined, - xAxisSortSeriesAscending: groupby.length + xAxisSortSeries: isMultiSeries ? xAxisSortSeries : undefined, + xAxisSortSeriesAscending: isMultiSeries ? xAxisSortSeriesAscending : undefined, }); @@ -240,7 +243,7 @@ export default function transformProps( formatter, showValue, onlyTotal, - totalStackedValues, + totalStackedValues: sortedTotalValues, showValueIndexes, thresholdValues, richTooltip, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts index bca13a0584660..54eaab2812233 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts @@ -23,6 +23,7 @@ import { ContributionType, QueryFormColumn, QueryFormData, + QueryFormMetric, TimeFormatter, TimeGranularity, } from '@superset-ui/core'; @@ -65,6 +66,7 @@ export type EchartsTimeseriesFormData = QueryFormData & { logAxis: boolean; markerEnabled: boolean; markerSize: number; + metrics: QueryFormMetric[]; minorSplitLine: boolean; opacity: number; orderDesc: boolean; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts index ea1691d11c554..41554347d4da1 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts @@ -153,11 +153,12 @@ export function sortAndFilterSeries( export function sortRows( rows: DataRecord[], + totalStackedValues: number[], xAxis: string, xAxisSortSeries: SortSeriesType, xAxisSortSeriesAscending: boolean, ) { - const sortedRows = rows.map(row => { + const sortedRows = rows.map((row, idx) => { let sortKey: DataRecordValue = ''; let aggregate: number | undefined; let entries = 0; @@ -219,6 +220,7 @@ export function sortRows( key: sortKey, value, row, + totalStackedValue: totalStackedValues[idx], }; }); @@ -226,7 +228,7 @@ export function sortRows( sortedRows, ['value'], [xAxisSortSeriesAscending ? 'asc' : 'desc'], - ).map(({ row }) => row); + ).map(({ row, totalStackedValue }) => ({ row, totalStackedValue })); } export function extractSeries( @@ -244,7 +246,7 @@ export function extractSeries( xAxisSortSeries?: SortSeriesType; xAxisSortSeriesAscending?: boolean; } = {}, -): SeriesOption[] { +): [SeriesOption[], number[]] { const { fillNeighborValue, xAxis = DTTM_ALIAS, @@ -258,7 +260,7 @@ export function extractSeries( xAxisSortSeries, xAxisSortSeriesAscending, } = opts; - if (data.length === 0) return []; + if (data.length === 0) return [[], []]; const rows: DataRecord[] = data.map(datum => ({ ...datum, [xAxis]: datum[xAxis], @@ -272,14 +274,23 @@ export function extractSeries( ); const sortedRows = isDefined(xAxisSortSeries) && isDefined(xAxisSortSeriesAscending) - ? sortRows(rows, xAxis, xAxisSortSeries!, xAxisSortSeriesAscending!) - : rows; + ? sortRows( + rows, + totalStackedValues, + xAxis, + xAxisSortSeries!, + xAxisSortSeriesAscending!, + ) + : rows.map((row, idx) => ({ + row, + totalStackedValue: totalStackedValues[idx], + })); - return sortedSeries.map(name => ({ + const finalSeries = sortedSeries.map(name => ({ id: name, name, data: sortedRows - .map((row, idx) => { + .map(({ row, totalStackedValue }, idx) => { const isNextToDefinedValue = isDefined(rows[idx - 1]?.[name]) || isDefined(rows[idx + 1]?.[name]); const isFillNeighborValue = @@ -291,15 +302,19 @@ export function extractSeries( value = fillNeighborValue; } else if ( stack === StackControlsValue.Expand && - totalStackedValues.length > 0 + totalStackedValue !== undefined ) { - value = ((value || 0) as number) / totalStackedValues[idx]; + value = ((value || 0) as number) / totalStackedValue; } return [row[xAxis], value]; }) .filter(obs => !removeNulls || (obs[0] !== null && obs[1] !== null)) .map(obs => (isHorizontal ? [obs[1], obs[0]] : obs)), })); + return [ + finalSeries, + sortedRows.map(({ totalStackedValue }) => totalStackedValue), + ]; } export function formatSeriesName( diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts index 69570ff0074e3..8a2229fbe0580 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts @@ -56,83 +56,165 @@ const sortData: DataRecord[] = [ { my_x_axis: null, x: 4, y: 3, z: 7 }, ]; +const totalStackedValues = [3, 15, 14]; + test('sortRows by name ascending', () => { - expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Name, true)).toEqual([ - { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, - { my_x_axis: 'foo', x: null, y: 10, z: 5 }, - { my_x_axis: null, x: 4, y: 3, z: 7 }, + expect( + sortRows( + sortData, + totalStackedValues, + 'my_x_axis', + SortSeriesType.Name, + true, + ), + ).toEqual([ + { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 }, + { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 }, + { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 }, ]); }); test('sortRows by name descending', () => { - expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Name, false)).toEqual([ - { my_x_axis: null, x: 4, y: 3, z: 7 }, - { my_x_axis: 'foo', x: null, y: 10, z: 5 }, - { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, + expect( + sortRows( + sortData, + totalStackedValues, + 'my_x_axis', + SortSeriesType.Name, + false, + ), + ).toEqual([ + { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 }, + { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 }, + { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 }, ]); }); test('sortRows by sum ascending', () => { - expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Sum, true)).toEqual([ - { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, - { my_x_axis: null, x: 4, y: 3, z: 7 }, - { my_x_axis: 'foo', x: null, y: 10, z: 5 }, + expect( + sortRows( + sortData, + totalStackedValues, + 'my_x_axis', + SortSeriesType.Sum, + true, + ), + ).toEqual([ + { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 }, + { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 }, + { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 }, ]); }); test('sortRows by sum descending', () => { - expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Sum, false)).toEqual([ - { my_x_axis: 'foo', x: null, y: 10, z: 5 }, - { my_x_axis: null, x: 4, y: 3, z: 7 }, - { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, + expect( + sortRows( + sortData, + totalStackedValues, + 'my_x_axis', + SortSeriesType.Sum, + false, + ), + ).toEqual([ + { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 }, + { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 }, + { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 }, ]); }); test('sortRows by avg ascending', () => { - expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Avg, true)).toEqual([ - { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, - { my_x_axis: null, x: 4, y: 3, z: 7 }, - { my_x_axis: 'foo', x: null, y: 10, z: 5 }, + expect( + sortRows( + sortData, + totalStackedValues, + 'my_x_axis', + SortSeriesType.Avg, + true, + ), + ).toEqual([ + { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 }, + { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 }, + { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 }, ]); }); test('sortRows by avg descending', () => { - expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Avg, false)).toEqual([ - { my_x_axis: 'foo', x: null, y: 10, z: 5 }, - { my_x_axis: null, x: 4, y: 3, z: 7 }, - { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, + expect( + sortRows( + sortData, + totalStackedValues, + 'my_x_axis', + SortSeriesType.Avg, + false, + ), + ).toEqual([ + { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 }, + { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 }, + { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 }, ]); }); test('sortRows by min ascending', () => { - expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Min, true)).toEqual([ - { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, - { my_x_axis: null, x: 4, y: 3, z: 7 }, - { my_x_axis: 'foo', x: null, y: 10, z: 5 }, + expect( + sortRows( + sortData, + totalStackedValues, + 'my_x_axis', + SortSeriesType.Min, + true, + ), + ).toEqual([ + { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 }, + { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 }, + { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 }, ]); }); test('sortRows by min descending', () => { - expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Min, false)).toEqual([ - { my_x_axis: 'foo', x: null, y: 10, z: 5 }, - { my_x_axis: null, x: 4, y: 3, z: 7 }, - { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, + expect( + sortRows( + sortData, + totalStackedValues, + 'my_x_axis', + SortSeriesType.Min, + false, + ), + ).toEqual([ + { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 }, + { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 }, + { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 }, ]); }); test('sortRows by max ascending', () => { - expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Min, true)).toEqual([ - { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, - { my_x_axis: null, x: 4, y: 3, z: 7 }, - { my_x_axis: 'foo', x: null, y: 10, z: 5 }, + expect( + sortRows( + sortData, + totalStackedValues, + 'my_x_axis', + SortSeriesType.Min, + true, + ), + ).toEqual([ + { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 }, + { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 }, + { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 }, ]); }); test('sortRows by max descending', () => { - expect(sortRows(sortData, 'my_x_axis', SortSeriesType.Min, false)).toEqual([ - { my_x_axis: 'foo', x: null, y: 10, z: 5 }, - { my_x_axis: null, x: 4, y: 3, z: 7 }, - { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, + expect( + sortRows( + sortData, + totalStackedValues, + 'my_x_axis', + SortSeriesType.Min, + false, + ), + ).toEqual([ + { row: { my_x_axis: 'foo', x: null, y: 10, z: 5 }, totalStackedValue: 15 }, + { row: { my_x_axis: null, x: 4, y: 3, z: 7 }, totalStackedValue: 14 }, + { row: { my_x_axis: 'abc', x: 1, y: 0, z: 2 }, totalStackedValue: 3 }, ]); }); @@ -215,25 +297,29 @@ describe('extractSeries', () => { abc: 5, }, ]; - expect(extractSeries(data)).toEqual([ - { - id: 'Hulk', - name: 'Hulk', - data: [ - ['2000-01-01', null], - ['2000-02-01', 2], - ['2000-03-01', 1], - ], - }, - { - id: 'abc', - name: 'abc', - data: [ - ['2000-01-01', 2], - ['2000-02-01', 10], - ['2000-03-01', 5], - ], - }, + const totalStackedValues = [2, 12, 6]; + expect(extractSeries(data, { totalStackedValues })).toEqual([ + [ + { + id: 'Hulk', + name: 'Hulk', + data: [ + ['2000-01-01', null], + ['2000-02-01', 2], + ['2000-03-01', 1], + ], + }, + { + id: 'abc', + name: 'abc', + data: [ + ['2000-01-01', 2], + ['2000-02-01', 10], + ['2000-03-01', 5], + ], + }, + ], + totalStackedValues, ]); }); @@ -255,20 +341,30 @@ describe('extractSeries', () => { abc: 5, }, ]; - expect(extractSeries(data, { xAxis: 'x', removeNulls: true })).toEqual([ - { - id: 'Hulk', - name: 'Hulk', - data: [[2, 1]], - }, - { - id: 'abc', - name: 'abc', - data: [ - [1, 2], - [2, 5], - ], - }, + const totalStackedValues = [3, 12, 8]; + expect( + extractSeries(data, { + totalStackedValues, + xAxis: 'x', + removeNulls: true, + }), + ).toEqual([ + [ + { + id: 'Hulk', + name: 'Hulk', + data: [[2, 1]], + }, + { + id: 'abc', + name: 'abc', + data: [ + [1, 2], + [2, 5], + ], + }, + ], + totalStackedValues, ]); }); @@ -315,23 +411,29 @@ describe('extractSeries', () => { abc: null, }, ]; - expect(extractSeries(data, { fillNeighborValue: 0 })).toEqual([ - { - id: 'abc', - name: 'abc', - data: [ - ['2000-01-01', null], - ['2000-02-01', 0], - ['2000-03-01', 1], - ['2000-04-01', 0], - ['2000-05-01', null], - ['2000-06-01', 0], - ['2000-07-01', 2], - ['2000-08-01', 3], - ['2000-09-01', 0], - ['2000-10-01', null], - ], - }, + const totalStackedValues = [0, 0, 1, 0, 0, 0, 2, 3, 0, 0]; + expect( + extractSeries(data, { totalStackedValues, fillNeighborValue: 0 }), + ).toEqual([ + [ + { + id: 'abc', + name: 'abc', + data: [ + ['2000-01-01', null], + ['2000-02-01', 0], + ['2000-03-01', 1], + ['2000-04-01', 0], + ['2000-05-01', null], + ['2000-06-01', 0], + ['2000-07-01', 2], + ['2000-08-01', 3], + ['2000-09-01', 0], + ['2000-10-01', null], + ], + }, + ], + totalStackedValues, ]); }); }); diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx index 8ec1cb9ad11d4..9d5718120f665 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx @@ -17,22 +17,24 @@ * under the License. */ import React, { useCallback, useMemo } from 'react'; -import { PlusSquareOutlined, MinusSquareOutlined } from '@ant-design/icons'; +import { MinusSquareOutlined, PlusSquareOutlined } from '@ant-design/icons'; import { AdhocMetric, + BinaryQueryObjectFilterClause, DataRecordValue, + FeatureFlag, getColumnLabel, getNumberFormatter, + getSelectedText, + isAdhocColumn, + isFeatureEnabled, isPhysicalColumn, NumberFormatter, styled, - useTheme, - isAdhocColumn, - BinaryQueryObjectFilterClause, t, - getSelectedText, + useTheme, } from '@superset-ui/core'; -import { PivotTable, sortAs, aggregatorTemplates } from './react-pivottable'; +import { aggregatorTemplates, PivotTable, sortAs } from './react-pivottable'; import { FilterType, MetricsLayoutEnum, @@ -407,7 +409,10 @@ export default function PivotTableChart(props: PivotTableProps) { clickColumnHeaderCallback: toggleFilter, colTotals, rowTotals, - highlightHeaderCellsOnHover: emitCrossFilters, + highlightHeaderCellsOnHover: + emitCrossFilters || + isFeatureEnabled(FeatureFlag.DRILL_BY) || + isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL), highlightedHeaderCells: selectedFilters, omittedHighlightHeaderGroups: [METRIC_KEY], cellColorFormatters: { [METRIC_KEY]: metricColorFormatters }, diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.jsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.jsx index 4ca3f6d5af84b..8915ed7e9c986 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.jsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.jsx @@ -398,11 +398,10 @@ export class TableRenderer extends React.Component { const colSpan = attrIdx < colKey.length ? colAttrSpans[i][attrIdx] : 1; let colLabelClass = 'pvtColLabel'; if (attrIdx < colKey.length) { - if ( - highlightHeaderCellsOnHover && - !omittedHighlightHeaderGroups.includes(colAttrs[attrIdx]) - ) { - colLabelClass += ' hoverable'; + if (!omittedHighlightHeaderGroups.includes(colAttrs[attrIdx])) { + if (highlightHeaderCellsOnHover) { + colLabelClass += ' hoverable'; + } handleContextMenu = e => this.props.onContextMenu(e, colKey, undefined, { [attrName]: colKey[attrIdx], @@ -598,11 +597,10 @@ export class TableRenderer extends React.Component { const attrValueCells = rowKey.map((r, i) => { let handleContextMenu; let valueCellClassName = 'pvtRowLabel'; - if ( - highlightHeaderCellsOnHover && - !omittedHighlightHeaderGroups.includes(rowAttrs[i]) - ) { - valueCellClassName += ' hoverable'; + if (!omittedHighlightHeaderGroups.includes(rowAttrs[i])) { + if (highlightHeaderCellsOnHover) { + valueCellClassName += ' hoverable'; + } handleContextMenu = e => this.props.onContextMenu(e, undefined, rowKey, { [rowAttrs[i]]: r, diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js index 260f9944f9b04..7ec2e77616a10 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.js @@ -50,7 +50,6 @@ export const EXPAND_TABLE = 'EXPAND_TABLE'; export const COLLAPSE_TABLE = 'COLLAPSE_TABLE'; export const QUERY_EDITOR_SETDB = 'QUERY_EDITOR_SETDB'; export const QUERY_EDITOR_SET_SCHEMA = 'QUERY_EDITOR_SET_SCHEMA'; -export const QUERY_EDITOR_SET_TABLE_OPTIONS = 'QUERY_EDITOR_SET_TABLE_OPTIONS'; export const QUERY_EDITOR_SET_TITLE = 'QUERY_EDITOR_SET_TITLE'; export const QUERY_EDITOR_SET_AUTORUN = 'QUERY_EDITOR_SET_AUTORUN'; export const QUERY_EDITOR_SET_SQL = 'QUERY_EDITOR_SET_SQL'; @@ -80,6 +79,7 @@ export const STOP_QUERY = 'STOP_QUERY'; export const REQUEST_QUERY_RESULTS = 'REQUEST_QUERY_RESULTS'; export const QUERY_SUCCESS = 'QUERY_SUCCESS'; export const QUERY_FAILED = 'QUERY_FAILED'; +export const CLEAR_INACTIVE_QUERIES = 'CLEAR_INACTIVE_QUERIES'; export const CLEAR_QUERY_RESULTS = 'CLEAR_QUERY_RESULTS'; export const REMOVE_DATA_PREVIEW = 'REMOVE_DATA_PREVIEW'; export const CHANGE_DATA_PREVIEW_ID = 'CHANGE_DATA_PREVIEW_ID'; @@ -219,6 +219,10 @@ export function estimateQueryCost(queryEditor) { }; } +export function clearInactiveQueries() { + return { type: CLEAR_INACTIVE_QUERIES }; +} + export function startQuery(query) { Object.assign(query, { id: query.id ? query.id : shortid.generate(), @@ -952,10 +956,6 @@ export function queryEditorSetSchema(queryEditor, schema) { }; } -export function queryEditorSetTableOptions(queryEditor, options) { - return { type: QUERY_EDITOR_SET_TABLE_OPTIONS, queryEditor, options }; -} - export function queryEditorSetAutorun(queryEditor, autorun) { return function (dispatch) { const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx index d14d532dcd81b..b60faacbe043f 100644 --- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx +++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx @@ -39,7 +39,7 @@ import { FullSQLEditor as AceEditor, } from 'src/components/AsyncAceEditor'; import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor'; -import { useSchemas } from 'src/hooks/apiResources'; +import { useSchemas, useTables } from 'src/hooks/apiResources'; type HotKey = { key: string; @@ -65,9 +65,6 @@ const StyledAceEditor = styled(AceEditor)` // double class is better than !important border: 1px solid ${theme.colors.grayscale.light2}; font-feature-settings: 'liga' off, 'calt' off; - // Fira Code causes problem with Ace under Firefox - font-family: 'Menlo', 'Consolas', 'Courier New', 'Ubuntu Mono', - 'source-code-pro', 'Lucida Console', monospace; &.ace_autocomplete { // Use !important because Ace Editor applies extra CSS at the last second @@ -99,11 +96,19 @@ const AceEditorWrapper = ({ 'dbId', 'sql', 'functionNames', - 'tableOptions', 'validationResult', 'schema', ]); - const { data: schemaOptions } = useSchemas({ dbId: queryEditor.dbId }); + const { data: schemaOptions } = useSchemas({ + ...(autocomplete && { dbId: queryEditor.dbId }), + }); + const { data: tableData } = useTables({ + ...(autocomplete && { + dbId: queryEditor.dbId, + schema: queryEditor.schema, + }), + }); + const currentSql = queryEditor.sql ?? ''; const functionNames = queryEditor.functionNames ?? []; @@ -120,7 +125,7 @@ const AceEditorWrapper = ({ }), [schemaOptions], ); - const tables = queryEditor.tableOptions ?? []; + const tables = tableData?.options ?? []; const [sql, setSql] = useState(currentSql); const [words, setWords] = useState([]); diff --git a/superset-frontend/src/SqlLab/components/App/index.jsx b/superset-frontend/src/SqlLab/components/App/index.jsx index 4689f8ec21912..bbd1bba9aead2 100644 --- a/superset-frontend/src/SqlLab/components/App/index.jsx +++ b/superset-frontend/src/SqlLab/components/App/index.jsx @@ -168,7 +168,7 @@ class App extends React.PureComponent { } render() { - const { queries, actions, queriesLastUpdate } = this.props; + const { queries, queriesLastUpdate } = this.props; if (this.state.hash && this.state.hash === '#search') { return window.location.replace('/superset/sqllab/history/'); } @@ -176,7 +176,6 @@ class App extends React.PureComponent { diff --git a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx index 32bf401f22139..9b2c1feaefdaf 100644 --- a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx +++ b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx @@ -16,15 +16,26 @@ * specific language governing permissions and limitations * under the License. */ +import fetchMock from 'fetch-mock'; import React from 'react'; -import { render } from '@testing-library/react'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { render, waitFor } from 'spec/helpers/testing-library'; +import { + CLEAR_INACTIVE_QUERIES, + REFRESH_QUERIES, +} from 'src/SqlLab/actions/sqlLab'; import QueryAutoRefresh, { isQueryRunning, shouldCheckForQueries, + QUERY_UPDATE_FREQ, } from 'src/SqlLab/components/QueryAutoRefresh'; import { successfulQuery, runningQuery } from 'src/SqlLab/fixtures'; import { QueryDictionary } from 'src/SqlLab/types'; +const middlewares = [thunk]; +const mockStore = configureStore(middlewares); + // NOTE: The uses of @ts-ignore in this file is to enable testing of bad inputs to verify the // function / component handles bad data elegantly describe('QueryAutoRefresh', () => { @@ -34,10 +45,14 @@ describe('QueryAutoRefresh', () => { const successfulQueries: QueryDictionary = {}; successfulQueries[successfulQuery.id] = successfulQuery; - const refreshQueries = jest.fn(); - const queriesLastUpdate = Date.now(); + const refreshApi = 'glob:*/api/v1/query/updated_since?*'; + + afterEach(() => { + fetchMock.reset(); + }); + it('isQueryRunning returns true for valid running query', () => { const running = isQueryRunning(runningQuery); expect(running).toBe(true); @@ -91,43 +106,119 @@ describe('QueryAutoRefresh', () => { ).toBe(false); }); - it('Attempts to refresh when given pending query', () => { + it('Attempts to refresh when given pending query', async () => { + const store = mockStore(); + fetchMock.get(refreshApi, { + result: [ + { + id: runningQuery.id, + status: 'success', + }, + ], + }); + render( , + { useRedux: true, store }, + ); + await waitFor( + () => + expect(store.getActions()).toContainEqual( + expect.objectContaining({ + type: REFRESH_QUERIES, + }), + ), + { timeout: QUERY_UPDATE_FREQ + 100 }, ); - setTimeout(() => { - expect(refreshQueries).toHaveBeenCalled(); - }, 1000); }); - it('Does not fail and attempts to refresh when given pending query and invlaid query', () => { + it('Attempts to clear inactive queries when updated queries are empty', async () => { + const store = mockStore(); + fetchMock.get(refreshApi, { + result: [], + }); + + render( + , + { useRedux: true, store }, + ); + await waitFor( + () => + expect(store.getActions()).toContainEqual( + expect.objectContaining({ + type: CLEAR_INACTIVE_QUERIES, + }), + ), + { timeout: QUERY_UPDATE_FREQ + 100 }, + ); + expect( + store.getActions().filter(({ type }) => type === REFRESH_QUERIES), + ).toHaveLength(0); + expect(fetchMock.calls(refreshApi)).toHaveLength(1); + }); + + it('Does not fail and attempts to refresh when given pending query and invlaid query', async () => { + const store = mockStore(); + fetchMock.get(refreshApi, { + result: [ + { + id: runningQuery.id, + status: 'success', + }, + ], + }); + render( , + { useRedux: true, store }, + ); + await waitFor( + () => + expect(store.getActions()).toContainEqual( + expect.objectContaining({ + type: REFRESH_QUERIES, + }), + ), + { timeout: QUERY_UPDATE_FREQ + 100 }, ); - setTimeout(() => { - expect(refreshQueries).toHaveBeenCalled(); - }, 1000); }); - it('Does NOT Attempt to refresh when given only completed queries', () => { + it('Does NOT Attempt to refresh when given only completed queries', async () => { + const store = mockStore(); + fetchMock.get(refreshApi, { + result: [ + { + id: runningQuery.id, + status: 'success', + }, + ], + }); render( , + { useRedux: true, store }, + ); + await waitFor( + () => + expect(store.getActions()).toContainEqual( + expect.objectContaining({ + type: CLEAR_INACTIVE_QUERIES, + }), + ), + { timeout: QUERY_UPDATE_FREQ + 100 }, ); - setTimeout(() => { - expect(refreshQueries).not.toHaveBeenCalled(); - }, 1000); + expect(fetchMock.calls(refreshApi)).toHaveLength(0); }); }); diff --git a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx index 2d01e724e2479..65a6d11a1d1a8 100644 --- a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx +++ b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { useState } from 'react'; +import { useRef } from 'react'; +import { useDispatch } from 'react-redux'; import { isObject } from 'lodash'; import rison from 'rison'; import { @@ -27,19 +28,18 @@ import { } from '@superset-ui/core'; import { QueryDictionary } from 'src/SqlLab/types'; import useInterval from 'src/SqlLab/utils/useInterval'; +import { + refreshQueries, + clearInactiveQueries, +} from 'src/SqlLab/actions/sqlLab'; -const QUERY_UPDATE_FREQ = 2000; +export const QUERY_UPDATE_FREQ = 2000; const QUERY_UPDATE_BUFFER_MS = 5000; const MAX_QUERY_AGE_TO_POLL = 21600000; const QUERY_TIMEOUT_LIMIT = 10000; -interface RefreshQueriesFunc { - (alteredQueries: any): any; -} - export interface QueryAutoRefreshProps { queries: QueryDictionary; - refreshQueries: RefreshQueriesFunc; queriesLastUpdate: number; } @@ -61,20 +61,22 @@ export const shouldCheckForQueries = (queryList: QueryDictionary): boolean => { function QueryAutoRefresh({ queries, - refreshQueries, queriesLastUpdate, }: QueryAutoRefreshProps) { // We do not want to spam requests in the case of slow connections and potentially receive responses out of order // pendingRequest check ensures we only have one active http call to check for query statuses - const [pendingRequest, setPendingRequest] = useState(false); + const pendingRequestRef = useRef(false); + const cleanInactiveRequestRef = useRef(false); + const dispatch = useDispatch(); const checkForRefresh = () => { - if (!pendingRequest && shouldCheckForQueries(queries)) { + const shouldRequestChecking = shouldCheckForQueries(queries); + if (!pendingRequestRef.current && shouldRequestChecking) { const params = rison.encode({ last_updated_ms: queriesLastUpdate - QUERY_UPDATE_BUFFER_MS, }); - setPendingRequest(true); + pendingRequestRef.current = true; SupersetClient.get({ endpoint: `/api/v1/query/updated_since?q=${params}`, timeout: QUERY_TIMEOUT_LIMIT, @@ -82,19 +84,27 @@ function QueryAutoRefresh({ .then(({ json }) => { if (json) { const jsonPayload = json as { result?: QueryResponse[] }; - const queries = - jsonPayload?.result?.reduce((acc, current) => { - acc[current.id] = current; - return acc; - }, {}) ?? {}; - refreshQueries?.(queries); + if (jsonPayload?.result?.length) { + const queries = + jsonPayload?.result?.reduce((acc, current) => { + acc[current.id] = current; + return acc; + }, {}) ?? {}; + dispatch(refreshQueries(queries)); + } else { + dispatch(clearInactiveQueries()); + } } }) .catch(() => {}) .finally(() => { - setPendingRequest(false); + pendingRequestRef.current = false; }); } + if (!cleanInactiveRequestRef.current && !shouldRequestChecking) { + dispatch(clearInactiveQueries()); + cleanInactiveRequestRef.current = true; + } }; // Solves issue where direct usage of setInterval in function components diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx index 402e26462e041..b1ab18eb09a94 100644 --- a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx +++ b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx @@ -174,6 +174,7 @@ export const SaveDatasetModal = ({ const [selectedDatasetToOverwrite, setSelectedDatasetToOverwrite] = useState< SelectValue | undefined >(undefined); + const [loading, setLoading] = useState(false); const user = useSelector(user => getInitialState(user), @@ -197,6 +198,7 @@ export const SaveDatasetModal = ({ setShouldOverwriteDataset(true); return; } + setLoading(true); const [, key] = await Promise.all([ updateDataset( datasource?.dbId, @@ -220,6 +222,7 @@ export const SaveDatasetModal = ({ }), }), ]); + setLoading(false); const url = mountExploreUrl(null, { [URL_PARAMS.formDataKey.name]: key, @@ -269,6 +272,7 @@ export const SaveDatasetModal = ({ ); const handleSaveInDataset = () => { + setLoading(true); const selectedColumns = datasource?.columns ?? []; // The filters param is only used to test jinja templates. @@ -306,6 +310,7 @@ export const SaveDatasetModal = ({ }), ) .then((key: string) => { + setLoading(false); const url = mountExploreUrl(null, { [URL_PARAMS.formDataKey.name]: key, }); @@ -314,6 +319,7 @@ export const SaveDatasetModal = ({ onHide(); }) .catch(() => { + setLoading(false); addDangerToast(t('An error occurred saving dataset')); }); }; @@ -356,6 +362,7 @@ export const SaveDatasetModal = ({ disabled={disableSaveAndExploreBtn} buttonStyle="primary" onClick={handleSaveInDataset} + loading={loading} > {buttonTextOnSave} @@ -370,6 +377,7 @@ export const SaveDatasetModal = ({ buttonStyle="primary" onClick={handleOverwriteDataset} disabled={disableSaveAndExploreBtn} + loading={loading} > {buttonTextOnOverwrite} diff --git a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx index b513637b27385..4071b9e2d71d4 100644 --- a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx +++ b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx @@ -80,7 +80,6 @@ const SaveQuery = ({ 'schema', 'selectedText', 'sql', - 'tableOptions', 'templateParams', ]); const query = useMemo( diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx index 67f25d4d396c1..99551b9d41e81 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx @@ -25,7 +25,6 @@ import thunk from 'redux-thunk'; import configureStore from 'redux-mock-store'; import fetchMock from 'fetch-mock'; import { - SET_QUERY_EDITOR_SQL_DEBOUNCE_MS, SQL_EDITOR_GUTTER_HEIGHT, SQL_EDITOR_GUTTER_MARGIN, SQL_TOOLBAR_HEIGHT, @@ -33,6 +32,8 @@ import { import AceEditorWrapper from 'src/SqlLab/components/AceEditorWrapper'; import SouthPane from 'src/SqlLab/components/SouthPane'; import SqlEditor from 'src/SqlLab/components/SqlEditor'; +import { setupStore } from 'src/views/store'; +import { reducers } from 'src/SqlLab/reducers'; import QueryProvider from 'src/views/QueryProvider'; import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; import { @@ -41,6 +42,7 @@ import { table, defaultQueryEditor, } from 'src/SqlLab/fixtures'; +import SqlEditorLeftBar from 'src/SqlLab/components/SqlEditorLeftBar'; jest.mock('src/components/AsyncAceEditor', () => ({ ...jest.requireActual('src/components/AsyncAceEditor'), @@ -54,9 +56,7 @@ jest.mock('src/components/AsyncAceEditor', () => ({ ), })); -jest.mock('src/SqlLab/components/SqlEditorLeftBar', () => () => ( -
-)); +jest.mock('src/SqlLab/components/SqlEditorLeftBar', () => jest.fn()); const MOCKED_SQL_EDITOR_HEIGHT = 500; @@ -66,7 +66,7 @@ fetchMock.post('glob:*/sqllab/execute/*', { result: [] }); const middlewares = [thunk]; const mockStore = configureStore(middlewares); -const store = mockStore({ +const mockInitialState = { ...initialState, sqlLab: { ...initialState.sqlLab, @@ -89,7 +89,8 @@ const store = mockStore({ dbId: 'dbid1', }, }, -}); +}; +const store = mockStore(mockInitialState); const setup = (props = {}, store) => render(, { @@ -110,6 +111,13 @@ describe('SqlEditor', () => { displayLimit: 100, }; + beforeEach(() => { + SqlEditorLeftBar.mockClear(); + SqlEditorLeftBar.mockImplementation(() => ( +
+ )); + }); + const buildWrapper = (props = {}) => mount( @@ -143,6 +151,21 @@ describe('SqlEditor', () => { expect(await findByTestId('react-ace')).toBeInTheDocument(); }); + it('avoids rerendering EditorLeftBar while typing', async () => { + const sqlLabStore = setupStore({ + initialState: mockInitialState, + rootReducers: reducers, + }); + const { findByTestId } = setup(mockedProps, sqlLabStore); + const editor = await findByTestId('react-ace'); + const sql = 'select *'; + const renderCount = SqlEditorLeftBar.mock.calls.length; + expect(SqlEditorLeftBar).toHaveBeenCalledTimes(renderCount); + fireEvent.change(editor, { target: { value: sql } }); + // Verify the rendering regression + expect(SqlEditorLeftBar).toHaveBeenCalledTimes(renderCount); + }); + it('renders sql from unsaved change', async () => { const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE'; const { findByTestId } = setup( @@ -173,6 +196,7 @@ describe('SqlEditor', () => { }, }), ); + const editor = await findByTestId('react-ace'); expect(editor).toHaveValue(expectedSql); }); @@ -184,28 +208,6 @@ describe('SqlEditor', () => { ).toBeInTheDocument(); }); - it('triggers setQueryEditorAndSaveSql with debounced call to avoid performance regression', async () => { - const { findByTestId } = setup(mockedProps, store); - const editor = await findByTestId('react-ace'); - const sql = 'select *'; - fireEvent.change(editor, { target: { value: sql } }); - // Verify no immediate sql update triggered - expect( - store.getActions().filter(({ type }) => type === 'QUERY_EDITOR_SET_SQL'), - ).toHaveLength(0); - await waitFor( - () => - expect( - store - .getActions() - .filter(({ type }) => type === 'QUERY_EDITOR_SET_SQL'), - ).toHaveLength(1), - { - timeout: SET_QUERY_EDITOR_SQL_DEBOUNCE_MS + 100, - }, - ); - }); - it('runs query action with ctas false', async () => { const expectedStore = mockStore({ ...initialState, diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx index ba6d89511d1b5..d23c3099208eb 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx @@ -26,7 +26,7 @@ import React, { useCallback, } from 'react'; import { CSSTransition } from 'react-transition-group'; -import { useDispatch, useSelector } from 'react-redux'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import Split from 'react-split'; import { css, FeatureFlag, styled, t, useTheme } from '@superset-ui/core'; @@ -49,6 +49,7 @@ import { persistEditorHeight, postStopQuery, queryEditorSetAutorun, + queryEditorSetSql, queryEditorSetAndSaveSql, queryEditorSetTemplateParams, runQueryFromSqlEditor, @@ -229,6 +230,7 @@ const SqlEditor = ({ hideLeftBar, }; }, + shallowEqual, ); const [height, setHeight] = useState(0); @@ -455,6 +457,7 @@ const SqlEditor = ({ ); const onSqlChanged = sql => { + dispatch(queryEditorSetSql(queryEditor, sql)); setQueryEditorAndSaveSqlWithDebounce(sql); // Request server-side validation of the query text if (canValidateQuery()) { diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx index 0cacdb86caf0c..1298722d5dd2d 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx @@ -35,7 +35,6 @@ import { collapseTable, expandTable, queryEditorSetSchema, - queryEditorSetTableOptions, setDatabases, addDangerToast, resetState, @@ -218,15 +217,6 @@ const SqlEditorLeftBar = ({ [dispatch, queryEditor], ); - const handleTablesLoad = useCallback( - (options: Array) => { - if (queryEditor) { - dispatch(queryEditorSetTableOptions(queryEditor, options)); - } - }, - [dispatch, queryEditor], - ); - const handleDbList = useCallback( (result: DatabaseObject) => { dispatch(setDatabases(result)); @@ -256,7 +246,6 @@ const SqlEditorLeftBar = ({ onDbChange={onDbChange} onSchemaChange={handleSchemaChange} onTableSelectChange={onTablesChange} - onTablesLoad={handleTablesLoad} schema={schema} tableValue={selectedTableNames} sqlLabMode diff --git a/superset-frontend/src/SqlLab/fixtures.ts b/superset-frontend/src/SqlLab/fixtures.ts index ba88a41b0accc..cfd15bedbe70a 100644 --- a/superset-frontend/src/SqlLab/fixtures.ts +++ b/superset-frontend/src/SqlLab/fixtures.ts @@ -185,7 +185,6 @@ export const defaultQueryEditor = { name: 'Untitled Query 1', schema: 'main', remoteId: null, - tableOptions: [], functionNames: [], hideLeftBar: false, templateParams: '{}', diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.js b/superset-frontend/src/SqlLab/reducers/sqlLab.js index ff2cb340bbd08..eafc326aaa0d1 100644 --- a/superset-frontend/src/SqlLab/reducers/sqlLab.js +++ b/superset-frontend/src/SqlLab/reducers/sqlLab.js @@ -587,18 +587,6 @@ export default function sqlLabReducer(state = {}, action) { ), }; }, - [actions.QUERY_EDITOR_SET_TABLE_OPTIONS]() { - return { - ...state, - ...alterUnsavedQueryEditorState( - state, - { - tableOptions: action.options, - }, - action.queryEditor.id, - ), - }; - }, [actions.QUERY_EDITOR_SET_TITLE]() { return { ...state, @@ -742,6 +730,21 @@ export default function sqlLabReducer(state = {}, action) { } return { ...state, queries: newQueries, queriesLastUpdate }; }, + [actions.CLEAR_INACTIVE_QUERIES]() { + const { queries } = state; + const cleanedQueries = Object.fromEntries( + Object.entries(queries).filter(([, query]) => { + if ( + ['running', 'pending'].includes(query.state) && + query.progress === 0 + ) { + return false; + } + return true; + }), + ); + return { ...state, queries: cleanedQueries }; + }, [actions.SET_USER_OFFLINE]() { return { ...state, offline: action.offline }; }, diff --git a/superset-frontend/src/SqlLab/types.ts b/superset-frontend/src/SqlLab/types.ts index ab63e1c76080a..e209be04be504 100644 --- a/superset-frontend/src/SqlLab/types.ts +++ b/superset-frontend/src/SqlLab/types.ts @@ -39,7 +39,6 @@ export interface QueryEditor { autorun: boolean; sql: string; remoteId: number | null; - tableOptions: any[]; functionNames: string[]; validationResult?: { completed: boolean; diff --git a/superset-frontend/src/components/AsyncAceEditor/index.tsx b/superset-frontend/src/components/AsyncAceEditor/index.tsx index dc5a37a61460c..297ff7b552223 100644 --- a/superset-frontend/src/components/AsyncAceEditor/index.tsx +++ b/superset-frontend/src/components/AsyncAceEditor/index.tsx @@ -84,6 +84,7 @@ export type AsyncAceEditorOptions = { defaultMode?: AceEditorMode; defaultTheme?: AceEditorTheme; defaultTabSize?: number; + fontFamily?: string; placeholder?: React.ComponentType< PlaceholderProps & Partial > | null; @@ -98,6 +99,7 @@ export default function AsyncAceEditor( defaultMode, defaultTheme, defaultTabSize = 2, + fontFamily = 'Menlo, Consolas, Courier New, Ubuntu Mono, source-code-pro, Lucida Console, monospace', placeholder, }: AsyncAceEditorOptions = {}, ) { @@ -153,6 +155,7 @@ export default function AsyncAceEditor( theme={theme} tabSize={tabSize} defaultValue={defaultValue} + setOptions={{ fontFamily }} {...props} /> ); diff --git a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx index 063ed787b13a8..6ab80aad9e7ee 100644 --- a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx +++ b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx @@ -234,8 +234,7 @@ const ChartContextMenu = ( } menuItems.push( ) => render( , @@ -133,7 +132,7 @@ test('render disabled menu item for unsupported chart', async () => { }); test('render disabled menu item for supported chart, no filters', async () => { - renderMenu({ filters: [] }); + renderMenu({ drillByConfig: { filters: [], groupbyFieldName: 'groupby' } }); await expectDrillByDisabled('Drill by is not available for this data point'); }); @@ -237,6 +236,6 @@ test('When menu item is clicked, call onSelection with clicked column and drill column_name: 'col1', groupby: true, }, - defaultFilters, + { filters: defaultFilters, groupbyFieldName: 'groupby' }, ); }); diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx index 9b57e2fdec722..17e013d29f1ff 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx @@ -29,8 +29,8 @@ import { Menu } from 'src/components/Menu'; import { BaseFormData, Behavior, - BinaryQueryObjectFilterClause, Column, + ContextMenuFilters, css, ensureIsArray, getChartMetadataRegistry, @@ -39,6 +39,7 @@ import { } from '@superset-ui/core'; import Icons from 'src/components/Icons'; import { Input } from 'src/components/Input'; +import { useToasts } from 'src/components/MessageToasts/withToasts'; import { cachedSupersetGet, supersetGetCache, @@ -54,11 +55,10 @@ const SHOW_COLUMNS_SEARCH_THRESHOLD = 10; const SEARCH_INPUT_HEIGHT = 48; export interface DrillByMenuItemsProps { - filters?: BinaryQueryObjectFilterClause[]; + drillByConfig?: ContextMenuFilters['drillBy']; formData: BaseFormData & { [key: string]: any }; contextMenuY?: number; submenuIndex?: number; - groupbyFieldName?: string; onSelection?: (...args: any) => void; onClick?: (event: MouseEvent) => void; openNewModal?: boolean; @@ -66,8 +66,7 @@ export interface DrillByMenuItemsProps { } export const DrillByMenuItems = ({ - filters, - groupbyFieldName, + drillByConfig, formData, contextMenuY = 0, submenuIndex = 0, @@ -78,22 +77,22 @@ export const DrillByMenuItems = ({ ...rest }: DrillByMenuItemsProps) => { const theme = useTheme(); + const { addDangerToast } = useToasts(); const [searchInput, setSearchInput] = useState(''); const [dataset, setDataset] = useState(); const [columns, setColumns] = useState([]); const [showModal, setShowModal] = useState(false); const [currentColumn, setCurrentColumn] = useState(); - const handleSelection = useCallback( (event, column) => { onClick(event); - onSelection(column, filters); + onSelection(column, drillByConfig); setCurrentColumn(column); if (openNewModal) { setShowModal(true); } }, - [filters, onClick, onSelection, openNewModal], + [drillByConfig, onClick, onSelection, openNewModal], ); const closeModal = useCallback(() => { setShowModal(false); @@ -105,7 +104,9 @@ export const DrillByMenuItems = ({ setSearchInput(''); }, [columns.length]); - const hasDrillBy = ensureIsArray(filters).length && groupbyFieldName; + const hasDrillBy = + ensureIsArray(drillByConfig?.filters).length && + drillByConfig?.groupbyFieldName; const handlesDimensionContextMenu = useMemo( () => @@ -128,17 +129,30 @@ export const DrillByMenuItems = ({ .filter(column => column.groupby) .filter( column => - !ensureIsArray(formData[groupbyFieldName]).includes( - column.column_name, + !ensureIsArray( + formData[drillByConfig.groupbyFieldName ?? ''], + ).includes(column.column_name) && + column.column_name !== formData.x_axis && + ensureIsArray(excludedColumns)?.every( + excludedCol => + excludedCol.column_name !== column.column_name, ), ), ); }) .catch(() => { supersetGetCache.delete(`/api/v1/dataset/${datasetId}`); + addDangerToast(t('Failed to load dimensions for drill by')); }); } - }, [formData, groupbyFieldName, handlesDimensionContextMenu, hasDrillBy]); + }, [ + addDangerToast, + excludedColumns, + formData, + drillByConfig?.groupbyFieldName, + handlesDimensionContextMenu, + hasDrillBy, + ]); const handleInput = useCallback((e: ChangeEvent) => { e.stopPropagation(); @@ -148,16 +162,12 @@ export const DrillByMenuItems = ({ const filteredColumns = useMemo( () => - columns.filter( - column => - (column.verbose_name || column.column_name) - .toLowerCase() - .includes(searchInput.toLowerCase()) && - !ensureIsArray(excludedColumns)?.some( - col => col.column_name === column.column_name, - ), + columns.filter(column => + (column.verbose_name || column.column_name) + .toLowerCase() + .includes(searchInput.toLowerCase()), ), - [columns, excludedColumns, searchInput], + [columns, searchInput], ); const submenuYOffset = useMemo( @@ -257,9 +267,8 @@ export const DrillByMenuItems = ({ {showModal && ( diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx index f08a9701a85a2..95e2f6027f211 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx @@ -21,12 +21,12 @@ import React, { useState } from 'react'; import fetchMock from 'fetch-mock'; import { omit, isUndefined, omitBy } from 'lodash'; import userEvent from '@testing-library/user-event'; -import { waitFor } from '@testing-library/react'; +import { waitFor, within } from '@testing-library/react'; import { render, screen } from 'spec/helpers/testing-library'; import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries'; import mockState from 'spec/fixtures/mockState'; import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage'; -import DrillByModal from './DrillByModal'; +import DrillByModal, { DrillByModalProps } from './DrillByModal'; const CHART_DATA_ENDPOINT = 'glob:*/api/v1/chart/data*'; const FORM_DATA_KEY_ENDPOINT = 'glob:*/api/v1/explore/form_data'; @@ -60,9 +60,15 @@ const dataset = { last_name: 'Connor', }, ], + columns: [ + { + column_name: 'gender', + }, + { column_name: 'name' }, + ], }; -const renderModal = async () => { +const renderModal = async (modalProps: Partial = {}) => { const DrillByModalWrapper = () => { const [showModal, setShowModal] = useState(false); @@ -76,6 +82,8 @@ const renderModal = async () => { formData={formData} onHideModal={() => setShowModal(false)} dataset={dataset} + drillByConfig={{ groupbyFieldName: 'groupby', filters: [] }} + {...modalProps} /> )} @@ -97,7 +105,7 @@ beforeEach(() => { .post(CHART_DATA_ENDPOINT, { body: {} }, {}) .post(FORM_DATA_KEY_ENDPOINT, { key: '123' }); }); -afterEach(fetchMock.restore); +afterEach(() => fetchMock.restore()); test('should render the title', async () => { await renderModal(); @@ -120,14 +128,32 @@ test('should close the modal', async () => { }); test('should render loading indicator', async () => { - await renderModal(); - await waitFor(() => - expect(screen.getByLabelText('Loading')).toBeInTheDocument(), + fetchMock.post( + CHART_DATA_ENDPOINT, + { body: {} }, + // delay is missing in fetch-mock types + // @ts-ignore + { overwriteRoutes: true, delay: 1000 }, ); + await renderModal(); + expect(screen.getByLabelText('Loading')).toBeInTheDocument(); }); -test('should generate Explore url', async () => { +test('should render alert banner when results fail to load', async () => { await renderModal(); + expect( + await screen.findByText('There was an error loading the chart data'), + ).toBeInTheDocument(); +}); + +test('should generate Explore url', async () => { + await renderModal({ + column: { column_name: 'name' }, + drillByConfig: { + filters: [{ col: 'gender', op: '==', val: 'boy' }], + groupbyFieldName: 'groupby', + }, + }); await waitFor(() => fetchMock.called(CHART_DATA_ENDPOINT)); const expectedRequestPayload = { form_data: { @@ -135,6 +161,18 @@ test('should generate Explore url', async () => { omit(formData, ['slice_id', 'slice_name', 'dashboards']), isUndefined, ), + groupby: ['name'], + adhoc_filters: [ + ...formData.adhoc_filters, + { + clause: 'WHERE', + comparator: 'boy', + expressionType: 'SIMPLE', + operator: '==', + operatorId: 'EQUALS', + subject: 'gender', + }, + ], slice_id: 0, result_format: 'json', result_type: 'full', @@ -170,3 +208,28 @@ test('should render radio buttons', async () => { expect(chartRadio).not.toBeChecked(); expect(tableRadio).toBeChecked(); }); + +test('render breadcrumbs', async () => { + await renderModal({ + column: { column_name: 'name' }, + drillByConfig: { + filters: [{ col: 'gender', op: '==', val: 'boy' }], + groupbyFieldName: 'groupby', + }, + }); + + const breadcrumbItems = screen.getAllByTestId('drill-by-breadcrumb-item'); + expect(breadcrumbItems).toHaveLength(2); + expect( + within(breadcrumbItems[0]).getByText('gender (boy)'), + ).toBeInTheDocument(); + expect(within(breadcrumbItems[1]).getByText('name')).toBeInTheDocument(); + + userEvent.click(screen.getByText('gender (boy)')); + + const newBreadcrumbItems = screen.getAllByTestId('drill-by-breadcrumb-item'); + // we need to assert that there is only 1 element now + // eslint-disable-next-line jest-dom/prefer-in-document + expect(newBreadcrumbItems).toHaveLength(1); + expect(within(breadcrumbItems[0]).getByText('gender')).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx index 6a9a82b5733ec..df754f66a9bbc 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx @@ -26,40 +26,48 @@ import React, { } from 'react'; import { BaseFormData, - BinaryQueryObjectFilterClause, Column, QueryData, css, ensureIsArray, + isDefined, t, useTheme, + ContextMenuFilters, } from '@superset-ui/core'; import { useSelector } from 'react-redux'; import { Link } from 'react-router-dom'; import Modal from 'src/components/Modal'; import Loading from 'src/components/Loading'; import Button from 'src/components/Button'; -import { Radio } from 'src/components/Radio'; import { RootState } from 'src/dashboard/types'; import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage'; import { postFormData } from 'src/explore/exploreUtils/formData'; import { noOp } from 'src/utils/common'; import { simpleFilterToAdhoc } from 'src/utils/simpleFilterToAdhoc'; import { useDatasetMetadataBar } from 'src/features/datasets/metadataBar/useDatasetMetadataBar'; -import { SingleQueryResultPane } from 'src/explore/components/DataTablesPane/components/SingleQueryResultPane'; +import { useToasts } from 'src/components/MessageToasts/withToasts'; +import Alert from 'src/components/Alert'; import { Dataset, DrillByType } from '../types'; import DrillByChart from './DrillByChart'; import { ContextMenuItem } from '../ChartContextMenu/ChartContextMenu'; import { useContextMenu } from '../ChartContextMenu/useContextMenu'; import { getChartDataRequest } from '../chartAction'; +import { useDisplayModeToggle } from './useDisplayModeToggle'; +import { + DrillByBreadcrumb, + useDrillByBreadcrumbs, +} from './useDrillByBreadcrumbs'; +import { useResultsTableView } from './useResultsTableView'; -const DATA_SIZE = 15; +const DEFAULT_ADHOC_FILTER_FIELD_NAME = 'adhoc_filters'; interface ModalFooterProps { closeModal?: () => void; formData: BaseFormData; } const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => { + const { addDangerToast } = useToasts(); const [url, setUrl] = useState(''); const dashboardPageId = useContext(DashboardPageIdContext); const [datasource_id, datasource_type] = formData.datasource.split('__'); @@ -70,13 +78,24 @@ const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => { `/explore/?form_data_key=${key}&dashboard_page_id=${dashboardPageId}`, ); }) - .catch(e => { - console.log(e); + .catch(() => { + addDangerToast(t('Failed to generate chart edit URL')); }); - }, [dashboardPageId, datasource_id, datasource_type, formData]); + }, [ + addDangerToast, + dashboardPageId, + datasource_id, + datasource_type, + formData, + ]); return ( <> -
diff --git a/superset-frontend/src/components/Chart/DrillBy/useDisplayModeToggle.tsx b/superset-frontend/src/components/Chart/DrillBy/useDisplayModeToggle.tsx new file mode 100644 index 0000000000000..5e7b812cc44da --- /dev/null +++ b/superset-frontend/src/components/Chart/DrillBy/useDisplayModeToggle.tsx @@ -0,0 +1,64 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useMemo, useState } from 'react'; +import { css, SupersetTheme, t } from '@superset-ui/core'; +import { Radio } from 'src/components/Radio'; +import { DrillByType } from '../types'; + +export const useDisplayModeToggle = () => { + const [drillByDisplayMode, setDrillByDisplayMode] = useState( + DrillByType.Chart, + ); + + const displayModeToggle = useMemo( + () => ( +
css` + margin-bottom: ${theme.gridUnit * 6}px; + .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):focus-within { + box-shadow: none; + } + `} + > + { + setDrillByDisplayMode(value); + }} + defaultValue={DrillByType.Chart} + > + + {t('Chart')} + + + {t('Table')} + + +
+ ), + [], + ); + return { displayModeToggle, drillByDisplayMode }; +}; diff --git a/superset-frontend/src/components/Chart/DrillBy/useDrillByBreadcrumbs.test.ts b/superset-frontend/src/components/Chart/DrillBy/useDrillByBreadcrumbs.test.ts new file mode 100644 index 0000000000000..48cc328f7a6df --- /dev/null +++ b/superset-frontend/src/components/Chart/DrillBy/useDrillByBreadcrumbs.test.ts @@ -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 { renderHook } from '@testing-library/react-hooks'; +import userEvent from '@testing-library/user-event'; +import { render, screen } from 'spec/helpers/testing-library'; +import { + DrillByBreadcrumb, + useDrillByBreadcrumbs, +} from './useDrillByBreadcrumbs'; + +const BREADCRUMBS_DATA: DrillByBreadcrumb[] = [ + { + groupby: [{ column_name: 'col1' }, { column_name: 'col2' }], + filters: [ + { col: 'col1', op: '==', val: 'col1 filter' }, + { col: 'col2', op: '==', val: 'col2 filter' }, + ], + }, + { + groupby: [{ column_name: 'col3', verbose_name: 'Column 3' }], + filters: [{ col: 'col3', op: '==', val: 'col3 filter' }], + }, + { groupby: [{ column_name: 'col4' }] }, +]; + +test('Render breadcrumbs', () => { + const { result } = renderHook(() => useDrillByBreadcrumbs(BREADCRUMBS_DATA)); + render(result.current); + expect(screen.getAllByTestId('drill-by-breadcrumb-item')).toHaveLength(3); + expect( + screen.getByText('col1, col2 (col1 filter, col2 filter)'), + ).toBeInTheDocument(); + expect(screen.getByText('Column 3 (col3 filter)')).toBeInTheDocument(); + expect(screen.getByText('col4')).toBeInTheDocument(); +}); + +test('Call click handler with correct arguments when breadcrumb is clicked', () => { + const onClick = jest.fn(); + const { result } = renderHook(() => + useDrillByBreadcrumbs(BREADCRUMBS_DATA, onClick), + ); + render(result.current); + + userEvent.click(screen.getByText('col1, col2 (col1 filter, col2 filter)')); + expect(onClick).toHaveBeenCalledWith(BREADCRUMBS_DATA[0], 0); + onClick.mockClear(); + + userEvent.click(screen.getByText('Column 3 (col3 filter)')); + expect(onClick).toHaveBeenCalledWith(BREADCRUMBS_DATA[1], 1); + onClick.mockClear(); + + userEvent.click(screen.getByText('col4')); + expect(onClick).not.toHaveBeenCalled(); + onClick.mockClear(); +}); diff --git a/superset-frontend/src/components/Chart/DrillBy/useDrillByBreadcrumbs.tsx b/superset-frontend/src/components/Chart/DrillBy/useDrillByBreadcrumbs.tsx new file mode 100644 index 0000000000000..fc7c0b2bf5507 --- /dev/null +++ b/superset-frontend/src/components/Chart/DrillBy/useDrillByBreadcrumbs.tsx @@ -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. + */ + +import React, { useMemo } from 'react'; +import { + BinaryQueryObjectFilterClause, + Column, + css, + ensureIsArray, + styled, + SupersetTheme, +} from '@superset-ui/core'; +import { AntdBreadcrumb } from 'src/components/index'; +import { noOp } from 'src/utils/common'; + +export interface DrillByBreadcrumb { + groupby: Column | Column[]; + filters?: BinaryQueryObjectFilterClause[]; +} + +const BreadcrumbItem = styled(AntdBreadcrumb.Item)<{ isClickable: boolean }>` + ${({ theme, isClickable }) => css` + cursor: ${isClickable ? 'pointer' : 'auto'}; + color: ${theme.colors.grayscale.light1}; + transition: color ease-in ${theme.transitionTiming}s; + .ant-breadcrumb > span:last-child > & { + color: ${theme.colors.grayscale.dark1}; + } + &:hover { + color: ${isClickable ? theme.colors.grayscale.dark1 : 'inherit'}; + } + `} +`; + +export const useDrillByBreadcrumbs = ( + breadcrumbsData: DrillByBreadcrumb[], + onBreadcrumbClick: ( + breadcrumb: DrillByBreadcrumb, + index: number, + ) => void = noOp, +) => + useMemo(() => { + // the last breadcrumb is not clickable + const isClickable = (index: number) => index < breadcrumbsData.length - 1; + const getBreadcrumbText = (breadcrumb: DrillByBreadcrumb) => + `${ensureIsArray(breadcrumb.groupby) + .map(column => column.verbose_name || column.column_name) + .join(', ')} ${ + breadcrumb.filters + ? `(${breadcrumb.filters + .map(filter => filter.formattedVal || filter.val) + .join(', ')})` + : '' + }`; + return ( + css` + margin: ${theme.gridUnit * 2}px 0 ${theme.gridUnit * 4}px; + `} + > + {breadcrumbsData.map((breadcrumb, index) => ( + onBreadcrumbClick(breadcrumb, index) + : noOp + } + data-test="drill-by-breadcrumb-item" + > + {getBreadcrumbText(breadcrumb)} + + ))} + + ); + }, [breadcrumbsData, onBreadcrumbClick]); diff --git a/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.test.ts b/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.test.ts new file mode 100644 index 0000000000000..36cc51a68431a --- /dev/null +++ b/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.test.ts @@ -0,0 +1,108 @@ +/** + * 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 { renderHook } from '@testing-library/react-hooks'; +import userEvent from '@testing-library/user-event'; +import { render, screen, within, waitFor } from 'spec/helpers/testing-library'; +import { useResultsTableView } from './useResultsTableView'; + +const MOCK_CHART_DATA_RESULT = [ + { + colnames: ['name', 'sum__num'], + coltypes: [1, 0], + data: [ + { + name: 'Michael', + sum__num: 2467063, + }, + { + name: 'Christopher', + sum__num: 1725265, + }, + { + name: 'David', + sum__num: 1570516, + }, + { + name: 'James', + sum__num: 1506025, + }, + ], + }, + { + colnames: ['gender', 'year', 'count'], + coltypes: [1, 0, 0], + data: [ + { + gender: 'boy', + year: 2000, + count: 1000, + }, + { + gender: 'girl', + year: 2000, + count: 2000, + }, + ], + }, +]; + +test('Displays results table for 1 query', () => { + const { result } = renderHook(() => + useResultsTableView(MOCK_CHART_DATA_RESULT.slice(0, 1), '1__table'), + ); + render(result.current, { useRedux: true }); + expect(screen.queryByRole('tablist')).not.toBeInTheDocument(); + expect(screen.getByRole('table')).toBeInTheDocument(); + expect(screen.getAllByRole('columnheader')).toHaveLength(2); + expect(screen.getAllByTestId('table-row')).toHaveLength(4); +}); + +test('Displays results for 2 queries', async () => { + const { result } = renderHook(() => + useResultsTableView(MOCK_CHART_DATA_RESULT, '1__table'), + ); + render(result.current, { useRedux: true }); + const getActiveTabElement = () => + document.querySelector('.ant-tabs-tabpane-active') as HTMLElement; + + const tablistElement = screen.getByRole('tablist'); + expect(tablistElement).toBeInTheDocument(); + expect(within(tablistElement).getByText('Results 1')).toBeInTheDocument(); + expect(within(tablistElement).getByText('Results 2')).toBeInTheDocument(); + + expect(within(getActiveTabElement()).getByRole('table')).toBeInTheDocument(); + expect( + within(getActiveTabElement()).getAllByRole('columnheader'), + ).toHaveLength(2); + expect( + within(getActiveTabElement()).getAllByTestId('table-row'), + ).toHaveLength(4); + + userEvent.click(screen.getByText('Results 2')); + + await waitFor(() => { + expect( + within(getActiveTabElement()).getAllByRole('columnheader'), + ).toHaveLength(3); + }); + expect( + within(getActiveTabElement()).getAllByTestId('table-row'), + ).toHaveLength(2); +}); diff --git a/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx b/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx new file mode 100644 index 0000000000000..2778fab72c431 --- /dev/null +++ b/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx @@ -0,0 +1,73 @@ +/** + * 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 { css, styled, isDefined, QueryData, t } from '@superset-ui/core'; +import { SingleQueryResultPane } from 'src/explore/components/DataTablesPane/components/SingleQueryResultPane'; +import Tabs from 'src/components/Tabs'; + +const DATA_SIZE = 15; + +const PaginationContainer = styled.div` + ${({ theme }) => css` + & .pagination-container { + bottom: ${-theme.gridUnit * 4}px; + } + `} +`; + +export const useResultsTableView = ( + chartDataResult: QueryData[] | undefined, + datasourceId: string, +) => { + if (!isDefined(chartDataResult)) { + return
; + } + if (chartDataResult.length === 1) { + return ( + + + + ); + } + return ( + + {chartDataResult.map((res, index) => ( + + + + + + ))} + + ); +}; diff --git a/superset-frontend/src/components/Datasource/ChangeDatasourceModal.test.jsx b/superset-frontend/src/components/Datasource/ChangeDatasourceModal.test.jsx index 419af6e3cf752..5dbc52c38a5ee 100644 --- a/superset-frontend/src/components/Datasource/ChangeDatasourceModal.test.jsx +++ b/superset-frontend/src/components/Datasource/ChangeDatasourceModal.test.jsx @@ -49,7 +49,7 @@ const datasourceData = { const DATASOURCES_ENDPOINT = 'glob:*/api/v1/dataset/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)'; -const DATASOURCE_ENDPOINT = `glob:*/datasource/get/${datasourceData.type}/${datasourceData.id}`; +const DATASOURCE_ENDPOINT = `glob:*/api/v1/dataset/${datasourceData.id}`; const DATASOURCE_PAYLOAD = { new: 'data' }; const INFO_ENDPOINT = 'glob:*/api/v1/dataset/_info?*'; @@ -112,6 +112,6 @@ describe('ChangeDatasourceModal', () => { }); await waitForComponentToPaint(wrapper); - expect(fetchMock.calls(/datasource\/get\/table\/7/)).toHaveLength(1); + expect(fetchMock.calls(/api\/v1\/dataset\/7/)).toHaveLength(1); }); }); diff --git a/superset-frontend/src/components/Datasource/ChangeDatasourceModal.tsx b/superset-frontend/src/components/Datasource/ChangeDatasourceModal.tsx index b5b99e1089f4a..5e837f0a26846 100644 --- a/superset-frontend/src/components/Datasource/ChangeDatasourceModal.tsx +++ b/superset-frontend/src/components/Datasource/ChangeDatasourceModal.tsx @@ -173,10 +173,12 @@ const ChangeDatasourceModal: FunctionComponent = ({ const handleChangeConfirm = () => { SupersetClient.get({ - endpoint: `/datasource/get/${confirmedDataset?.type}/${confirmedDataset?.id}/`, + endpoint: `/api/v1/dataset/${confirmedDataset?.id}`, }) .then(({ json }) => { - onDatasourceSave(json); + // eslint-disable-next-line no-param-reassign + json.result.type = 'table'; + onDatasourceSave(json.result); onChange(`${confirmedDataset?.id}__table`); }) .catch(response => { diff --git a/superset-frontend/src/components/Datasource/DatasourceModal.test.jsx b/superset-frontend/src/components/Datasource/DatasourceModal.test.jsx index 12be350521515..7d378902e6e93 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal.test.jsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal.test.jsx @@ -40,7 +40,8 @@ const datasource = mockDatasource['7__table']; const SAVE_ENDPOINT = 'glob:*/api/v1/dataset/7'; const SAVE_PAYLOAD = { new: 'data' }; -const SAVE_DATASOURCE_ENDPOINT = 'glob:*/datasource/save/'; +const SAVE_DATASOURCE_ENDPOINT = 'glob:*/api/v1/dataset/7'; +const GET_DATASOURCE_ENDPOINT = SAVE_DATASOURCE_ENDPOINT; const mockedProps = { datasource, @@ -96,7 +97,8 @@ describe('DatasourceModal', () => { it('saves on confirm', async () => { const callsP = fetchMock.post(SAVE_ENDPOINT, SAVE_PAYLOAD); - fetchMock.post(SAVE_DATASOURCE_ENDPOINT, {}); + fetchMock.put(SAVE_DATASOURCE_ENDPOINT, {}); + fetchMock.get(GET_DATASOURCE_ENDPOINT, {}); act(() => { wrapper .find('button[data-test="datasource-modal-save"]') @@ -111,7 +113,11 @@ describe('DatasourceModal', () => { okButton.simulate('click'); }); await waitForComponentToPaint(wrapper); - const expected = ['http://localhost/datasource/save/']; + // one call to PUT, then one to GET + const expected = [ + 'http://localhost/api/v1/dataset/7', + 'http://localhost/api/v1/dataset/7', + ]; expect(callsP._calls.map(call => call[0])).toEqual( expected, ); /* eslint no-underscore-dangle: 0 */ diff --git a/superset-frontend/src/components/Datasource/DatasourceModal.tsx b/superset-frontend/src/components/Datasource/DatasourceModal.tsx index 90135f40f9ecd..b5b698f8315bb 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal.tsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal.tsx @@ -96,39 +96,81 @@ const DatasourceModal: FunctionComponent = ({ currentDatasource.schema; setIsSaving(true); - SupersetClient.post({ - endpoint: '/datasource/save/', - postPayload: { - data: { - ...currentDatasource, - cache_timeout: - currentDatasource.cache_timeout === '' - ? null - : currentDatasource.cache_timeout, - schema, - metrics: currentDatasource?.metrics?.map( - (metric: Record) => ({ - ...metric, + SupersetClient.put({ + endpoint: `/api/v1/dataset/${currentDatasource.id}`, + jsonPayload: { + table_name: currentDatasource.table_name, + database_id: currentDatasource.database?.id, + sql: currentDatasource.sql, + filter_select_enabled: currentDatasource.filter_select_enabled, + fetch_values_predicate: currentDatasource.fetch_values_predicate, + schema, + description: currentDatasource.description, + main_dttm_col: currentDatasource.main_dttm_col, + offset: currentDatasource.offset, + default_endpoint: currentDatasource.default_endpoint, + cache_timeout: + currentDatasource.cache_timeout === '' + ? null + : currentDatasource.cache_timeout, + is_sqllab_view: currentDatasource.is_sqllab_view, + template_params: currentDatasource.template_params, + extra: currentDatasource.extra, + is_managed_externally: currentDatasource.is_managed_externally, + external_url: currentDatasource.external_url, + metrics: currentDatasource?.metrics?.map( + (metric: Record) => { + const metricBody: any = { + expression: metric.expression, + description: metric.description, + metric_name: metric.metric_name, + metric_type: metric.metric_type, + d3format: metric.d3format, + verbose_name: metric.verbose_name, + warning_text: metric.warning_text, + uuid: metric.uuid, extra: buildExtraJsonObject(metric), - }), - ), - columns: currentDatasource?.columns?.map( - (column: Record) => ({ - ...column, - extra: buildExtraJsonObject(column), - }), - ), - type: currentDatasource.type || currentDatasource.datasource_type, - owners: currentDatasource.owners.map( - (o: Record) => o.value || o.id, - ), - }, + }; + if (!Number.isNaN(Number(metric.id))) { + metricBody.id = metric.id; + } + return metricBody; + }, + ), + columns: currentDatasource?.columns?.map( + (column: Record) => ({ + id: column.id, + column_name: column.column_name, + type: column.type, + advanced_data_type: column.advanced_data_type, + verbose_name: column.verbose_name, + description: column.description, + expression: column.expression, + filterable: column.filterable, + groupby: column.groupby, + is_active: column.is_active, + is_dttm: column.is_dttm, + python_date_format: column.python_date_format, + uuid: column.uuid, + extra: buildExtraJsonObject(column), + }), + ), + owners: currentDatasource.owners.map( + (o: Record) => o.value || o.id, + ), }, }) - .then(({ json }) => { + .then(() => { addSuccessToast(t('The dataset has been saved')); + return SupersetClient.get({ + endpoint: `/api/v1/dataset/${currentDatasource?.id}`, + }); + }) + .then(({ json }) => { + // eslint-disable-next-line no-param-reassign + json.result.type = 'table'; onDatasourceSave({ - ...json, + ...json.result, owners: currentDatasource.owners, }); onHide(); diff --git a/superset-frontend/src/components/DeprecatedSelect/DeprecatedSelect.tsx b/superset-frontend/src/components/DeprecatedSelect/DeprecatedSelect.tsx index 2e2c1e9546f49..185829b23a5d2 100644 --- a/superset-frontend/src/components/DeprecatedSelect/DeprecatedSelect.tsx +++ b/superset-frontend/src/components/DeprecatedSelect/DeprecatedSelect.tsx @@ -161,13 +161,15 @@ function styled< filterOption, ignoreAccents = false, // default is `true`, but it is slow + asText = (value: any) => String(value ?? ''), + getOptionValue = option => typeof option === 'string' ? option : option[valueKey], getOptionLabel = option => typeof option === 'string' ? option - : option[labelKey] || option[valueKey], + : asText(option[labelKey]) || asText(option[valueKey]), formatOptionLabel = ( option: OptionType, diff --git a/superset-frontend/src/components/TableSelector/TableSelector.test.tsx b/superset-frontend/src/components/TableSelector/TableSelector.test.tsx index 3ab045a8a9454..1e6083e1b46f5 100644 --- a/superset-frontend/src/components/TableSelector/TableSelector.test.tsx +++ b/superset-frontend/src/components/TableSelector/TableSelector.test.tsx @@ -21,7 +21,6 @@ import React from 'react'; import { render, screen, waitFor, within } from 'spec/helpers/testing-library'; import { queryClient } from 'src/views/QueryProvider'; import fetchMock from 'fetch-mock'; -import { act } from 'react-dom/test-utils'; import userEvent from '@testing-library/user-event'; import TableSelector, { TableSelectorMultiple } from '.'; @@ -36,11 +35,6 @@ const createProps = (props = {}) => ({ ...props, }); -const getSchemaMockFunction = () => - ({ - result: ['schema_a', 'schema_b'], - } as any); - const getTableMockFunction = () => ({ count: 4, @@ -124,47 +118,6 @@ test('renders disabled without schema', async () => { }); }); -test('table options are notified after schema selection', async () => { - fetchMock.get(schemaApiRoute, getSchemaMockFunction()); - - const callback = jest.fn(); - const props = createProps({ - onTablesLoad: callback, - schema: undefined, - }); - render(, { useRedux: true }); - - const schemaSelect = screen.getByRole('combobox', { - name: 'Select schema or type to search schemas', - }); - expect(schemaSelect).toBeInTheDocument(); - expect(callback).not.toHaveBeenCalled(); - - userEvent.click(schemaSelect); - - expect( - await screen.findByRole('option', { name: 'schema_a' }), - ).toBeInTheDocument(); - expect( - await screen.findByRole('option', { name: 'schema_b' }), - ).toBeInTheDocument(); - - fetchMock.get(tablesApiRoute, getTableMockFunction()); - - act(() => { - userEvent.click(screen.getAllByText('schema_a')[1]); - }); - - await waitFor(() => { - expect(callback).toHaveBeenCalledWith([ - { label: 'table_a', value: 'table_a' }, - { label: 'table_b', value: 'table_b' }, - { label: 'table_c', value: 'table_c' }, - { label: 'table_d', value: 'table_d' }, - ]); - }); -}); - test('table select retain value if not in SQL Lab mode', async () => { fetchMock.get(schemaApiRoute, { result: ['test_schema'] }); fetchMock.get(tablesApiRoute, getTableMockFunction()); diff --git a/superset-frontend/src/components/TableSelector/index.tsx b/superset-frontend/src/components/TableSelector/index.tsx index 2c51462e6d255..3886a86fd20af 100644 --- a/superset-frontend/src/components/TableSelector/index.tsx +++ b/superset-frontend/src/components/TableSelector/index.tsx @@ -97,7 +97,6 @@ interface TableSelectorProps { isDatabaseSelectEnabled?: boolean; onDbChange?: (db: DatabaseObject) => void; onSchemaChange?: (schema?: string) => void; - onTablesLoad?: (options: Array) => void; readOnly?: boolean; schema?: string; onEmptyResults?: (searchText?: string) => void; @@ -158,7 +157,6 @@ const TableSelector: FunctionComponent = ({ isDatabaseSelectEnabled = true, onDbChange, onSchemaChange, - onTablesLoad, readOnly = false, onEmptyResults, schema, @@ -199,14 +197,6 @@ const TableSelector: FunctionComponent = ({ }, }); - useEffect(() => { - // Set the tableOptions in the queryEditor so autocomplete - // works on new tabs - if (data && isFetched) { - onTablesLoad?.(data.options); - } - }, [data, isFetched, onTablesLoad]); - const tableOptions = useMemo( () => data diff --git a/superset-frontend/src/components/index.ts b/superset-frontend/src/components/index.ts index bfa341a9ddd18..4c425b053464e 100644 --- a/superset-frontend/src/components/index.ts +++ b/superset-frontend/src/components/index.ts @@ -55,6 +55,7 @@ export { * or extending the components in src/components. */ export { + Breadcrumb as AntdBreadcrumb, Button as AntdButton, Card as AntdCard, Checkbox as AntdCheckbox, diff --git a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx index 845a9a9515d1e..3ee67e7f3868d 100644 --- a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx @@ -242,7 +242,7 @@ const SliceHeader: FC = ({ diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx index e5802414955c7..d617e51e48774 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx @@ -651,7 +651,7 @@ const FiltersConfigForm = ( const hasAvailableFilters = availableFilters.length > 0; const hasTimeDependency = availableFilters .filter(filter => filter.type === 'filter_time') - .some(filter => dependencies.includes(filter.value)); + .some(filter => dependencies?.includes(filter.value)); useEffect(() => { if (datasetId) { diff --git a/superset-frontend/src/dashboard/constants.ts b/superset-frontend/src/dashboard/constants.ts index 97753abe45c54..aa9f4d8bd3619 100644 --- a/superset-frontend/src/dashboard/constants.ts +++ b/superset-frontend/src/dashboard/constants.ts @@ -28,7 +28,7 @@ export const PLACEHOLDER_DATASOURCE: Datasource = { columns: [], column_types: [], metrics: [], - column_format: {}, + column_formats: {}, verbose_map: {}, main_dttm_col: '', description: '', diff --git a/superset-frontend/src/explore/actions/datasourcesActions.test.ts b/superset-frontend/src/explore/actions/datasourcesActions.test.ts index 6317f1f4a6d15..996758b262323 100644 --- a/superset-frontend/src/explore/actions/datasourcesActions.test.ts +++ b/superset-frontend/src/explore/actions/datasourcesActions.test.ts @@ -34,7 +34,7 @@ const CURRENT_DATASOURCE = { type: DatasourceType.Table, columns: [], metrics: [], - column_format: {}, + column_formats: {}, verbose_map: {}, main_dttm_col: '__timestamp', // eg. ['["ds", true]', 'ds [asc]'] @@ -47,7 +47,7 @@ const NEW_DATASOURCE = { type: DatasourceType.Table, columns: [], metrics: [], - column_format: {}, + column_formats: {}, verbose_map: {}, main_dttm_col: '__timestamp', // eg. ['["ds", true]', 'ds [asc]'] diff --git a/superset-frontend/src/explore/actions/saveModalActions.js b/superset-frontend/src/explore/actions/saveModalActions.js index 1c3c3b765f781..9bd5161391539 100644 --- a/superset-frontend/src/explore/actions/saveModalActions.js +++ b/superset-frontend/src/explore/actions/saveModalActions.js @@ -40,29 +40,6 @@ export function setSaveChartModalVisibility(isVisible) { return { type: SET_SAVE_CHART_MODAL_VISIBILITY, isVisible }; } -export function fetchDashboards(userId) { - return function fetchDashboardsThunk(dispatch) { - return SupersetClient.get({ - endpoint: `/dashboardasync/api/read?_flt_0_owners=${userId}`, - }) - .then(({ json }) => { - const choices = json.pks.map((id, index) => ({ - value: id, - label: (json.result[index] || {}).dashboard_title, - })); - choices.sort((a, b) => - a.label.localeCompare(b.label, { - sensitivity: 'base', - numeric: true, - }), - ); - - return dispatch(fetchDashboardsSucceeded(choices)); - }) - .catch(() => dispatch(fetchDashboardsFailed(userId))); - }; -} - export const SAVE_SLICE_FAILED = 'SAVE_SLICE_FAILED'; export function saveSliceFailed() { return { type: SAVE_SLICE_FAILED }; @@ -241,20 +218,6 @@ export const createDashboard = dashboardName => async dispatch => { } }; -// Get existing dashboard from ID -export const getDashboard = dashboardId => async dispatch => { - try { - const response = await SupersetClient.get({ - endpoint: `/api/v1/dashboard/${dashboardId}`, - }); - - return response.json; - } catch (error) { - dispatch(saveSliceFailed()); - throw error; - } -}; - // Get dashboards the slice is added to export const getSliceDashboards = slice => async dispatch => { try { diff --git a/superset-frontend/src/explore/actions/saveModalActions.test.js b/superset-frontend/src/explore/actions/saveModalActions.test.js index f89729f5ff981..1662ead63e5f0 100644 --- a/superset-frontend/src/explore/actions/saveModalActions.test.js +++ b/superset-frontend/src/explore/actions/saveModalActions.test.js @@ -23,10 +23,6 @@ import { ADD_TOAST } from 'src/components/MessageToasts/actions'; import { createDashboard, createSlice, - fetchDashboards, - FETCH_DASHBOARDS_FAILED, - FETCH_DASHBOARDS_SUCCEEDED, - getDashboard, getSliceDashboards, SAVE_SLICE_FAILED, SAVE_SLICE_SUCCESS, @@ -34,37 +30,6 @@ import { getSlicePayload, } from './saveModalActions'; -/** - * Tests fetchDashboards action - */ - -const userId = 1; -const fetchDashboardsEndpoint = `glob:*/dashboardasync/api/read?_flt_0_owners=${1}`; -const mockDashboardData = { - pks: ['id'], - result: [{ id: 'id', dashboard_title: 'dashboard title' }], -}; - -test('fetchDashboards handles success', async () => { - fetchMock.reset(); - fetchMock.get(fetchDashboardsEndpoint, mockDashboardData); - const dispatch = sinon.spy(); - await fetchDashboards(userId)(dispatch); - expect(fetchMock.calls(fetchDashboardsEndpoint)).toHaveLength(1); - expect(dispatch.callCount).toBe(1); - expect(dispatch.getCall(0).args[0].type).toBe(FETCH_DASHBOARDS_SUCCEEDED); -}); - -test('fetchDashboards handles failure', async () => { - fetchMock.reset(); - fetchMock.get(fetchDashboardsEndpoint, { throws: 'error' }); - const dispatch = sinon.spy(); - await fetchDashboards(userId)(dispatch); - expect(fetchMock.calls(fetchDashboardsEndpoint)).toHaveLength(4); // 3 retries - expect(dispatch.callCount).toBe(1); - expect(dispatch.getCall(0).args[0].type).toBe(FETCH_DASHBOARDS_FAILED); -}); - const sliceId = 10; const sliceName = 'New chart'; const vizType = 'sample_viz_type'; @@ -176,7 +141,6 @@ test('createSlice handles failure', async () => { expect(dispatch.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED); }); -const dashboardId = 14; const dashboardName = 'New dashboard'; const dashboardResponsePayload = { id: 14, @@ -214,38 +178,6 @@ test('createDashboard handles failure', async () => { expect(dispatch.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED); }); -/** - * Tests getDashboard action - */ - -const getDashboardEndpoint = `glob:*/api/v1/dashboard/${dashboardId}`; -test('getDashboard handles success', async () => { - fetchMock.reset(); - fetchMock.get(getDashboardEndpoint, dashboardResponsePayload); - const dispatch = sinon.spy(); - const dashboard = await getDashboard(dashboardId)(dispatch); - expect(fetchMock.calls(getDashboardEndpoint)).toHaveLength(1); - expect(dispatch.callCount).toBe(0); - expect(dashboard).toEqual(dashboardResponsePayload); -}); - -test('getDashboard handles failure', async () => { - fetchMock.reset(); - fetchMock.get(getDashboardEndpoint, { throws: sampleError }); - const dispatch = sinon.spy(); - let caughtError; - try { - await getDashboard(dashboardId)(dispatch); - } catch (error) { - caughtError = error; - } - - expect(caughtError).toEqual(sampleError); - expect(fetchMock.calls(getDashboardEndpoint)).toHaveLength(4); - expect(dispatch.callCount).toBe(1); - expect(dispatch.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED); -}); - test('updateSlice with add to new dashboard handles success', async () => { fetchMock.reset(); fetchMock.put(updateSliceEndpoint, sliceResponsePayload); diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx index 4a4a5203f5af9..34dcf01bca7d1 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx @@ -108,7 +108,7 @@ describe('DataTablesPane', () => { userEvent.click(screen.getByLabelText('Copy')); expect(copyToClipboardSpy).toHaveBeenCalledTimes(1); const value = await copyToClipboardSpy.mock.calls[0][0](); - expect(value).toBe('2009-01-01 00:00:00\tAction\n'); + expect(value).toBe('__timestamp\tgenre\n2009-01-01 00:00:00\tAction\n'); copyToClipboardSpy.mockRestore(); fetchMock.restore(); }); diff --git a/superset-frontend/src/explore/components/SaveModal.test.jsx b/superset-frontend/src/explore/components/SaveModal.test.jsx index 15bfc64e7588b..74d1c1199c75e 100644 --- a/superset-frontend/src/explore/components/SaveModal.test.jsx +++ b/superset-frontend/src/explore/components/SaveModal.test.jsx @@ -20,10 +20,8 @@ import React from 'react'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { bindActionCreators } from 'redux'; -import { Provider } from 'react-redux'; import { shallow } from 'enzyme'; -import { styledMount as mount } from 'spec/helpers/theming'; import { Radio } from 'src/components/Radio'; import Button from 'src/components/Button'; import sinon from 'sinon'; @@ -39,6 +37,7 @@ const initialState = { chart: {}, saveModal: { dashboards: [], + isVisible: true, }, explore: { datasource: {}, @@ -57,6 +56,7 @@ const initialState = { const initialStore = mockStore(initialState); const defaultProps = { + addDangerToast: jest.fn(), onHide: () => ({}), actions: bindActionCreators(saveModalActions, arg => { if (typeof arg === 'function') { @@ -83,6 +83,7 @@ const queryStore = mockStore({ chart: {}, saveModal: { dashboards: [], + isVisible: true, }, explore: { datasource: { name: 'test', type: 'query' }, @@ -144,8 +145,7 @@ test('renders the right footer buttons when existing dashboard selected', () => test('renders the right footer buttons when new dashboard selected', () => { const wrapper = getWrapper(); wrapper.setState({ - saveToDashboardId: null, - newDashboardName: 'Test new dashboard', + dashboard: { label: 'Test new dashboard', value: 'Test new dashboard' }, }); const footerWrapper = shallow(wrapper.find(StyledModal).props().footer); const saveAndGoDash = footerWrapper @@ -186,18 +186,6 @@ test('sets action when overwriting slice', () => { expect(wrapperForOverwrite.state().action).toBe('overwrite'); }); -test('fetches dashboards on component mount', () => { - sinon.spy(defaultProps.actions, 'fetchDashboards'); - mount( - - - , - ); - expect(defaultProps.actions.fetchDashboards.calledOnce).toBe(true); - - defaultProps.actions.fetchDashboards.restore(); -}); - test('updates slice name and selected dashboard', () => { const wrapper = getWrapper(); const dashboardId = mockEvent.value; @@ -205,8 +193,8 @@ test('updates slice name and selected dashboard', () => { wrapper.instance().onSliceNameChange(mockEvent); expect(wrapper.state().newSliceName).toBe(mockEvent.target.value); - wrapper.instance().onDashboardSelectChange(dashboardId); - expect(wrapper.state().saveToDashboardId).toBe(dashboardId); + wrapper.instance().onDashboardChange({ value: dashboardId }); + expect(wrapper.state().dashboard.value).toBe(dashboardId); }); test('removes alert', () => { diff --git a/superset-frontend/src/explore/components/SaveModal.tsx b/superset-frontend/src/explore/components/SaveModal.tsx index 9e63f10b61488..849baa1417b01 100644 --- a/superset-frontend/src/explore/components/SaveModal.tsx +++ b/superset-frontend/src/explore/components/SaveModal.tsx @@ -19,17 +19,18 @@ /* eslint camelcase: 0 */ import React from 'react'; import { Dispatch } from 'redux'; -import { SelectValue } from 'antd/lib/select'; +import { isFeatureEnabled } from 'src/featureFlags'; +import rison from 'rison'; import { connect } from 'react-redux'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls'; import { css, DatasourceType, - ensureIsArray, FeatureFlag, isDefined, styled, + SupersetClient, t, } from '@superset-ui/core'; import { Input } from 'src/components/Input'; @@ -38,11 +39,10 @@ import Alert from 'src/components/Alert'; import Modal from 'src/components/Modal'; import { Radio } from 'src/components/Radio'; import Button from 'src/components/Button'; -import { Select } from 'src/components'; +import { AsyncSelect } from 'src/components'; import Loading from 'src/components/Loading'; import { setSaveChartModalVisibility } from 'src/explore/actions/saveModalActions'; import { SaveActionType } from 'src/explore/types'; -import { isFeatureEnabled } from 'src/featureFlags'; // Session storage key for recent dashboard const SK_DASHBOARD_ID = 'save_chart_recent_dashboard'; @@ -52,7 +52,6 @@ interface SaveModalProps extends RouteComponentProps { actions: Record; form_data?: Record; userId: number; - dashboards: Array; alert?: string; sliceName?: string; slice?: Record; @@ -63,15 +62,14 @@ interface SaveModalProps extends RouteComponentProps { } type SaveModalState = { - saveToDashboardId: number | string | null; newSliceName?: string; - newDashboardName?: string; datasetName: string; alert: string | null; action: SaveActionType; isLoading: boolean; saveStatus?: string | null; vizType?: string; + dashboard?: { label: string; value: string | number }; }; export const StyledModal = styled(Modal)` @@ -89,15 +87,15 @@ class SaveModal extends React.Component { constructor(props: SaveModalProps) { super(props); this.state = { - saveToDashboardId: null, newSliceName: props.sliceName, datasetName: props.datasource?.name, alert: null, action: this.canOverwriteSlice() ? 'overwrite' : 'saveas', isLoading: false, vizType: props.form_data?.viz_type, + dashboard: undefined, }; - this.onDashboardSelectChange = this.onDashboardSelectChange.bind(this); + this.onDashboardChange = this.onDashboardChange.bind(this); this.onSliceNameChange = this.onSliceNameChange.bind(this); this.changeAction = this.changeAction.bind(this); this.saveOrOverwrite = this.saveOrOverwrite.bind(this); @@ -107,7 +105,8 @@ class SaveModal extends React.Component { } isNewDashboard(): boolean { - return !!(!this.state.saveToDashboardId && this.state.newDashboardName); + const { dashboard } = this.state; + return typeof dashboard?.value === 'string'; } canOverwriteSlice(): boolean { @@ -117,30 +116,26 @@ class SaveModal extends React.Component { ); } - componentDidMount() { - this.props.actions.fetchDashboards(this.props.userId).then(() => { - if (ensureIsArray(this.props.dashboards).length === 0) { - return; - } - const dashboardIds = this.props.dashboards?.map( - dashboard => dashboard.value, - ); + async componentDidMount() { + let { dashboardId } = this.props; + if (!dashboardId) { const lastDashboard = sessionStorage.getItem(SK_DASHBOARD_ID); - let recentDashboard = lastDashboard && parseInt(lastDashboard, 10); - - if (this.props.dashboardId) { - recentDashboard = this.props.dashboardId; - } - - if ( - recentDashboard !== null && - dashboardIds.indexOf(recentDashboard) !== -1 - ) { - this.setState({ - saveToDashboardId: recentDashboard, - }); + dashboardId = lastDashboard && parseInt(lastDashboard, 10); + } + if (dashboardId) { + try { + const result = await this.loadDashboard(dashboardId); + if (result) { + this.setState({ + dashboard: { label: result.dashboard_title, value: result.id }, + }); + } + } catch (error) { + this.props.actions.addDangerToast( + t('An error occurred while loading dashboard information.'), + ); } - }); + } } handleDatasetNameChange = (e: React.FormEvent) => { @@ -152,11 +147,8 @@ class SaveModal extends React.Component { this.setState({ newSliceName: event.target.value }); } - onDashboardSelectChange(selected: SelectValue) { - const newDashboardName = selected ? String(selected) : undefined; - const saveToDashboardId = - selected && typeof selected === 'number' ? selected : null; - this.setState({ saveToDashboardId, newDashboardName }); + onDashboardChange(dashboard: { label: string; value: string | number }) { + this.setState({ dashboard }); } changeAction(action: SaveActionType) { @@ -206,19 +198,22 @@ class SaveModal extends React.Component { delete formData.url_params; let dashboard: DashboardGetResponse | null = null; - if (this.state.newDashboardName || this.state.saveToDashboardId) { - let saveToDashboardId = this.state.saveToDashboardId || null; - if (!this.state.saveToDashboardId) { + if (this.state.dashboard) { + let validId = this.state.dashboard.value; + if (this.isNewDashboard()) { const response = await this.props.actions.createDashboard( - this.state.newDashboardName, + this.state.dashboard.label, ); - saveToDashboardId = response.id; + validId = response.id; + } + + try { + dashboard = await this.loadDashboard(validId as number); + } catch (error) { + this.props.actions.saveSliceFailed(); + return; } - const response = await this.props.actions.getDashboard( - saveToDashboardId, - ); - dashboard = response.result; if (isDefined(dashboard) && isDefined(dashboard?.id)) { sliceDashboards = sliceDashboards.includes(dashboard.id) ? sliceDashboards @@ -240,7 +235,7 @@ class SaveModal extends React.Component { dashboard ? { title: dashboard.dashboard_title, - new: !this.state.saveToDashboardId, + new: this.isNewDashboard(), } : null, ); @@ -251,7 +246,7 @@ class SaveModal extends React.Component { dashboard ? { title: dashboard.dashboard_title, - new: !this.state.saveToDashboardId, + new: this.isNewDashboard(), } : null, ); @@ -284,94 +279,131 @@ class SaveModal extends React.Component { } } - renderSaveChartModal = () => { - const dashboardSelectValue = - this.state.saveToDashboardId || this.state.newDashboardName; + loadDashboard = async (id: number) => { + const response = await SupersetClient.get({ + endpoint: `/api/v1/dashboard/${id}`, + }); + return response.json.result; + }; - return ( -
- {(this.state.alert || this.props.alert) && ( - { + const queryParams = rison.encode({ + columns: ['id', 'dashboard_title'], + filters: [ + { + col: 'dashboard_title', + opr: 'ct', + value: search, + }, + { + col: 'owners', + opr: 'rel_m_m', + value: this.props.userId, + }, + ], + page, + page_size: pageSize, + order_column: 'dashboard_title', + }); + + const { json } = await SupersetClient.get({ + endpoint: `/api/v1/dashboard/?q=${queryParams}`, + }); + const { result, count } = json; + return { + data: result.map( + (dashboard: { id: number; dashboard_title: string }) => ({ + value: dashboard.id, + label: dashboard.dashboard_title, + }), + ), + totalCount: count, + }; + }; + + renderSaveChartModal = () => ( + + {(this.state.alert || this.props.alert) && ( + + )} + + this.changeAction('overwrite')} + data-test="save-overwrite-radio" + > + {t('Save (Overwrite)')} + + this.changeAction('saveas')} + > + {t('Save as...')} + + +
+ + + + {this.props.datasource?.type === 'query' && ( + + - )} - - this.changeAction('overwrite')} - data-test="save-overwrite-radio" - > - {t('Save (Overwrite)')} - - this.changeAction('saveas')} - > - {t('Save as...')} - - -
- - {this.props.datasource?.type === 'query' && ( - - - - - )} - {!( - isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) && - this.state.vizType === 'filter_box' - ) && ( - -