Compare commits

..

2 Commits

Author SHA1 Message Date
Maxime Beauchemin
774910f40d touching superset/ 2024-12-02 12:09:33 -08:00
Maxime Beauchemin
dbbcc11a98 fix: add python-base layer to fix CI builds in 4.1 2024-12-02 12:02:09 -08:00
153 changed files with 1753 additions and 7788 deletions

1
.gitignore vendored
View File

@@ -104,7 +104,6 @@ ghostdriver.log
testCSV.csv
.terser-plugin-cache/
apache-superset-*.tar.gz*
apache_superset-*.tar.gz*
release.json
# Translation-related files

View File

@@ -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)

View File

@@ -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**

View File

@@ -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)

View File

@@ -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)

View File

@@ -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])

View File

@@ -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
######################################################################

View File

@@ -22,9 +22,9 @@ under the License.
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/apache/superset?sort=semver)](https://github.com/apache/superset/tree/latest)
[![Build Status](https://github.com/apache/superset/workflows/Python/badge.svg)](https://github.com/apache/superset/actions)
[![PyPI version](https://badge.fury.io/py/apache_superset.svg)](https://badge.fury.io/py/apache_superset)
[![PyPI version](https://badge.fury.io/py/apache-superset.svg)](https://badge.fury.io/py/apache-superset)
[![Coverage Status](https://codecov.io/github/apache/superset/coverage.svg?branch=master)](https://codecov.io/github/apache/superset)
[![PyPI](https://img.shields.io/pypi/pyversions/apache_superset.svg?maxAge=2592000)](https://pypi.python.org/pypi/apache_superset)
[![PyPI](https://img.shields.io/pypi/pyversions/apache-superset.svg?maxAge=2592000)](https://pypi.python.org/pypi/apache-superset)
[![Get on Slack](https://img.shields.io/badge/slack-join-orange.svg)](http://bit.ly/join-superset-slack)
[![Documentation](https://img.shields.io/badge/docs-apache.org-blue.svg)](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/)

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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__`

View File

@@ -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

View File

@@ -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}"

View File

@@ -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}"

View File

@@ -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}"

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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`.

View File

@@ -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` |

View File

@@ -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:

View File

@@ -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.

View File

@@ -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:

View File

@@ -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

View File

@@ -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 |

View File

@@ -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'),

File diff suppressed because it is too large Load Diff

View File

@@ -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"]

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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",

View File

@@ -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",

View File

@@ -262,7 +262,6 @@ export interface BaseControlConfig<
props: ControlPanelsContainerProps,
controlData: AnyDict,
) => boolean;
disableStash?: boolean;
hidden?:
| boolean
| ((props: ControlPanelsContainerProps, controlData: AnyDict) => boolean);

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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);

View File

@@ -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;

View File

@@ -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: '',
},
},
],
],
},
],

View File

@@ -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: [

View File

@@ -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('Its 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,
},
},
],

View File

@@ -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));

View File

@@ -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),

View File

@@ -75,8 +75,6 @@ export type EchartsTimeseriesFormData = QueryFormData & {
stack: StackType;
timeCompare?: string[];
tooltipTimeFormat?: string;
showTooltipTotal?: boolean;
showTooltipPercentage?: boolean;
truncateXAxis: boolean;
truncateYAxis: boolean;
yAxisFormat?: string;

View File

@@ -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],
];

View File

@@ -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],
],
]);
});
});

View File

@@ -1206,7 +1206,6 @@ export function popSavedQuery(saveQueryId) {
schema: queryEditorProps.schema,
sql: queryEditorProps.sql,
templateParams: queryEditorProps.templateParams,
remoteId: queryEditorProps.remoteId,
};
return dispatch(addQueryEditor(tmpAdaptedProps));
})

View File

@@ -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);

View File

@@ -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}

View File

@@ -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),
);
});

View File

@@ -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

View File

@@ -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,
);

View File

@@ -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);
}

View File

@@ -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}

View File

@@ -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 () => {

View File

@@ -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,

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -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}

View File

@@ -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();
});

View File

@@ -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),
),
);

View File

@@ -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,
);
};

View File

@@ -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({

View File

@@ -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

View File

@@ -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: {

View File

@@ -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}`}
>

View File

@@ -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) => {

View File

@@ -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();
});
});

View File

@@ -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}
/>

View File

@@ -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';

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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');

View File

@@ -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;

View File

@@ -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))"`,
);
});

View File

@@ -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',

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;
},
);

View File

@@ -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

View File

@@ -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 = [

View File

@@ -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,
)

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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")

View File

@@ -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,
),
)

View File

@@ -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

View File

@@ -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"

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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 *
# -------------------------------------------------------------------

View File

@@ -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

View File

@@ -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