Compare commits

...

6 Commits

Author SHA1 Message Date
Evan
9c273b6d52 fix(ci): pin PY_VER build-arg so PR docker builds use Python 3.11+
supersetbot's "dev"/"lean" presets hardcode --build-arg PY_VER=3.10-slim-bookworm,
which lands ahead of our --extra-flags and overrides the Dockerfile's own
3.11 default. Now that requires-python is >=3.11, that stale pin makes uv
resolve apache-superset-core against Python 3.10 and fail. Append our own
PY_VER override, which wins as the last occurrence of the flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 00:00:43 -07:00
Evan
f9df4da8ae fix: restore mypy type: ignore for LegacyPrefixRedirectMiddleware wsgi_app arg
Dropping the sys.version_info conditional import in app.py also dropped
the `# type: ignore[arg-type]` mypy needs for the wsgi_app reassignment
after the conditional AppRootMiddleware rewrap, unrelated to the Python
version gate itself. Restores the exact comment/ignore already present
on master (identical code, unmodified there).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 20:33:05 -07:00
Evan Rusackas
332fe2e74b chore: drop Python 3.10 support, require Python 3.11+
CI, packaging metadata, and docs now reflect Python 3.11 as the minimum
supported version. Also collapses the sys.version_info compat shims in
superset/app.py, superset/middleware/legacy_prefix_redirect.py, and
superset/utils/backports.py, which existed solely to support running on
Python 3.10 (StrEnum and wsgiref.types were both added in 3.11).

Removing the 3.10 floor also drops the now-dead pyhive<0.7.0 marker for
python_version<'3.11' and obsoletes the need to pin syntaqlite below
0.5.0 for 3.10 compatibility.

Closes #42007

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 09:47:01 -07:00
JUST.in DO IT
4b07d43050 fix(sqllab): collapse left sidebar shows compact db selector in top bar (#41898) 2026-07-14 09:01:30 -07:00
dependabot[bot]
ef3a9e925e chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.62.0 to 8.63.0 in /superset-websocket (#42020)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2026-07-14 08:58:58 -07:00
Mehmet Salih Yavuz
76bb5f8e69 fix(dashboard): prevent native filter loss when saving chart customizations (#42032)
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
2026-07-14 18:30:12 +03:00
54 changed files with 423 additions and 364 deletions

View File

@@ -50,9 +50,8 @@ body:
attributes:
label: Python version
options:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
- Not applicable
- I don't know
validations:

View File

@@ -32,8 +32,6 @@ runs:
elif [ "$INPUT_PYTHON_VERSION" = "next" ]; then
# currently disabled in GHA matrixes because of library compatibility issues
RESOLVED_VERSION="3.12"
elif [ "$INPUT_PYTHON_VERSION" = "previous" ]; then
RESOLVED_VERSION="3.10"
elif printf '%s' "$INPUT_PYTHON_VERSION" | grep -Eq '^[0-9]+\.[0-9]+(\.[0-9]+)?$'; then
RESOLVED_VERSION="$INPUT_PYTHON_VERSION"
else

View File

@@ -42,7 +42,7 @@ jobs:
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.10"
python-version: "3.11"
- name: Install uv
run: pip install uv

View File

@@ -47,7 +47,7 @@ jobs:
steps:
- id: set_matrix
run: |
MATRIX_CONFIG=$(if [ "${{ github.event_name }}" == "pull_request" ]; then echo '["dev", "lean"]'; else echo '["dev", "lean", "py310", "websocket", "dockerize", "py311", "py312"]'; fi)
MATRIX_CONFIG=$(if [ "${{ github.event_name }}" == "pull_request" ]; then echo '["dev", "lean"]'; else echo '["dev", "lean", "websocket", "dockerize", "py311", "py312"]'; fi)
echo "matrix_config=${MATRIX_CONFIG}" >> $GITHUB_OUTPUT
echo $GITHUB_OUTPUT
@@ -123,13 +123,20 @@ jobs:
# pull timeouts, 504/401 on push, ECONNRESET) that otherwise fail
# the whole job. buildx reuses the buildkit layer cache from the
# failed attempt, so a retry mostly re-does just the failed push.
#
# supersetbot's "dev"/"lean" presets pin their own --build-arg
# PY_VER, which lands ahead of --extra-flags on the assembled
# buildx command line; docker/buildx keeps the last value for a
# repeated --build-arg key, so appending PY_VER here overrides
# supersetbot's pin and keeps the build on the Dockerfile's own
# supported Python version.
for attempt in 1 2 3; do
if supersetbot docker \
$PUSH_OR_LOAD \
--preset "$BUILD_PRESET" \
--context "$EVENT" \
--context-ref "$RELEASE" $FORCE_LATEST \
--extra-flags "--build-arg INCLUDE_CHROMIUM=false --tag $IMAGE_TAG" \
--extra-flags "--build-arg PY_VER=3.11.14-slim-trixie --build-arg INCLUDE_CHROMIUM=false --tag $IMAGE_TAG" \
$PLATFORM_ARG; then
break
fi

View File

@@ -25,7 +25,7 @@ jobs:
# Run the full version spread on push (master/release) and nightly,
# but only the current version on PRs — lint/format/type results
# rarely differ across patch versions, so 3x per PR is wasteful.
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "previous", "next"]') }}
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "next"]') }}
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

View File

@@ -98,7 +98,7 @@ jobs:
# Mirror the same matrix the release publisher uses so every variant
# operators consume from Docker Hub gets the refreshed base.
matrix:
build_preset: ["dev", "lean", "py310", "websocket", "dockerize", "py311", "py312"]
build_preset: ["dev", "lean", "websocket", "dockerize", "py311", "py312"]
fail-fast: false
steps:
- name: "Checkout release tag: ${{ needs.config.outputs.latest-release }}"

View File

@@ -25,7 +25,7 @@ jobs:
matrix:
# Full version spread on push (master/release) + nightly; current only
# on PRs to cut runner cost (cross-version breaks are caught at merge).
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["previous", "current", "next"]') }}
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "next"]') }}
defaults:
run:
working-directory: superset-extensions-cli

