mirror of
https://github.com/apache/superset.git
synced 2026-07-08 07:45:37 +00:00
Compare commits
2 Commits
4.1
...
fix_docker
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
774910f40d | ||
|
|
dbbcc11a98 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -104,7 +104,6 @@ ghostdriver.log
|
||||
testCSV.csv
|
||||
.terser-plugin-cache/
|
||||
apache-superset-*.tar.gz*
|
||||
apache_superset-*.tar.gz*
|
||||
release.json
|
||||
|
||||
# Translation-related files
|
||||
|
||||
@@ -44,7 +44,3 @@ under the License.
|
||||
- [4.0.1](./CHANGELOG/4.0.1.md)
|
||||
- [4.0.2](./CHANGELOG/4.0.2.md)
|
||||
- [4.1.0](./CHANGELOG/4.1.0.md)
|
||||
- [4.1.1](./CHANGELOG/4.1.1.md)
|
||||
- [4.1.2](./CHANGELOG/4.1.2.md)
|
||||
- [4.1.3](./CHANGELOG/4.1.3.md)
|
||||
- [4.1.4](./CHANGELOG/4.1.4.md)
|
||||
|
||||
@@ -19,7 +19,7 @@ under the License.
|
||||
|
||||
## Change Log
|
||||
|
||||
### 4.1.1 (Fri Nov 15 22:13:57 2024 +0530)
|
||||
### 4.1 (Fri Nov 15 22:13:57 2024 +0530)
|
||||
|
||||
**Database Migrations**
|
||||
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
## Change Log
|
||||
|
||||
### 4.1.2 (Fri Mar 7 13:28:05 2025 -0800)
|
||||
|
||||
**Database Migrations**
|
||||
|
||||
- [#32538](https://github.com/apache/superset/pull/32538) fix(migrations): Handle comparator None in old time comparison migration (@Antonio-RiveroMartnez)
|
||||
- [#32155](https://github.com/apache/superset/pull/32155) fix(migrations): Handle no params in time comparison migration (@Antonio-RiveroMartnez)
|
||||
- [#31185](https://github.com/apache/superset/pull/31185) fix: check for column before adding in migrations (@betodealmeida)
|
||||
|
||||
**Features**
|
||||
|
||||
- [#29974](https://github.com/apache/superset/pull/29974) feat(sqllab): Adds refresh button to table metadata in SQL Lab (@Usiel)
|
||||
|
||||
**Fixes**
|
||||
|
||||
- [#32515](https://github.com/apache/superset/pull/32515) fix(sqllab): Allow clear on schema and catalog (@justinpark)
|
||||
- [#32500](https://github.com/apache/superset/pull/32500) fix: dashboard, chart and dataset import validation (@dpgaspar)
|
||||
- [#31353](https://github.com/apache/superset/pull/31353) fix(sqllab): duplicate error message (@betodealmeida)
|
||||
- [#31407](https://github.com/apache/superset/pull/31407) fix: Big Number side cut fixed (@fardin-developer)
|
||||
- [#31480](https://github.com/apache/superset/pull/31480) fix(sunburst): Use metric label from verbose map (@gerbermichi)
|
||||
- [#31427](https://github.com/apache/superset/pull/31427) fix(tags): clean up bulk create api and schema (@villebro)
|
||||
- [#31334](https://github.com/apache/superset/pull/31334) fix(docs): add custom editUrl path for intro page (@dwgrossberg)
|
||||
- [#31353](https://github.com/apache/superset/pull/31353) fix(sqllab): duplicate error message (@betodealmeida)
|
||||
- [#31323](https://github.com/apache/superset/pull/31323) fix: Use clickhouse sqlglot dialect for YDB (@vgvoleg)
|
||||
- [#31198](https://github.com/apache/superset/pull/31198) fix: add more clickhouse disallowed functions on config (@dpgaspar)
|
||||
- [#31194](https://github.com/apache/superset/pull/31194) fix(embedded): Hide anchor links in embedded mode (@Vitor-Avila)
|
||||
- [#31960](https://github.com/apache/superset/pull/31960) fix(sqllab): Missing allowHTML props in ResultTableExtension (@justinpark)
|
||||
- [#31332](https://github.com/apache/superset/pull/31332) fix: prevent multiple pvm errors on migration (@eschutho)
|
||||
- [#31437](https://github.com/apache/superset/pull/31437) fix(database import): Gracefully handle error to get catalog schemas (@Vitor-Avila)
|
||||
- [#31173](https://github.com/apache/superset/pull/31173) fix: cache-warmup fails (@nsivarajan)
|
||||
- [#30442](https://github.com/apache/superset/pull/30442) fix(fe/src/dashboard): optional chaining for possibly nullable parent attribute in LayoutItem type (@hainenber)
|
||||
- [#31639](https://github.com/apache/superset/pull/31639) fix(sqllab): unable to update saved queries (@DamianPendrak)
|
||||
- [#29898](https://github.com/apache/superset/pull/29898) fix: parse pandas pivot null values (@eschutho)
|
||||
- [#31414](https://github.com/apache/superset/pull/31414) fix(Pivot Table): Fix column width to respect currency config (@Vitor-Avila)
|
||||
- [#31335](https://github.com/apache/superset/pull/31335) fix(histogram): axis margin padding consistent with other graphs (@tatiana-cherne)
|
||||
- [#31301](https://github.com/apache/superset/pull/31301) fix(AllEntitiesTable): show Tags (@alexandrusoare)
|
||||
- [#31329](https://github.com/apache/superset/pull/31329) fix: pass string to `process_template` (@betodealmeida)
|
||||
- [#31341](https://github.com/apache/superset/pull/31341) fix(pinot): remove query aliases from SELECT and ORDER BY clauses in Pinot (@yuribogomolov)
|
||||
- [#31308](https://github.com/apache/superset/pull/31308) fix: annotations on horizontal bar chart (@DamianPendrak)
|
||||
- [#31294](https://github.com/apache/superset/pull/31294) fix(sqllab): Remove update_saved_query_exec_info to reduce lag (@justinpark)
|
||||
- [#30897](https://github.com/apache/superset/pull/30897) fix: Exception handling for SQL Lab views (@michael-s-molina)
|
||||
- [#31199](https://github.com/apache/superset/pull/31199) fix(Databricks): Escape catalog and schema names in pre-queries (@Vitor-Avila)
|
||||
- [#31265](https://github.com/apache/superset/pull/31265) fix(trino): db session error in handle cursor (@justinpark)
|
||||
- [#31024](https://github.com/apache/superset/pull/31024) fix(dataset): use sqlglot for DML check (@betodealmeida)
|
||||
- [#29885](https://github.com/apache/superset/pull/29885) fix: add mutator to get_columns_description (@eschutho)
|
||||
- [#30821](https://github.com/apache/superset/pull/30821) fix: x axis title disappears when editing bar chart (@DamianPendrak)
|
||||
- [#31181](https://github.com/apache/superset/pull/31181) fix: Time-series Line Chart Display unnecessary total (@michael-s-molina)
|
||||
- [#31163](https://github.com/apache/superset/pull/31163) fix(Dashboard): Backward compatible shared_label_colors field (@geido)
|
||||
- [#31156](https://github.com/apache/superset/pull/31156) fix: check orderby (@betodealmeida)
|
||||
- [#31154](https://github.com/apache/superset/pull/31154) fix: Remove unwanted commit on Trino's handle_cursor (@michael-s-molina)
|
||||
- [#31151](https://github.com/apache/superset/pull/31151) fix: Revert "feat(trino): Add functionality to upload data (#29164)" (@michael-s-molina)
|
||||
- [#31031](https://github.com/apache/superset/pull/31031) fix(Dashboard): Ensure shared label colors are updated (@geido)
|
||||
- [#30967](https://github.com/apache/superset/pull/30967) fix(release validation): scripts now support RSA and EDDSA keys. (@rusackas)
|
||||
- [#30881](https://github.com/apache/superset/pull/30881) fix(Dashboard): Native & Cross-Filters Scoping Performance (@geido)
|
||||
- [#30887](https://github.com/apache/superset/pull/30887) fix(imports): import query_context for imports with charts (@lindenh)
|
||||
- [#31008](https://github.com/apache/superset/pull/31008) fix(explore): verified props is not updated (@justinpark)
|
||||
- [#30646](https://github.com/apache/superset/pull/30646) fix(Dashboard): Retain colors when color scheme not set (@geido)
|
||||
- [#30962](https://github.com/apache/superset/pull/30962) fix(Dashboard): Exclude edit param in async screenshot (@geido)
|
||||
|
||||
**Others**
|
||||
|
||||
- [#32043](https://github.com/apache/superset/pull/32043) chore: Skip the creation of secondary perms during catalog migrations (@Vitor-Avila)
|
||||
- [#30865](https://github.com/apache/superset/pull/30865) docs: Updating 4.1 Release Notes (@yousoph)
|
||||
@@ -1,58 +0,0 @@
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
## Change Log
|
||||
|
||||
### 4.1.3 (Thu May 29 02:31:07 2025 -0500)
|
||||
|
||||
**Database Migrations**
|
||||
|
||||
**Features**
|
||||
|
||||
**Fixes**
|
||||
|
||||
- [#33522](https://github.com/apache/superset/pull/33522) fix(Sqllab): Autocomplete got stuck in UI when open it too fast (@rebenitez1802)
|
||||
- [#33425](https://github.com/apache/superset/pull/33425) fix(table-chart): time shift is not working (@justinpark)
|
||||
- [#32414](https://github.com/apache/superset/pull/32414) fix(api): Added uuid to list api calls (@withnale)
|
||||
- [#33354](https://github.com/apache/superset/pull/33354) fix: loading examples from raw.githubusercontent.com fails with 429 errors (@mistercrunch)
|
||||
- [#32382](https://github.com/apache/superset/pull/32382) fix(pinot): revert join and subquery flags (@yuribogomolov)
|
||||
- [#32473](https://github.com/apache/superset/pull/32473) fix(plugin-chart-echarts): remove erroneous upper bound value (@villebro)
|
||||
- [#33048](https://github.com/apache/superset/pull/33048) fix: improve error type on parse error (@justinpark)
|
||||
- [#32968](https://github.com/apache/superset/pull/32968) fix(pivot-table): Revert "fix(Pivot Table): Fix column width to respect currency config (#31414)" (@justinpark)
|
||||
- [#32795](https://github.com/apache/superset/pull/32795) fix(log): store navigation path to get correct logging path (@justinpark)
|
||||
- [#33216](https://github.com/apache/superset/pull/33216) fix: Downgrade to marshmallow<4 (@amotl)
|
||||
- [#32866](https://github.com/apache/superset/pull/32866) fix: make packages PEP 625 compliant (@sadpandajoe)
|
||||
- [#32035](https://github.com/apache/superset/pull/32035) fix(fe/dashboard-list): display modifier info for `Last modified` data (@hainenber)
|
||||
- [#32708](https://github.com/apache/superset/pull/32708) fix(logging): missing path in event data (@justinpark)
|
||||
- [#32699](https://github.com/apache/superset/pull/32699) fix: Signature of Celery pruner jobs (@michael-s-molina)
|
||||
- [#32681](https://github.com/apache/superset/pull/32681) fix(log): Update recent_activity by event name (@justinpark)
|
||||
- [#32608](https://github.com/apache/superset/pull/32608) fix(welcome): perf on distinct recent activities (@justinpark)
|
||||
- [#32572](https://github.com/apache/superset/pull/32572) fix: Log table retention policy (@michael-s-molina)
|
||||
- [#32406](https://github.com/apache/superset/pull/32406) fix(model/helper): represent RLS filter clause in proper textual SQL string (@hainenber)
|
||||
- [#32240](https://github.com/apache/superset/pull/32240) fix: upgrade to 3.11.11-slim-bookworm to address critical vulnerabilities (@gpchandran)
|
||||
- [#30858](https://github.com/apache/superset/pull/30858) fix(chart data): removing query from /chart/data payload when accessing as guest user (@fisjac)
|
||||
|
||||
**Others**
|
||||
|
||||
- [#33612](https://github.com/apache/superset/pull/33612) chore: update Dockerfile - Upgrade to 3.11.12 (@gpchandran)
|
||||
- [#33435](https://github.com/apache/superset/pull/33435) docs: CVEs fixed on 4.1.2 (@sha174n)
|
||||
- [#33339](https://github.com/apache/superset/pull/33339) chore(🦾): bump python h11 0.14.0 -> 0.16.0 (@github-actions[bot])
|
||||
- [#32745](https://github.com/apache/superset/pull/32745) chore(🦾): bump python sqlglot 26.1.3 -> 26.11.1 (@github-actions[bot])
|
||||
- [#32782](https://github.com/apache/superset/pull/32782) chore: Revert "chore: bump base image in Dockerfile with `ARG PY_VER=3.11.11-slim-bookworm`" (@sadpandajoe)
|
||||
- [#32780](https://github.com/apache/superset/pull/32780) chore: bump base image in Dockerfile with `ARG PY_VER=3.11.11-slim-bookworm` (@gpchandran)
|
||||
@@ -1,33 +0,0 @@
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
## Change Log
|
||||
|
||||
### 4.1.4 (Thu Jul 24 08:30:04 2025 -0300)
|
||||
|
||||
**Database Migrations**
|
||||
|
||||
**Features**
|
||||
|
||||
**Fixes**
|
||||
- [#34289](https://github.com/apache/superset/pull/34289) fix: Saved queries list break if one query can't be parsed (@michael-s-molina)
|
||||
- [#33059](https://github.com/apache/superset/pull/33059) fix: Adds missing __init__ file to commands/logs (@michael-s-molina)
|
||||
|
||||
**Others**
|
||||
- [#32236](https://github.com/apache/superset/pull/32236) chore(deps): bump cryptography from 43.0.3 to 44.0.1 (@dependabot[bot])
|
||||
123
Dockerfile
123
Dockerfile
@@ -18,7 +18,7 @@
|
||||
######################################################################
|
||||
# Node stage to deal with static asset construction
|
||||
######################################################################
|
||||
ARG PY_VER=3.11.12-slim-bookworm
|
||||
ARG PY_VER=3.10-slim-bookworm
|
||||
|
||||
# if BUILDPLATFORM is null, set it to 'amd64' (or leave as is otherwise).
|
||||
ARG BUILDPLATFORM=${BUILDPLATFORM:-amd64}
|
||||
@@ -28,22 +28,22 @@ ARG NPM_BUILD_CMD="build"
|
||||
|
||||
# Somehow we need python3 + build-essential on this side of the house to install node-gyp
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install \
|
||||
-yqq --no-install-recommends \
|
||||
build-essential \
|
||||
python3
|
||||
&& apt-get install \
|
||||
-yqq --no-install-recommends \
|
||||
build-essential \
|
||||
python3
|
||||
|
||||
ENV BUILD_CMD=${NPM_BUILD_CMD} \
|
||||
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||
# NPM ci first, as to NOT invalidate previous steps except for when package.json changes
|
||||
|
||||
RUN --mount=type=bind,target=/frontend-mem-nag.sh,src=./docker/frontend-mem-nag.sh \
|
||||
/frontend-mem-nag.sh
|
||||
/frontend-mem-nag.sh
|
||||
|
||||
WORKDIR /app/superset-frontend
|
||||
RUN --mount=type=bind,target=./package.json,src=./superset-frontend/package.json \
|
||||
--mount=type=bind,target=./package-lock.json,src=./superset-frontend/package-lock.json \
|
||||
npm ci
|
||||
--mount=type=bind,target=./package-lock.json,src=./superset-frontend/package-lock.json \
|
||||
npm ci
|
||||
|
||||
# Runs the webpack build process
|
||||
COPY superset-frontend /app/superset-frontend
|
||||
@@ -57,45 +57,46 @@ RUN npm run build-translation
|
||||
RUN rm /app/superset/translations/*/LC_MESSAGES/*.po
|
||||
RUN rm /app/superset/translations/messages.pot
|
||||
|
||||
FROM python:${PY_VER} AS python-base
|
||||
######################################################################
|
||||
# Final lean image...
|
||||
######################################################################
|
||||
FROM python:${PY_VER} AS lean
|
||||
FROM python-base AS lean
|
||||
|
||||
WORKDIR /app
|
||||
ENV LANG=C.UTF-8 \
|
||||
LC_ALL=C.UTF-8 \
|
||||
SUPERSET_ENV=production \
|
||||
FLASK_APP="superset.app:create_app()" \
|
||||
PYTHONPATH="/app/pythonpath" \
|
||||
SUPERSET_HOME="/app/superset_home" \
|
||||
SUPERSET_PORT=8088
|
||||
LC_ALL=C.UTF-8 \
|
||||
SUPERSET_ENV=production \
|
||||
FLASK_APP="superset.app:create_app()" \
|
||||
PYTHONPATH="/app/pythonpath" \
|
||||
SUPERSET_HOME="/app/superset_home" \
|
||||
SUPERSET_PORT=8088
|
||||
|
||||
RUN mkdir -p ${PYTHONPATH} superset/static requirements superset-frontend apache_superset.egg-info requirements \
|
||||
&& useradd --user-group -d ${SUPERSET_HOME} -m --no-log-init --shell /bin/bash superset \
|
||||
&& apt-get update -qq && apt-get install -yqq --no-install-recommends \
|
||||
curl \
|
||||
default-libmysqlclient-dev \
|
||||
libsasl2-dev \
|
||||
libsasl2-modules-gssapi-mit \
|
||||
libpq-dev \
|
||||
libecpg-dev \
|
||||
libldap2-dev \
|
||||
&& touch superset/static/version_info.json \
|
||||
&& chown -R superset:superset ./* \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& useradd --user-group -d ${SUPERSET_HOME} -m --no-log-init --shell /bin/bash superset \
|
||||
&& apt-get update -qq && apt-get install -yqq --no-install-recommends \
|
||||
curl \
|
||||
default-libmysqlclient-dev \
|
||||
libsasl2-dev \
|
||||
libsasl2-modules-gssapi-mit \
|
||||
libpq-dev \
|
||||
libecpg-dev \
|
||||
libldap2-dev \
|
||||
&& touch superset/static/version_info.json \
|
||||
&& chown -R superset:superset ./* \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --chown=superset:superset pyproject.toml setup.py MANIFEST.in README.md ./
|
||||
# setup.py uses the version information in package.json
|
||||
COPY --chown=superset:superset superset-frontend/package.json superset-frontend/
|
||||
COPY --chown=superset:superset requirements/base.txt requirements/
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
apt-get update -qq && apt-get install -yqq --no-install-recommends \
|
||||
build-essential \
|
||||
&& pip install --upgrade setuptools pip \
|
||||
&& pip install -r requirements/base.txt \
|
||||
&& apt-get autoremove -yqq --purge build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
apt-get update -qq && apt-get install -yqq --no-install-recommends \
|
||||
build-essential \
|
||||
&& pip install --upgrade setuptools pip \
|
||||
&& pip install -r requirements/base.txt \
|
||||
&& apt-get autoremove -yqq --purge build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy the compiled frontend assets
|
||||
COPY --chown=superset:superset --from=superset-node /app/superset/static/assets superset/static/assets
|
||||
@@ -103,7 +104,7 @@ COPY --chown=superset:superset --from=superset-node /app/superset/static/assets
|
||||
## Lastly, let's install superset itself
|
||||
COPY --chown=superset:superset superset superset
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip install -e .
|
||||
pip install -e .
|
||||
|
||||
# Copy the .json translations from the frontend layer
|
||||
COPY --chown=superset:superset --from=superset-node /app/superset/translations superset/translations
|
||||
@@ -111,9 +112,9 @@ COPY --chown=superset:superset --from=superset-node /app/superset/translations s
|
||||
# Compile translations for the backend - this generates .mo files, then deletes the .po files
|
||||
COPY ./scripts/translations/generate_mo_files.sh ./scripts/translations/
|
||||
RUN ./scripts/translations/generate_mo_files.sh \
|
||||
&& chown -R superset:superset superset/translations \
|
||||
&& rm superset/translations/messages.pot \
|
||||
&& rm superset/translations/*/LC_MESSAGES/*.po
|
||||
&& chown -R superset:superset superset/translations \
|
||||
&& rm superset/translations/messages.pot \
|
||||
&& rm superset/translations/*/LC_MESSAGES/*.po
|
||||
|
||||
COPY --chmod=755 ./docker/run-server.sh /usr/bin/
|
||||
USER superset
|
||||
@@ -131,42 +132,42 @@ FROM lean AS dev
|
||||
|
||||
USER root
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install -yqq --no-install-recommends \
|
||||
libnss3 \
|
||||
libdbus-glib-1-2 \
|
||||
libgtk-3-0 \
|
||||
libx11-xcb1 \
|
||||
libasound2 \
|
||||
libxtst6 \
|
||||
git \
|
||||
pkg-config \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& apt-get install -yqq --no-install-recommends \
|
||||
libnss3 \
|
||||
libdbus-glib-1-2 \
|
||||
libgtk-3-0 \
|
||||
libx11-xcb1 \
|
||||
libasound2 \
|
||||
libxtst6 \
|
||||
git \
|
||||
pkg-config \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip install playwright
|
||||
pip install playwright
|
||||
RUN playwright install-deps
|
||||
RUN playwright install chromium
|
||||
|
||||
# Install GeckoDriver WebDriver
|
||||
ARG GECKODRIVER_VERSION=v0.34.0 \
|
||||
FIREFOX_VERSION=125.0.3
|
||||
FIREFOX_VERSION=125.0.3
|
||||
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install -yqq --no-install-recommends wget bzip2 \
|
||||
&& wget -q https://github.com/mozilla/geckodriver/releases/download/${GECKODRIVER_VERSION}/geckodriver-${GECKODRIVER_VERSION}-linux64.tar.gz -O - | tar xfz - -C /usr/local/bin \
|
||||
# Install Firefox
|
||||
&& wget -q https://download-installer.cdn.mozilla.net/pub/firefox/releases/${FIREFOX_VERSION}/linux-x86_64/en-US/firefox-${FIREFOX_VERSION}.tar.bz2 -O - | tar xfj - -C /opt \
|
||||
&& ln -s /opt/firefox/firefox /usr/local/bin/firefox \
|
||||
&& apt-get autoremove -yqq --purge wget bzip2 && rm -rf /var/[log,tmp]/* /tmp/* /var/lib/apt/lists/*
|
||||
&& apt-get install -yqq --no-install-recommends wget bzip2 \
|
||||
&& wget -q https://github.com/mozilla/geckodriver/releases/download/${GECKODRIVER_VERSION}/geckodriver-${GECKODRIVER_VERSION}-linux64.tar.gz -O - | tar xfz - -C /usr/local/bin \
|
||||
# Install Firefox
|
||||
&& wget -q https://download-installer.cdn.mozilla.net/pub/firefox/releases/${FIREFOX_VERSION}/linux-x86_64/en-US/firefox-${FIREFOX_VERSION}.tar.bz2 -O - | tar xfj - -C /opt \
|
||||
&& ln -s /opt/firefox/firefox /usr/local/bin/firefox \
|
||||
&& apt-get autoremove -yqq --purge wget bzip2 && rm -rf /var/[log,tmp]/* /tmp/* /var/lib/apt/lists/*
|
||||
# Cache everything for dev purposes...
|
||||
|
||||
COPY --chown=superset:superset requirements/development.txt requirements/
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
apt-get update -qq && apt-get install -yqq --no-install-recommends \
|
||||
build-essential \
|
||||
&& pip install -r requirements/development.txt \
|
||||
&& apt-get autoremove -yqq --purge build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
apt-get update -qq && apt-get install -yqq --no-install-recommends \
|
||||
build-essential \
|
||||
&& pip install -r requirements/development.txt \
|
||||
&& apt-get autoremove -yqq --purge build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
USER superset
|
||||
######################################################################
|
||||
|
||||
15
README.md
15
README.md
@@ -22,9 +22,9 @@ under the License.
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://github.com/apache/superset/tree/latest)
|
||||
[](https://github.com/apache/superset/actions)
|
||||
[](https://badge.fury.io/py/apache_superset)
|
||||
[](https://badge.fury.io/py/apache-superset)
|
||||
[](https://codecov.io/github/apache/superset)
|
||||
[](https://pypi.python.org/pypi/apache_superset)
|
||||
[](https://pypi.python.org/pypi/apache-superset)
|
||||
[](http://bit.ly/join-superset-slack)
|
||||
[](https://superset.apache.org)
|
||||
|
||||
@@ -72,10 +72,8 @@ Superset provides:
|
||||
## Screenshots & Gifs
|
||||
|
||||
**Video Overview**
|
||||
|
||||
<!-- File hosted here https://github.com/apache/superset-site/raw/lfs/superset-video-4k.mp4 -->
|
||||
|
||||
[superset-video-1080p.webm](https://github.com/user-attachments/assets/b37388f7-a971-409c-96a7-90c4e31322e6)
|
||||
[superset-video-4k.webm](https://github.com/apache/superset/assets/812905/da036bc2-150c-4ee7-80f9-75e63210ff76)
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -153,7 +151,7 @@ Want to add support for your datastore or data engine? Read more [here](https://
|
||||
and please read our [Slack Community Guidelines](https://github.com/apache/superset/blob/master/CODE_OF_CONDUCT.md#slack-community-guidelines)
|
||||
- [Join our dev@superset.apache.org Mailing list](https://lists.apache.org/list.html?dev@superset.apache.org). To join, simply send an email to [dev-subscribe@superset.apache.org](mailto:dev-subscribe@superset.apache.org)
|
||||
- If you want to help troubleshoot GitHub Issues involving the numerous database drivers that Superset supports, please consider adding your name and the databases you have access to on the [Superset Database Familiarity Rolodex](https://docs.google.com/spreadsheets/d/1U1qxiLvOX0kBTUGME1AHHi6Ywel6ECF8xk_Qy-V9R8c/edit#gid=0)
|
||||
- Join Superset's Town Hall and [Operational Model](https://preset.io/blog/the-superset-operational-model-wants-you/) recurring meetings. Meeting info is available on the [Superset Community Calendar](https://superset.apache.org/community)
|
||||
- Join Superset's Town Hall and [Operational Model](https://preset.io/blog/the-superset-operational-model-wants-you/) recurring meetings. Meeting info is available on the [Superset Community Calendar](https://superset.apache.org/community)
|
||||
|
||||
## Contributor Guide
|
||||
|
||||
@@ -181,16 +179,14 @@ Understanding the Superset Points of View
|
||||
- [Building New Database Connectors](https://preset.io/blog/building-database-connector/)
|
||||
- [Create Your First Dashboard](https://superset.apache.org/docs/using-superset/creating-your-first-dashboard/)
|
||||
- [Comprehensive Tutorial for Contributing Code to Apache Superset
|
||||
](https://preset.io/blog/tutorial-contributing-code-to-apache-superset/)
|
||||
](https://preset.io/blog/tutorial-contributing-code-to-apache-superset/)
|
||||
- [Resources to master Superset by Preset](https://preset.io/resources/)
|
||||
|
||||
- Deploying Superset
|
||||
|
||||
- [Official Docker image](https://hub.docker.com/r/apache/superset)
|
||||
- [Helm Chart](https://github.com/apache/superset/tree/master/helm/superset)
|
||||
|
||||
- Recordings of Past [Superset Community Events](https://preset.io/events)
|
||||
|
||||
- [Mixed Time Series Charts](https://preset.io/events/mixed-time-series-visualization-in-superset-workshop/)
|
||||
- [How the Bing Team Customized Superset for the Internal Self-Serve Data & Analytics Platform](https://preset.io/events/how-the-bing-team-heavily-customized-superset-for-their-internal-data/)
|
||||
- [Live Demo: Visualizing MongoDB and Pinot Data using Trino](https://preset.io/events/2021-04-13-visualizing-mongodb-and-pinot-data-using-trino/)
|
||||
@@ -198,7 +194,6 @@ Understanding the Superset Points of View
|
||||
- [Building a Database Connector for Superset](https://preset.io/events/2021-02-16-building-a-database-connector-for-superset/)
|
||||
|
||||
- Visualizations
|
||||
|
||||
- [Creating Viz Plugins](https://superset.apache.org/docs/contributing/creating-viz-plugins/)
|
||||
- [Managing and Deploying Custom Viz Plugins](https://medium.com/nmc-techblog/apache-superset-manage-custom-viz-plugins-in-production-9fde1a708e55)
|
||||
- [Why Apache Superset is Betting on Apache ECharts](https://preset.io/blog/2021-4-1-why-echarts/)
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN useradd --user-group --create-home --no-log-init --shell /bin/bash superset
|
||||
|
||||
# Configure environment
|
||||
ENV LANG=C.UTF-8 \
|
||||
LC_ALL=C.UTF-8
|
||||
LC_ALL=C.UTF-8
|
||||
|
||||
RUN apt-get update -y
|
||||
|
||||
@@ -30,14 +30,14 @@ RUN apt-get install -y apt-transport-https apt-utils
|
||||
# Install superset dependencies
|
||||
# https://superset.apache.org/docs/installation/installing-superset-from-scratch
|
||||
RUN apt-get install -y build-essential libssl-dev \
|
||||
libffi-dev python3-dev libsasl2-dev libldap2-dev libxi-dev chromium zstd
|
||||
libffi-dev python3-dev libsasl2-dev libldap2-dev libxi-dev chromium
|
||||
|
||||
# Install nodejs for custom build
|
||||
# https://nodejs.org/en/download/package-manager/
|
||||
RUN set -eux; \
|
||||
curl -sL https://deb.nodesource.com/setup_20.x | bash -; \
|
||||
apt-get install -y nodejs; \
|
||||
node --version;
|
||||
curl -sL https://deb.nodesource.com/setup_18.x | bash -; \
|
||||
apt-get install -y nodejs; \
|
||||
node --version;
|
||||
RUN if ! which npm; then apt-get install -y npm; fi
|
||||
|
||||
RUN mkdir -p /home/superset
|
||||
@@ -50,21 +50,21 @@ ARG SUPERSET_RELEASE_RC_TARBALL
|
||||
# Can fetch source from svn or copy tarball from local mounted directory
|
||||
COPY $SUPERSET_RELEASE_RC_TARBALL ./
|
||||
RUN tar -xvf *.tar.gz
|
||||
WORKDIR /home/superset/apache_superset-$VERSION/superset-frontend
|
||||
WORKDIR /home/superset/apache-superset-$VERSION/superset-frontend
|
||||
|
||||
RUN npm ci \
|
||||
&& npm run build \
|
||||
&& rm -rf node_modules
|
||||
&& npm run build \
|
||||
&& rm -rf node_modules
|
||||
|
||||
WORKDIR /home/superset/apache_superset-$VERSION
|
||||
WORKDIR /home/superset/apache-superset-$VERSION
|
||||
RUN pip install --upgrade setuptools pip \
|
||||
&& pip install -r requirements/base.txt \
|
||||
&& pip install --no-cache-dir .
|
||||
&& pip install -r requirements/base.txt \
|
||||
&& pip install --no-cache-dir .
|
||||
|
||||
RUN flask fab babel-compile --target superset/translations
|
||||
|
||||
ENV PATH=/home/superset/superset/bin:$PATH \
|
||||
PYTHONPATH=/home/superset/superset/ \
|
||||
SUPERSET_TESTENV=true
|
||||
PYTHONPATH=/home/superset/superset/:$PYTHONPATH \
|
||||
SUPERSET_TESTENV=true
|
||||
COPY from_tarball_entrypoint.sh /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN useradd --user-group --create-home --no-log-init --shell /bin/bash superset
|
||||
|
||||
# Configure environment
|
||||
ENV LANG=C.UTF-8 \
|
||||
LC_ALL=C.UTF-8
|
||||
LC_ALL=C.UTF-8
|
||||
|
||||
RUN apt-get update -y
|
||||
|
||||
@@ -29,16 +29,13 @@ RUN apt-get install -y apt-transport-https apt-utils
|
||||
|
||||
# Install superset dependencies
|
||||
# https://superset.apache.org/docs/installation/installing-superset-from-scratch
|
||||
RUN apt-get install -y subversion build-essential libssl-dev \
|
||||
libffi-dev python3-dev libsasl2-dev libldap2-dev libxi-dev chromium zstd
|
||||
RUN apt-get install -y build-essential libssl-dev \
|
||||
libffi-dev python3-dev libsasl2-dev libldap2-dev libxi-dev chromium
|
||||
|
||||
# Install nodejs for custom build
|
||||
# https://nodejs.org/en/download/package-manager/
|
||||
RUN set -eux; \
|
||||
curl -sL https://deb.nodesource.com/setup_20.x | bash -; \
|
||||
apt-get install -y nodejs; \
|
||||
node --version;
|
||||
RUN if ! which npm; then apt-get install -y npm; fi
|
||||
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
RUN mkdir -p /home/superset
|
||||
RUN chown superset /home/superset
|
||||
@@ -49,20 +46,22 @@ ARG VERSION
|
||||
# Can fetch source from svn or copy tarball from local mounted directory
|
||||
RUN svn co https://dist.apache.org/repos/dist/dev/superset/$VERSION ./
|
||||
RUN tar -xvf *.tar.gz
|
||||
WORKDIR /home/superset/apache_superset-$VERSION/superset-frontend
|
||||
WORKDIR apache-superset-$VERSION
|
||||
|
||||
RUN npm ci \
|
||||
&& npm run build \
|
||||
&& rm -rf node_modules
|
||||
RUN cd superset-frontend \
|
||||
&& npm ci \
|
||||
&& npm run build \
|
||||
&& rm -rf node_modules
|
||||
|
||||
WORKDIR /home/superset/apache_superset-$VERSION
|
||||
|
||||
WORKDIR /home/superset/apache-superset-$VERSION
|
||||
RUN pip install --upgrade setuptools pip \
|
||||
&& pip install -r requirements/base.txt \
|
||||
&& pip install --no-cache-dir .
|
||||
&& pip install -r requirements/base.txt \
|
||||
&& pip install --no-cache-dir .
|
||||
|
||||
RUN flask fab babel-compile --target superset/translations
|
||||
|
||||
ENV PATH=/home/superset/superset/bin:$PATH \
|
||||
PYTHONPATH=/home/superset/superset/
|
||||
PYTHONPATH=/home/superset/superset/:$PYTHONPATH
|
||||
COPY from_tarball_entrypoint.sh /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
@@ -123,10 +123,10 @@ SUPERSET_RC=1
|
||||
SUPERSET_GITHUB_BRANCH=1.5
|
||||
SUPERSET_PGP_FULLNAME=villebro@apache.org
|
||||
SUPERSET_VERSION_RC=1.5.1rc1
|
||||
SUPERSET_RELEASE=apache_superset-1.5.1
|
||||
SUPERSET_RELEASE_RC=apache_superset-1.5.1rc1
|
||||
SUPERSET_RELEASE_TARBALL=apache_superset-1.5.1-source.tar.gz
|
||||
SUPERSET_RELEASE_RC_TARBALL=apache_superset-1.5.1rc1-source.tar.gz
|
||||
SUPERSET_RELEASE=apache-superset-1.5.1
|
||||
SUPERSET_RELEASE_RC=apache-superset-1.5.1rc1
|
||||
SUPERSET_RELEASE_TARBALL=apache-superset-1.5.1-source.tar.gz
|
||||
SUPERSET_RELEASE_RC_TARBALL=apache-superset-1.5.1rc1-source.tar.gz
|
||||
SUPERSET_TMP_ASF_SITE_PATH=/tmp/incubator-superset-site-1.5.1
|
||||
-------------------------------
|
||||
```
|
||||
@@ -380,7 +380,7 @@ Official instructions:
|
||||
https://www.apache.org/info/verification.html
|
||||
|
||||
We now have a handy script for anyone validating a release to use. The core of it is in this very folder, `verify_release.py`. Just make sure you have all three release files in the same directory (`{some version}.tar.gz`, `{some version}.tar.gz.asc` and `{some version}tar.gz.sha512`). Then you can pass this script the path to the `.gz` file like so:
|
||||
`python verify_release.py ~/path/tp/apache_superset-{version/candidate}-source.tar.gz`
|
||||
`python verify_release.py ~/path/tp/apache-superset-{version/candidate}-source.tar.gz`
|
||||
|
||||
If all goes well, you will see this result in your terminal:
|
||||
|
||||
@@ -466,8 +466,7 @@ an account first if you don't have one, and reference your username
|
||||
while requesting access to push packages.
|
||||
|
||||
```bash
|
||||
twine upload dist/apache_superset-${SUPERSET_VERSION}-py3-none-any.whl
|
||||
twine upload dist/apache_superset-${SUPERSET_VERSION}.tar.gz
|
||||
twine upload dist/apache-superset-${SUPERSET_VERSION}.tar.gz
|
||||
```
|
||||
|
||||
Set your username to `__token__`
|
||||
|
||||
@@ -31,7 +31,7 @@ The official source release:
|
||||
https://downloads.apache.org/{{ project_module }}/{{ version }}
|
||||
|
||||
The PyPI package:
|
||||
https://pypi.org/project/apache_superset/{{ version }}
|
||||
https://pypi.org/project/apache-superset/{{ version }}
|
||||
|
||||
The CHANGELOG for the release:
|
||||
https://github.com/apache/{{ project_module }}/blob/{{ version }}/CHANGELOG/{{ version }}.md
|
||||
|
||||
@@ -32,7 +32,7 @@ else
|
||||
SUPERSET_VERSION="${1}"
|
||||
SUPERSET_RC="${2}"
|
||||
SUPERSET_PGP_FULLNAME="${3}"
|
||||
SUPERSET_RELEASE_RC_TARBALL="apache_superset-${SUPERSET_VERSION_RC}-source.tar.gz"
|
||||
SUPERSET_RELEASE_RC_TARBALL="apache-superset-${SUPERSET_VERSION_RC}-source.tar.gz"
|
||||
fi
|
||||
|
||||
SUPERSET_VERSION_RC="${SUPERSET_VERSION}rc${SUPERSET_RC}"
|
||||
|
||||
@@ -22,7 +22,7 @@ if [ -z "${SUPERSET_VERSION_RC}" ] || [ -z "${SUPERSET_SVN_DEV_PATH}" ] || [ -z
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SUPERSET_RELEASE_RC=apache_superset-"${SUPERSET_VERSION_RC}"
|
||||
SUPERSET_RELEASE_RC=apache-superset-"${SUPERSET_VERSION_RC}"
|
||||
SUPERSET_RELEASE_RC_TARBALL="${SUPERSET_RELEASE_RC}"-source.tar.gz
|
||||
SUPERSET_RELEASE_RC_BASE_PATH="${SUPERSET_SVN_DEV_PATH}"/"${SUPERSET_VERSION_RC}"
|
||||
SUPERSET_RELEASE_RC_TARBALL_PATH="${SUPERSET_RELEASE_RC_BASE_PATH}"/"${SUPERSET_RELEASE_RC_TARBALL}"
|
||||
|
||||
@@ -50,8 +50,8 @@ else
|
||||
export SUPERSET_GITHUB_BRANCH="${VERSION_MAJOR}.${VERSION_MINOR}"
|
||||
export SUPERSET_PGP_FULLNAME="${2}"
|
||||
export SUPERSET_VERSION_RC="${SUPERSET_VERSION}rc${VERSION_RC}"
|
||||
export SUPERSET_RELEASE=apache_superset-"${SUPERSET_VERSION}"
|
||||
export SUPERSET_RELEASE_RC=apache_superset-"${SUPERSET_VERSION_RC}"
|
||||
export SUPERSET_RELEASE=apache-superset-"${SUPERSET_VERSION}"
|
||||
export SUPERSET_RELEASE_RC=apache-superset-"${SUPERSET_VERSION_RC}"
|
||||
export SUPERSET_RELEASE_TARBALL="${SUPERSET_RELEASE}"-source.tar.gz
|
||||
export SUPERSET_RELEASE_RC_TARBALL="${SUPERSET_RELEASE_RC}"-source.tar.gz
|
||||
export SUPERSET_TMP_ASF_SITE_PATH="/tmp/incubator-superset-site-${SUPERSET_VERSION}"
|
||||
|
||||
@@ -27,7 +27,7 @@ if [ -z "${SUPERSET_SVN_DEV_PATH}" ]; then
|
||||
fi
|
||||
|
||||
if [[ -n ${1} ]] && [[ ${1} == "local" ]]; then
|
||||
SUPERSET_RELEASE_RC=apache_superset-"${SUPERSET_VERSION_RC}"
|
||||
SUPERSET_RELEASE_RC=apache-superset-"${SUPERSET_VERSION_RC}"
|
||||
SUPERSET_RELEASE_RC_TARBALL="${SUPERSET_RELEASE_RC}"-source.tar.gz
|
||||
SUPERSET_TARBALL_PATH="${SUPERSET_SVN_DEV_PATH}"/${SUPERSET_VERSION_RC}/${SUPERSET_RELEASE_RC_TARBALL}
|
||||
SUPERSET_TMP_TARBALL_FILENAME=_tmp_"${SUPERSET_VERSION_RC}".tar.gz
|
||||
|
||||
@@ -38,7 +38,7 @@ get_pip_command() {
|
||||
PYTHON=$(get_python_command)
|
||||
PIP=$(get_pip_command)
|
||||
|
||||
# Get the release directory's path. If you unzip an Apache release and just run the npm script to validate the release, this will be a file name like `apache_superset-x.x.xrcx-source.tar.gz`
|
||||
# Get the release directory's path. If you unzip an Apache release and just run the npm script to validate the release, this will be a file name like `apache-superset-x.x.xrcx-source.tar.gz`
|
||||
RELEASE_ZIP_PATH="../../$(basename "$(dirname "$(pwd)")")-source.tar.gz"
|
||||
|
||||
# Install dependencies from requirements.txt if the file exists
|
||||
|
||||
@@ -65,43 +65,35 @@ def get_gpg_info(filename: str) -> tuple[Optional[str], Optional[str]]:
|
||||
output = result.stderr.decode()
|
||||
|
||||
rsa_key = re.search(r"RSA key ([0-9A-F]+)", output)
|
||||
eddsa_key = re.search(r"EDDSA key ([0-9A-F]+)", output)
|
||||
email = re.search(r'issuer "([^"]+)"', output)
|
||||
|
||||
rsa_key_result = rsa_key.group(1) if rsa_key else None
|
||||
eddsa_key_result = eddsa_key.group(1) if eddsa_key else None
|
||||
email_result = email.group(1) if email else None
|
||||
|
||||
key_result = rsa_key_result or eddsa_key_result
|
||||
|
||||
# Debugging:
|
||||
if key_result:
|
||||
print("RSA or EDDSA Key found")
|
||||
else:
|
||||
print("Warning: No RSA or EDDSA key found in GPG verification output.")
|
||||
if email_result:
|
||||
print("email found")
|
||||
else:
|
||||
# Debugging: print warnings if rsa_key or email is not found
|
||||
if rsa_key_result is None:
|
||||
print("Warning: No RSA key found in GPG verification output.")
|
||||
if email_result is None:
|
||||
print("Warning: No email address found in GPG verification output.")
|
||||
|
||||
return key_result, email_result
|
||||
return rsa_key_result, email_result
|
||||
|
||||
|
||||
def verify_key(key: str, email: Optional[str]) -> str:
|
||||
"""Fetch the KEYS file and verify if the RSA/EDDSA key and email match."""
|
||||
def verify_rsa_key(rsa_key: str, email: Optional[str]) -> str:
|
||||
"""Fetch the KEYS file and verify if the RSA key and email match."""
|
||||
url = "https://downloads.apache.org/superset/KEYS"
|
||||
response = requests.get(url)
|
||||
if response.status_code == 200:
|
||||
if key not in response.text:
|
||||
return "RSA/EDDSA key not found on KEYS page"
|
||||
if rsa_key not in response.text:
|
||||
return "RSA key not found on KEYS page"
|
||||
|
||||
# Check if email is None or not in response.text
|
||||
if email and email in response.text:
|
||||
return "RSA/EDDSA key and email verified against Apache KEYS file"
|
||||
return "RSA key and email verified against Apache KEYS file"
|
||||
elif email:
|
||||
return "RSA/EDDSA key verified, but Email not found on KEYS page"
|
||||
return "RSA key verified, but Email not found on KEYS page"
|
||||
else:
|
||||
return "RSA/EDDSA key verified, but Email not available for verification"
|
||||
return "RSA key verified, but Email not available for verification"
|
||||
else:
|
||||
return "Failed to fetch KEYS file"
|
||||
|
||||
@@ -111,9 +103,9 @@ def verify_sha512_and_rsa(filename: str) -> None:
|
||||
sha_result = verify_sha512(filename)
|
||||
print(sha_result)
|
||||
|
||||
key, email = get_gpg_info(filename)
|
||||
if key:
|
||||
rsa_result = verify_key(key, email)
|
||||
rsa_key, email = get_gpg_info(filename)
|
||||
if rsa_key:
|
||||
rsa_result = verify_rsa_key(rsa_key, email)
|
||||
print(rsa_result)
|
||||
else:
|
||||
print("GPG verification failed: RSA key or email not found")
|
||||
|
||||
@@ -22,13 +22,6 @@ under the License.
|
||||
This file documents any backwards-incompatible changes in Superset and
|
||||
assists people when migrating to a new version.
|
||||
|
||||
## 4.1.2
|
||||
|
||||
- [31198](https://github.com/apache/superset/pull/31198) Disallows by default the use of the following ClickHouse functions: "version", "currentDatabase", "hostName".
|
||||
- [31173](https://github.com/apache/superset/pull/31173) Modified `fetch_csrf_token` to align with HTTP standards, particularly regarding how cookies are handled. If you encounter any issues related to CSRF functionality, please report them as a new issue and reference this PR for context.
|
||||
|
||||
### Potential Downtime
|
||||
|
||||
## 4.1.0
|
||||
|
||||
- [29274](https://github.com/apache/superset/pull/29274): We made it easier to trigger CI on your
|
||||
@@ -236,7 +229,7 @@ assists people when migrating to a new version.
|
||||
- [19231](https://github.com/apache/superset/pull/19231): The `ENABLE_REACT_CRUD_VIEWS` feature flag has been removed (permanently enabled). Any deployments which had set this flag to false will need to verify that the React views support their use case.
|
||||
- [19230](https://github.com/apache/superset/pull/19230): The `ROW_LEVEL_SECURITY` feature flag has been removed (permanently enabled). Any deployments which had set this flag to false will need to verify that the presence of the Row Level Security feature does not interfere with their use case.
|
||||
- [19168](https://github.com/apache/superset/pull/19168): Celery upgrade to 5.X resulted in breaking changes to its command line invocation.
|
||||
html#step-1-adjust-your-command-line-invocation) instructions for adjustments. Also consider migrating you Celery config per [here](https://docs.celeryq.dev/en/stable/userguide/configuration.html#conf-old-settings-map).
|
||||
html#step-1-adjust-your-command-line-invocation) instructions for adjustments. Also consider migrating you Celery config per [here](https://docs.celeryq.dev/en/stable/userguide/configuration.html#conf-old-settings-map).
|
||||
- [19142](https://github.com/apache/superset/pull/19142): The `VERSIONED_EXPORT` config key is now `True` by default.
|
||||
- [19113](https://github.com/apache/superset/pull/19113): The `ENABLE_JAVASCRIPT_CONTROLS` config key has moved from an app config to a feature flag. Any deployments who overrode this setting will now need to override the feature flag from here onward.
|
||||
- [19107](https://github.com/apache/superset/pull/19107): The `SQLLAB_BACKEND_PERSISTENCE` feature flag is now `True` by default, which enables persisting SQL Lab tabs in the backend instead of the browser's `localStorage`.
|
||||
|
||||
@@ -70,7 +70,7 @@ are compatible with Superset.
|
||||
| [PostgreSQL](/docs/configuration/databases#postgres) | `pip install psycopg2` | `postgresql://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
|
||||
| [Presto](/docs/configuration/databases#presto) | `pip install pyhive` | `presto://` |
|
||||
| [Rockset](/docs/configuration/databases#rockset) | `pip install rockset-sqlalchemy` | `rockset://<api_key>:@<api_server>` |
|
||||
| [SAP Hana](/docs/configuration/databases#hana) | `pip install hdbcli sqlalchemy-hana` or `pip install apache_superset[hana]` | `hana://{username}:{password}@{host}:{port}` |
|
||||
| [SAP Hana](/docs/configuration/databases#hana) | `pip install hdbcli sqlalchemy-hana` or `pip install apache-superset[hana]` | `hana://{username}:{password}@{host}:{port}` |
|
||||
| [StarRocks](/docs/configuration/databases#starrocks) | `pip install starrocks` | `starrocks://<User>:<Password>@<Host>:<Port>/<Catalog>.<Database>` |
|
||||
| [Snowflake](/docs/configuration/databases#snowflake) | `pip install snowflake-sqlalchemy` | `snowflake://{user}:{password}@{account}.{region}/{database}?role={role}&warehouse={warehouse}` |
|
||||
| SQLite | No additional library needed | `sqlite://path/to/file.db?check_same_thread=false` |
|
||||
|
||||
@@ -11,7 +11,7 @@ version: 1
|
||||
To configure CORS, or cross-origin resource sharing, the following dependency must be installed:
|
||||
|
||||
```python
|
||||
pip install apache_superset[cors]
|
||||
pip install apache-superset[cors]
|
||||
```
|
||||
|
||||
The following keys in `superset_config.py` can be specified to configure CORS:
|
||||
|
||||
@@ -26,9 +26,9 @@ More references:
|
||||
Here's a list of repositories that contain Superset-related packages:
|
||||
|
||||
- [apache/superset](https://github.com/apache/superset)
|
||||
is the main repository containing the `apache_superset` Python package
|
||||
is the main repository containing the `apache-superset` Python package
|
||||
distributed on
|
||||
[pypi](https://pypi.org/project/apache_superset/). This repository
|
||||
[pypi](https://pypi.org/project/apache-superset/). This repository
|
||||
also includes Superset's main TypeScript/JavaScript bundles and react apps under
|
||||
the [superset-frontend](https://github.com/apache/superset/tree/master/superset-frontend)
|
||||
folder.
|
||||
|
||||
@@ -12,7 +12,7 @@ import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||
<img src={useBaseUrl("/img/pypi.png" )} width="150" />
|
||||
<br /><br />
|
||||
|
||||
This page describes how to install Superset using the `apache_superset` package [published on PyPI](https://pypi.org/project/apache_superset/).
|
||||
This page describes how to install Superset using the `apache-superset` package [published on PyPI](https://pypi.org/project/apache-superset/).
|
||||
|
||||
## OS Dependencies
|
||||
|
||||
@@ -128,10 +128,10 @@ command line.
|
||||
|
||||
### Installing and Initializing Superset
|
||||
|
||||
First, start by installing `apache_superset`:
|
||||
First, start by installing `apache-superset`:
|
||||
|
||||
```bash
|
||||
pip install apache_superset
|
||||
pip install apache-superset
|
||||
```
|
||||
|
||||
Then, you need to initialize the database:
|
||||
|
||||
@@ -32,7 +32,7 @@ docker compose up
|
||||
To upgrade superset in a native installation, run the following commands:
|
||||
|
||||
```bash
|
||||
pip install apache_superset --upgrade
|
||||
pip install apache-superset --upgrade
|
||||
```
|
||||
|
||||
## Upgrading the Metadata Database
|
||||
|
||||
@@ -2,21 +2,6 @@
|
||||
title: CVEs fixed by release
|
||||
sidebar_position: 2
|
||||
---
|
||||
#### Version 4.1.2
|
||||
|
||||
| CVE | Title | Affected |
|
||||
|:---------------|:-----------------------------------------------------------------------------------|---------:|
|
||||
| CVE-2025-27696 | Improper authorization leading to resource ownership takeover | < 4.1.2 |
|
||||
|
||||
#### Version 4.1.0
|
||||
|
||||
| CVE | Title | Affected |
|
||||
|:---------------|:-----------------------------------------------------------------------------------|---------:|
|
||||
| CVE-2024-53947 | Improper SQL authorisation, parse for specific postgres functions | < 4.1.0 |
|
||||
| CVE-2024-53948 | Error verbosity exposes metadata in analytics databases | < 4.1.0 |
|
||||
| CVE-2024-53949 | Lower privilege users are able to create Role when FAB_ADD_SECURITY_API is enabled | < 4.1.0 |
|
||||
| CVE-2024-55633 | SQLLab Improper readonly query validation allows unauthorized write access | < 4.1.0 |
|
||||
|
||||
#### Version 4.0.2
|
||||
|
||||
| CVE | Title | Affected |
|
||||
|
||||
@@ -203,18 +203,13 @@ const config = {
|
||||
({
|
||||
docs: {
|
||||
sidebarPath: require.resolve('./sidebars.js'),
|
||||
editUrl:
|
||||
({versionDocsDirPath, docPath}) => {
|
||||
if (docPath === 'intro.md') {
|
||||
return 'https://github.com/apache/superset/edit/master/README.md'
|
||||
}
|
||||
return `https://github.com/apache/superset/edit/master/docs/${versionDocsDirPath}/${docPath}`
|
||||
}
|
||||
editUrl: 'https://github.com/apache/superset/edit/master/docs',
|
||||
},
|
||||
blog: {
|
||||
showReadingTime: true,
|
||||
// Please change this to your repo.
|
||||
editUrl: 'https://github.com/facebook/docusaurus/edit/main/website/blog/',
|
||||
editUrl:
|
||||
'https://github.com/facebook/docusaurus/edit/main/website/blog/',
|
||||
},
|
||||
theme: {
|
||||
customCss: require.resolve('./src/styles/custom.css'),
|
||||
|
||||
4241
docs/static/resources/openapi.json
vendored
4241
docs/static/resources/openapi.json
vendored
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ requires = ["setuptools>=40.9.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "apache_superset"
|
||||
name = "apache-superset"
|
||||
description = "A modern, enterprise-ready business intelligence web application"
|
||||
readme = "README.md"
|
||||
dynamic = ["version", "scripts", "entry-points"]
|
||||
@@ -42,7 +42,7 @@ dependencies = [
|
||||
"colorama",
|
||||
"croniter>=0.3.28",
|
||||
"cron-descriptor",
|
||||
"cryptography>=42.0.4, <45.0.0",
|
||||
"cryptography>=42.0.4, <43.0.0",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
"flask>=2.2.5, <3.0.0",
|
||||
"flask-appbuilder>=4.5.0, <5.0.0",
|
||||
@@ -64,8 +64,6 @@ dependencies = [
|
||||
"jsonpath-ng>=1.6.1, <2",
|
||||
"Mako>=1.2.2",
|
||||
"markdown>=3.0",
|
||||
# marshmallow>=4 has issues: https://github.com/apache/superset/issues/33162
|
||||
"marshmallow<4",
|
||||
"msgpack>=1.0.0, <1.1",
|
||||
"nh3>=0.2.11, <0.3",
|
||||
"numpy==1.23.5",
|
||||
@@ -91,7 +89,7 @@ dependencies = [
|
||||
"slack_sdk>=3.19.0, <4",
|
||||
"sqlalchemy>=1.4, <2",
|
||||
"sqlalchemy-utils>=0.38.3, <0.39",
|
||||
"sqlglot>=25.24.0,<27",
|
||||
"sqlglot>=25.24.0,<26",
|
||||
"sqlparse>=0.5.0",
|
||||
"tabulate>=0.8.9, <0.9",
|
||||
"typing-extensions>=4, <5",
|
||||
@@ -135,6 +133,7 @@ gevent = ["gevent>=23.9.1"]
|
||||
gsheets = ["shillelagh[gsheetsapi]>=1.2.18, <2"]
|
||||
hana = ["hdbcli==2.4.162", "sqlalchemy_hana==0.4.0"]
|
||||
hive = [
|
||||
"boto3",
|
||||
"pyhive[hive]>=0.6.5;python_version<'3.11'",
|
||||
"pyhive[hive_pure_sasl]>=0.7.0",
|
||||
"tableschema",
|
||||
@@ -157,7 +156,7 @@ pinot = ["pinotdb>=5.0.0, <6.0.0"]
|
||||
playwright = ["playwright>=1.37.0, <2"]
|
||||
postgres = ["psycopg2-binary==2.9.6"]
|
||||
presto = ["pyhive[presto]>=0.6.5"]
|
||||
trino = ["trino>=0.328.0"]
|
||||
trino = ["boto3", "trino>=0.328.0"]
|
||||
prophet = ["prophet>=1.1.5, <2"]
|
||||
redshift = ["sqlalchemy-redshift>=0.8.1, <0.9"]
|
||||
rockset = ["rockset-sqlalchemy>=0.0.1, <1"]
|
||||
|
||||
@@ -147,8 +147,6 @@ greenlet==3.0.3
|
||||
# via shillelagh
|
||||
gunicorn==22.0.0
|
||||
# via apache-superset
|
||||
h11==0.16.0
|
||||
# via wsproto
|
||||
hashids==1.3.1
|
||||
# via apache-superset
|
||||
holidays==0.25
|
||||
@@ -348,7 +346,7 @@ sqlalchemy-utils==0.38.3
|
||||
# via
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
sqlglot==26.11.1
|
||||
sqlglot==25.24.0
|
||||
# via apache-superset
|
||||
sqlparse==0.5.0
|
||||
# via apache-superset
|
||||
|
||||
@@ -101,10 +101,6 @@ grpcio==1.62.1
|
||||
# grpcio-status
|
||||
grpcio-status==1.60.1
|
||||
# via google-api-core
|
||||
h11==0.16.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# wsproto
|
||||
identify==2.5.36
|
||||
# via pre-commit
|
||||
ijson==3.2.3
|
||||
@@ -225,10 +221,6 @@ s3transfer==0.10.1
|
||||
# via boto3
|
||||
sqlalchemy-bigquery==1.11.0
|
||||
# via apache-superset
|
||||
sqlglot==26.11.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
sqloxide==0.1.43
|
||||
# via apache-superset
|
||||
statsd==4.0.1
|
||||
|
||||
1
setup.py
1
setup.py
@@ -52,7 +52,6 @@ with open(VERSION_INFO_FILE, "w") as version_file:
|
||||
version_string = version_string.replace("-dev", ".dev0")
|
||||
|
||||
setup(
|
||||
name="apache_superset",
|
||||
version=version_string,
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
|
||||
17
superset-frontend/package-lock.json
generated
17
superset-frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "superset",
|
||||
"version": "4.1.3",
|
||||
"version": "4.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "superset",
|
||||
"version": "4.1.3",
|
||||
"version": "4.1.1",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
@@ -41663,10 +41663,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
|
||||
"integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==",
|
||||
"license": "MIT",
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
|
||||
"integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -99673,9 +99672,9 @@
|
||||
}
|
||||
},
|
||||
"luxon": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
|
||||
"integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
|
||||
"integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ=="
|
||||
},
|
||||
"lz-string": {
|
||||
"version": "1.5.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superset",
|
||||
"version": "4.1.4",
|
||||
"version": "4.1.1",
|
||||
"description": "Superset is a data exploration platform designed to be visual, intuitive, and interactive.",
|
||||
"keywords": [
|
||||
"big",
|
||||
|
||||
@@ -262,7 +262,6 @@ export interface BaseControlConfig<
|
||||
props: ControlPanelsContainerProps,
|
||||
controlData: AnyDict,
|
||||
) => boolean;
|
||||
disableStash?: boolean;
|
||||
hidden?:
|
||||
| boolean
|
||||
| ((props: ControlPanelsContainerProps, controlData: AnyDict) => boolean);
|
||||
|
||||
@@ -57,21 +57,11 @@ export default async function parseResponse<T extends ParseMethod = 'json'>(
|
||||
const json = JSONbig.parse(rawData);
|
||||
const result: JsonResponse = {
|
||||
response,
|
||||
json: cloneDeepWith(json, (value: any) => {
|
||||
if (
|
||||
value?.isInteger?.() === true &&
|
||||
(value?.isGreaterThan?.(Number.MAX_SAFE_INTEGER) ||
|
||||
value?.isLessThan?.(Number.MIN_SAFE_INTEGER))
|
||||
) {
|
||||
return BigInt(value);
|
||||
}
|
||||
// // `json-bigint` could not handle floats well, see sidorares/json-bigint#62
|
||||
// // TODO: clean up after json-bigint>1.0.1 is released
|
||||
if (value?.isNaN?.() === false) {
|
||||
return value?.toNumber?.();
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
// `json-bigint` could not handle floats well, see sidorares/json-bigint#62
|
||||
// TODO: clean up after json-bigint>1.0.1 is released
|
||||
json: cloneDeepWith(json, (value: any) =>
|
||||
value?.isInteger?.() === false ? Number(value) : undefined,
|
||||
),
|
||||
};
|
||||
return result as ReturnType;
|
||||
}
|
||||
|
||||
@@ -134,14 +134,13 @@ export interface SQLFormExtensionProps {
|
||||
startQuery: (ctasArg?: any, ctas_method?: any) => void;
|
||||
}
|
||||
|
||||
export interface SQLResultTableExtensionProps {
|
||||
export interface SQLResultTableExtentionProps {
|
||||
queryId: string;
|
||||
orderedColumnKeys: string[];
|
||||
data: Record<string, unknown>[];
|
||||
height: number;
|
||||
filterText?: string;
|
||||
expandedColumns?: string[];
|
||||
allowHTML?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,7 +223,7 @@ export type Extensions = Partial<{
|
||||
'database.delete.related': ComponentType<DatabaseDeleteRelatedExtensionProps>;
|
||||
'dataset.delete.related': ComponentType<DatasetDeleteRelatedExtensionProps>;
|
||||
'sqleditor.extension.form': ComponentType<SQLFormExtensionProps>;
|
||||
'sqleditor.extension.resultTable': ComponentType<SQLResultTableExtensionProps>;
|
||||
'sqleditor.extension.resultTable': ComponentType<SQLResultTableExtentionProps>;
|
||||
'dashboard.slice.header': ComponentType<SliceHeaderExtension>;
|
||||
'sqleditor.extension.customAutocomplete': (
|
||||
args: CustomAutoCompleteArgs,
|
||||
|
||||
@@ -67,10 +67,6 @@ class LRUCache<T> {
|
||||
public get size() {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
public values(): T[] {
|
||||
return [...this.cache.values()];
|
||||
}
|
||||
}
|
||||
|
||||
export function lruCache<T>(capacity = 100) {
|
||||
|
||||
@@ -141,7 +141,7 @@ describe('parseResponse()', () => {
|
||||
const mockBigIntUrl = '/mock/get/bigInt';
|
||||
const mockGetBigIntPayload = `{
|
||||
"value": 9223372036854775807, "minus": { "value": -483729382918228373892, "str": "something" },
|
||||
"number": 1234, "floatValue": { "plus": 0.3452211361231223, "minus": -0.3452211361231223, "even": 1234567890123456.0000000 },
|
||||
"number": 1234, "floatValue": { "plus": 0.3452211361231223, "minus": -0.3452211361231223 },
|
||||
"string.constructor": "data.constructor",
|
||||
"constructor": "constructor"
|
||||
}`;
|
||||
@@ -159,7 +159,6 @@ describe('parseResponse()', () => {
|
||||
expect(responseBigNumber.json.floatValue.minus).toEqual(
|
||||
-0.3452211361231223,
|
||||
);
|
||||
expect(responseBigNumber.json.floatValue.even).toEqual(1234567890123456);
|
||||
expect(
|
||||
responseBigNumber.json.floatValue.plus +
|
||||
responseBigNumber.json.floatValue.minus,
|
||||
|
||||
@@ -35,11 +35,8 @@ test('LRU operations', () => {
|
||||
expect(cache.size).toBe(3);
|
||||
expect(cache.has('1')).toBeFalsy();
|
||||
expect(cache.get('1')).toBeUndefined();
|
||||
expect(cache.values()).toEqual(['b', 'c', 'd']);
|
||||
cache.get('2');
|
||||
expect(cache.values()).toEqual(['c', 'd', 'b']);
|
||||
cache.set('5', 'e');
|
||||
expect(cache.values()).toEqual(['d', 'b', 'e']);
|
||||
expect(cache.has('2')).toBeTruthy();
|
||||
expect(cache.has('3')).toBeFalsy();
|
||||
// @ts-expect-error
|
||||
@@ -47,7 +44,6 @@ test('LRU operations', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => cache.get(0)).toThrow(TypeError);
|
||||
expect(cache.size).toBe(3);
|
||||
expect(cache.values()).toEqual(['d', 'b', 'e']);
|
||||
cache.clear();
|
||||
expect(cache.size).toBe(0);
|
||||
expect(cache.capacity).toBe(3);
|
||||
|
||||
@@ -120,7 +120,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||
className="kicker"
|
||||
style={{
|
||||
fontSize,
|
||||
height: 'auto',
|
||||
height: maxHeight,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
@@ -156,7 +156,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||
document.body.append(container);
|
||||
const fontSize = computeMaxFontSize({
|
||||
text,
|
||||
maxWidth: width * 0.9, // reduced it's max width
|
||||
maxWidth: width - 8, // Decrease 8px for more precise font size
|
||||
maxHeight,
|
||||
className: 'header-line',
|
||||
container,
|
||||
@@ -174,10 +174,8 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||
<div
|
||||
className="header-line"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize,
|
||||
height: 'auto',
|
||||
height: maxHeight,
|
||||
color: numberColor,
|
||||
}}
|
||||
onContextMenu={onContextMenu}
|
||||
@@ -206,7 +204,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
|
||||
document.body.append(container);
|
||||
fontSize = computeMaxFontSize({
|
||||
text,
|
||||
maxWidth: width * 0.9, // max width reduced
|
||||
maxWidth: width,
|
||||
maxHeight,
|
||||
className: 'subheader-line',
|
||||
container,
|
||||
@@ -358,7 +356,6 @@ export default styled(BigNumberVis)`
|
||||
position: relative;
|
||||
line-height: 1em;
|
||||
white-space: nowrap;
|
||||
margin-bottom:${theme.gridUnit * 2}px;
|
||||
span {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
formatSelectOptionsForRange,
|
||||
dndGroupByControl,
|
||||
columnsByType,
|
||||
sections,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { showLegendControl, showValueControl } from '../controls';
|
||||
|
||||
@@ -105,7 +104,6 @@ const config: ControlPanelConfig = {
|
||||
],
|
||||
],
|
||||
},
|
||||
sections.titleControls,
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
@@ -113,6 +111,28 @@ const config: ControlPanelConfig = {
|
||||
['color_scheme'],
|
||||
[showValueControl],
|
||||
[showLegendControl],
|
||||
[
|
||||
{
|
||||
name: 'x_axis_title',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('X Axis Title'),
|
||||
renderTrigger: true,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'y_axis_title',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Y Axis Title'),
|
||||
renderTrigger: true,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -188,11 +188,7 @@ export default function transformProps(
|
||||
showTotal,
|
||||
sliceId,
|
||||
} = formData;
|
||||
const {
|
||||
currencyFormats = {},
|
||||
columnFormats = {},
|
||||
verboseMap = {},
|
||||
} = datasource;
|
||||
const { currencyFormats = {}, columnFormats = {} } = datasource;
|
||||
const refs: Refs = {};
|
||||
const primaryValueFormatter = getValueFormatter(
|
||||
metric,
|
||||
@@ -338,10 +334,8 @@ export default function transformProps(
|
||||
secondaryValueFormatter,
|
||||
colorByCategory,
|
||||
totalValue,
|
||||
metricLabel: verboseMap[metricLabel] || metricLabel,
|
||||
secondaryMetricLabel: secondaryMetricLabel
|
||||
? verboseMap[secondaryMetricLabel] || secondaryMetricLabel
|
||||
: undefined,
|
||||
metricLabel,
|
||||
secondaryMetricLabel,
|
||||
}),
|
||||
},
|
||||
series: [
|
||||
|
||||
@@ -73,8 +73,6 @@ function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||
description: t('Changing this control takes effect instantly'),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
isXAxis ? isVertical(controls) : isHorizontal(controls),
|
||||
disableStash: true,
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -92,8 +90,6 @@ function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||
description: t('Changing this control takes effect instantly'),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
isXAxis ? isVertical(controls) : isHorizontal(controls),
|
||||
disableStash: true,
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -108,8 +104,6 @@ function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||
description: t('Changing this control takes effect instantly'),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
isXAxis ? isHorizontal(controls) : isVertical(controls),
|
||||
disableStash: true,
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -127,8 +121,6 @@ function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||
description: t('Changing this control takes effect instantly'),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
isXAxis ? isHorizontal(controls) : isVertical(controls),
|
||||
disableStash: true,
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -146,8 +138,6 @@ function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||
description: t('Changing this control takes effect instantly'),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
isXAxis ? isHorizontal(controls) : isVertical(controls),
|
||||
disableStash: true,
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -170,8 +160,6 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
isXAxis ? isVertical(controls) : isHorizontal(controls),
|
||||
disableStash: true,
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -182,8 +170,6 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||
...xAxisLabelRotation.config,
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
isXAxis ? isVertical(controls) : isHorizontal(controls),
|
||||
disableStash: true,
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -195,8 +181,6 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||
label: t('Axis Format'),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
isXAxis ? isHorizontal(controls) : isVertical(controls),
|
||||
disableStash: true,
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -212,8 +196,6 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||
description: t('Logarithmic axis'),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
isXAxis ? isHorizontal(controls) : isVertical(controls),
|
||||
disableStash: true,
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -228,8 +210,6 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||
description: t('Draw split lines for minor axis ticks'),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
isXAxis ? isHorizontal(controls) : isVertical(controls),
|
||||
disableStash: true,
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -246,8 +226,6 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||
description: t('It’s not recommended to truncate axis in Bar chart.'),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
isXAxis ? isHorizontal(controls) : isVertical(controls),
|
||||
disableStash: true,
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -268,8 +246,6 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
Boolean(controls?.truncateYAxis?.value) &&
|
||||
(isXAxis ? isHorizontal(controls) : isVertical(controls)),
|
||||
disableStash: true,
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -169,8 +169,6 @@ export default function transformProps(
|
||||
stack,
|
||||
tooltipTimeFormat,
|
||||
tooltipSortByMetric,
|
||||
showTooltipTotal,
|
||||
showTooltipPercentage,
|
||||
truncateXAxis,
|
||||
truncateYAxis,
|
||||
xAxis: xAxisOrig,
|
||||
@@ -192,9 +190,7 @@ export default function transformProps(
|
||||
}: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData };
|
||||
const refs: Refs = {};
|
||||
const groupBy = ensureIsArray(groupby);
|
||||
const labelMap: { [key: string]: string[] } = Object.entries(
|
||||
label_map,
|
||||
).reduce((acc, entry) => {
|
||||
const labelMap = Object.entries(label_map).reduce((acc, entry) => {
|
||||
if (
|
||||
entry[1].length > groupBy.length &&
|
||||
Array.isArray(timeCompare) &&
|
||||
@@ -380,7 +376,6 @@ export default function transformProps(
|
||||
xAxisType,
|
||||
colorScale,
|
||||
sliceId,
|
||||
orientation,
|
||||
),
|
||||
);
|
||||
else if (isIntervalAnnotationLayer(layer)) {
|
||||
@@ -392,7 +387,6 @@ export default function transformProps(
|
||||
colorScale,
|
||||
theme,
|
||||
sliceId,
|
||||
orientation,
|
||||
),
|
||||
);
|
||||
} else if (isEventAnnotationLayer(layer)) {
|
||||
@@ -404,7 +398,6 @@ export default function transformProps(
|
||||
colorScale,
|
||||
theme,
|
||||
sliceId,
|
||||
orientation,
|
||||
),
|
||||
);
|
||||
} else if (isTimeseriesAnnotationLayer(layer)) {
|
||||
@@ -416,7 +409,6 @@ export default function transformProps(
|
||||
annotationData,
|
||||
colorScale,
|
||||
sliceId,
|
||||
orientation,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -491,9 +483,7 @@ export default function transformProps(
|
||||
minorTick: { show: minorTicks },
|
||||
minInterval:
|
||||
xAxisType === AxisType.Time && timeGrainSqla
|
||||
? TIMEGRAIN_TO_TIMESTAMP[
|
||||
timeGrainSqla as keyof typeof TIMEGRAIN_TO_TIMESTAMP
|
||||
]
|
||||
? TIMEGRAIN_TO_TIMESTAMP[timeGrainSqla]
|
||||
: 0,
|
||||
...getMinAndMaxFromBounds(
|
||||
xAxisType,
|
||||
@@ -573,9 +563,8 @@ export default function transformProps(
|
||||
value.observation !== undefined ? acc + value.observation : acc,
|
||||
0,
|
||||
);
|
||||
const allowTotal = Boolean(isMultiSeries) && richTooltip && !isForecast;
|
||||
const showPercentage =
|
||||
allowTotal && !forcePercentFormatter && showTooltipPercentage;
|
||||
const showTotal = Boolean(isMultiSeries) && richTooltip && !isForecast;
|
||||
const showPercentage = showTotal && !forcePercentFormatter;
|
||||
const keys = Object.keys(forecastValues);
|
||||
let focusedRow;
|
||||
sortedKeys
|
||||
@@ -606,7 +595,7 @@ export default function transformProps(
|
||||
focusedRow = rows.length - focusedRow - 1;
|
||||
}
|
||||
}
|
||||
if (allowTotal && showTooltipTotal) {
|
||||
if (showTotal) {
|
||||
const totalRow = ['Total', formatter.format(total)];
|
||||
if (showPercentage) {
|
||||
totalRow.push(percentFormatter.format(1));
|
||||
|
||||
@@ -53,7 +53,6 @@ import {
|
||||
EchartsTimeseriesSeriesType,
|
||||
ForecastSeriesEnum,
|
||||
LegendOrientation,
|
||||
OrientationType,
|
||||
StackType,
|
||||
} from '../types';
|
||||
|
||||
@@ -223,7 +222,7 @@ export function transformSeries(
|
||||
stackId = forecastSeries.name;
|
||||
} else if (stack && isObservation) {
|
||||
// the suffix of the observation series is '' (falsy), which disables
|
||||
// stacking. Therefore, we need to set something that is truthy.
|
||||
// stacking. Therefore we need to set something that is truthy.
|
||||
stackId = getTimeCompareStackId('obs', timeCompare, name);
|
||||
} else if (stack && isTrend) {
|
||||
stackId = getTimeCompareStackId(forecastSeries.type, timeCompare, name);
|
||||
@@ -316,15 +315,6 @@ export function transformSeries(
|
||||
show: !!showValue,
|
||||
position: isHorizontal ? 'right' : 'top',
|
||||
formatter: (params: any) => {
|
||||
// don't show confidence band value labels, as they're already visible on the tooltip
|
||||
if (
|
||||
[
|
||||
ForecastSeriesEnum.ForecastUpper,
|
||||
ForecastSeriesEnum.ForecastLower,
|
||||
].includes(forecastSeries.type)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
const { value, dataIndex, seriesIndex, seriesName } = params;
|
||||
const numericValue = isHorizontal ? value[0] : value[1];
|
||||
const isSelectedLegend = !legendState || legendState[seriesName];
|
||||
@@ -360,11 +350,8 @@ export function transformFormulaAnnotation(
|
||||
xAxisType: AxisType,
|
||||
colorScale: CategoricalColorScale,
|
||||
sliceId?: number,
|
||||
orientation?: OrientationType,
|
||||
): SeriesOption {
|
||||
const { name, color, opacity, width, style } = layer;
|
||||
const isHorizontal = orientation === OrientationType.Horizontal;
|
||||
|
||||
return {
|
||||
name,
|
||||
id: name,
|
||||
@@ -378,9 +365,7 @@ export function transformFormulaAnnotation(
|
||||
},
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: evalFormula(layer, data, xAxisCol, xAxisType).map(([x, y]) =>
|
||||
isHorizontal ? [y, x] : [x, y],
|
||||
),
|
||||
data: evalFormula(layer, data, xAxisCol, xAxisType),
|
||||
symbolSize: 0,
|
||||
};
|
||||
}
|
||||
@@ -392,7 +377,6 @@ export function transformIntervalAnnotation(
|
||||
colorScale: CategoricalColorScale,
|
||||
theme: SupersetTheme,
|
||||
sliceId?: number,
|
||||
orientation?: OrientationType,
|
||||
): SeriesOption[] {
|
||||
const series: SeriesOption[] = [];
|
||||
const annotations = extractRecordAnnotations(layer, annotationData);
|
||||
@@ -400,7 +384,6 @@ export function transformIntervalAnnotation(
|
||||
const { name, color, opacity, showLabel } = layer;
|
||||
const { descriptions, intervalEnd, time, title } = annotation;
|
||||
const label = formatAnnotationLabel(name, title, descriptions);
|
||||
const isHorizontal = orientation === OrientationType.Horizontal;
|
||||
const intervalData: (
|
||||
| MarkArea1DDataItemOption
|
||||
| MarkArea2DDataItemOption
|
||||
@@ -408,9 +391,11 @@ export function transformIntervalAnnotation(
|
||||
[
|
||||
{
|
||||
name: label,
|
||||
...(isHorizontal ? { yAxis: time } : { xAxis: time }),
|
||||
xAxis: time,
|
||||
},
|
||||
{
|
||||
xAxis: intervalEnd,
|
||||
},
|
||||
isHorizontal ? { yAxis: intervalEnd } : { xAxis: intervalEnd },
|
||||
],
|
||||
];
|
||||
const intervalLabel: SeriesLabelOption = showLabel
|
||||
@@ -467,7 +452,6 @@ export function transformEventAnnotation(
|
||||
colorScale: CategoricalColorScale,
|
||||
theme: SupersetTheme,
|
||||
sliceId?: number,
|
||||
orientation?: OrientationType,
|
||||
): SeriesOption[] {
|
||||
const series: SeriesOption[] = [];
|
||||
const annotations = extractRecordAnnotations(layer, annotationData);
|
||||
@@ -475,11 +459,10 @@ export function transformEventAnnotation(
|
||||
const { name, color, opacity, style, width, showLabel } = layer;
|
||||
const { descriptions, time, title } = annotation;
|
||||
const label = formatAnnotationLabel(name, title, descriptions);
|
||||
const isHorizontal = orientation === OrientationType.Horizontal;
|
||||
const eventData: MarkLine1DDataItemOption[] = [
|
||||
{
|
||||
name: label,
|
||||
...(isHorizontal ? { yAxis: time } : { xAxis: time }),
|
||||
xAxis: time,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -542,12 +525,10 @@ export function transformTimeseriesAnnotation(
|
||||
annotationData: AnnotationData,
|
||||
colorScale: CategoricalColorScale,
|
||||
sliceId?: number,
|
||||
orientation?: OrientationType,
|
||||
): SeriesOption[] {
|
||||
const series: SeriesOption[] = [];
|
||||
const { hideLine, name, opacity, showMarkers, style, width, color } = layer;
|
||||
const result = annotationData[name];
|
||||
const isHorizontal = orientation === OrientationType.Horizontal;
|
||||
if (isTimeseriesAnnotationResult(result)) {
|
||||
result.forEach(annotation => {
|
||||
const { key, values } = annotation;
|
||||
@@ -555,11 +536,7 @@ export function transformTimeseriesAnnotation(
|
||||
type: 'line',
|
||||
id: key,
|
||||
name: key,
|
||||
data: values.map(({ x, y }) =>
|
||||
isHorizontal
|
||||
? ([y, x] as [number, OptionName])
|
||||
: ([x, y] as [OptionName, number]),
|
||||
),
|
||||
data: values.map(row => [row.x, row.y] as [OptionName, number]),
|
||||
symbolSize: showMarkers ? markerSize : 0,
|
||||
lineStyle: {
|
||||
opacity: parseAnnotationOpacity(opacity),
|
||||
|
||||
@@ -75,8 +75,6 @@ export type EchartsTimeseriesFormData = QueryFormData & {
|
||||
stack: StackType;
|
||||
timeCompare?: string[];
|
||||
tooltipTimeFormat?: string;
|
||||
showTooltipTotal?: boolean;
|
||||
showTooltipPercentage?: boolean;
|
||||
truncateXAxis: boolean;
|
||||
truncateYAxis: boolean;
|
||||
yAxisFormat?: string;
|
||||
|
||||
@@ -210,40 +210,9 @@ const tooltipSortByMetricControl: ControlSetItem = {
|
||||
},
|
||||
};
|
||||
|
||||
const tooltipTotalControl: ControlSetItem = {
|
||||
name: 'showTooltipTotal',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show total'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t('Whether to display the total value in the tooltip'),
|
||||
visibility: ({ controls, form_data }: ControlPanelsContainerProps) =>
|
||||
Boolean(controls?.rich_tooltip?.value) &&
|
||||
form_data.viz_type !== 'mixed_timeseries',
|
||||
},
|
||||
};
|
||||
|
||||
const tooltipPercentageControl: ControlSetItem = {
|
||||
name: 'showTooltipPercentage',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show percentage'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t('Whether to display the percentage value in the tooltip'),
|
||||
visibility: ({ controls, form_data }: ControlPanelsContainerProps) =>
|
||||
Boolean(controls?.rich_tooltip?.value) &&
|
||||
!controls?.contributionMode?.value &&
|
||||
form_data.viz_type !== 'mixed_timeseries',
|
||||
},
|
||||
};
|
||||
|
||||
export const richTooltipSection: ControlSetRow[] = [
|
||||
[<ControlSubSectionHeader>{t('Tooltip')}</ControlSubSectionHeader>],
|
||||
[richTooltipControl],
|
||||
[tooltipTotalControl],
|
||||
[tooltipPercentageControl],
|
||||
[tooltipSortByMetricControl],
|
||||
[tooltipTimeFormatControl],
|
||||
];
|
||||
|
||||
@@ -1,349 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AnnotationData,
|
||||
AnnotationSourceType,
|
||||
AnnotationStyle,
|
||||
AnnotationType,
|
||||
AxisType,
|
||||
CategoricalColorNamespace,
|
||||
EventAnnotationLayer,
|
||||
FormulaAnnotationLayer,
|
||||
IntervalAnnotationLayer,
|
||||
supersetTheme,
|
||||
TimeseriesAnnotationLayer,
|
||||
TimeseriesDataRecord,
|
||||
} from '@superset-ui/core';
|
||||
import { OrientationType } from '@superset-ui/plugin-chart-echarts';
|
||||
import {
|
||||
transformEventAnnotation,
|
||||
transformFormulaAnnotation,
|
||||
transformIntervalAnnotation,
|
||||
transformTimeseriesAnnotation,
|
||||
} from '../../src/Timeseries/transformers';
|
||||
|
||||
const mockData: TimeseriesDataRecord[] = [
|
||||
{
|
||||
__timestamp: 10,
|
||||
},
|
||||
{
|
||||
__timestamp: 20,
|
||||
},
|
||||
];
|
||||
|
||||
const mockFormulaAnnotationLayer: FormulaAnnotationLayer = {
|
||||
annotationType: AnnotationType.Formula as const,
|
||||
name: 'My Formula',
|
||||
show: true,
|
||||
style: AnnotationStyle.Solid,
|
||||
value: '50',
|
||||
showLabel: true,
|
||||
};
|
||||
|
||||
describe('transformFormulaAnnotation', () => {
|
||||
it('should transform data correctly', () => {
|
||||
expect(
|
||||
transformFormulaAnnotation(
|
||||
mockFormulaAnnotationLayer,
|
||||
mockData,
|
||||
'__timestamp',
|
||||
AxisType.Value,
|
||||
CategoricalColorNamespace.getScale(''),
|
||||
undefined,
|
||||
).data,
|
||||
).toEqual([
|
||||
[10, 50],
|
||||
[20, 50],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should swap x and y for horizontal chart', () => {
|
||||
expect(
|
||||
transformFormulaAnnotation(
|
||||
mockFormulaAnnotationLayer,
|
||||
mockData,
|
||||
'__timestamp',
|
||||
AxisType.Value,
|
||||
CategoricalColorNamespace.getScale(''),
|
||||
undefined,
|
||||
OrientationType.Horizontal,
|
||||
).data,
|
||||
).toEqual([
|
||||
[50, 10],
|
||||
[50, 20],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
const mockIntervalAnnotationLayer: IntervalAnnotationLayer = {
|
||||
name: 'Interval annotation layer',
|
||||
annotationType: AnnotationType.Interval as const,
|
||||
sourceType: AnnotationSourceType.Native as const,
|
||||
color: null,
|
||||
style: AnnotationStyle.Solid,
|
||||
width: 1,
|
||||
show: true,
|
||||
showLabel: false,
|
||||
value: 1,
|
||||
};
|
||||
|
||||
const mockIntervalAnnotationData: AnnotationData = {
|
||||
'Interval annotation layer': {
|
||||
records: [
|
||||
{
|
||||
start_dttm: 10,
|
||||
end_dttm: 12,
|
||||
short_descr: 'Timeseries 1',
|
||||
long_descr: '',
|
||||
json_metadata: '',
|
||||
},
|
||||
{
|
||||
start_dttm: 13,
|
||||
end_dttm: 15,
|
||||
short_descr: 'Timeseries 2',
|
||||
long_descr: '',
|
||||
json_metadata: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe('transformIntervalAnnotation', () => {
|
||||
it('should transform data correctly', () => {
|
||||
expect(
|
||||
transformIntervalAnnotation(
|
||||
mockIntervalAnnotationLayer,
|
||||
mockData,
|
||||
mockIntervalAnnotationData,
|
||||
CategoricalColorNamespace.getScale(''),
|
||||
supersetTheme,
|
||||
)
|
||||
.map(annotation => annotation.markArea)
|
||||
.map(markArea => markArea.data),
|
||||
).toEqual([
|
||||
[
|
||||
[
|
||||
{ name: 'Interval annotation layer - Timeseries 1', xAxis: 10 },
|
||||
{ xAxis: 12 },
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
{ name: 'Interval annotation layer - Timeseries 2', xAxis: 13 },
|
||||
{ xAxis: 15 },
|
||||
],
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use yAxis for horizontal chart data', () => {
|
||||
expect(
|
||||
transformIntervalAnnotation(
|
||||
mockIntervalAnnotationLayer,
|
||||
mockData,
|
||||
mockIntervalAnnotationData,
|
||||
CategoricalColorNamespace.getScale(''),
|
||||
supersetTheme,
|
||||
undefined,
|
||||
OrientationType.Horizontal,
|
||||
)
|
||||
.map(annotation => annotation.markArea)
|
||||
.map(markArea => markArea.data),
|
||||
).toEqual([
|
||||
[
|
||||
[
|
||||
{ name: 'Interval annotation layer - Timeseries 1', yAxis: 10 },
|
||||
{ yAxis: 12 },
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
{ name: 'Interval annotation layer - Timeseries 2', yAxis: 13 },
|
||||
{ yAxis: 15 },
|
||||
],
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
const mockEventAnnotationLayer: EventAnnotationLayer = {
|
||||
annotationType: AnnotationType.Event,
|
||||
color: null,
|
||||
name: 'Event annotation layer',
|
||||
show: true,
|
||||
showLabel: false,
|
||||
sourceType: AnnotationSourceType.Native,
|
||||
style: AnnotationStyle.Solid,
|
||||
value: 1,
|
||||
width: 1,
|
||||
};
|
||||
|
||||
const mockEventAnnotationData: AnnotationData = {
|
||||
'Event annotation layer': {
|
||||
records: [
|
||||
{
|
||||
start_dttm: 10,
|
||||
end_dttm: 12,
|
||||
short_descr: 'Test annotation',
|
||||
long_descr: '',
|
||||
json_metadata: '',
|
||||
},
|
||||
{
|
||||
start_dttm: 13,
|
||||
end_dttm: 15,
|
||||
short_descr: 'Test annotation 2',
|
||||
long_descr: '',
|
||||
json_metadata: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe('transformEventAnnotation', () => {
|
||||
it('should transform data correctly', () => {
|
||||
expect(
|
||||
transformEventAnnotation(
|
||||
mockEventAnnotationLayer,
|
||||
mockData,
|
||||
mockEventAnnotationData,
|
||||
CategoricalColorNamespace.getScale(''),
|
||||
supersetTheme,
|
||||
)
|
||||
.map(annotation => annotation.markLine)
|
||||
.map(markLine => markLine.data),
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
name: 'Event annotation layer - Test annotation',
|
||||
xAxis: 10,
|
||||
},
|
||||
],
|
||||
[{ name: 'Event annotation layer - Test annotation 2', xAxis: 13 }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use yAxis for horizontal chart data', () => {
|
||||
expect(
|
||||
transformEventAnnotation(
|
||||
mockEventAnnotationLayer,
|
||||
mockData,
|
||||
mockEventAnnotationData,
|
||||
CategoricalColorNamespace.getScale(''),
|
||||
supersetTheme,
|
||||
undefined,
|
||||
OrientationType.Horizontal,
|
||||
)
|
||||
.map(annotation => annotation.markLine)
|
||||
.map(markLine => markLine.data),
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
name: 'Event annotation layer - Test annotation',
|
||||
yAxis: 10,
|
||||
},
|
||||
],
|
||||
[{ name: 'Event annotation layer - Test annotation 2', yAxis: 13 }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
const mockTimeseriesAnnotationLayer: TimeseriesAnnotationLayer = {
|
||||
annotationType: AnnotationType.Timeseries,
|
||||
color: null,
|
||||
hideLine: false,
|
||||
name: 'Timeseries annotation layer',
|
||||
overrides: {
|
||||
time_range: null,
|
||||
},
|
||||
show: true,
|
||||
showLabel: false,
|
||||
showMarkers: false,
|
||||
sourceType: AnnotationSourceType.Line,
|
||||
style: AnnotationStyle.Solid,
|
||||
value: 1,
|
||||
width: 1,
|
||||
};
|
||||
|
||||
const mockTimeseriesAnnotationData: AnnotationData = {
|
||||
'Timeseries annotation layer': [
|
||||
{
|
||||
key: 'Key 1',
|
||||
values: [
|
||||
{
|
||||
x: 10,
|
||||
y: 12,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'Key 2',
|
||||
values: [
|
||||
{
|
||||
x: 12,
|
||||
y: 15,
|
||||
},
|
||||
{
|
||||
x: 15,
|
||||
y: 20,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('transformTimeseriesAnnotation', () => {
|
||||
it('should transform data correctly', () => {
|
||||
expect(
|
||||
transformTimeseriesAnnotation(
|
||||
mockTimeseriesAnnotationLayer,
|
||||
1,
|
||||
mockData,
|
||||
mockTimeseriesAnnotationData,
|
||||
CategoricalColorNamespace.getScale(''),
|
||||
).map(annotation => annotation.data),
|
||||
).toEqual([
|
||||
[[10, 12]],
|
||||
[
|
||||
[12, 15],
|
||||
[15, 20],
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should swap x and y for horizontal chart', () => {
|
||||
expect(
|
||||
transformTimeseriesAnnotation(
|
||||
mockTimeseriesAnnotationLayer,
|
||||
1,
|
||||
mockData,
|
||||
mockTimeseriesAnnotationData,
|
||||
CategoricalColorNamespace.getScale(''),
|
||||
undefined,
|
||||
OrientationType.Horizontal,
|
||||
).map(annotation => annotation.data),
|
||||
).toEqual([
|
||||
[[12, 10]],
|
||||
[
|
||||
[15, 12],
|
||||
[20, 15],
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1206,7 +1206,6 @@ export function popSavedQuery(saveQueryId) {
|
||||
schema: queryEditorProps.schema,
|
||||
sql: queryEditorProps.sql,
|
||||
templateParams: queryEditorProps.templateParams,
|
||||
remoteId: queryEditorProps.remoteId,
|
||||
};
|
||||
return dispatch(addQueryEditor(tmpAdaptedProps));
|
||||
})
|
||||
|
||||
@@ -30,9 +30,6 @@ import {
|
||||
initialState,
|
||||
queryId,
|
||||
} from 'src/SqlLab/fixtures';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import { ADD_TOAST } from 'src/components/MessageToasts/actions';
|
||||
import { ToastType } from '../../components/MessageToasts/types';
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureMockStore(middlewares);
|
||||
@@ -456,112 +453,6 @@ describe('async actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('popSavedQuery', () => {
|
||||
const supersetClientGetSpy = jest.spyOn(SupersetClient, 'get');
|
||||
const store = mockStore({});
|
||||
|
||||
const mockSavedQueryApiResponse = {
|
||||
catalog: null,
|
||||
changed_by: {
|
||||
first_name: 'Superset',
|
||||
id: 1,
|
||||
last_name: 'Admin',
|
||||
},
|
||||
changed_on: '2024-12-28T20:06:14.246743',
|
||||
changed_on_delta_humanized: '8 days ago',
|
||||
created_by: {
|
||||
first_name: 'Superset',
|
||||
id: 1,
|
||||
last_name: 'Admin',
|
||||
},
|
||||
database: {
|
||||
database_name: 'examples',
|
||||
id: 2,
|
||||
},
|
||||
description: '',
|
||||
id: 1,
|
||||
label: 'Query 1',
|
||||
schema: 'public',
|
||||
sql: 'SELECT * FROM channels',
|
||||
sql_tables: [
|
||||
{
|
||||
catalog: null,
|
||||
schema: null,
|
||||
table: 'channels',
|
||||
},
|
||||
],
|
||||
template_parameters: null,
|
||||
};
|
||||
|
||||
const makeRequest = id => {
|
||||
const request = actions.popSavedQuery(id);
|
||||
const { dispatch } = store;
|
||||
|
||||
return request(dispatch, () => initialState);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
supersetClientGetSpy.mockClear();
|
||||
store.clearActions();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
supersetClientGetSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('calls API endpint with correct params', async () => {
|
||||
supersetClientGetSpy.mockResolvedValue({
|
||||
json: { result: mockSavedQueryApiResponse },
|
||||
});
|
||||
|
||||
await makeRequest(123);
|
||||
|
||||
expect(supersetClientGetSpy).toHaveBeenCalledWith({
|
||||
endpoint: '/api/v1/saved_query/123',
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches addQueryEditor with correct params on successful API call', async () => {
|
||||
supersetClientGetSpy.mockResolvedValue({
|
||||
json: { result: mockSavedQueryApiResponse },
|
||||
});
|
||||
|
||||
const expectedParams = {
|
||||
name: 'Query 1',
|
||||
dbId: 2,
|
||||
catalog: null,
|
||||
schema: 'public',
|
||||
sql: 'SELECT * FROM channels',
|
||||
templateParams: null,
|
||||
remoteId: 1,
|
||||
};
|
||||
|
||||
await makeRequest(1);
|
||||
|
||||
const addQueryEditorAction = store
|
||||
.getActions()
|
||||
.find(action => action.type === actions.ADD_QUERY_EDITOR);
|
||||
|
||||
expect(addQueryEditorAction).toBeTruthy();
|
||||
expect(addQueryEditorAction?.queryEditor).toEqual(
|
||||
expect.objectContaining(expectedParams),
|
||||
);
|
||||
});
|
||||
|
||||
it('should dispatch addDangerToast on API error', async () => {
|
||||
supersetClientGetSpy.mockResolvedValue(new Error());
|
||||
|
||||
await makeRequest(1);
|
||||
|
||||
const addToastAction = store
|
||||
.getActions()
|
||||
.find(action => action.type === ADD_TOAST);
|
||||
|
||||
expect(addToastAction).toBeTruthy();
|
||||
expect(addToastAction?.payload?.toastType).toBe(ToastType.Danger);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addQueryEditor', () => {
|
||||
it('creates new query editor', () => {
|
||||
expect.assertions(1);
|
||||
|
||||
@@ -322,8 +322,6 @@ const SqlEditor: FC<Props> = ({
|
||||
|
||||
const SqlFormExtension = extensionsRegistry.get('sqleditor.extension.form');
|
||||
|
||||
const isTempId = (value: unknown): boolean => Number.isNaN(Number(value));
|
||||
|
||||
const startQuery = useCallback(
|
||||
(ctasArg = false, ctas_method = CtasEnum.Table) => {
|
||||
if (!database) {
|
||||
@@ -901,7 +899,7 @@ const SqlEditor: FC<Props> = ({
|
||||
)}
|
||||
{isActive && (
|
||||
<AceEditorWrapper
|
||||
autocomplete={autocompleteEnabled && !isTempId(queryEditor.id)}
|
||||
autocomplete={autocompleteEnabled}
|
||||
onBlur={onSqlChanged}
|
||||
onChange={onSqlChanged}
|
||||
queryEditorId={queryEditor.id}
|
||||
|
||||
@@ -51,13 +51,11 @@ const getTableMetadataEndpoint =
|
||||
/\/api\/v1\/database\/\d+\/table_metadata\/(?:\?.*)?$/;
|
||||
const getExtraTableMetadataEndpoint =
|
||||
/\/api\/v1\/database\/\d+\/table_metadata\/extra\/(?:\?.*)?$/;
|
||||
const updateTableSchemaExpandedEndpoint = 'glob:*/tableschemaview/*/expanded';
|
||||
const updateTableSchemaEndpoint = 'glob:*/tableschemaview/';
|
||||
const updateTableSchemaEndpoint = 'glob:*/tableschemaview/*/expanded';
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.get(getTableMetadataEndpoint, table);
|
||||
fetchMock.get(getExtraTableMetadataEndpoint, {});
|
||||
fetchMock.post(updateTableSchemaExpandedEndpoint, {});
|
||||
fetchMock.post(updateTableSchemaEndpoint, {});
|
||||
});
|
||||
|
||||
@@ -86,7 +84,7 @@ test('has 4 IconTooltip elements', async () => {
|
||||
initialState,
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(getAllByTestId('mock-icon-tooltip')).toHaveLength(5),
|
||||
expect(getAllByTestId('mock-icon-tooltip')).toHaveLength(4),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -106,7 +104,7 @@ test('fades table', async () => {
|
||||
initialState,
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(getAllByTestId('mock-icon-tooltip')).toHaveLength(5),
|
||||
expect(getAllByTestId('mock-icon-tooltip')).toHaveLength(4),
|
||||
);
|
||||
const style = window.getComputedStyle(getAllByTestId('fade')[0]);
|
||||
expect(style.opacity).toBe('0');
|
||||
@@ -127,7 +125,7 @@ test('sorts columns', async () => {
|
||||
},
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(getAllByTestId('mock-icon-tooltip')).toHaveLength(5),
|
||||
expect(getAllByTestId('mock-icon-tooltip')).toHaveLength(4),
|
||||
);
|
||||
expect(
|
||||
getAllByTestId('mock-column-element').map(el => el.textContent),
|
||||
@@ -156,7 +154,7 @@ test('removes the table', async () => {
|
||||
},
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(getAllByTestId('mock-icon-tooltip')).toHaveLength(5),
|
||||
expect(getAllByTestId('mock-icon-tooltip')).toHaveLength(4),
|
||||
);
|
||||
expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(0);
|
||||
fireEvent.click(getByText('Remove table preview'));
|
||||
@@ -176,29 +174,6 @@ test('fetches table metadata when expanded', async () => {
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1),
|
||||
);
|
||||
expect(fetchMock.calls(updateTableSchemaExpandedEndpoint)).toHaveLength(0);
|
||||
expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(0);
|
||||
expect(fetchMock.calls(getExtraTableMetadataEndpoint)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('refreshes table metadata when triggered', async () => {
|
||||
const { getAllByTestId, getByText } = render(
|
||||
<TableElement {...mockedProps} />,
|
||||
{
|
||||
useRedux: true,
|
||||
initialState,
|
||||
},
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(getAllByTestId('mock-icon-tooltip')).toHaveLength(5),
|
||||
);
|
||||
expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(0);
|
||||
expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1);
|
||||
|
||||
fireEvent.click(getByText('Refresh table schema'));
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(2),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
syncTable,
|
||||
} from 'src/SqlLab/actions/sqlLab';
|
||||
import {
|
||||
tableApiUtil,
|
||||
useTableExtendedMetadataQuery,
|
||||
useTableMetadataQuery,
|
||||
} from 'src/hooks/apiResources';
|
||||
@@ -108,7 +107,7 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
|
||||
const {
|
||||
currentData: tableMetadata,
|
||||
isSuccess: isMetadataSuccess,
|
||||
isFetching: isMetadataFetching,
|
||||
isLoading: isMetadataLoading,
|
||||
isError: hasMetadataError,
|
||||
} = useTableMetadataQuery(
|
||||
{
|
||||
@@ -178,13 +177,6 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
|
||||
setSortColumns(prevState => !prevState);
|
||||
};
|
||||
|
||||
const refreshTableMetadata = () => {
|
||||
dispatch(
|
||||
tableApiUtil.invalidateTags([{ type: 'TableMetadatas', id: name }]),
|
||||
);
|
||||
dispatch(syncTable(table, tableData));
|
||||
};
|
||||
|
||||
const renderWell = () => {
|
||||
let partitions;
|
||||
let metadata;
|
||||
@@ -276,11 +268,6 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
|
||||
}
|
||||
`}
|
||||
>
|
||||
<IconTooltip
|
||||
className="fa fa-refresh pull-left m-l-2 pointer"
|
||||
onClick={refreshTableMetadata}
|
||||
tooltip={t('Refresh table schema')}
|
||||
/>
|
||||
{keyLink}
|
||||
<IconTooltip
|
||||
className={
|
||||
@@ -354,7 +341,7 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
|
||||
</Tooltip>
|
||||
|
||||
<div className="pull-right header-right-side">
|
||||
{isMetadataFetching || isExtraMetadataLoading ? (
|
||||
{isMetadataLoading || isExtraMetadataLoading ? (
|
||||
<Loading position="inline" />
|
||||
) : (
|
||||
<Fade
|
||||
|
||||
@@ -392,7 +392,6 @@ export default function DatabaseSelector({
|
||||
options={catalogOptions}
|
||||
showSearch
|
||||
value={currentCatalog || undefined}
|
||||
allowClear
|
||||
/>,
|
||||
refreshIcon,
|
||||
);
|
||||
@@ -419,7 +418,6 @@ export default function DatabaseSelector({
|
||||
options={schemaOptions}
|
||||
showSearch
|
||||
value={currentSchema}
|
||||
allowClear
|
||||
/>,
|
||||
refreshIcon,
|
||||
);
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
t,
|
||||
getClientErrorObject,
|
||||
getCategoricalSchemeRegistry,
|
||||
promiseTimeout,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
addChart,
|
||||
@@ -69,7 +68,6 @@ import { SET_FILTER_CONFIG_COMPLETE } from './nativeFilters';
|
||||
import getOverwriteItems from '../util/getOverwriteItems';
|
||||
import {
|
||||
applyColors,
|
||||
enforceSharedLabelsColorsArray,
|
||||
isLabelsColorMapSynced,
|
||||
getColorSchemeDomain,
|
||||
getColorNamespace,
|
||||
@@ -295,9 +293,8 @@ export function saveDashboardRequest(data, id, saveType) {
|
||||
const metadataCrossFiltersEnabled = data.metadata?.cross_filters_enabled;
|
||||
const colorScheme = data.metadata?.color_scheme;
|
||||
const customLabelsColor = data.metadata?.label_colors || {};
|
||||
const sharedLabelsColor = enforceSharedLabelsColorsArray(
|
||||
data.metadata?.shared_label_colors,
|
||||
);
|
||||
const sharedLabelsColor = data.metadata?.shared_label_colors || [];
|
||||
// making sure the data is what the backend expects
|
||||
const cleanedData = {
|
||||
...data,
|
||||
certified_by: certified_by || '',
|
||||
@@ -740,9 +737,9 @@ export const persistDashboardLabelsColor = () => async (dispatch, getState) => {
|
||||
} = getState();
|
||||
|
||||
if (labelsColorMapMustSync || sharedLabelsColorsMustSync) {
|
||||
storeDashboardMetadata(id, metadata);
|
||||
dispatch(setDashboardLabelsColorMapSynced());
|
||||
dispatch(setDashboardSharedLabelsColorsSynced());
|
||||
storeDashboardMetadata(id, metadata);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -758,13 +755,16 @@ export const applyDashboardLabelsColorOnLoad = metadata => async dispatch => {
|
||||
try {
|
||||
const updatedMetadata = { ...metadata };
|
||||
const customLabelsColor = metadata.label_colors || {};
|
||||
const sharedLabelsColor = metadata.shared_label_colors || [];
|
||||
let hasChanged = false;
|
||||
|
||||
// backward compatibility of shared_label_colors
|
||||
const sharedLabels = metadata.shared_label_colors || [];
|
||||
if (!Array.isArray(sharedLabels) && Object.keys(sharedLabels).length > 0) {
|
||||
hasChanged = true;
|
||||
updatedMetadata.shared_label_colors = [];
|
||||
updatedMetadata.shared_label_colors = getFreshSharedLabels(
|
||||
Object.keys(sharedLabelsColor),
|
||||
);
|
||||
}
|
||||
// backward compatibility of map_label_colors
|
||||
const hasMapLabelColors =
|
||||
@@ -828,28 +828,27 @@ export const applyDashboardLabelsColorOnLoad = metadata => async dispatch => {
|
||||
* @returns void
|
||||
*/
|
||||
export const ensureSyncedLabelsColorMap = metadata => (dispatch, getState) => {
|
||||
const syncLabelsColorMap = () => {
|
||||
const {
|
||||
dashboardState: { labelsColorMapMustSync },
|
||||
} = getState();
|
||||
const updatedMetadata = { ...metadata };
|
||||
const customLabelsColor = metadata.label_colors || {};
|
||||
const isMapSynced = isLabelsColorMapSynced(metadata);
|
||||
const mustSync = !isMapSynced;
|
||||
const {
|
||||
dashboardState: { labelsColorMapMustSync },
|
||||
} = getState();
|
||||
const updatedMetadata = { ...metadata };
|
||||
const customLabelsColor = metadata.label_colors || {};
|
||||
const isMapSynced = isLabelsColorMapSynced(metadata);
|
||||
const mustSync = !isMapSynced;
|
||||
|
||||
if (mustSync) {
|
||||
const freshestColorMapEntries =
|
||||
getLabelsColorMapEntries(customLabelsColor);
|
||||
updatedMetadata.map_label_colors = freshestColorMapEntries;
|
||||
dispatch(setDashboardMetadata(updatedMetadata));
|
||||
}
|
||||
if (mustSync) {
|
||||
const freshestColorMapEntries = getLabelsColorMapEntries(customLabelsColor);
|
||||
updatedMetadata.map_label_colors = freshestColorMapEntries;
|
||||
dispatch(setDashboardMetadata(updatedMetadata));
|
||||
}
|
||||
|
||||
if (mustSync && !labelsColorMapMustSync) {
|
||||
// prepare to persist the just applied labels color map
|
||||
dispatch(setDashboardLabelsColorMapSync());
|
||||
}
|
||||
};
|
||||
promiseTimeout(syncLabelsColorMap, 500);
|
||||
if (mustSync && !labelsColorMapMustSync) {
|
||||
// prepare to persist the just applied labels color map
|
||||
dispatch(setDashboardLabelsColorMapSync());
|
||||
}
|
||||
if (!mustSync && labelsColorMapMustSync) {
|
||||
dispatch(setDashboardLabelsColorMapSynced());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -857,23 +856,18 @@ export const ensureSyncedLabelsColorMap = metadata => (dispatch, getState) => {
|
||||
* Ensure that the stored shared labels colors match current.
|
||||
*
|
||||
* @param {*} metadata - the dashboard metadata
|
||||
* @param {*} forceFresh - when true it will use the fresh shared labels ignoring stored ones
|
||||
* @returns void
|
||||
*/
|
||||
export const ensureSyncedSharedLabelsColors =
|
||||
(metadata, forceFresh = false) =>
|
||||
(dispatch, getState) => {
|
||||
const syncSharedLabelsColors = () => {
|
||||
metadata => (dispatch, getState) => {
|
||||
// using a timeout to let the rendered charts finish processing labels
|
||||
setTimeout(() => {
|
||||
const {
|
||||
dashboardState: { sharedLabelsColorsMustSync },
|
||||
} = getState();
|
||||
const updatedMetadata = { ...metadata };
|
||||
const sharedLabelsColors = enforceSharedLabelsColorsArray(
|
||||
metadata.shared_label_colors,
|
||||
);
|
||||
const freshLabelsColors = getFreshSharedLabels(
|
||||
forceFresh ? [] : sharedLabelsColors,
|
||||
);
|
||||
const sharedLabelsColors = metadata.shared_label_colors || [];
|
||||
const freshLabelsColors = getFreshSharedLabels(sharedLabelsColors);
|
||||
const isSharedLabelsColorsSynced = isEqual(
|
||||
sharedLabelsColors,
|
||||
freshLabelsColors,
|
||||
@@ -890,8 +884,10 @@ export const ensureSyncedSharedLabelsColors =
|
||||
// prepare to persist the shared labels colors
|
||||
dispatch(setDashboardSharedLabelsColorsSync());
|
||||
}
|
||||
};
|
||||
promiseTimeout(syncSharedLabelsColors, 500);
|
||||
if (!mustSync && sharedLabelsColorsMustSync) {
|
||||
dispatch(setDashboardSharedLabelsColorsSynced());
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -911,10 +907,9 @@ export const updateDashboardLabelsColor =
|
||||
const colorScheme = metadata.color_scheme;
|
||||
const labelsColorMapInstance = getLabelsColorMap();
|
||||
const fullLabelsColors = metadata.map_label_colors || {};
|
||||
const sharedLabelsColors = enforceSharedLabelsColorsArray(
|
||||
metadata.shared_label_colors,
|
||||
);
|
||||
const sharedLabelsColors = metadata.shared_label_colors || [];
|
||||
const customLabelsColors = metadata.label_colors || {};
|
||||
const updatedMetadata = { ...metadata };
|
||||
|
||||
// for dashboards with no color scheme, the charts should always use their individual schemes
|
||||
// this logic looks for unique labels (not shared across multiple charts) of each rendered chart
|
||||
@@ -970,7 +965,11 @@ export const updateDashboardLabelsColor =
|
||||
const shouldGoFresh = shouldReset.length > 0 ? shouldReset : false;
|
||||
const shouldMerge = !shouldGoFresh;
|
||||
// re-apply the color map first to get fresh maps accordingly
|
||||
applyColors(metadata, shouldGoFresh, shouldMerge);
|
||||
applyColors(updatedMetadata, shouldGoFresh, shouldMerge);
|
||||
// new data may have appeared in the map (data changes)
|
||||
// or new slices may have appeared while changing tabs
|
||||
dispatch(ensureSyncedLabelsColorMap(updatedMetadata));
|
||||
dispatch(ensureSyncedSharedLabelsColors(updatedMetadata));
|
||||
} catch (e) {
|
||||
console.error('Failed to update colors for new charts and labels:', e);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function AnchorLink({
|
||||
}, [id, scrollIntoView]);
|
||||
|
||||
return (
|
||||
<span className="anchor-link-container" id={id} data-test="anchor-link">
|
||||
<span className="anchor-link-container" id={id}>
|
||||
{showShortLinkButton && dashboardId && (
|
||||
<URLShortLinkButton
|
||||
anchorLinkId={id}
|
||||
|
||||
@@ -47,8 +47,6 @@ import {
|
||||
applyDashboardLabelsColorOnLoad,
|
||||
updateDashboardLabelsColor,
|
||||
persistDashboardLabelsColor,
|
||||
ensureSyncedSharedLabelsColors,
|
||||
ensureSyncedLabelsColorMap,
|
||||
} from 'src/dashboard/actions/dashboardState';
|
||||
import { getColorNamespace, resetColors } from 'src/utils/colorScheme';
|
||||
import { NATIVE_FILTER_DIVIDER_PREFIX } from '../nativeFilters/FiltersConfigModal/utils';
|
||||
@@ -98,6 +96,7 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
|
||||
const [dashboardLabelsColorInitiated, setDashboardLabelsColorInitiated] =
|
||||
useState(false);
|
||||
const prevRenderedChartIds = useRef<number[]>([]);
|
||||
|
||||
const prevTabIndexRef = useRef();
|
||||
const tabIndex = useMemo(() => {
|
||||
const nextTabIndex = findTabIndexByComponentId({
|
||||
@@ -111,18 +110,6 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
|
||||
prevTabIndexRef.current = nextTabIndex;
|
||||
return nextTabIndex;
|
||||
}, [dashboardLayout, directPathToChild]);
|
||||
// when all charts have rendered, enforce fresh shared labels
|
||||
const shouldForceFreshSharedLabelsColors =
|
||||
dashboardLabelsColorInitiated &&
|
||||
renderedChartIds.length > 0 &&
|
||||
chartIds.length === renderedChartIds.length &&
|
||||
prevRenderedChartIds.current.length < renderedChartIds.length;
|
||||
|
||||
const onBeforeUnload = useCallback(() => {
|
||||
dispatch(persistDashboardLabelsColor());
|
||||
resetColors(getColorNamespace(dashboardInfo?.metadata?.color_namespace));
|
||||
prevRenderedChartIds.current = [];
|
||||
}, [dashboardInfo?.metadata?.color_namespace, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (nativeFilterScopes.length === 0) {
|
||||
@@ -161,12 +148,11 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
|
||||
const activeKey = min === 0 ? DASHBOARD_GRID_ID : min.toString();
|
||||
const TOP_OF_PAGE_RANGE = 220;
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldForceFreshSharedLabelsColors) {
|
||||
// all available charts have rendered, enforce freshest shared label colors
|
||||
dispatch(ensureSyncedSharedLabelsColors(dashboardInfo.metadata, true));
|
||||
}
|
||||
}, [dashboardInfo.metadata, dispatch, shouldForceFreshSharedLabelsColors]);
|
||||
const onBeforeUnload = useCallback(() => {
|
||||
dispatch(persistDashboardLabelsColor());
|
||||
resetColors(getColorNamespace(dashboardInfo?.metadata?.color_namespace));
|
||||
prevRenderedChartIds.current = [];
|
||||
}, [dashboardInfo?.metadata?.color_namespace, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
// verify freshness of color map
|
||||
@@ -175,6 +161,7 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
|
||||
|
||||
if (
|
||||
dashboardLabelsColorInitiated &&
|
||||
dashboardInfo?.id &&
|
||||
numRenderedCharts > 0 &&
|
||||
prevRenderedChartIds.current.length < numRenderedCharts
|
||||
) {
|
||||
@@ -183,20 +170,12 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
|
||||
);
|
||||
prevRenderedChartIds.current = renderedChartIds;
|
||||
dispatch(updateDashboardLabelsColor(newRenderedChartIds));
|
||||
// new data may have appeared in the map (data changes)
|
||||
// or new slices may have appeared while changing tabs
|
||||
dispatch(ensureSyncedLabelsColorMap(dashboardInfo.metadata));
|
||||
|
||||
if (!shouldForceFreshSharedLabelsColors) {
|
||||
dispatch(ensureSyncedSharedLabelsColors(dashboardInfo.metadata));
|
||||
}
|
||||
}
|
||||
}, [
|
||||
dashboardInfo?.id,
|
||||
renderedChartIds,
|
||||
dispatch,
|
||||
dashboardLabelsColorInitiated,
|
||||
dashboardInfo.metadata,
|
||||
shouldForceFreshSharedLabelsColors,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -204,9 +183,9 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
|
||||
labelsColorMap.source = LabelsColorMapSource.Dashboard;
|
||||
|
||||
if (dashboardInfo?.id && !dashboardLabelsColorInitiated) {
|
||||
dispatch(applyDashboardLabelsColorOnLoad(dashboardInfo.metadata));
|
||||
// apply labels color as dictated by stored metadata (if any)
|
||||
setDashboardLabelsColorInitiated(true);
|
||||
dispatch(applyDashboardLabelsColorOnLoad(dashboardInfo.metadata));
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
} from 'src/utils/localStorageHelpers';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
|
||||
import { enforceSharedLabelsColorsArray } from 'src/utils/colorScheme';
|
||||
|
||||
type Props = { dashboardPageId: string };
|
||||
|
||||
@@ -68,9 +67,7 @@ const SyncDashboardState: FC<Props> = ({ dashboardPageId }) => {
|
||||
({ dashboardInfo, dashboardState, nativeFilters, dataMask }) => ({
|
||||
labelsColor: dashboardInfo.metadata?.label_colors || EMPTY_OBJECT,
|
||||
labelsColorMap: dashboardInfo.metadata?.map_label_colors || EMPTY_OBJECT,
|
||||
sharedLabelsColors: enforceSharedLabelsColorsArray(
|
||||
dashboardInfo.metadata?.shared_label_colors,
|
||||
),
|
||||
sharedLabelsColors: dashboardInfo.metadata?.shared_label_colors || [],
|
||||
colorScheme: dashboardState?.colorScheme,
|
||||
chartConfiguration:
|
||||
dashboardInfo.metadata?.chart_configuration || EMPTY_OBJECT,
|
||||
|
||||
@@ -47,7 +47,6 @@ const propTypes = {
|
||||
parentComponent: componentShape.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
editMode: PropTypes.bool.isRequired,
|
||||
embeddedMode: PropTypes.bool.isRequired,
|
||||
|
||||
// redux
|
||||
handleComponentDrop: PropTypes.func.isRequired,
|
||||
@@ -167,7 +166,6 @@ class Header extends PureComponent {
|
||||
index,
|
||||
handleComponentDrop,
|
||||
editMode,
|
||||
embeddedMode,
|
||||
} = this.props;
|
||||
|
||||
const headerStyle = headerStyleOptions.find(
|
||||
@@ -236,7 +234,7 @@ class Header extends PureComponent {
|
||||
onSaveTitle={this.handleChangeText}
|
||||
showTooltip={false}
|
||||
/>
|
||||
{!editMode && !embeddedMode && (
|
||||
{!editMode && (
|
||||
<AnchorLink id={component.id} dashboardId={dashboardId} />
|
||||
)}
|
||||
</HeaderStyles>
|
||||
|
||||
@@ -45,7 +45,6 @@ describe('Header', () => {
|
||||
parentComponent: newComponentFactory(DASHBOARD_GRID_TYPE),
|
||||
index: 0,
|
||||
editMode: false,
|
||||
embeddedMode: false,
|
||||
filters: {},
|
||||
handleComponentDrop() {},
|
||||
deleteComponent() {},
|
||||
@@ -119,19 +118,4 @@ describe('Header', () => {
|
||||
|
||||
expect(deleteComponent.callCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should render the AnchorLink in view mode', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find('AnchorLink')).toExist();
|
||||
});
|
||||
|
||||
it('should not render the AnchorLink in edit mode', () => {
|
||||
const wrapper = setup({ editMode: true });
|
||||
expect(wrapper.find('AnchorLink')).not.toExist();
|
||||
});
|
||||
|
||||
it('should not render the AnchorLink in embedded mode', () => {
|
||||
const wrapper = setup({ embeddedMode: true });
|
||||
expect(wrapper.find('AnchorLink')).not.toExist();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,23 +16,23 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import classNames from 'classnames';
|
||||
import { PureComponent, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Fragment, PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
|
||||
import EditableTitle from 'src/components/EditableTitle';
|
||||
import { EmptyStateMedium } from 'src/components/EmptyState';
|
||||
import EditableTitle from 'src/components/EditableTitle';
|
||||
import { setEditMode } from 'src/dashboard/actions/dashboardState';
|
||||
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
|
||||
import AnchorLink from 'src/dashboard/components/AnchorLink';
|
||||
import DragDroppable, {
|
||||
Droppable,
|
||||
} from 'src/dashboard/components/dnd/DragDroppable';
|
||||
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
|
||||
import { TAB_TYPE } from 'src/dashboard/util/componentTypes';
|
||||
import { componentShape } from 'src/dashboard/util/propShapes';
|
||||
import { TAB_TYPE } from 'src/dashboard/util/componentTypes';
|
||||
|
||||
export const RENDER_TAB = 'RENDER_TAB';
|
||||
export const RENDER_TAB_CONTENT = 'RENDER_TAB_CONTENT';
|
||||
@@ -50,7 +50,6 @@ const propTypes = {
|
||||
onHoverTab: PropTypes.func,
|
||||
editMode: PropTypes.bool.isRequired,
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
embeddedMode: PropTypes.bool,
|
||||
|
||||
// grid related
|
||||
availableColumnCount: PropTypes.number,
|
||||
@@ -269,7 +268,6 @@ class Tab extends PureComponent {
|
||||
editMode,
|
||||
isFocused,
|
||||
isHighlighted,
|
||||
embeddedMode,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -299,7 +297,7 @@ class Tab extends PureComponent {
|
||||
showTooltip={false}
|
||||
editing={editMode && isFocused}
|
||||
/>
|
||||
{!editMode && !embeddedMode && (
|
||||
{!editMode && (
|
||||
<AnchorLink
|
||||
id={component.id}
|
||||
dashboardId={this.props.dashboardId}
|
||||
|
||||
@@ -90,7 +90,6 @@ const createProps = () => ({
|
||||
type: 'TABS',
|
||||
},
|
||||
editMode: false,
|
||||
embeddedMode: false,
|
||||
undoLength: 0,
|
||||
redoLength: 0,
|
||||
filters: {},
|
||||
@@ -401,41 +400,3 @@ test('Render tab content with no children, editMode: true, canEdit: true', () =>
|
||||
screen.getByRole('link', { name: 'create a new chart' }),
|
||||
).toHaveAttribute('href', '/chart/add?dashboard_id=23');
|
||||
});
|
||||
|
||||
test('AnchorLink renders in view mode', () => {
|
||||
const props = createProps();
|
||||
props.renderType = 'RENDER_TAB';
|
||||
|
||||
render(<Tab {...props} />, {
|
||||
useRedux: true,
|
||||
useDnd: true,
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('anchor-link')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('AnchorLink does not render in edit mode', () => {
|
||||
const props = createProps();
|
||||
props.editMode = true;
|
||||
props.renderType = 'RENDER_TAB';
|
||||
|
||||
render(<Tab {...props} />, {
|
||||
useRedux: true,
|
||||
useDnd: true,
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('anchor-link')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('AnchorLink does not render in embedded mode', () => {
|
||||
const props = createProps();
|
||||
props.embeddedMode = true;
|
||||
props.renderType = 'RENDER_TAB';
|
||||
|
||||
render(<Tab {...props} />, {
|
||||
useRedux: true,
|
||||
useDnd: true,
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('anchor-link')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Filter, t } from '@superset-ui/core';
|
||||
import { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Filter, t } from '@superset-ui/core';
|
||||
import { Layout, LayoutItem, RootState } from 'src/dashboard/types';
|
||||
import { useChartIds } from 'src/dashboard/util/charts/useChartIds';
|
||||
import { CHART_TYPE } from 'src/dashboard/util/componentTypes';
|
||||
import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
|
||||
import { CHART_TYPE } from 'src/dashboard/util/componentTypes';
|
||||
import { useChartIds } from 'src/dashboard/util/charts/useChartIds';
|
||||
|
||||
const extractTabLabel = (tab?: LayoutItem) =>
|
||||
tab?.meta?.text || tab?.meta?.defaultText || '';
|
||||
@@ -95,11 +95,10 @@ export const useFilterScope = (filter: Filter) => {
|
||||
if (topLevelTabs) {
|
||||
// We start assuming that all charts are in scope for all tabs in the root path
|
||||
const topLevelTabsInFullScope = [...filter.scope.rootPath];
|
||||
const layoutChartElementsInTabsInScope = layoutCharts.filter(
|
||||
element =>
|
||||
element.parents?.some(parent =>
|
||||
topLevelTabsInFullScope.includes(parent),
|
||||
),
|
||||
const layoutChartElementsInTabsInScope = layoutCharts.filter(element =>
|
||||
element.parents.some(parent =>
|
||||
topLevelTabsInFullScope.includes(parent),
|
||||
),
|
||||
);
|
||||
// Exclude the tabs that contain excluded charts
|
||||
filter.scope.excluded.forEach(chartId => {
|
||||
@@ -107,7 +106,7 @@ export const useFilterScope = (filter: Filter) => {
|
||||
tabId =>
|
||||
layoutChartElementsInTabsInScope
|
||||
.find(chart => chart.meta.chartId === chartId)
|
||||
?.parents?.includes(tabId),
|
||||
?.parents.includes(tabId),
|
||||
);
|
||||
if (excludedIndex > -1) {
|
||||
topLevelTabsInFullScope.splice(excludedIndex, 1);
|
||||
@@ -121,7 +120,7 @@ export const useFilterScope = (filter: Filter) => {
|
||||
layoutChartElementsInTabsInScope.find(
|
||||
element =>
|
||||
element.meta.chartId === chartId &&
|
||||
element.parents?.every(
|
||||
element.parents.every(
|
||||
parent => !topLevelTabsInFullScope.includes(parent),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -83,7 +83,7 @@ function useSelectChartTabParents() {
|
||||
const chartLayoutItem = Object.values(dashboardLayout).find(
|
||||
layoutItem => layoutItem.meta?.chartId === chartId,
|
||||
);
|
||||
return chartLayoutItem?.parents?.filter(
|
||||
return chartLayoutItem?.parents.filter(
|
||||
(parent: string) => dashboardLayout[parent]?.type === TAB_TYPE,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
import getFormDataWithExtraFilters from 'src/dashboard/util/charts/getFormDataWithExtraFilters';
|
||||
import Chart from 'src/dashboard/components/gridComponents/Chart';
|
||||
import { PLACEHOLDER_DATASOURCE } from 'src/dashboard/constants';
|
||||
import { enforceSharedLabelsColorsArray } from 'src/utils/colorScheme';
|
||||
|
||||
const EMPTY_OBJECT = {};
|
||||
|
||||
@@ -67,9 +66,7 @@ function mapStateToProps(
|
||||
} = dashboardState;
|
||||
const labelsColor = dashboardInfo?.metadata?.label_colors || {};
|
||||
const labelsColorMap = dashboardInfo?.metadata?.map_label_colors || {};
|
||||
const sharedLabelsColors = enforceSharedLabelsColorsArray(
|
||||
dashboardInfo?.metadata?.shared_label_colors,
|
||||
);
|
||||
const sharedLabelsColors = dashboardInfo?.metadata?.shared_label_colors || [];
|
||||
const ownColorScheme = chart.form_data?.color_scheme;
|
||||
// note: this method caches filters if possible to prevent render cascades
|
||||
const formData = getFormDataWithExtraFilters({
|
||||
|
||||
@@ -80,7 +80,6 @@ function mapStateToProps(
|
||||
dashboardId: dashboardInfo.id,
|
||||
dashboardInfo,
|
||||
fullSizeChartId: dashboardState.fullSizeChartId,
|
||||
embeddedMode: !dashboardInfo?.userId,
|
||||
};
|
||||
|
||||
// rows and columns need more data about their child dimensions
|
||||
|
||||
@@ -180,7 +180,7 @@ export type ComponentType = (typeof componentTypes)[ComponentTypesKeys];
|
||||
/** State of dashboardLayout item in redux */
|
||||
export type LayoutItem = {
|
||||
children: string[];
|
||||
parents?: string[];
|
||||
parents: string[];
|
||||
type: ComponentType;
|
||||
id: string;
|
||||
meta: {
|
||||
|
||||
@@ -448,7 +448,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
||||
|
||||
const renderControl = ({ name, config }: CustomControlItem) => {
|
||||
const { controls, chart, exploreState } = props;
|
||||
const { visibility, hidden, disableStash, ...restConfig } = config;
|
||||
const { visibility, hidden, ...restConfig } = config;
|
||||
|
||||
// If the control item is not an object, we have to look up the control data from
|
||||
// the centralized controls file.
|
||||
@@ -529,7 +529,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
||||
|
||||
return (
|
||||
<StashFormDataContainer
|
||||
shouldStash={isVisible === false && disableStash !== true}
|
||||
shouldStash={isVisible === false}
|
||||
fieldNames={[name]}
|
||||
key={`control-container-${name}`}
|
||||
>
|
||||
|
||||
@@ -359,14 +359,7 @@ function ExploreViewContainer(props) {
|
||||
}
|
||||
|
||||
useComponentDidMount(() => {
|
||||
props.actions.logEvent(
|
||||
LOG_ACTIONS_MOUNT_EXPLORER,
|
||||
props.slice?.slice_id
|
||||
? {
|
||||
slice_id: props.slice.slice_id,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
props.actions.logEvent(LOG_ACTIONS_MOUNT_EXPLORER);
|
||||
});
|
||||
|
||||
useChangeEffect(tabId, (previous, current) => {
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { render } from 'spec/helpers/testing-library';
|
||||
import { screen } from '@testing-library/react';
|
||||
import * as useQueryParamsModule from 'use-query-params';
|
||||
import AllEntitiesTable from './AllEntitiesTable';
|
||||
|
||||
describe('AllEntitiesTable', () => {
|
||||
const mockSetShowTagModal = jest.fn();
|
||||
|
||||
const mockObjects = {
|
||||
dashboard: [],
|
||||
chart: [],
|
||||
query: [],
|
||||
};
|
||||
|
||||
const mockObjectsWithTags = {
|
||||
dashboard: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'dashboard',
|
||||
name: 'Sales Dashboard',
|
||||
url: '/dashboard/1',
|
||||
changed_on: '2023-11-20T12:34:56Z',
|
||||
created_by: 1,
|
||||
creator: 'John Doe',
|
||||
owners: [{ id: 1, first_name: 'John', last_name: 'Doe' }],
|
||||
tags: [
|
||||
{ id: 101, name: 'Sales', type: 'TagType.custom' },
|
||||
{ id: 42, name: 'Current Tag', type: 'TagType.custom' },
|
||||
],
|
||||
},
|
||||
],
|
||||
chart: [
|
||||
{
|
||||
id: 2,
|
||||
type: 'chart',
|
||||
name: 'Monthly Revenue',
|
||||
url: '/chart/2',
|
||||
changed_on: '2023-11-19T12:00:00Z',
|
||||
created_by: 2,
|
||||
creator: 'Jane Smith',
|
||||
owners: [{ id: 2, first_name: 'Jane', last_name: 'Smith' }],
|
||||
tags: [
|
||||
{ id: 102, name: 'Revenue', type: 'TagType.custom' },
|
||||
{ id: 42, name: 'Current Tag', type: 'TagType.custom' },
|
||||
],
|
||||
},
|
||||
],
|
||||
query: [
|
||||
{
|
||||
id: 3,
|
||||
type: 'query',
|
||||
name: 'User Engagement',
|
||||
url: '/query/3',
|
||||
changed_on: '2023-11-18T09:30:00Z',
|
||||
created_by: 3,
|
||||
creator: 'Alice Brown',
|
||||
owners: [{ id: 3, first_name: 'Alice', last_name: 'Brown' }],
|
||||
tags: [
|
||||
{ id: 103, name: 'Engagement', type: 'TagType.custom' },
|
||||
{ id: 42, name: 'Current Tag', type: 'TagType.custom' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(useQueryParamsModule, 'useQueryParam')
|
||||
.mockReturnValue([42, jest.fn()]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders when empty', () => {
|
||||
render(
|
||||
<AllEntitiesTable
|
||||
search=""
|
||||
setShowTagModal={mockSetShowTagModal}
|
||||
objects={mockObjects}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText('No entities have this tag currently assigned'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Add tag to entities')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the correct tags for each object type, excluding the current tag', () => {
|
||||
render(
|
||||
<AllEntitiesTable
|
||||
search=""
|
||||
setShowTagModal={mockSetShowTagModal}
|
||||
objects={mockObjectsWithTags}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Sales Dashboard')).toBeInTheDocument();
|
||||
expect(screen.getByText('Sales')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Monthly Revenue')).toBeInTheDocument();
|
||||
expect(screen.getByText('Revenue')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('User Engagement')).toBeInTheDocument();
|
||||
expect(screen.getByText('Engagement')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('Current Tag')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,6 @@ import FacePile from 'src/components/FacePile';
|
||||
import Tag from 'src/types/TagType';
|
||||
import Owner from 'src/types/Owner';
|
||||
import { EmptyStateBig } from 'src/components/EmptyState';
|
||||
import { NumberParam, useQueryParam } from 'use-query-params';
|
||||
|
||||
const MAX_TAGS_TO_SHOW = 3;
|
||||
const PAGE_SIZE = 10;
|
||||
@@ -80,7 +79,6 @@ export default function AllEntitiesTable({
|
||||
}: AllEntitiesTableProps) {
|
||||
type objectType = 'dashboard' | 'chart' | 'query';
|
||||
|
||||
const [tagId] = useQueryParam('id', NumberParam);
|
||||
const showListViewObjs =
|
||||
objects.dashboard.length > 0 ||
|
||||
objects.chart.length > 0 ||
|
||||
@@ -121,9 +119,7 @@ export default function AllEntitiesTable({
|
||||
<TagsList
|
||||
tags={tags.filter(
|
||||
(tag: Tag) =>
|
||||
tag.type !== undefined &&
|
||||
['TagType.custom', 1].includes(tag.type) &&
|
||||
tag.id !== tagId,
|
||||
tag.type === 'TagTypes.custom' || tag.type === 1,
|
||||
)}
|
||||
maxTags={MAX_TAGS_TO_SHOW}
|
||||
/>
|
||||
|
||||
@@ -33,7 +33,19 @@ import { Chart } from 'src/types/Chart';
|
||||
import Icons from 'src/components/Icons';
|
||||
import SubMenu from './SubMenu';
|
||||
import EmptyState from './EmptyState';
|
||||
import { WelcomeTable, RecentActivity } from './types';
|
||||
import { WelcomeTable } from './types';
|
||||
|
||||
/**
|
||||
* Return result from /api/v1/log/recent_activity/
|
||||
*/
|
||||
interface RecentActivity {
|
||||
action: string;
|
||||
item_type: 'slice' | 'dashboard';
|
||||
item_url: string;
|
||||
item_title: string;
|
||||
time: number;
|
||||
time_delta_humanized?: string;
|
||||
}
|
||||
|
||||
interface RecentSlice extends RecentActivity {
|
||||
item_type: 'slice';
|
||||
|
||||
@@ -278,7 +278,7 @@ const SavedQueries = ({
|
||||
url={`/sqllab?savedQueryId=${q.id}`}
|
||||
title={q.label}
|
||||
imgFallbackURL="/static/assets/images/empty-query.svg"
|
||||
description={t('Modified %s', q.changed_on_delta_humanized)}
|
||||
description={t('Ran %s', q.changed_on_delta_humanized)}
|
||||
cover={
|
||||
q?.sql?.length && showThumbnails && featureFlag ? (
|
||||
<QueryContainer>
|
||||
|
||||
@@ -55,15 +55,3 @@ export enum GlobalMenuDataOptions {
|
||||
ExcelUpload = 'excelUpload',
|
||||
ColumnarUpload = 'columnarUpload',
|
||||
}
|
||||
|
||||
/**
|
||||
* Return result from /api/v1/log/recent_activity/
|
||||
*/
|
||||
export interface RecentActivity {
|
||||
action: string;
|
||||
item_type: 'slice' | 'dashboard';
|
||||
item_url: string;
|
||||
item_title: string;
|
||||
time: number;
|
||||
time_delta_humanized?: string;
|
||||
}
|
||||
|
||||
@@ -117,13 +117,6 @@ const tableApi = api.injectEndpoints({
|
||||
}),
|
||||
}),
|
||||
tableMetadata: builder.query<TableMetaData, FetchTableMetadataQueryParams>({
|
||||
providesTags: result =>
|
||||
result
|
||||
? [
|
||||
{ type: 'TableMetadatas', id: result.name },
|
||||
{ type: 'TableMetadatas', id: 'LIST' },
|
||||
]
|
||||
: [{ type: 'TableMetadatas', id: 'LIST' }],
|
||||
query: ({ dbId, catalog, schema, table }) => ({
|
||||
endpoint: `/api/v1/database/${dbId}/table_metadata/${toQueryString({
|
||||
name: table,
|
||||
|
||||
@@ -16,22 +16,18 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import sinon from 'sinon';
|
||||
import { LOG_EVENT } from 'src/logger/actions';
|
||||
import {
|
||||
LOG_ACTIONS_LOAD_CHART,
|
||||
LOG_ACTIONS_SPA_NAVIGATION,
|
||||
} from 'src/logger/LogUtils';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import logger from 'src/middleware/loggerMiddleware';
|
||||
import { LOG_EVENT } from 'src/logger/actions';
|
||||
import { LOG_ACTIONS_LOAD_CHART } from 'src/logger/LogUtils';
|
||||
|
||||
describe('logger middleware', () => {
|
||||
const dashboardId = 123;
|
||||
const next = sinon.spy();
|
||||
const mockStore = {
|
||||
getState: () => ({
|
||||
dashboardInfo: {
|
||||
id: dashboardId,
|
||||
id: 1,
|
||||
},
|
||||
impressionId: 'impression_id',
|
||||
}),
|
||||
@@ -47,10 +43,6 @@ describe('logger middleware', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const timeSandbox = sinon.createSandbox({
|
||||
useFakeTimers: true,
|
||||
});
|
||||
|
||||
let postStub;
|
||||
beforeEach(() => {
|
||||
postStub = sinon.stub(SupersetClient, 'post');
|
||||
@@ -58,7 +50,6 @@ describe('logger middleware', () => {
|
||||
afterEach(() => {
|
||||
next.resetHistory();
|
||||
postStub.restore();
|
||||
timeSandbox.clock.reset();
|
||||
});
|
||||
|
||||
it('should listen to LOG_EVENT action type', () => {
|
||||
@@ -73,10 +64,11 @@ describe('logger middleware', () => {
|
||||
});
|
||||
|
||||
it('should POST an event to /superset/log/ when called', () => {
|
||||
const clock = sinon.useFakeTimers();
|
||||
logger(mockStore)(next)(action);
|
||||
expect(next.callCount).toBe(0);
|
||||
|
||||
timeSandbox.clock.tick(2000);
|
||||
clock.tick(2000);
|
||||
expect(SupersetClient.post.callCount).toBe(1);
|
||||
expect(SupersetClient.post.getCall(0).args[0].endpoint).toMatch(
|
||||
'/superset/log/',
|
||||
@@ -84,19 +76,12 @@ describe('logger middleware', () => {
|
||||
});
|
||||
|
||||
it('should include ts, start_offset, event_name, impression_id, source, and source_id in every event', () => {
|
||||
const fetchLog = logger(mockStore)(next);
|
||||
fetchLog({
|
||||
type: LOG_EVENT,
|
||||
payload: {
|
||||
eventName: LOG_ACTIONS_SPA_NAVIGATION,
|
||||
eventData: { path: `/dashboard/${dashboardId}/` },
|
||||
},
|
||||
});
|
||||
timeSandbox.clock.tick(2000);
|
||||
fetchLog(action);
|
||||
timeSandbox.clock.tick(2000);
|
||||
expect(SupersetClient.post.callCount).toBe(2);
|
||||
const { events } = SupersetClient.post.getCall(1).args[0].postPayload;
|
||||
const clock = sinon.useFakeTimers();
|
||||
logger(mockStore)(next)(action);
|
||||
clock.tick(2000);
|
||||
|
||||
expect(SupersetClient.post.callCount).toBe(1);
|
||||
const { events } = SupersetClient.post.getCall(0).args[0].postPayload;
|
||||
const mockEventdata = action.payload.eventData;
|
||||
const mockEventname = action.payload.eventName;
|
||||
expect(events[0]).toMatchObject({
|
||||
@@ -106,7 +91,6 @@ describe('logger middleware', () => {
|
||||
source: 'dashboard',
|
||||
source_id: mockStore.getState().dashboardInfo.id,
|
||||
event_type: 'timing',
|
||||
dashboard_id: mockStore.getState().dashboardInfo.id,
|
||||
});
|
||||
|
||||
expect(typeof events[0].ts).toBe('number');
|
||||
|
||||
@@ -23,10 +23,7 @@ import { SupersetClient } from '@superset-ui/core';
|
||||
|
||||
import { safeStringify } from '../utils/safeStringify';
|
||||
import { LOG_EVENT } from '../logger/actions';
|
||||
import {
|
||||
LOG_EVENT_TYPE_TIMING,
|
||||
LOG_ACTIONS_SPA_NAVIGATION,
|
||||
} from '../logger/LogUtils';
|
||||
import { LOG_EVENT_TYPE_TIMING } from '../logger/LogUtils';
|
||||
import DebouncedMessageQueue from '../utils/DebouncedMessageQueue';
|
||||
|
||||
const LOG_ENDPOINT = '/superset/log/?explode=events';
|
||||
@@ -70,87 +67,73 @@ const logMessageQueue = new DebouncedMessageQueue({
|
||||
delayThreshold: 1000,
|
||||
});
|
||||
let lastEventId = 0;
|
||||
const loggerMiddleware = store => next => {
|
||||
let navPath;
|
||||
return action => {
|
||||
if (action.type !== LOG_EVENT) {
|
||||
return next(action);
|
||||
}
|
||||
const loggerMiddleware = store => next => action => {
|
||||
if (action.type !== LOG_EVENT) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
const { dashboardInfo, explore, impressionId, dashboardLayout, sqlLab } =
|
||||
store.getState();
|
||||
let logMetadata = {
|
||||
impression_id: impressionId,
|
||||
version: 'v2',
|
||||
};
|
||||
const { eventName } = action.payload;
|
||||
let { eventData = {} } = action.payload;
|
||||
|
||||
if (eventName === LOG_ACTIONS_SPA_NAVIGATION) {
|
||||
navPath = eventData.path;
|
||||
}
|
||||
const path = navPath || window?.location?.href;
|
||||
|
||||
if (dashboardInfo?.id && path?.includes('/dashboard/')) {
|
||||
logMetadata = {
|
||||
source: 'dashboard',
|
||||
source_id: dashboardInfo.id,
|
||||
dashboard_id: dashboardInfo.id,
|
||||
...logMetadata,
|
||||
};
|
||||
} else if (explore?.slice) {
|
||||
logMetadata = {
|
||||
source: 'explore',
|
||||
source_id: explore.slice ? explore.slice.slice_id : 0,
|
||||
...(explore.slice.slice_id && { slice_id: explore.slice.slice_id }),
|
||||
...logMetadata,
|
||||
};
|
||||
} else if (path?.includes('/sqllab/')) {
|
||||
const editor = sqlLab.queryEditors.find(
|
||||
({ id }) => id === sqlLab.tabHistory.slice(-1)[0],
|
||||
);
|
||||
logMetadata = {
|
||||
source: 'sqlLab',
|
||||
source_id: editor?.id,
|
||||
db_id: editor?.dbId,
|
||||
schema: editor?.schema,
|
||||
};
|
||||
}
|
||||
|
||||
eventData = {
|
||||
...logMetadata,
|
||||
ts: new Date().getTime(),
|
||||
event_name: eventName,
|
||||
...eventData,
|
||||
};
|
||||
if (LOG_EVENT_TYPE_TIMING.has(eventName)) {
|
||||
eventData = {
|
||||
...eventData,
|
||||
event_type: 'timing',
|
||||
trigger_event: lastEventId,
|
||||
};
|
||||
} else {
|
||||
lastEventId = nanoid();
|
||||
eventData = {
|
||||
...eventData,
|
||||
event_type: 'user',
|
||||
event_id: lastEventId,
|
||||
visibility: document.visibilityState,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
eventData.target_id &&
|
||||
dashboardLayout?.present?.[eventData.target_id]
|
||||
) {
|
||||
const { meta } = dashboardLayout.present[eventData.target_id];
|
||||
// chart name or tab/header text
|
||||
eventData.target_name = meta.chartId ? meta.sliceName : meta.text;
|
||||
}
|
||||
|
||||
logMessageQueue.append(eventData);
|
||||
return eventData;
|
||||
const { dashboardInfo, explore, impressionId, dashboardLayout, sqlLab } =
|
||||
store.getState();
|
||||
let logMetadata = {
|
||||
impression_id: impressionId,
|
||||
version: 'v2',
|
||||
};
|
||||
if (dashboardInfo?.id) {
|
||||
logMetadata = {
|
||||
source: 'dashboard',
|
||||
source_id: dashboardInfo.id,
|
||||
...logMetadata,
|
||||
};
|
||||
} else if (explore?.slice) {
|
||||
logMetadata = {
|
||||
source: 'explore',
|
||||
source_id: explore.slice ? explore.slice.slice_id : 0,
|
||||
...logMetadata,
|
||||
};
|
||||
} else if (sqlLab) {
|
||||
const editor = sqlLab.queryEditors.find(
|
||||
({ id }) => id === sqlLab.tabHistory.slice(-1)[0],
|
||||
);
|
||||
logMetadata = {
|
||||
source: 'sqlLab',
|
||||
source_id: editor?.id,
|
||||
db_id: editor?.dbId,
|
||||
schema: editor?.schema,
|
||||
};
|
||||
}
|
||||
|
||||
const { eventName } = action.payload;
|
||||
let { eventData = {} } = action.payload;
|
||||
eventData = {
|
||||
...logMetadata,
|
||||
ts: new Date().getTime(),
|
||||
event_name: eventName,
|
||||
...eventData,
|
||||
};
|
||||
if (LOG_EVENT_TYPE_TIMING.has(eventName)) {
|
||||
eventData = {
|
||||
...eventData,
|
||||
event_type: 'timing',
|
||||
trigger_event: lastEventId,
|
||||
};
|
||||
} else {
|
||||
lastEventId = nanoid();
|
||||
eventData = {
|
||||
...eventData,
|
||||
event_type: 'user',
|
||||
event_id: lastEventId,
|
||||
visibility: document.visibilityState,
|
||||
};
|
||||
}
|
||||
|
||||
if (eventData.target_id && dashboardLayout?.present?.[eventData.target_id]) {
|
||||
const { meta } = dashboardLayout.present[eventData.target_id];
|
||||
// chart name or tab/header text
|
||||
eventData.target_name = meta.chartId ? meta.sliceName : meta.text;
|
||||
}
|
||||
|
||||
logMessageQueue.append(eventData);
|
||||
return eventData;
|
||||
};
|
||||
|
||||
export default loggerMiddleware;
|
||||
|
||||
@@ -161,7 +161,7 @@ describe('DashboardList', () => {
|
||||
const callsD = fetchMock.calls(/dashboard\/\?q/);
|
||||
expect(callsD).toHaveLength(1);
|
||||
expect(callsD[0][0]).toMatchInlineSnapshot(
|
||||
`"http://localhost/api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25,select_columns:!(id,dashboard_title,published,url,slug,changed_by,changed_by.id,changed_by.first_name,changed_by.last_name,changed_on_delta_humanized,owners,owners.id,owners.first_name,owners.last_name,tags.id,tags.name,tags.type,status,certified_by,certification_details,changed_on))"`,
|
||||
`"http://localhost/api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25,select_columns:!(id,dashboard_title,published,url,slug,changed_by,changed_on_delta_humanized,owners.id,owners.first_name,owners.last_name,owners,tags.id,tags.name,tags.type,status,certified_by,certification_details,changed_on))"`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -118,14 +118,11 @@ const DASHBOARD_COLUMNS_TO_FETCH = [
|
||||
'url',
|
||||
'slug',
|
||||
'changed_by',
|
||||
'changed_by.id',
|
||||
'changed_by.first_name',
|
||||
'changed_by.last_name',
|
||||
'changed_on_delta_humanized',
|
||||
'owners',
|
||||
'owners.id',
|
||||
'owners.first_name',
|
||||
'owners.last_name',
|
||||
'owners',
|
||||
'tags.id',
|
||||
'tags.name',
|
||||
'tags.type',
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import * as uiCore from '@superset-ui/core';
|
||||
import { getExtensionsRegistry } from '@superset-ui/core';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import * as uiCore from '@superset-ui/core';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import Welcome from 'src/pages/Home';
|
||||
import { getExtensionsRegistry } from '@superset-ui/core';
|
||||
import setupExtensions from 'src/setup/setupExtensions';
|
||||
|
||||
const chartsEndpoint = 'glob:*/api/v1/chart/?*';
|
||||
@@ -62,35 +62,9 @@ fetchMock.get(savedQueryEndpoint, {
|
||||
result: [],
|
||||
});
|
||||
|
||||
const mockRecentActivityResult = [
|
||||
{
|
||||
action: 'dashboard',
|
||||
item_title: "World Bank's Data",
|
||||
item_type: 'dashboard',
|
||||
item_url: '/superset/dashboard/world_health/',
|
||||
time: 1741644942130.566,
|
||||
time_delta_humanized: 'a day ago',
|
||||
},
|
||||
{
|
||||
action: 'dashboard',
|
||||
item_title: '[ untitled dashboard ]',
|
||||
item_type: 'dashboard',
|
||||
item_url: '/superset/dashboard/19/',
|
||||
time: 1741644881695.7869,
|
||||
time_delta_humanized: 'a day ago',
|
||||
},
|
||||
{
|
||||
action: 'dashboard',
|
||||
item_title: '[ untitled dashboard ]',
|
||||
item_type: 'dashboard',
|
||||
item_url: '/superset/dashboard/19/',
|
||||
time: 1741644381695.7869,
|
||||
time_delta_humanized: 'two day ago',
|
||||
},
|
||||
];
|
||||
|
||||
fetchMock.get(recentActivityEndpoint, {
|
||||
result: mockRecentActivityResult,
|
||||
Created: [],
|
||||
Viewed: [],
|
||||
});
|
||||
|
||||
fetchMock.get(chartInfoEndpoint, {
|
||||
@@ -168,21 +142,6 @@ test('With sql role - renders all panels on the page on page load', async () =>
|
||||
expect(panels).toHaveLength(4);
|
||||
});
|
||||
|
||||
test('With sql role - renders distinct recent activities', async () => {
|
||||
await renderWelcome();
|
||||
const recentPanel = screen.getByRole('button', { name: 'right Recents' });
|
||||
userEvent.click(recentPanel);
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(
|
||||
screen.queryAllByText(mockRecentActivityResult[0].item_title),
|
||||
).toHaveLength(1), // eslint-disable-line jest-dom/prefer-in-document
|
||||
);
|
||||
expect(
|
||||
screen.queryAllByText(mockRecentActivityResult[1].item_title),
|
||||
).toHaveLength(1); // eslint-disable-line jest-dom/prefer-in-document
|
||||
});
|
||||
|
||||
test('With sql role - calls api methods in parallel on page load', async () => {
|
||||
await renderWelcome();
|
||||
expect(fetchMock.calls(chartsEndpoint)).toHaveLength(2);
|
||||
|
||||
@@ -159,7 +159,7 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
|
||||
const canReadSavedQueries = userHasPermission(user, 'SavedQuery', 'can_read');
|
||||
const userid = user.userId;
|
||||
const id = userid!.toString(); // confident that user is not a guest user
|
||||
const params = rison.encode({ page_size: 24, distinct: false });
|
||||
const params = rison.encode({ page_size: 6 });
|
||||
const recent = `/api/v1/log/recent_activity/?q=${params}`;
|
||||
const [activeChild, setActiveChild] = useState('Loading');
|
||||
const userKey = dangerouslyGetItemDoNotUse(id, null);
|
||||
|
||||
@@ -24,30 +24,17 @@ import {
|
||||
} from '@superset-ui/core';
|
||||
|
||||
/**
|
||||
* Force falsy namespace values to undefined to default to GLOBAL
|
||||
* Forces falsy namespace values to undefined to default to GLOBAL
|
||||
*
|
||||
* @param namespace
|
||||
* @returns - namespace or default undefined
|
||||
*/
|
||||
export const getColorNamespace = (namespace?: string) => namespace || undefined;
|
||||
|
||||
/**
|
||||
*
|
||||
* Field shared_label_colors used to be a dict of all colors for all labels.
|
||||
* Force shared_label_colors field to be a list of actual shared labels.
|
||||
*
|
||||
* @param sharedLabelsColors - the shared label colors list
|
||||
* @returns string[]
|
||||
*/
|
||||
export const enforceSharedLabelsColorsArray = (
|
||||
sharedLabelsColors: string[] | Record<string, string> | undefined,
|
||||
) => (Array.isArray(sharedLabelsColors) ? sharedLabelsColors : []);
|
||||
|
||||
/**
|
||||
* Get labels shared across all charts in a dashboard.
|
||||
* Merges a fresh instance of shared label colors with a stored one.
|
||||
*
|
||||
* @param currentSharedLabels - existing shared labels to merge with fresh
|
||||
* @returns Record<string, string>
|
||||
*/
|
||||
export const getFreshSharedLabels = (
|
||||
@@ -87,7 +74,7 @@ export const getSharedLabelsColorMapEntries = (
|
||||
* @returns all color entries except custom label colors
|
||||
*/
|
||||
export const getLabelsColorMapEntries = (
|
||||
customLabelsColor: Record<string, string> = {},
|
||||
customLabelsColor: Record<string, string>,
|
||||
): Record<string, string> => {
|
||||
const labelsColorMapInstance = getLabelsColorMap();
|
||||
const allEntries = Object.fromEntries(labelsColorMapInstance.getColorMap());
|
||||
@@ -188,9 +175,7 @@ export const applyColors = (
|
||||
CategoricalColorNamespace.getNamespace(colorNameSpace);
|
||||
const colorScheme = metadata?.color_scheme;
|
||||
const fullLabelsColor = metadata?.map_label_colors || {};
|
||||
const sharedLabels = enforceSharedLabelsColorsArray(
|
||||
metadata?.shared_label_colors,
|
||||
);
|
||||
const sharedLabels = metadata?.shared_label_colors || [];
|
||||
const customLabelsColor = metadata?.label_colors || {};
|
||||
const sharedLabelsColor = getSharedLabelsColorMapEntries(
|
||||
fullLabelsColor,
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
SupersetTheme,
|
||||
getClientErrorObject,
|
||||
t,
|
||||
lruCache,
|
||||
} from '@superset-ui/core';
|
||||
import Chart from 'src/types/Chart';
|
||||
import { intersection } from 'lodash';
|
||||
@@ -35,7 +34,7 @@ import { FetchDataConfig, FilterValue } from 'src/components/ListView';
|
||||
import SupersetText from 'src/utils/textUtils';
|
||||
import { findPermission } from 'src/utils/findPermission';
|
||||
import { User } from 'src/types/bootstrapTypes';
|
||||
import { RecentActivity, WelcomeTable } from 'src/features/home/types';
|
||||
import { WelcomeTable } from 'src/features/home/types';
|
||||
import { Dashboard, Filter, TableTab } from './types';
|
||||
|
||||
// Modifies the rison encoding slightly to match the backend's rison encoding/decoding. Applies globally.
|
||||
@@ -224,14 +223,10 @@ export const getRecentActivityObjs = (
|
||||
) =>
|
||||
SupersetClient.get({ endpoint: recent }).then(recentsRes => {
|
||||
const res: any = {};
|
||||
const distinctRes = lruCache<RecentActivity>(6);
|
||||
recentsRes.json.result.reverse().forEach((record: RecentActivity) => {
|
||||
distinctRes.set(record.item_url, record);
|
||||
});
|
||||
return getFilteredChartsandDashboards(addDangerToast, filters).then(
|
||||
({ other }) => {
|
||||
res.other = other;
|
||||
res.viewed = distinctRes.values().reverse();
|
||||
res.viewed = recentsRes.json.result;
|
||||
return res;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -31,8 +31,8 @@ from superset.extensions import (
|
||||
)
|
||||
from superset.security import SupersetSecurityManager # noqa: F401
|
||||
|
||||
# All of the fields located here should be considered legacy. The correct way
|
||||
# to declare "global" dependencies is to define it in extensions.py,
|
||||
# All of the fields located here should be considered legacy.
|
||||
# The correct way to declare "global" dependencies is to define it in extensions.py,
|
||||
# then initialize it in app.create_app(). These fields will be removed
|
||||
# in subsequent PRs as things are migrated towards the factory pattern
|
||||
app: Flask = current_app
|
||||
|
||||
@@ -201,7 +201,6 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||
"tags.id",
|
||||
"tags.name",
|
||||
"tags.type",
|
||||
"uuid",
|
||||
]
|
||||
list_select_columns = list_columns + ["changed_by_fk", "changed_on"]
|
||||
order_columns = [
|
||||
|
||||
@@ -394,13 +394,8 @@ class ChartDataRestApi(ChartRestApi):
|
||||
)
|
||||
|
||||
if result_format == ChartDataResultFormat.JSON:
|
||||
queries = result["queries"]
|
||||
if security_manager.is_guest_user():
|
||||
for query in queries:
|
||||
with contextlib.suppress(KeyError):
|
||||
del query["query"]
|
||||
response_data = json.dumps(
|
||||
{"result": queries},
|
||||
{"result": result["queries"]},
|
||||
default=json.json_int_dttm_ser,
|
||||
ignore_nan=True,
|
||||
)
|
||||
|
||||
@@ -29,7 +29,6 @@ for these chart types.
|
||||
from io import StringIO
|
||||
from typing import Any, Optional, TYPE_CHECKING, Union
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from flask_babel import gettext as __
|
||||
|
||||
@@ -84,11 +83,10 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s
|
||||
else:
|
||||
axis = {"columns": 1, "rows": 0}
|
||||
|
||||
# pivoting with null values will create an empty df
|
||||
df = df.fillna("SUPERSET_PANDAS_NAN")
|
||||
|
||||
# pivot data; we'll compute totals and subtotals later
|
||||
if rows or columns:
|
||||
# pivoting with null values will create an empty df
|
||||
df = df.fillna("NULL")
|
||||
df = df.pivot_table(
|
||||
index=rows,
|
||||
columns=columns,
|
||||
@@ -153,18 +151,6 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s
|
||||
# add subtotal for each group and overall total; we start from the
|
||||
# overall group, and iterate deeper into subgroups
|
||||
groups = df.columns
|
||||
if not apply_metrics_on_rows:
|
||||
for col in df.columns:
|
||||
# we need to replace the temporary placeholder with either a string
|
||||
# or np.nan, depending on the column type so that they can sum correctly
|
||||
if pd.api.types.is_numeric_dtype(df[col]):
|
||||
df[col].replace("SUPERSET_PANDAS_NAN", np.nan, inplace=True)
|
||||
else:
|
||||
df[col].replace("SUPERSET_PANDAS_NAN", "nan", inplace=True)
|
||||
else:
|
||||
# when we applied metrics on rows, we switched the columns and rows
|
||||
# so checking column type doesn't apply. Replace everything with np.nan
|
||||
df.replace("SUPERSET_PANDAS_NAN", np.nan, inplace=True)
|
||||
for level in range(df.columns.nlevels):
|
||||
subgroups = {group[:level] for group in groups}
|
||||
for subgroup in subgroups:
|
||||
@@ -185,7 +171,7 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s
|
||||
for subgroup in subgroups:
|
||||
slice_ = df.index.get_loc(subgroup)
|
||||
subtotal = pivot_v2_aggfunc_map[aggfunc](
|
||||
df.iloc[slice_, :].apply(pd.to_numeric, errors="coerce"), axis=0
|
||||
df.iloc[slice_, :].apply(pd.to_numeric), axis=0
|
||||
)
|
||||
depth = df.index.nlevels - len(subgroup) - 1
|
||||
total = metric_name if level == 0 else __("Subtotal")
|
||||
@@ -200,14 +186,6 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s
|
||||
if apply_metrics_on_rows:
|
||||
df = df.T
|
||||
|
||||
# replace the remaining temporary placeholder string for np.nan after pivoting
|
||||
df.replace("SUPERSET_PANDAS_NAN", np.nan, inplace=True)
|
||||
df.rename(
|
||||
index={"SUPERSET_PANDAS_NAN": np.nan},
|
||||
columns={"SUPERSET_PANDAS_NAN": np.nan},
|
||||
inplace=True,
|
||||
)
|
||||
|
||||
return df
|
||||
|
||||
|
||||
|
||||
@@ -50,12 +50,9 @@ def import_chart(
|
||||
) -> Slice:
|
||||
can_write = ignore_permissions or security_manager.can_access("can_write", "Chart")
|
||||
existing = db.session.query(Slice).filter_by(uuid=config["uuid"]).first()
|
||||
user = get_user()
|
||||
if existing:
|
||||
if overwrite and can_write and user:
|
||||
if not security_manager.can_access_chart(existing) or (
|
||||
user not in existing.owners and not security_manager.is_admin()
|
||||
):
|
||||
if overwrite and can_write and get_user():
|
||||
if not security_manager.can_access_chart(existing):
|
||||
raise ImportFailedError(
|
||||
"A chart already exists and user doesn't "
|
||||
"have permissions to overwrite it"
|
||||
|
||||
@@ -153,12 +153,9 @@ def import_dashboard(
|
||||
"Dashboard",
|
||||
)
|
||||
existing = db.session.query(Dashboard).filter_by(uuid=config["uuid"]).first()
|
||||
user = get_user()
|
||||
if existing:
|
||||
if overwrite and can_write and user:
|
||||
if not security_manager.can_access_dashboard(existing) or (
|
||||
user not in existing.owners and not security_manager.is_admin()
|
||||
):
|
||||
if overwrite and can_write and get_user():
|
||||
if not security_manager.can_access_dashboard(existing):
|
||||
raise ImportFailedError(
|
||||
"A dashboard already exists and user doesn't "
|
||||
"have permissions to overwrite it"
|
||||
|
||||
@@ -39,11 +39,11 @@ from superset.commands.database.ssh_tunnel.exceptions import (
|
||||
SSHTunnelInvalidError,
|
||||
)
|
||||
from superset.commands.database.test_connection import TestConnectionDatabaseCommand
|
||||
from superset.commands.database.utils import add_permissions
|
||||
from superset.daos.database import DatabaseDAO
|
||||
from superset.databases.ssh_tunnel.models import SSHTunnel
|
||||
from superset.db_engine_specs.base import GenericDBException
|
||||
from superset.exceptions import SupersetErrorsException
|
||||
from superset.extensions import event_logger
|
||||
from superset.extensions import event_logger, security_manager
|
||||
from superset.models.core import Database
|
||||
from superset.utils.decorators import on_error, transaction
|
||||
|
||||
@@ -100,7 +100,28 @@ class CreateDatabaseCommand(BaseCommand):
|
||||
).run()
|
||||
|
||||
# add catalog/schema permissions
|
||||
add_permissions(database, ssh_tunnel)
|
||||
if database.db_engine_spec.supports_catalog:
|
||||
catalogs = database.get_all_catalog_names(
|
||||
cache=False,
|
||||
ssh_tunnel=ssh_tunnel,
|
||||
)
|
||||
for catalog in catalogs:
|
||||
security_manager.add_permission_view_menu(
|
||||
"catalog_access",
|
||||
security_manager.get_catalog_perm(
|
||||
database.database_name, catalog
|
||||
),
|
||||
)
|
||||
else:
|
||||
# add a dummy catalog for DBs that don't support them
|
||||
catalogs = [None]
|
||||
|
||||
for catalog in catalogs:
|
||||
try:
|
||||
self.add_schema_permissions(database, catalog, ssh_tunnel)
|
||||
except GenericDBException: # pylint: disable=broad-except
|
||||
logger.warning("Error processing catalog '%s'", catalog)
|
||||
continue
|
||||
except (
|
||||
SSHTunnelInvalidError,
|
||||
SSHTunnelCreateFailedError,
|
||||
@@ -128,6 +149,26 @@ class CreateDatabaseCommand(BaseCommand):
|
||||
|
||||
return database
|
||||
|
||||
def add_schema_permissions(
|
||||
self,
|
||||
database: Database,
|
||||
catalog: str,
|
||||
ssh_tunnel: Optional[SSHTunnel],
|
||||
) -> None:
|
||||
for schema in database.get_all_schema_names(
|
||||
catalog=catalog,
|
||||
cache=False,
|
||||
ssh_tunnel=ssh_tunnel,
|
||||
):
|
||||
security_manager.add_permission_view_menu(
|
||||
"schema_access",
|
||||
security_manager.get_schema_perm(
|
||||
database.database_name,
|
||||
catalog,
|
||||
schema,
|
||||
),
|
||||
)
|
||||
|
||||
def validate(self) -> None:
|
||||
exceptions: list[ValidationError] = []
|
||||
sqlalchemy_uri: Optional[str] = self._properties.get("sqlalchemy_uri")
|
||||
|
||||
@@ -19,7 +19,6 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from superset import app, db, security_manager
|
||||
from superset.commands.database.utils import add_permissions
|
||||
from superset.commands.exceptions import ImportFailedError
|
||||
from superset.databases.ssh_tunnel.models import SSHTunnel
|
||||
from superset.databases.utils import make_url_safe
|
||||
@@ -87,3 +86,40 @@ def import_database(
|
||||
logger.warning(ex.message)
|
||||
|
||||
return database
|
||||
|
||||
|
||||
def add_permissions(database: Database, ssh_tunnel: SSHTunnel) -> None:
|
||||
"""
|
||||
Add DAR for catalogs and schemas.
|
||||
"""
|
||||
if database.db_engine_spec.supports_catalog:
|
||||
catalogs = database.get_all_catalog_names(
|
||||
cache=False,
|
||||
ssh_tunnel=ssh_tunnel,
|
||||
)
|
||||
|
||||
for catalog in catalogs:
|
||||
security_manager.add_permission_view_menu(
|
||||
"catalog_access",
|
||||
security_manager.get_catalog_perm(
|
||||
database.database_name,
|
||||
catalog,
|
||||
),
|
||||
)
|
||||
else:
|
||||
catalogs = [None]
|
||||
|
||||
for catalog in catalogs:
|
||||
for schema in database.get_all_schema_names(
|
||||
catalog=catalog,
|
||||
cache=False,
|
||||
ssh_tunnel=ssh_tunnel,
|
||||
):
|
||||
security_manager.add_permission_view_menu(
|
||||
"schema_access",
|
||||
security_manager.get_schema_perm(
|
||||
database.database_name,
|
||||
catalog,
|
||||
schema,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from superset import security_manager
|
||||
from superset.databases.ssh_tunnel.models import SSHTunnel
|
||||
from superset.db_engine_specs.base import GenericDBException
|
||||
from superset.models.core import Database
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def add_permissions(database: Database, ssh_tunnel: SSHTunnel | None) -> None:
|
||||
"""
|
||||
Add DAR for catalogs and schemas.
|
||||
"""
|
||||
if database.db_engine_spec.supports_catalog:
|
||||
catalogs = database.get_all_catalog_names(
|
||||
cache=False,
|
||||
ssh_tunnel=ssh_tunnel,
|
||||
)
|
||||
|
||||
for catalog in catalogs:
|
||||
security_manager.add_permission_view_menu(
|
||||
"catalog_access",
|
||||
security_manager.get_catalog_perm(
|
||||
database.database_name,
|
||||
catalog,
|
||||
),
|
||||
)
|
||||
else:
|
||||
catalogs = [None]
|
||||
|
||||
for catalog in catalogs:
|
||||
try:
|
||||
for schema in database.get_all_schema_names(
|
||||
catalog=catalog,
|
||||
cache=False,
|
||||
ssh_tunnel=ssh_tunnel,
|
||||
):
|
||||
security_manager.add_permission_view_menu(
|
||||
"schema_access",
|
||||
security_manager.get_schema_perm(
|
||||
database.database_name,
|
||||
catalog,
|
||||
schema,
|
||||
),
|
||||
)
|
||||
except GenericDBException: # pylint: disable=broad-except
|
||||
logger.warning("Error processing catalog '%s'", catalog)
|
||||
continue
|
||||
@@ -102,7 +102,6 @@ def validate_data_uri(data_uri: str) -> None:
|
||||
raise DatasetForbiddenDataURI()
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def import_dataset(
|
||||
config: dict[str, Any],
|
||||
overwrite: bool = False,
|
||||
@@ -114,18 +113,10 @@ def import_dataset(
|
||||
"Dataset",
|
||||
)
|
||||
existing = db.session.query(SqlaTable).filter_by(uuid=config["uuid"]).first()
|
||||
user = get_user()
|
||||
if existing:
|
||||
if overwrite and can_write and user:
|
||||
if user not in existing.owners and not security_manager.is_admin():
|
||||
raise ImportFailedError(
|
||||
"A dataset already exists and user doesn't "
|
||||
"have permissions to overwrite it"
|
||||
)
|
||||
if not overwrite or not can_write:
|
||||
return existing
|
||||
config["id"] = existing.id
|
||||
|
||||
elif not can_write:
|
||||
raise ImportFailedError(
|
||||
"Dataset doesn't exist and user doesn't have permission to create datasets"
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
@@ -1,111 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from superset import db
|
||||
from superset.commands.base import BaseCommand
|
||||
from superset.models.core import Log
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=consider-using-transaction
|
||||
class LogPruneCommand(BaseCommand):
|
||||
"""
|
||||
Command to prune the logs table by deleting rows older than the specified retention period.
|
||||
|
||||
This command deletes records from the `Log` table that have not been changed within the
|
||||
specified number of days. It helps in maintaining the database by removing outdated entries
|
||||
and freeing up space.
|
||||
|
||||
Attributes:
|
||||
retention_period_days (int): The number of days for which records should be retained.
|
||||
Records older than this period will be deleted.
|
||||
""" # noqa: E501
|
||||
|
||||
def __init__(self, retention_period_days: int):
|
||||
"""
|
||||
:param retention_period_days: Number of days to keep in the logs table
|
||||
"""
|
||||
self.retention_period_days = retention_period_days
|
||||
|
||||
def run(self) -> None:
|
||||
"""
|
||||
Executes the prune command
|
||||
"""
|
||||
batch_size = 999 # SQLite has a IN clause limit of 999
|
||||
total_deleted = 0
|
||||
start_time = time.time()
|
||||
|
||||
# Select all IDs that need to be deleted
|
||||
ids_to_delete = (
|
||||
db.session.execute(
|
||||
sa.select(Log.id).where(
|
||||
Log.dttm
|
||||
< datetime.now() - timedelta(days=self.retention_period_days)
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
total_rows = len(ids_to_delete)
|
||||
|
||||
logger.info("Total rows to be deleted: %s", f"{total_rows:,}")
|
||||
|
||||
next_logging_threshold = 1
|
||||
|
||||
# Iterate over the IDs in batches
|
||||
for i in range(0, total_rows, batch_size):
|
||||
batch_ids = ids_to_delete[i : i + batch_size]
|
||||
|
||||
# Delete the selected batch using IN clause
|
||||
result = db.session.execute(sa.delete(Log).where(Log.id.in_(batch_ids)))
|
||||
|
||||
# Update the total number of deleted records
|
||||
total_deleted += result.rowcount
|
||||
|
||||
# Explicitly commit the transaction given that if an error occurs, we want to ensure that the # noqa: E501
|
||||
# records that have been deleted so far are committed
|
||||
db.session.commit()
|
||||
|
||||
# Log the number of deleted records every 1% increase in progress
|
||||
percentage_complete = (total_deleted / total_rows) * 100
|
||||
if percentage_complete >= next_logging_threshold:
|
||||
logger.info(
|
||||
"Deleted %s rows from the logs table older than %s days (%d%% complete)", # noqa: E501
|
||||
f"{total_deleted:,}",
|
||||
self.retention_period_days,
|
||||
percentage_complete,
|
||||
)
|
||||
next_logging_threshold += 1
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
minutes, seconds = divmod(elapsed_time, 60)
|
||||
formatted_time = f"{int(minutes):02}:{int(seconds):02}"
|
||||
logger.info(
|
||||
"Pruning complete: %s rows deleted in %s",
|
||||
f"{total_deleted:,}",
|
||||
formatted_time,
|
||||
)
|
||||
|
||||
def validate(self) -> None:
|
||||
pass
|
||||
@@ -118,7 +118,7 @@ class BaseReportScheduleCommand(BaseCommand):
|
||||
|
||||
for _ in range(iterations):
|
||||
next_exec = next(schedule)
|
||||
diff, current_exec = next_exec - current_exec, next_exec # type: ignore
|
||||
diff, current_exec = next_exec - current_exec, next_exec
|
||||
if int(diff) < minimum_interval:
|
||||
raise ReportScheduleFrequencyNotAllowed(
|
||||
report_type=report_type, minimum_interval=minimum_interval
|
||||
|
||||
@@ -69,7 +69,7 @@ class QueryPruneCommand(BaseCommand):
|
||||
|
||||
total_rows = len(ids_to_delete)
|
||||
|
||||
logger.info("Total rows to be deleted: %s", f"{total_rows:,}")
|
||||
logger.info("Total rows to be deleted: %s", total_rows)
|
||||
|
||||
next_logging_threshold = 1
|
||||
|
||||
@@ -91,8 +91,8 @@ class QueryPruneCommand(BaseCommand):
|
||||
percentage_complete = (total_deleted / total_rows) * 100
|
||||
if percentage_complete >= next_logging_threshold:
|
||||
logger.info(
|
||||
"Deleted %s rows from the query table older than %s days (%d%% complete)", # noqa: E501
|
||||
f"{total_deleted:,}",
|
||||
"Deleted %s rows from the query table older than %s days (%d%% complete)",
|
||||
total_deleted,
|
||||
self.retention_period_days,
|
||||
percentage_complete,
|
||||
)
|
||||
@@ -102,9 +102,7 @@ class QueryPruneCommand(BaseCommand):
|
||||
minutes, seconds = divmod(elapsed_time, 60)
|
||||
formatted_time = f"{int(minutes):02}:{int(seconds):02}"
|
||||
logger.info(
|
||||
"Pruning complete: %s rows deleted in %s",
|
||||
f"{total_deleted:,}",
|
||||
formatted_time,
|
||||
"Pruning complete: %s rows deleted in %s", total_deleted, formatted_time
|
||||
)
|
||||
|
||||
def validate(self) -> None:
|
||||
|
||||
@@ -478,7 +478,7 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
|
||||
"PRESTO_EXPAND_DATA": False,
|
||||
# Exposes API endpoint to compute thumbnails
|
||||
"THUMBNAILS": False,
|
||||
# Enables the endpoints to cache and retrieve dashboard screenshots via webdriver.
|
||||
# Enable the endpoints to cache and retrieve dashboard screenshots via webdriver.
|
||||
# Requires configuring Celery and a cache using THUMBNAIL_CACHE_CONFIG.
|
||||
"ENABLE_DASHBOARD_SCREENSHOT_ENDPOINTS": False,
|
||||
# Generate screenshots (PDF or JPG) of dashboards using the web driver.
|
||||
@@ -803,8 +803,6 @@ EXPLORE_FORM_DATA_CACHE_CONFIG: CacheConfig = {
|
||||
STORE_CACHE_KEYS_IN_METADATA_DB = False
|
||||
|
||||
# CORS Options
|
||||
# NOTE: enabling this requires installing the cors-related python dependencies
|
||||
# `pip install .[cors]` or `pip install apache_superset[cors]`, depending
|
||||
ENABLE_CORS = False
|
||||
CORS_OPTIONS: dict[Any, Any] = {}
|
||||
|
||||
@@ -1027,12 +1025,6 @@ class CeleryConfig: # pylint: disable=too-few-public-methods
|
||||
# "schedule": crontab(minute=0, hour=0, day_of_month=1),
|
||||
# "options": {"retention_period_days": 180},
|
||||
# },
|
||||
# Uncomment to enable pruning of the logs table
|
||||
# "prune_logs": {
|
||||
# "task": "prune_logs",
|
||||
# "schedule": crontab(minute="*", hour="*"),
|
||||
# "kwargs": {"retention_period_days": 180},
|
||||
# },
|
||||
}
|
||||
|
||||
|
||||
@@ -1330,7 +1322,7 @@ DISALLOWED_SQL_FUNCTIONS: dict[str, set[str]] = {
|
||||
"table_to_xml_and_xmlschema",
|
||||
"version",
|
||||
},
|
||||
"clickhouse": {"url", "version", "currentDatabase", "hostName"},
|
||||
"clickhouse": {"url"},
|
||||
"mysql": {"version"},
|
||||
}
|
||||
|
||||
@@ -1854,15 +1846,6 @@ class ExtraDynamicQueryFilters(TypedDict, total=False):
|
||||
EXTRA_DYNAMIC_QUERY_FILTERS: ExtraDynamicQueryFilters = {}
|
||||
|
||||
|
||||
# The migrations that add catalog permissions might take a considerably long time
|
||||
# to execute as it has to create permissions to all schemas and catalogs from all
|
||||
# other catalogs accessible by the credentials. This flag allows to skip the
|
||||
# creation of these secondary perms, and focus only on permissions for the default
|
||||
# catalog. These secondary permissions can be created later by editing the DB
|
||||
# connection via the UI (without downtime).
|
||||
CATALOGS_SIMPLIFIED_MIGRATION: bool = False
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# * WARNING: STOP EDITING HERE *
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
@@ -38,8 +38,7 @@ from superset.exceptions import (
|
||||
)
|
||||
from superset.models.core import Database
|
||||
from superset.result_set import SupersetResultSet
|
||||
from superset.sql.parse import SQLScript
|
||||
from superset.sql_parse import Table
|
||||
from superset.sql_parse import ParsedQuery, Table
|
||||
from superset.superset_typing import ResultSetColumnType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -106,8 +105,8 @@ def get_virtual_table_metadata(dataset: SqlaTable) -> list[ResultSetColumnType]:
|
||||
sql = dataset.get_template_processor().process_template(
|
||||
dataset.sql, **dataset.template_params_dict
|
||||
)
|
||||
parsed_script = SQLScript(sql, engine=db_engine_spec.engine)
|
||||
if parsed_script.has_mutation():
|
||||
parsed_query = ParsedQuery(sql, engine=db_engine_spec.engine)
|
||||
if not db_engine_spec.is_readonly_query(parsed_query):
|
||||
raise SupersetSecurityException(
|
||||
SupersetError(
|
||||
error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR,
|
||||
@@ -115,7 +114,8 @@ def get_virtual_table_metadata(dataset: SqlaTable) -> list[ResultSetColumnType]:
|
||||
level=ErrorLevel.ERROR,
|
||||
)
|
||||
)
|
||||
if len(parsed_script.statements) > 1:
|
||||
statements = parsed_query.get_statements()
|
||||
if len(statements) > 1:
|
||||
raise SupersetSecurityException(
|
||||
SupersetError(
|
||||
error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR,
|
||||
@@ -127,7 +127,7 @@ def get_virtual_table_metadata(dataset: SqlaTable) -> list[ResultSetColumnType]:
|
||||
dataset.database,
|
||||
dataset.catalog,
|
||||
dataset.schema,
|
||||
sql,
|
||||
statements[0],
|
||||
)
|
||||
|
||||
|
||||
@@ -144,9 +144,8 @@ def get_columns_description(
|
||||
with database.get_raw_connection(catalog=catalog, schema=schema) as conn:
|
||||
cursor = conn.cursor()
|
||||
query = database.apply_limit_to_sql(query, limit=1)
|
||||
mutated_query = database.mutate_sql_based_on_config(query)
|
||||
cursor.execute(mutated_query)
|
||||
db_engine_spec.execute(cursor, mutated_query, database)
|
||||
cursor.execute(query)
|
||||
db_engine_spec.execute(cursor, query, database)
|
||||
result = db_engine_spec.fetch_data(cursor, limit=1)
|
||||
result_set = SupersetResultSet(result, cursor.description, db_engine_spec)
|
||||
return result_set.columns
|
||||
|
||||
@@ -59,14 +59,8 @@ class LogDAO(BaseDAO[Log]):
|
||||
.group_by(Log.dashboard_id, Log.slice_id, Log.action)
|
||||
.filter(
|
||||
and_(
|
||||
Log.action == "log",
|
||||
Log.action.in_(actions),
|
||||
Log.user_id == user_id,
|
||||
or_(
|
||||
*{
|
||||
Log.json.contains(f'"event_name": "{action}"')
|
||||
for action in actions
|
||||
},
|
||||
),
|
||||
# limit to one year of data to improve performance
|
||||
Log.dttm > one_year_ago,
|
||||
or_(Log.dashboard_id.isnot(None), Log.slice_id.isnot(None)),
|
||||
@@ -105,16 +99,7 @@ class LogDAO(BaseDAO[Log]):
|
||||
.outerjoin(Dashboard, Dashboard.id == Log.dashboard_id)
|
||||
.outerjoin(Slice, Slice.id == Log.slice_id)
|
||||
.filter(has_subject_title)
|
||||
.filter(
|
||||
Log.action == "log",
|
||||
Log.user_id == user_id,
|
||||
or_(
|
||||
*{
|
||||
Log.json.contains(f'"event_name": "{action}"')
|
||||
for action in actions
|
||||
},
|
||||
),
|
||||
)
|
||||
.filter(Log.action.in_(actions), Log.user_id == user_id)
|
||||
.order_by(Log.dttm.desc())
|
||||
.limit(page_size)
|
||||
.offset(page * page_size)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user