View File

@@ -51,7 +51,7 @@ jobs:
git show -s --format=raw HEAD
docker buildx build \
-t $TAG \
--cache-from=type=registry,ref=apache/superset-cache:3.10-slim-trixie \
--cache-from=type=registry,ref=apache/superset-cache:3.11-slim-trixie \
--target superset-node-ci \
.

View File

@@ -135,7 +135,7 @@ jobs:
matrix:
# Full version spread on push (master/release) + nightly; current only
# on PRs to cut runner cost (cross-version breaks are caught at merge).
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "previous", "next"]') }}
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "next"]') }}
env:
PYTHONPATH: ${{ github.workspace }}
SUPERSET_CONFIG: tests.integration_tests.superset_test_config

View File

@@ -50,7 +50,7 @@ jobs:
matrix:
# Full version spread on push (master/release) + nightly; current only
# on PRs to cut runner cost (cross-version breaks are caught at merge).
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["previous", "current", "next"]') }}
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "next"]') }}
env:
PYTHONPATH: ${{ github.workspace }}
# Promotes the SQLAlchemy 2.0 deprecation warnings already locked in as

View File

@@ -56,7 +56,7 @@ jobs:
strategy:
matrix:
build_preset:
["dev", "lean", "py310", "websocket", "dockerize", "py311", "py312"]
["dev", "lean", "websocket", "dockerize", "py311", "py312"]
fail-fast: false
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"

View File

@@ -14,7 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
FROM python:3.10-slim-trixie
FROM python:3.11-slim-trixie
RUN useradd --user-group --create-home --no-log-init --shell /bin/bash superset

View File

@@ -14,7 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
FROM python:3.10-slim-trixie
FROM python:3.11-slim-trixie
RUN useradd --user-group --create-home --no-log-init --shell /bin/bash superset

View File

@@ -14,7 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
FROM python:3.10-slim-trixie
FROM python:3.11-slim-trixie
ARG VERSION
RUN git clone --depth 1 --branch ${VERSION} https://github.com/apache/superset.git /superset

View File

@@ -14,7 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
FROM python:3.10-slim-trixie
FROM python:3.11-slim-trixie
RUN apt-get update -y
RUN apt-get install -y \

View File

@@ -24,6 +24,13 @@ assists people when migrating to a new version.
## Next
### Python 3.10 support removed
Python 3.10 is no longer supported. Superset now requires **Python 3.11 or higher**.
Update your environment (virtualenv, Docker base image, CI configuration, etc.) to
Python 3.11+ before upgrading. The `apache/superset-cache:3.10-slim-trixie` and
`py310` Docker image variants are no longer published.
### Owners, dashboard roles, and RLS roles replaced by Subjects
Superset now uses subject-based access assignments for dashboards, charts, datasets,

View File

@@ -71,7 +71,7 @@ x-common-build: &common-build
context: .
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`
cache_from:
- apache/superset-cache:3.10-slim-trixie
- apache/superset-cache:3.11-slim-trixie
args:
DEV_MODE: "true"
INCLUDE_CHROMIUM: ${INCLUDE_CHROMIUM:-false}

View File

@@ -33,7 +33,7 @@ x-common-build: &common-build
context: .
target: dev
cache_from:
- apache/superset-cache:3.10-slim-trixie
- apache/superset-cache:3.11-slim-trixie
services:
redis:

View File

@@ -38,7 +38,7 @@ x-common-build: &common-build
context: .
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`
cache_from:
- apache/superset-cache:3.10-slim-trixie
- apache/superset-cache:3.11-slim-trixie
args:
DEV_MODE: "true"
INCLUDE_CHROMIUM: ${INCLUDE_CHROMIUM:-false}

View File

@@ -99,7 +99,7 @@ See [Connecting AI Clients](#connecting-ai-clients) for Claude Code, Claude Web,
## Prerequisites
- Apache Superset 5.0+ running and accessible
- Python 3.10+
- Python 3.11+
- The `fastmcp` package (`pip install fastmcp`)
---

View File

@@ -379,7 +379,7 @@ functioning across environments.
Make sure your machine meets the [OS dependencies](https://superset.apache.org/docs/installation/pypi#os-dependencies) before following these steps.
You also need to install MySQL.
Ensure that you are using Python version 3.9, 3.10 or 3.11, then proceed with:
Ensure that you are using Python version 3.11 or 3.12, then proceed with:
```bash
# Create a virtual environment and activate it (recommended)

View File

@@ -82,7 +82,7 @@ If applicable, add screenshots or recordings.
### Environment
- Superset version: [e.g., 3.0.0]
- Python version: [e.g., 3.9.7]
- Python version: [e.g., 3.11.7]
- Node version: [e.g., 18.17.0]
- Database: [e.g., PostgreSQL 14]
- Browser: [e.g., Chrome 120]
@@ -125,7 +125,7 @@ No error messages in browser console or server logs.
### Environment
- Superset version: 3.0.0
- Python version: 3.9.16
- Python version: 3.11.16
- Database: PostgreSQL 14.9
- Browser: Chrome 120.0.6099.71
- OS: macOS 14.2

View File

@@ -52,7 +52,7 @@ Everything you need to contribute to the Apache Superset project. This section i
## Development Resources
### Prerequisites
- **Python**: 3.9, 3.10, or 3.11
- **Python**: 3.11 or 3.12
- **Node.js**: 18.x or 20.x
- **npm**: 9.x or 10.x
- **Git**: Basic understanding

View File

@@ -24,13 +24,12 @@ name = "apache_superset"
description = "A modern, enterprise-ready business intelligence web application"
readme = "README.md"
dynamic = ["version", "scripts", "entry-points"]
requires-python = ">=3.10"
requires-python = ">=3.11"
license = { file="LICENSE.txt" }
authors = [
{ name = "Apache Software Foundation", email = "dev@superset.apache.org" },
]
classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
@@ -167,7 +166,6 @@ gevent = ["gevent>=26.4.0"]
gsheets = ["shillelagh[gsheetsapi]>=1.4.4, <2"]
hana = ["hdbcli==2.28.21", "sqlalchemy_hana==3.0.3"]
hive = [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7.0",
"tableschema",
"thrift>=0.23.0, <1.0.0",
@@ -201,7 +199,6 @@ singlestore = ["sqlalchemy-singlestoredb>=1.2.1, <2"]
snowflake = ["snowflake-sqlalchemy>=1.10.2, <2"]
sqlite = ["syntaqlite>=0.6.0,<0.7.0"]
spark = [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
"pyhive[hive_pure_sasl]>=0.7",
"tableschema",
"thrift>=0.23.0, <1",
@@ -344,8 +341,8 @@ exclude = [
line-length = 88
indent-width = 4
# Assume Python 3.10
target-version = "py310"
# Assume Python 3.11
target-version = "py311"
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.

View File

@@ -133,8 +133,8 @@ def main(docker: bool, frontend: bool, backend: bool) -> None: # noqa: C901
requirements = [
Requirement(
"python",
(Version("3.10.0"), Version("3.10.999")),
(Version("3.9.0"), Version("3.11.999")),
(Version("3.11.0"), Version("3.11.999")),
(Version("3.11.0"), Version("3.12.999")),
"backend",
"python --version",
),

View File

@@ -21,7 +21,7 @@ under the License.
[![PyPI version](https://badge.fury.io/py/apache-superset-core.svg)](https://badge.fury.io/py/apache-superset-core)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
The official core package for building Apache Superset backend extensions and integrations. This package provides essential building blocks including base classes, API utilities, type definitions, and decorators for both the host application and extensions.

View File

@@ -26,7 +26,7 @@ authors = [
]
license = "Apache-2.0"
license-files = ["LICENSE.txt"]
requires-python = ">=3.10"
requires-python = ">=3.11"
keywords = ["superset", "apache", "analytics", "business-intelligence", "extensions", "visualization"]
classifiers = [
"Development Status :: 3 - Alpha",
@@ -34,7 +34,6 @@ classifiers = [
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Database",

View File

@@ -21,7 +21,7 @@ under the License.
[![PyPI version](https://badge.fury.io/py/apache-superset-extensions-cli.svg)](https://badge.fury.io/py/apache-superset-extensions-cli)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
Official command-line interface for building, bundling, and managing Apache Superset extensions. This CLI tool provides developers with everything needed to create, develop, and package extensions for the Superset ecosystem.

View File

@@ -25,7 +25,7 @@ authors = [
]
license = "Apache-2.0"
license-files = ["LICENSE.txt"]
requires-python = ">=3.10"
requires-python = ">=3.11"
keywords = ["superset", "apache", "cli", "extensions", "analytics", "business-intelligence", "development-tools"]
classifiers = [
"Development Status :: 3 - Alpha",
@@ -33,7 +33,6 @@ classifiers = [
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Database",

View File

@@ -1998,4 +1998,56 @@ describe('async actions', () => {
});
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('toggleLeftBar', () => {
const activeId = 'active-qe';
const makeState = (hideLeftBar: boolean, unsavedOverride?: boolean) => ({
sqlLab: {
tabHistory: [activeId],
queryEditors: [{ id: activeId, hideLeftBar }],
unsavedQueryEditor:
unsavedOverride !== undefined
? { id: activeId, hideLeftBar: unsavedOverride }
: {},
},
});
test('dispatches QUERY_EDITOR_TOGGLE_LEFT_BAR when state differs', () => {
const store = mockStore(makeState(false));
store.dispatch(actions.toggleLeftBar(true));
expect(store.getActions()).toEqual([
{
type: actions.QUERY_EDITOR_TOGGLE_LEFT_BAR,
queryEditorId: activeId,
hideLeftBar: true,
},
]);
});
test('does not dispatch when state is already the same', () => {
const store = mockStore(makeState(true));
store.dispatch(actions.toggleLeftBar(true));
expect(store.getActions()).toHaveLength(0);
});
test('uses unsavedQueryEditor state when available', () => {
// qe in queryEditors says false, but unsaved override says true
const store = mockStore(makeState(false, true));
store.dispatch(actions.toggleLeftBar(true));
expect(store.getActions()).toHaveLength(0);
});
test('does not dispatch when there is no active query editor', () => {
const store = mockStore({
sqlLab: {
tabHistory: [],
queryEditors: [],
unsavedQueryEditor: {},
},
});
store.dispatch(actions.toggleLeftBar(true));
expect(store.getActions()).toHaveLength(0);
});
});
});

View File

@@ -956,12 +956,24 @@ export function setActiveSouthPaneTab(tabId: string): SqlLabAction {
return { type: SET_ACTIVE_SOUTHPANE_TAB, tabId };
}
export function toggleLeftBar(queryEditor: QueryEditor): SqlLabAction {
const hideLeftBar = !queryEditor.hideLeftBar;
return {
type: QUERY_EDITOR_TOGGLE_LEFT_BAR,
queryEditor,
hideLeftBar,
export function toggleLeftBar(shouldHide: boolean): SqlLabThunkAction {
return (dispatch: AppDispatch, getState: GetState) => {
const { sqlLab } = getState();
const id = sqlLab.tabHistory.slice(-1)[0];
if (!id) return;
const qe = sqlLab.queryEditors.find(e => e.id === id);
const merged =
qe && sqlLab.unsavedQueryEditor?.id === id
? { ...qe, ...sqlLab.unsavedQueryEditor }
: qe;
if (!merged) return;
const isCurrentlyHidden = Boolean(merged.hideLeftBar);
if (shouldHide === isCurrentlyHidden) return;
dispatch({
type: QUERY_EDITOR_TOGGLE_LEFT_BAR,
queryEditorId: id,
hideLeftBar: shouldHide,
});
};
}

View File

@@ -21,6 +21,7 @@ import { render, userEvent, waitFor } from 'spec/helpers/testing-library';
import { initialState } from 'src/SqlLab/fixtures';
import useStoredSidebarWidth from 'src/components/ResizableSidebar/useStoredSidebarWidth';
import { ViewLocations } from 'src/SqlLab/contributions';
import * as sqlLabActions from 'src/SqlLab/actions/sqlLab';
import {
registerTestView,
cleanupExtensions,
@@ -108,6 +109,46 @@ test('right sidebar is hidden when no extensions registered', () => {
expect(queryByText('Right Sidebar Content')).not.toBeInTheDocument();
});
test('dispatches toggleLeftBar(true) when sidebar is resized to zero', async () => {
const toggleLeftBarSpy = jest
.spyOn(sqlLabActions, 'toggleLeftBar')
.mockReturnValue({
type: sqlLabActions.QUERY_EDITOR_TOGGLE_LEFT_BAR,
} as any);
const { getByRole } = render(<AppLayout {...defaultProps} />, {
useRedux: true,
initialState,
});
await userEvent.click(getByRole('button', { name: 'Resize to zero' }));
await waitFor(() => expect(toggleLeftBarSpy).toHaveBeenCalledWith(true));
toggleLeftBarSpy.mockRestore();
});
test('dispatches toggleLeftBar(false) when sidebar is resized to non-zero', async () => {
const collapsedState = {
...initialState,
sqlLab: {
...initialState.sqlLab,
unsavedQueryEditor: {
id: initialState.sqlLab.tabHistory[0],
hideLeftBar: true,
},
},
};
const toggleLeftBarSpy = jest
.spyOn(sqlLabActions, 'toggleLeftBar')
.mockReturnValue({
type: sqlLabActions.QUERY_EDITOR_TOGGLE_LEFT_BAR,
} as any);
const { getByRole } = render(<AppLayout {...defaultProps} />, {
useRedux: true,
initialState: collapsedState,
});
await userEvent.click(getByRole('button', { name: 'Resize' }));
await waitFor(() => expect(toggleLeftBarSpy).toHaveBeenCalledWith(false));
toggleLeftBarSpy.mockRestore();
});
test('renders right sidebar when view is contributed at rightSidebar location', () => {
registerTestView(
ViewLocations.sqllab.rightSidebar,

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { noop } from 'lodash-es';
import type { SqlLabRootState } from 'src/SqlLab/types';
import { css, styled } from '@apache-superset/core/theme';
@@ -32,6 +32,7 @@ import {
} from 'src/SqlLab/constants';
import { ViewLocations } from 'src/SqlLab/contributions';
import ViewListExtension from 'src/components/ViewListExtension';
import { toggleLeftBar } from 'src/SqlLab/actions/sqlLab';
import SqlEditorLeftBar from '../SqlEditorLeftBar';
import StatusBar from '../StatusBar';
@@ -66,6 +67,7 @@ const ContentWrapper = styled.div`
`;
const AppLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const dispatch = useDispatch();
const queryEditorId = useSelector<SqlLabRootState, string>(
({ sqlLab: { tabHistory } }) => tabHistory.slice(-1)[0],
);
@@ -91,6 +93,7 @@ const AppLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const onSidebarChange = (sizes: number[]) => {
const [updatedWidth, _, possibleRightWidth] = sizes;
setLeftWidth(updatedWidth);
dispatch(toggleLeftBar(updatedWidth === 0));
if (typeof possibleRightWidth === 'number') {
setRightWidth(possibleRightWidth);

View File

@@ -121,6 +121,7 @@ import KeyboardShortcutButton, {
KeyboardShortcut,
} from '../KeyboardShortcutButton';
import SqlEditorTopBar from '../SqlEditorTopBar';
import SqlEditorLeftBar from '../SqlEditorLeftBar';
const bootstrapData = getBootstrapData();
const scheduledQueriesConf = bootstrapData?.common?.conf?.SCHEDULED_QUERIES;
@@ -240,34 +241,36 @@ const SqlEditor: FC<Props> = ({
const theme = useTheme();
const dispatch = useAppDispatch();
const { database, latestQuery, currentQueryEditorId, hasSqlStatement } =
useSelector<
SqlLabRootState,
{
database?: DatabaseObject;
latestQuery?: QueryResponse;
hideLeftBar?: boolean;
currentQueryEditorId: QueryEditor['id'];
hasSqlStatement: boolean;
}
>(({ sqlLab: { unsavedQueryEditor, databases, queries, tabHistory } }) => {
let { dbId, latestQueryId, hideLeftBar } = queryEditor;
if (unsavedQueryEditor?.id === queryEditor.id) {
dbId = unsavedQueryEditor.dbId || dbId;
latestQueryId = unsavedQueryEditor.latestQueryId || latestQueryId;
hideLeftBar =
typeof unsavedQueryEditor.hideLeftBar === 'boolean'
? unsavedQueryEditor.hideLeftBar
: hideLeftBar;
}
return {
hasSqlStatement: Boolean(queryEditor.sql?.trim().length > 0),
database: databases[dbId || ''],
latestQuery: queries[latestQueryId || ''],
hideLeftBar,
currentQueryEditorId: tabHistory.slice(-1)[0],
};
}, shallowEqual);
const {
database,
latestQuery,
hideLeftBar,
currentQueryEditorId,
hasSqlStatement,
} = useSelector<
SqlLabRootState,
{
database?: DatabaseObject;
latestQuery?: QueryResponse;
hideLeftBar?: boolean;
currentQueryEditorId: QueryEditor['id'];
hasSqlStatement: boolean;
}
>(({ sqlLab: { unsavedQueryEditor, databases, queries, tabHistory } }) => {
let { dbId, latestQueryId, hideLeftBar } = queryEditor;
if (unsavedQueryEditor?.id === queryEditor.id) {
dbId = unsavedQueryEditor.dbId || dbId;
latestQueryId = unsavedQueryEditor.latestQueryId || latestQueryId;
hideLeftBar = unsavedQueryEditor.hideLeftBar === true;
}
return {
hasSqlStatement: Boolean(queryEditor.sql?.trim().length > 0),
database: databases[dbId || ''],
latestQuery: queries[latestQueryId || ''],
hideLeftBar,
currentQueryEditorId: tabHistory.slice(-1)[0],
};
}, shallowEqual);
const logAction = useLogAction({ queryEditorId: queryEditor.id });
const isActive = currentQueryEditorId === queryEditor.id;
@@ -975,6 +978,11 @@ const SqlEditor: FC<Props> = ({
queryEditorId={queryEditor.id}
defaultPrimaryActions={renderEditorPrimaryAction()}
defaultSecondaryActions={getSecondaryMenuItems()}
extra={
hideLeftBar && (
<SqlEditorLeftBar queryEditorId={queryEditor.id} collapsed />
)
}
/>
)}
{queryEditor.isDataset && renderDatasetWarning()}

View File

@@ -39,6 +39,7 @@ import TableExploreTree from '../TableExploreTree';
export interface SqlEditorLeftBarProps {
queryEditorId: string;
collapsed?: boolean;
}
const LeftBarStyles = styled.div`
@@ -63,7 +64,10 @@ const StyledDivider = styled.div`
margin: 0 -${({ theme }) => theme.sizeUnit * 2.5}px 0;
`;
const SqlEditorLeftBar = ({ queryEditorId }: SqlEditorLeftBarProps) => {
const SqlEditorLeftBar = ({
queryEditorId,
collapsed = false,
}: SqlEditorLeftBarProps) => {
const activeQEId = queryEditorId || EMPTY_STATE_QE_ID;
const dbSelectorProps = useDatabaseSelector(activeQEId);
const { db, catalog, schema, onDbChange, onCatalogChange, onSchemaChange } =
@@ -180,29 +184,38 @@ const SqlEditorLeftBar = ({ queryEditorId }: SqlEditorLeftBarProps) => {
</Flex>
);
const dbSelectorTrigger = (
<Popover
content={popoverContent}
open={selectorModalOpen}
onOpenChange={open => !open && closeSelectorModal()}
placement="bottomLeft"
trigger="click"
>
{/* Wrap in a span so the Popover can attach a ref without relying
on findDOMNode (deprecated in React 18+). */}
<span>
<DatabaseSelector
key={`db-selector-${db ? db.id : 'no-db'}:${catalog ?? 'no-catalog'}:${
schema ?? 'no-schema'
}`}
{...dbSelectorProps}
emptyState={<EmptyState />}
sqlLabMode
compactMode={collapsed}
onOpenModal={openSelectorModal}
/>
</span>
</Popover>
);
if (collapsed) {
return dbSelectorTrigger;
}
return (
<LeftBarStyles data-test="sql-editor-left-bar">
<Popover
content={popoverContent}
open={selectorModalOpen}
onOpenChange={open => !open && closeSelectorModal()}
placement="bottomLeft"
trigger="click"
>
{/* Wrap in a span so the Popover can attach a ref without relying
on findDOMNode (deprecated in React 18+). */}
<span>
<DatabaseSelector
key={`db-selector-${db ? db.id : 'no-db'}:${catalog ?? 'no-catalog'}:${
schema ?? 'no-schema'
}`}
{...dbSelectorProps}
emptyState={<EmptyState />}
sqlLabMode
onOpenModal={openSelectorModal}
/>
</span>
</Popover>
{dbSelectorTrigger}
<StyledDivider />
<TableExploreTree queryEditorId={activeQEId} />
{shouldShowReset && (

View File

@@ -41,7 +41,6 @@ import {
removeAllOtherQueryEditors,
queryEditorSetTitle,
cloneQueryToNewTab,
toggleLeftBar,
} from 'src/SqlLab/actions/sqlLab';
import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
import { Icons, type IconType } from '@superset-ui/core/components/Icons';
@@ -105,7 +104,6 @@ const SqlEditorTabHeader: FC<Props> = ({ queryEditor }) => {
removeAllOtherQueryEditors,
queryEditorSetTitle,
cloneQueryToNewTab,
toggleLeftBar,
},
dispatch,
),

View File

@@ -31,11 +31,13 @@ export interface SqlEditorTopBarProps {
queryEditorId: string;
defaultPrimaryActions: React.ReactNode;
defaultSecondaryActions: MenuItemType[];
extra?: React.ReactNode;
}
const SqlEditorTopBar = ({
defaultPrimaryActions,
defaultSecondaryActions,
extra,
}: SqlEditorTopBarProps) => (
<StyledFlex justify="space-between" gap="small" id="js-sql-toolbar">
<Flex gap="small" align="center">
@@ -47,6 +49,7 @@ const SqlEditorTopBar = ({
/>
</Flex>
</Flex>
{extra}
</StyledFlex>
);

View File

@@ -293,6 +293,45 @@ describe('sqlLabReducer', () => {
newQueryEditor.tabViewId,
);
});
test('should toggle hideLeftBar via queryEditorId for the active editor', () => {
const action = {
type: actions.QUERY_EDITOR_TOGGLE_LEFT_BAR,
queryEditorId: qe!.id,
hideLeftBar: true,
};
newState = sqlLabReducer(newState, action as SqlLabAction);
expect(newState.unsavedQueryEditor.hideLeftBar).toBe(true);
expect(newState.unsavedQueryEditor.id).toBe(qe!.id);
});
test('should toggle hideLeftBar back to false via queryEditorId', () => {
// first set to true
newState = sqlLabReducer(newState, {
type: actions.QUERY_EDITOR_TOGGLE_LEFT_BAR,
queryEditorId: qe!.id,
hideLeftBar: true,
} as SqlLabAction);
// then back to false
newState = sqlLabReducer(newState, {
type: actions.QUERY_EDITOR_TOGGLE_LEFT_BAR,
queryEditorId: qe!.id,
hideLeftBar: false,
} as SqlLabAction);
expect(newState.unsavedQueryEditor.hideLeftBar).toBe(false);
});
test('should toggle hideLeftBar in queryEditors array for non-active editor', () => {
const nonActiveId = defaultQueryEditor.id;
const action = {
type: actions.QUERY_EDITOR_TOGGLE_LEFT_BAR,
queryEditorId: nonActiveId,
hideLeftBar: true,
};
newState = sqlLabReducer(newState, action as SqlLabAction);
const updated = newState.queryEditors.find(e => e.id === nonActiveId);
expect(updated?.hideLeftBar).toBe(true);
});
test('should clear the destroyed query editors', () => {
const expectedQEId = '1233289';
const action = {

View File

@@ -715,7 +715,7 @@ export default function sqlLabReducer(
{
hideLeftBar: action.hideLeftBar,
},
action.queryEditor!.id!,
action.queryEditorId!,
),
};
},

View File

@@ -186,6 +186,7 @@ export function DatabaseSelector({
onSchemaChange,
schema,
readOnly = false,
compactMode = false,
sqlLabMode = false,
onOpenModal,
}: DatabaseSelectorProps) {
@@ -382,6 +383,7 @@ export function DatabaseSelector({
});
const catalogOptions = catalogData || EMPTY_CATALOG_OPTIONS;
const sqlLabCompactMode = sqlLabMode && compactMode;
function changeDatabase(
value: { label: string; value: number },
@@ -604,8 +606,8 @@ export function DatabaseSelector({
>
{renderDatabaseSelect()}
{renderError()}
{showCatalogSelector && renderCatalogSelect()}
{showSchemaSelector && renderSchemaSelect()}
{!sqlLabCompactMode && showCatalogSelector && renderCatalogSelect()}
{!sqlLabCompactMode && showSchemaSelector && renderSchemaSelect()}
</DatabaseSelectorWrapper>
);
}

View File

@@ -50,5 +50,6 @@ export interface DatabaseSelectorProps {
schema?: string;
readOnly?: boolean;
sqlLabMode?: boolean;
compactMode?: boolean;
onOpenModal?: () => void;
}

View File

@@ -34,7 +34,7 @@ export default function useStoredSidebarWidth(
widthsMapRef.current =
widthsMapRef.current ??
getItem(LocalStorageKeys.CommonResizableSidebarWidths, {});
if (widthsMapRef.current[id]) {
if (typeof widthsMapRef.current[id] === 'number') {
setSidebarWidth(widthsMapRef.current[id]);
}
}, [id]);

View File

@@ -137,6 +137,7 @@ export function saveChartCustomization(
dispatch({
type: SET_NATIVE_FILTERS_CONFIG_COMPLETE,
filterChanges: mergedResult,
deletedIds,
});
dispatch(

View File

@@ -50,6 +50,7 @@ export interface SetNativeFiltersConfigComplete {
filterChanges: Array<
Filter | Divider | ChartCustomization | ChartCustomizationDivider
>;
deletedIds?: string[];
}
export const SET_NATIVE_FILTERS_CONFIG_FAIL = 'SET_NATIVE_FILTERS_CONFIG_FAIL';
@@ -92,6 +93,7 @@ export const setFilterConfiguration =
dispatch({
type: SET_NATIVE_FILTERS_CONFIG_COMPLETE,
filterChanges: response.result,
deletedIds: filterChanges.deleted,
});
dispatch(nativeFiltersConfigChanged(response.result));
dispatch(setDataMaskForFilterChangesComplete(filterChanges, oldFilters));

View File

@@ -287,13 +287,14 @@ test('SET_NATIVE_FILTERS_CONFIG_COMPLETE removes deleted filters from state', ()
},
};
// Backend response only includes filter1 and filter3 (filter2 was deleted)
// filter2 was deleted: it's absent from the payload and listed in deletedIds
const action = {
type: SET_NATIVE_FILTERS_CONFIG_COMPLETE as typeof SET_NATIVE_FILTERS_CONFIG_COMPLETE,
filterChanges: [
createMockFilter('filter1', [1, 2], ['tab1']),
createMockFilter('filter3', [5, 6], ['tab3']),
],
deletedIds: ['filter2'],
};
const result = nativeFilterReducer(initialState, action);
@@ -305,7 +306,7 @@ test('SET_NATIVE_FILTERS_CONFIG_COMPLETE removes deleted filters from state', ()
expect(Object.keys(result.filters)).toHaveLength(2);
});
test('SET_NATIVE_FILTERS_CONFIG_COMPLETE removes all filters when backend returns empty array', () => {
test('SET_NATIVE_FILTERS_CONFIG_COMPLETE removes all filters when all ids are deleted', () => {
const initialState = {
filters: {
filter1: createMockFilter('filter1', [1, 2], ['tab1']),
@@ -316,6 +317,7 @@ test('SET_NATIVE_FILTERS_CONFIG_COMPLETE removes all filters when backend return
const action = {
type: SET_NATIVE_FILTERS_CONFIG_COMPLETE as typeof SET_NATIVE_FILTERS_CONFIG_COMPLETE,
filterChanges: [],
deletedIds: ['filter1', 'filter2'],
};
const result = nativeFilterReducer(initialState, action);
@@ -324,6 +326,36 @@ test('SET_NATIVE_FILTERS_CONFIG_COMPLETE removes all filters when backend return
expect(result.filters).toEqual({});
});
test('SET_NATIVE_FILTERS_CONFIG_COMPLETE preserves untouched filters when payload omits them', () => {
// Regression for the crash where saving a chart customization wiped native
// filters: a customization-only payload (no deletedIds) must not drop the
// native filters sharing the map.
const initialState = {
filters: {
nativeFilter1: createMockFilter('nativeFilter1', [1, 2], ['tab1']),
customization1: createMockChartCustomization(
'customization1',
[3, 4],
['tab2'],
),
},
};
const action = {
type: SET_NATIVE_FILTERS_CONFIG_COMPLETE as typeof SET_NATIVE_FILTERS_CONFIG_COMPLETE,
filterChanges: [
createMockChartCustomization('customization1', [5, 6], ['tab3']),
],
};
const result = nativeFilterReducer(initialState, action);
expect(result.filters.nativeFilter1).toBeDefined();
expect(result.filters.nativeFilter1.chartsInScope).toEqual([1, 2]);
expect(result.filters.customization1.chartsInScope).toEqual([5, 6]);
expect(Object.keys(result.filters)).toHaveLength(2);
});
test('SET_NATIVE_FILTERS_CONFIG_COMPLETE handles mixed Filter and ChartCustomization types', () => {
const initialState = {
filters: {
@@ -360,13 +392,14 @@ test('SET_NATIVE_FILTERS_CONFIG_COMPLETE adds new filters while removing deleted
},
};
// Backend response: keep filter1, delete filter2, add filter3
// keep filter1, delete filter2, add filter3
const action = {
type: SET_NATIVE_FILTERS_CONFIG_COMPLETE as typeof SET_NATIVE_FILTERS_CONFIG_COMPLETE,
filterChanges: [
createMockFilter('filter1', [1, 2], ['tab1']),
createMockFilter('filter3', [5, 6], ['tab3']),
],
deletedIds: ['filter2'],
};
const result = nativeFilterReducer(initialState, action);
@@ -378,7 +411,7 @@ test('SET_NATIVE_FILTERS_CONFIG_COMPLETE adds new filters while removing deleted
expect(Object.keys(result.filters)).toHaveLength(2);
});
test('SET_NATIVE_FILTERS_CONFIG_COMPLETE treats backend response as source of truth', () => {
test('SET_NATIVE_FILTERS_CONFIG_COMPLETE merges payload and applies explicit deletions', () => {
const initialState = {
filters: {
filter1: createMockFilter('filter1', [1, 2], ['tab1']),
@@ -392,25 +425,28 @@ test('SET_NATIVE_FILTERS_CONFIG_COMPLETE treats backend response as source of tr
},
};
// Backend only returns filter2 and customization1
// Payload updates filter2 and customization1; filter3 is explicitly deleted;
// filter1 is untouched and must survive.
const action = {
type: SET_NATIVE_FILTERS_CONFIG_COMPLETE as typeof SET_NATIVE_FILTERS_CONFIG_COMPLETE,
filterChanges: [
createMockFilter('filter2', [10, 11], ['tab5']),
createMockChartCustomization('customization1', [12, 13], ['tab6']),
],
deletedIds: ['filter3'],
};
const result = nativeFilterReducer(initialState, action);
// Only filter2 and customization1 should remain
expect(result.filters.filter1).toBeUndefined();
expect(result.filters.filter1).toBeDefined();
expect(result.filters.filter2).toBeDefined();
expect(result.filters.filter3).toBeUndefined();
expect(result.filters.customization1).toBeDefined();
expect(Object.keys(result.filters)).toHaveLength(2);
expect(Object.keys(result.filters)).toHaveLength(3);
// Values should be from backend response
// Untouched filter keeps its previous values
expect(result.filters.filter1.chartsInScope).toEqual([1, 2]);
// Updated values come from the payload
expect(result.filters.filter2.chartsInScope).toEqual([10, 11]);
expect(result.filters.filter2.tabsInScope).toEqual(['tab5']);
expect(result.filters.customization1.chartsInScope).toEqual([12, 13]);

View File

@@ -76,12 +76,17 @@ function handleFilterChangesComplete(
filters: Array<
Filter | Divider | ChartCustomization | ChartCustomizationDivider
>,
deletedIds: string[] = [],
) {
// Create new filters object from backend response (deleted filters won't be included)
// Native filters and chart customizations share this map, but each save path
// only reports its own domain. Merge the incoming changes into the existing
// map (instead of rebuilding from scratch) so, e.g., saving a chart
// customization doesn't drop native filters and vice versa. Deletions are
// applied explicitly via deletedIds.
const newFilters: Record<
string,
Filter | Divider | ChartCustomization | ChartCustomizationDivider
> = {};
> = { ...state.filters };
filters.forEach(filter => {
const existingFilter = state.filters[filter.id];
@@ -96,6 +101,10 @@ function handleFilterChangesComplete(
}
});
deletedIds.forEach(id => {
delete newFilters[id];
});
return {
...state,
filters: newFilters,
@@ -143,7 +152,11 @@ export default function nativeFilterReducer(
return getInitialState({ filterConfig: action.filterConfig, state });
case SET_NATIVE_FILTERS_CONFIG_COMPLETE:
return handleFilterChangesComplete(state, action.filterChanges);
return handleFilterChangesComplete(
state,
action.filterChanges,
action.deletedIds,
);
case SET_FOCUSED_NATIVE_FILTER:
return {

View File

@@ -168,3 +168,10 @@ test('getRelatedChartsForChartCustomization falls back to ID match when type is
getRelatedChartsForChartCustomization(customization, mixedSlices).sort(),
).toEqual([10, 11]);
});
test('getRelatedCharts returns empty array when the filter is undefined', () => {
// A native filter can transiently disappear from the redux map (e.g. right
// after saving a chart customization) while it is still hovered/focused.
// Guard against reading .scope on undefined so the dashboard doesn't crash.
expect(getRelatedCharts('missing', undefined, slices)).toEqual([]);
});

View File

@@ -84,10 +84,13 @@ function getRelatedChartsForCrossFilter(
export function getRelatedCharts(
filterKey: string,
filter: AppliedNativeFilterType | AppliedCrossFilterType | Filter,
filter: AppliedNativeFilterType | AppliedCrossFilterType | Filter | undefined,
slices: Record<string, Slice>,
) {
let related: number[] = [];
if (!filter) {
return related;
}
const isCrossFilter =
Object.keys(slices).includes(filterKey) && isAppliedCrossFilterType(filter);

View File

@@ -24,7 +24,7 @@
"@types/lodash-es": "^4.17.12",
"@types/node": "^26.1.0",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/eslint-plugin": "^8.63.0",
"@typescript-eslint/parser": "^8.63.0",
"eslint": "^10.5.0",
"eslint-config-prettier": "^10.1.8",
@@ -741,17 +741,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz",
"integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
"integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.63.0",
"@typescript-eslint/type-utils": "8.63.0",
"@typescript-eslint/utils": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0",
"@typescript-eslint/scope-manager": "8.64.0",
"@typescript-eslint/type-utils": "8.64.0",
"@typescript-eslint/utils": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -764,22 +764,22 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.63.0",
"@typescript-eslint/parser": "^8.64.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz",
"integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz",
"integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.63.0",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/typescript-estree": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0",
"@typescript-eslint/scope-manager": "8.64.0",
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/typescript-estree": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0",
"debug": "^4.4.3"
},
"engines": {
@@ -794,145 +794,15 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz",
"integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.63.0",
"@typescript-eslint/types": "^8.63.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz",
"integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz",
"integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz",
"integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz",
"integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.63.0",
"@typescript-eslint/tsconfig-utils": "8.63.0",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz",
"integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.63.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz",
"integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz",
"integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.63.0",
"@typescript-eslint/types": "^8.63.0",
"@typescript-eslint/tsconfig-utils": "^8.64.0",
"@typescript-eslint/types": "^8.64.0",
"debug": "^4.4.3"
},
"engines": {
@@ -947,14 +817,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz",
"integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz",
"integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0"
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -965,9 +835,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz",
"integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz",
"integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -982,15 +852,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz",
"integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz",
"integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/typescript-estree": "8.63.0",
"@typescript-eslint/utils": "8.63.0",
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/typescript-estree": "8.64.0",
"@typescript-eslint/utils": "8.64.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1007,9 +877,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz",
"integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz",
"integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1021,16 +891,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz",
"integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz",
"integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.63.0",
"@typescript-eslint/tsconfig-utils": "8.63.0",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0",
"@typescript-eslint/project-service": "8.64.0",
"@typescript-eslint/tsconfig-utils": "8.64.0",
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -1049,16 +919,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz",
"integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz",
"integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.63.0",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/typescript-estree": "8.63.0"
"@typescript-eslint/scope-manager": "8.64.0",
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/typescript-estree": "8.64.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1073,13 +943,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz",
"integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz",
"integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/types": "8.64.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -2991,41 +2861,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.63.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz",
"integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==",
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.64.0.tgz",
"integrity": "sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.63.0",
"@typescript-eslint/parser": "8.63.0",
"@typescript-eslint/typescript-estree": "8.63.0",
"@typescript-eslint/utils": "8.63.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": {
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz",
"integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.62.1",
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1",
"debug": "^4.4.3"
"@typescript-eslint/eslint-plugin": "8.64.0",
"@typescript-eslint/parser": "8.64.0",
"@typescript-eslint/typescript-estree": "8.64.0",
"@typescript-eslint/utils": "8.64.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View File

@@ -33,7 +33,7 @@
"@types/lodash-es": "^4.17.12",
"@types/node": "^26.1.0",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/eslint-plugin": "^8.63.0",
"@typescript-eslint/parser": "^8.63.0",
"eslint": "^10.5.0",
"eslint-config-prettier": "^10.1.8",

View File

@@ -18,21 +18,12 @@ from __future__ import annotations
import logging
import os
import sys
from typing import cast, Iterable, Optional
from wsgiref.types import StartResponse, WSGIApplication, WSGIEnvironment
from alembic.config import Config
from alembic.runtime.migration import MigrationContext
from alembic.script import ScriptDirectory
if sys.version_info >= (3, 11):
from wsgiref.types import StartResponse, WSGIApplication, WSGIEnvironment
else:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from _typeshed.wsgi import StartResponse, WSGIApplication, WSGIEnvironment
from flask import Flask, Response
from werkzeug.exceptions import NotFound

View File

@@ -84,7 +84,7 @@ If Docker is not available, you can set up manually:
git clone https://github.com/apache/superset.git
cd superset
# 2. Set up Python environment (Python 3.10 or 3.11 required)
# 2. Set up Python environment (Python 3.11 or 3.12 required)
python3 -m venv venv
source venv/bin/activate

View File

@@ -58,20 +58,12 @@ closed-set discipline.
from __future__ import annotations
import re
import sys
from typing import Iterable, Optional
from urllib.parse import quote
from wsgiref.types import StartResponse, WSGIApplication, WSGIEnvironment
from werkzeug.wrappers import Response
if sys.version_info >= (3, 11):
from wsgiref.types import StartResponse, WSGIApplication, WSGIEnvironment
else:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from _typeshed.wsgi import StartResponse, WSGIApplication, WSGIEnvironment
#: The legacy URL token the shim recognises. Hard-coded — not configurable.
_LEGACY_PREFIX: str = "/superset"

View File

@@ -14,13 +14,4 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import sys
from enum import Enum
if sys.version_info >= (3, 11):
# pylint: disable=unused-import
from enum import StrEnum # nopycln: import
else:
class StrEnum(str, Enum):
pass
from enum import StrEnum # noqa: F401