Compare commits

..

12 Commits

Author SHA1 Message Date
Evan
29d8f9da8a fix(sqllab): centralize add-tab timing marker and use antd Flex for northPane wrapper
Fold Logger.markTimeOrigin() into newQueryEditor so every add-tab entry
point (the + button, its dropdown, and antd's onEdit) records add-tab
performance telemetry consistently, and swap the northPane view wrapper
from a custom flex div to the antd Flex component.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:19:06 -07:00
Enzo Martellucci
8b86bbc95d test(sqllab): cover NewTabButton dropdown, direct-add, and skip paths
The new-tab "+" button (NewTabButton) had no test for its contributed
behavior: the dropdown listing "SQL Editor" plus extension items, the
empty-contributions branch that adds a tab directly, and the guard that
skips a contributed menu item whose command isn't registered.

Add regression tests that register a sqllab.newTab command/menu item and
assert each branch, mirroring how SqlEditor.test.tsx registers a view.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 10:13:02 -07:00
Evan
27f05d7639 fix(sqllab): intercept new-tab activation on antd's add button for keyboard parity
Move the new-tab interception from the inner span to a capture-phase click
listener on antd's add button, which is the element that actually receives
focus and keyboard activation. A native button synthesizes a click for both
mouse and Enter/Space, so one capture listener keeps keyboard and mouse
behavior in sync and drops the duplicate focusable span tab stop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:13:01 -07:00
Evan
e3b4ac215a fix(sqllab): skip new-tab menu items whose command isn't registered
Guards against executeCommand throwing "Command not found" when an
extension contributes a menu item before its command finishes loading.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:13:01 -07:00
Evan
a17472bbb1 test(sqllab,extensions): bridge store state cast through unknown for tsc
The concrete store-state cast tripped TS2352 because getState()'s static
type doesn't overlap the narrowed shape. Bridge through unknown, which
tsc recommends and keeps the cast off any.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:13:00 -07:00
Evan
1181df7fd6 test(sqllab,extensions): replace any casts with concrete types
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:12:59 -07:00
Evan
276cae3ba7 test(sqllab): cover Tab backendId mapping from editor tabViewId
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:12:59 -07:00
Evan
d817a3be77 fix(sqllab,extensions): populate Tab.backendId and guard northPane storage
Wire the backend-assigned tabViewId through to the public Tab.backendId
field so extensions can correlate tabs with tabstateview rows, and wrap
the northPane localStorage access in try/catch so a storage-restricted
browser can't crash the editor mount.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:12:58 -07:00
Evan
7af82a9041 test(sqllab,extensions): assert toast via store state; fix northPane key type
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:12:57 -07:00
Claude Code
0378c85ed9 test(sqllab): cover northPane view rendering in SqlEditor
Addresses the review note about missing coverage for the northPane
contribution surface: registers a view at sqllab.northPane, sets the
per-tab localStorage key, and asserts SqlEditor renders the resolved
view in place of the default editor pane.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 10:12:56 -07:00
Evan
0c7b5d34f2 fix(sqllab,extensions): address review feedback on northPane keys, tab a11y, extension load errors
- Resolve the northPane localStorage key consistently as `tabViewId ?? id`
  for read, write, and the storage listener so backend-persisted tabs restore
  and cross-tab sync correctly.
- Add keyboard activation to the SQL Lab new-tab button so extension-contributed
  tab types are reachable via Enter/Space.
- Make ExtensionsLoader rethrow on failure so ExtensionsStartup surfaces a
  warning toast instead of swallowing the error; reset the promise to allow retry.
- Clarify the public Tab.backendId docstring (opaque string, not an internal
  numeric id).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:12:55 -07:00
Amin Ghadersohi
72e2340715 feat(sqllab,extensions): contribution surfaces for tab/pane extensions
Lets extensions contribute first-class SQL Lab experiences — replacing
the default editor split with their own pane, and adding their own tab
types to the new-tab dropdown.

Changes:
- Add two view locations to SqlLab/contributions.ts:
  - sqllab.northPane — full-pane replacement for the default editor+SouthPane split
  - sqllab.newTab — tab types listed in the '+' new-tab dropdown
- Expose PENDING_NORTH_PANE_VIEW_KEY: extensions set this localStorage key
  before calling sqlLab.createTab() to declare which northPane view the new
  tab opens with. SqlEditor consumes/removes the key on init, then persists
  the choice per-tab so the mode survives reloads.
- Expose Tab.backendId on the public superset-core Tab interface so
  extensions can correlate UI tabs with their tabstateview row.
- TabbedSqlEditors: the '+' button becomes a Dropdown when extensions
  contribute newTab items, listing 'SQL Editor' (built-in) plus contributed
  tab types.
- ExtensionsStartup: surface extension load errors as warning toasts
  instead of only logging.

Rebased onto current apache/master (dropping the unmerged storage-tiers
stack this was originally branched on). Adapted to master's evolution:
SqlEditor now consumes master's reactive useViews() hook (#40915) instead
of a custom onViewsChange() subscription, so the northPane view appears
when an extension registers it asynchronously — without blocking initial
render. ExtensionsStartup keeps master's non-blocking async load and adds
the error-toast surfacing.

Co-Authored-By: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 10:12:18 -07:00
69 changed files with 1374 additions and 3878 deletions

View File

@@ -2,8 +2,7 @@ name: 🎪 Superset Showtime
# Ultra-simple: just sync on any PR state change
on:
# zizmor: ignore[dangerous-triggers] - required to react to PR label changes; PR code is
# only checked out and built after the maintainer-authorization gate (write/admin actors)
# zizmor: ignore[dangerous-triggers] - required to react to PR label changes; this workflow does not check out or execute PR-provided code
pull_request_target:
types: [labeled, unlabeled, synchronize, closed]
@@ -157,10 +156,6 @@ jobs:
with:
ref: ${{ steps.check.outputs.target_sha }}
persist-credentials: false
# Building fork PR code is Showtime's purpose: deploys are gated on the
# maintainer-authorization step above (write/admin actors only), so this
# checkout is an explicit, authorized opt-in rather than an automatic one.
allow-unsafe-pr-checkout: true
- name: Setup Docker Environment (only if build needed)
if: steps.auth.outputs.authorized == 'true' && steps.check.outputs.build_needed == 'true'

View File

@@ -120,7 +120,7 @@ RUN useradd --user-group -d ${SUPERSET_HOME} -m --no-log-init --shell /bin/bash
# Some bash scripts needed throughout the layers
COPY --chmod=755 docker/*.sh /app/docker/
RUN pip install --no-cache-dir --upgrade uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
# Using uv as it's faster/simpler than pip
RUN uv venv /app/.venv
@@ -141,7 +141,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
COPY superset/translations/ /app/translations_mo/
RUN if [ "${BUILD_TRANSLATIONS}" = "true" ]; then \
pybabel compile --use-fuzzy -d /app/translations_mo || true; \
pybabel compile -d /app/translations_mo | true; \
fi; \
rm -f /app/translations_mo/*/*/*.[po,json]

View File

@@ -79,19 +79,6 @@ When the MCP service has JWT auth enabled (`MCP_AUTH_ENABLED = True`), an audien
The git SHA and build number surfaced in the "About" section, the bootstrap payload, and the public `/version` endpoint are now only included for admin users by default; the release version string is still shown to everyone. To expose the build details to all users (the previous behavior), set the `SUPERSET_EXPOSE_BUILD_DETAILS` environment variable (or `EXPOSE_BUILD_DETAILS_TO_USERS = True` in `superset_config.py`).
### Helm chart adopts Kubernetes recommended labels (breaking upgrade)
The Helm chart now labels and selects workloads using the [Kubernetes recommended labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/) (`app.kubernetes.io/*`) instead of the legacy `app`/`release` labels. Because a Deployment's `spec.selector.matchLabels` is immutable, `helm upgrade` against an existing release will fail with a `field is immutable` error.
To upgrade, delete the affected workloads (which selector labels changed) before upgrading, then run the upgrade so they are recreated with the new labels:
```bash
kubectl delete deployment,statefulset -l release=<release-name> -n <namespace>
helm upgrade <release-name> superset/superset
```
Alternatively, perform a fresh install. This is a one-time migration; subsequent upgrades are unaffected.
### Pivot table First/Last aggregations follow data order
The pivot table chart's `First` and `Last` aggregations now return the first and last value in data (query result) order, instead of effectively returning the minimum and maximum. Existing pivot tables that use these aggregations for totals/subtotals may show different values after upgrading. For deterministic results, ensure the underlying query has a stable sort order.

View File

@@ -332,28 +332,15 @@ cd superset-frontend
npm run build-translation
# Backend
pybabel compile --use-fuzzy -d superset/translations
pybabel compile -d superset/translations
```
`--use-fuzzy` includes `#, fuzzy` entries in the compiled `.mo` files. Superset
serves fuzzy translations on purpose: the frontend build (`po2json --fuzzy`)
already includes them, `flask fab babel-compile` (used by the release images)
compiles with `-f`, and the production `Dockerfile` compiles with `--use-fuzzy`
as well. This keeps machine-generated (and other draft) translations visible in
the UI rather than falling back to English while they await review.
### Backfilling missing translations with AI
For languages with many untranslated strings, the repo includes a script that
uses Claude AI to generate draft translations for any missing entries. All
AI-generated strings are marked `#, fuzzy` and tagged with an attribution
comment so that human reviewers know they need to be checked.
Note that `#, fuzzy` marks a translation as *needing review*, not as *withheld*:
both the frontend and backend builds serve fuzzy entries (see [Applying
translations](#applying-translations) above), so an AI-generated string is shown
in the UI as soon as it is built and deployed. Reviewers should verify each
entry and remove the `#, fuzzy` flag to promote it to a confirmed translation.
comment so that human reviewers know they need to be checked before merging.
#### Prerequisites

View File

@@ -749,7 +749,7 @@ const config: Config = {
showReadingTime: true,
// Please change this to your repo.
editUrl:
'https://github.com/apache/superset/tree/master/docs',
'https://github.com/facebook/docusaurus/edit/main/website/blog/',
},
theme: {
customCss: require.resolve('./src/styles/custom.css'),

View File

@@ -58,14 +58,24 @@
"@fontsource/inter": "^5.2.8",
"@mdx-js/react": "^3.1.1",
"@saucelabs/theme-github-codeblock": "^0.3.0",
"@storybook/addon-docs": "^10.4.5",
"@storybook/addon-docs": "^8.6.18",
"@storybook/blocks": "^8.6.15",
"@storybook/channels": "^8.6.18",
"@storybook/client-logger": "^8.6.18",
"@storybook/components": "^8.6.18",
"@storybook/core": "^8.6.18",
"@storybook/core-events": "^8.6.18",
"@storybook/csf": "^0.1.13",
"@storybook/docs-tools": "^8.6.18",
"@storybook/preview-api": "^8.6.18",
"@storybook/theming": "^8.6.15",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.43",
"antd": "^6.4.5",
"baseline-browser-mapping": "^2.10.38",
"caniuse-lite": "^1.0.30001799",
"docusaurus-plugin-openapi-docs": "^5.1.0",
"docusaurus-theme-openapi-docs": "^5.1.0",
"docusaurus-theme-openapi-docs": "^5.0.2",
"js-yaml": "^5.1.0",
"js-yaml-loader": "^1.2.2",
"json-bigint": "^1.0.0",
@@ -78,7 +88,7 @@
"react-table": "^7.8.0",
"remark-import-partial": "^0.0.2",
"reselect": "^5.2.0",
"storybook": "^10.4.5",
"storybook": "^8.6.18",
"swagger-ui-react": "^5.32.8",
"swc-loader": "^0.2.7",
"tinycolor2": "^1.4.2",

View File

@@ -168,6 +168,60 @@ export default function webpackExtendPlugin(): Plugin<void> {
__dirname,
'../../superset-frontend/packages/superset-core/src',
),
// Add proper Storybook aliases
'@storybook/blocks': path.resolve(
__dirname,
'../node_modules/@storybook/blocks',
),
'@storybook/components': path.resolve(
__dirname,
'../node_modules/@storybook/components',
),
'@storybook/theming': path.resolve(
__dirname,
'../node_modules/@storybook/theming',
),
'@storybook/client-logger': path.resolve(
__dirname,
'../node_modules/@storybook/client-logger',
),
'@storybook/core-events': path.resolve(
__dirname,
'../node_modules/@storybook/core-events',
),
// Add internal Storybook aliases
'storybook/internal/components': path.resolve(
__dirname,
'../node_modules/@storybook/components',
),
'storybook/internal/theming': path.resolve(
__dirname,
'../node_modules/@storybook/theming',
),
'storybook/internal/client-logger': path.resolve(
__dirname,
'../node_modules/@storybook/client-logger',
),
'storybook/internal/csf': path.resolve(
__dirname,
'../node_modules/@storybook/csf',
),
'storybook/internal/preview-api': path.resolve(
__dirname,
'../node_modules/@storybook/preview-api',
),
'storybook/internal/docs-tools': path.resolve(
__dirname,
'../node_modules/@storybook/docs-tools',
),
'storybook/internal/core-events': path.resolve(
__dirname,
'../node_modules/@storybook/core-events',
),
'storybook/internal/channels': path.resolve(
__dirname,
'../node_modules/@storybook/channels',
),
},
},
};

File diff suppressed because it is too large Load Diff

View File

@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.19.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
version: 0.18.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
dependencies:
- name: postgresql
version: 16.7.27

View File

@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.19.0](https://img.shields.io/badge/Version-0.19.0-informational?style=flat-square)
![Version: 0.18.0](https://img.shields.io/badge/Version-0.18.0-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application
@@ -46,21 +46,6 @@ It should be a long random bytes or str.
On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverrides.secrets`
## Upgrade Notes
### Kubernetes recommended labels (breaking)
This chart labels and selects workloads using the [Kubernetes recommended labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/) (`app.kubernetes.io/*`) instead of the legacy `app`/`release` labels. A Deployment's `spec.selector.matchLabels` is immutable, so `helm upgrade` against a release created before this change fails with a `field is immutable` error.
To upgrade an existing release, delete the affected workloads first (their selector labels changed), then upgrade so they are recreated:
```console
kubectl delete deployment,statefulset -l release=<release-name> -n <namespace>
helm upgrade <release-name> superset/superset
```
Alternatively, perform a fresh install. This is a one-time migration; subsequent upgrades are unaffected.
## Requirements
| Repository | Name | Version |

View File

@@ -45,21 +45,6 @@ It should be a long random bytes or str.
On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverrides.secrets`
## Upgrade Notes
### Kubernetes recommended labels (breaking)
This chart labels and selects workloads using the [Kubernetes recommended labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/) (`app.kubernetes.io/*`) instead of the legacy `app`/`release` labels. A Deployment's `spec.selector.matchLabels` is immutable, so `helm upgrade` against a release created before this change fails with a `field is immutable` error.
To upgrade an existing release, delete the affected workloads first (their selector labels changed), then upgrade so they are recreated:
```console
kubectl delete deployment,statefulset -l release=<release-name> -n <namespace>
helm upgrade <release-name> superset/superset
```
Alternatively, perform a fresh install. This is a one-time migration; subsequent upgrades are unaffected.
{{ template "chart.requirementsSection" . }}
{{ template "chart.valuesSection" . }}

View File

@@ -61,49 +61,6 @@ Create chart name and version as used by the chart label.
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Common labels for all resources - follows Kubernetes recommended labels
https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
*/}}
{{- define "superset.labels" -}}
helm.sh/chart: {{ include "superset.chart" . }}
{{ include "superset.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: superset
{{- if .Values.extraLabels }}
{{ toYaml .Values.extraLabels }}
{{- end }}
{{- end -}}
{{/*
Selector labels - used by selectors and matchLabels
*/}}
{{- define "superset.selectorLabels" -}}
app.kubernetes.io/name: {{ include "superset.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{/*
Component labels - extends superset.labels with component-specific labels
Usage: {{ include "superset.componentLabels" (dict "component" "web" "root" .) }}
*/}}
{{- define "superset.componentLabels" -}}
{{ include "superset.labels" .root }}
app.kubernetes.io/component: {{ .component }}
{{- end -}}
{{/*
Component selector labels - for matchLabels with component
Usage: {{ include "superset.componentSelectorLabels" (dict "component" "web" "root" .) }}
*/}}
{{- define "superset.componentSelectorLabels" -}}
{{ include "superset.selectorLabels" .root }}
app.kubernetes.io/component: {{ .component }}
{{- end -}}
{{- define "superset-config" }}
import os
@@ -189,32 +146,27 @@ RESULTS_BACKEND = RedisCache(
{{- end }}
{{- define "supersetNode.selectorLabels" -}}
app.kubernetes.io/name: {{ include "superset.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: web
{{- end }}
{{- define "supersetCeleryBeat.selectorLabels" -}}
app.kubernetes.io/name: {{ include "superset.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: celerybeat
app: {{ include "superset.name" . }}-celerybeat
release: {{ .Release.Name }}
{{- end }}
{{- define "supersetCeleryFlower.selectorLabels" -}}
app.kubernetes.io/name: {{ include "superset.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: flower
app: {{ include "superset.name" . }}-flower
release: {{ .Release.Name }}
{{- end }}
{{- define "supersetNode.selectorLabels" -}}
app: {{ include "superset.name" . }}
release: {{ .Release.Name }}
{{- end }}
{{- define "supersetWebsockets.selectorLabels" -}}
app.kubernetes.io/name: {{ include "superset.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: websocket
app: {{ include "superset.name" . }}-ws
release: {{ .Release.Name }}
{{- end }}
{{- define "supersetWorker.selectorLabels" -}}
app.kubernetes.io/name: {{ include "superset.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: worker
app: {{ include "superset.name" . }}-worker
release: {{ .Release.Name }}
{{- end }}

View File

@@ -24,7 +24,13 @@ metadata:
name: {{ template "superset.fullname" . }}-extra-config
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.labels" . | nindent 4 }}
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
data:
{{- range $path, $config := .Values.extraConfigs }}
{{ $path }}: |

View File

@@ -24,7 +24,13 @@ metadata:
name: {{ template "superset.fullname" . }}-celerybeat
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.componentLabels" (dict "component" "celerybeat" "root" .) | nindent 4 }}
app: {{ template "superset.name" . }}-celerybeat
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- if .Values.supersetCeleryBeat.deploymentAnnotations }}
annotations: {{- toYaml .Values.supersetCeleryBeat.deploymentAnnotations | nindent 4 }}
{{- end }}
@@ -53,7 +59,11 @@ spec:
{{- toYaml .Values.supersetCeleryBeat.podAnnotations | nindent 8 }}
{{- end }}
labels:
{{- include "supersetCeleryBeat.selectorLabels" . | nindent 8 }}
app: "{{ template "superset.name" . }}-celerybeat"
release: {{ .Release.Name }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 8 }}
{{- end }}
{{- if .Values.supersetCeleryBeat.podLabels }}
{{- toYaml .Values.supersetCeleryBeat.podLabels | nindent 8 }}
{{- end }}

View File

@@ -24,7 +24,13 @@ metadata:
name: {{ template "superset.fullname" . }}-flower
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.componentLabels" (dict "component" "flower" "root" .) | nindent 4 }}
app: {{ template "superset.name" . }}-flower
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- if .Values.supersetCeleryFlower.deploymentAnnotations }}
annotations: {{- toYaml .Values.supersetCeleryFlower.deploymentAnnotations | nindent 4 }}
{{- end }}
@@ -42,7 +48,11 @@ spec:
{{- toYaml .Values.supersetCeleryFlower.podAnnotations | nindent 8 }}
{{- end }}
labels:
{{- include "supersetCeleryFlower.selectorLabels" . | nindent 8 }}
app: "{{ template "superset.name" . }}-flower"
release: {{ .Release.Name }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 8 }}
{{- end }}
{{- if .Values.supersetCeleryFlower.podLabels }}
{{- toYaml .Values.supersetCeleryFlower.podLabels | nindent 8 }}
{{- end }}

View File

@@ -23,9 +23,15 @@ metadata:
name: {{ template "superset.fullname" . }}-worker
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.componentLabels" (dict "component" "worker" "root" .) | nindent 4 }}
{{- with .Values.supersetWorker.deploymentLabels }}
{{- toYaml . | nindent 4 }}
app: {{ template "superset.name" . }}-worker
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- if .Values.supersetWorker.deploymentLabels }}
{{- toYaml .Values.supersetWorker.deploymentLabels | nindent 4 }}
{{- end }}
{{- if .Values.supersetWorker.deploymentAnnotations }}
annotations: {{- toYaml .Values.supersetWorker.deploymentAnnotations | nindent 4 }}
@@ -59,7 +65,11 @@ spec:
{{- toYaml .Values.supersetWorker.podAnnotations | nindent 8 }}
{{- end }}
labels:
{{- include "supersetWorker.selectorLabels" . | nindent 8 }}
app: {{ template "superset.name" . }}-worker
release: {{ .Release.Name }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 8 }}
{{- end }}
{{- if .Values.supersetWorker.podLabels }}
{{- toYaml .Values.supersetWorker.podLabels | nindent 8 }}
{{- end }}

View File

@@ -24,7 +24,13 @@ metadata:
name: "{{ template "superset.fullname" . }}-ws"
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.componentLabels" (dict "component" "websocket" "root" .) | nindent 4 }}
app: "{{ template "superset.name" . }}-ws"
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- if .Values.supersetWebsockets.deploymentAnnotations }}
annotations: {{- toYaml .Values.supersetWebsockets.deploymentAnnotations | nindent 4 }}
{{- end }}
@@ -45,7 +51,11 @@ spec:
{{- toYaml .Values.supersetWebsockets.podAnnotations | nindent 8 }}
{{- end }}
labels:
{{- include "supersetWebsockets.selectorLabels" . | nindent 8 }}
app: "{{ template "superset.name" . }}-ws"
release: {{ .Release.Name }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 8 }}
{{- end }}
{{- if .Values.supersetWebsockets.podLabels }}
{{- toYaml .Values.supersetWebsockets.podLabels | nindent 8 }}
{{- end }}

View File

@@ -23,9 +23,15 @@ metadata:
name: {{ template "superset.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.componentLabels" (dict "component" "web" "root" .) | nindent 4 }}
{{- with .Values.supersetNode.deploymentLabels }}
{{- toYaml . | nindent 4 }}
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- if .Values.supersetNode.deploymentLabels }}
{{- toYaml .Values.supersetNode.deploymentLabels | nindent 4 }}
{{- end }}
{{- if .Values.supersetNode.deploymentAnnotations }}
annotations: {{- toYaml .Values.supersetNode.deploymentAnnotations | nindent 4 }}
@@ -61,7 +67,11 @@ spec:
{{- toYaml .Values.supersetNode.podAnnotations | nindent 8 }}
{{- end }}
labels:
{{- include "supersetNode.selectorLabels" . | nindent 8 }}
app: {{ template "superset.name" . }}
release: {{ .Release.Name }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 8 }}
{{- end }}
{{- if .Values.supersetNode.podLabels }}
{{- toYaml .Values.supersetNode.podLabels | nindent 8 }}
{{- end }}

View File

@@ -23,7 +23,13 @@ kind: HorizontalPodAutoscaler
metadata:
name: {{ include "superset.fullname" . }}-hpa
labels:
{{- include "superset.labels" . | nindent 4 }}
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
spec:
scaleTargetRef:
apiVersion: apps/v1

View File

@@ -23,7 +23,13 @@ kind: HorizontalPodAutoscaler
metadata:
name: {{ include "superset.fullname" . }}-hpa-worker
labels:
{{- include "superset.labels" . | nindent 4 }}
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
spec:
scaleTargetRef:
apiVersion: apps/v1

View File

@@ -25,7 +25,13 @@ metadata:
name: {{ $fullName }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.componentLabels" (dict "component" "ingress" "root" .) | nindent 4 }}
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- with .Values.ingress.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}

View File

@@ -24,7 +24,13 @@ metadata:
name: {{ template "superset.fullname" . }}-init-db
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.componentLabels" (dict "component" "init" "root" .) | nindent 4 }}
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- if .Values.init.jobAnnotations }}
annotations: {{- toYaml .Values.init.jobAnnotations | nindent 4 }}
{{- end }}
@@ -36,7 +42,9 @@ spec:
annotations: {{- toYaml .Values.init.podAnnotations | nindent 8 }}
{{- end }}
labels:
{{- include "superset.componentSelectorLabels" (dict "component" "init" "root" .) | nindent 8 }}
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
job: {{ template "superset.fullname" . }}-init-db
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 8 }}

View File

@@ -27,7 +27,13 @@ kind: PodDisruptionBudget
metadata:
name: {{ include "superset.fullname" $ }}-celerybeat-pdb
labels:
{{- include "superset.componentLabels" (dict "component" "celerybeat" "root" $) | nindent 4 }}
app: {{ template "superset.name" $ }}-celerybeat
chart: {{ template "superset.chart" $ }}
release: {{ $.Release.Name }}
heritage: {{ $.Release.Service }}
{{- if $.Values.extraLabels }}
{{- toYaml $.Values.extraLabels | nindent 4 }}
{{- end }}
spec:
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}

View File

@@ -27,7 +27,13 @@ kind: PodDisruptionBudget
metadata:
name: {{ include "superset.fullname" $ }}-flower-pdb
labels:
{{- include "superset.componentLabels" (dict "component" "flower" "root" $) | nindent 4 }}
app: {{ template "superset.name" $ }}-flower
chart: {{ template "superset.chart" $ }}
release: {{ $.Release.Name }}
heritage: {{ $.Release.Service }}
{{- if $.Values.extraLabels }}
{{- toYaml $.Values.extraLabels | nindent 4 }}
{{- end }}
spec:
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}

View File

@@ -27,7 +27,13 @@ kind: PodDisruptionBudget
metadata:
name: {{ include "superset.fullname" $ }}-worker-pdb
labels:
{{- include "superset.componentLabels" (dict "component" "worker" "root" $) | nindent 4 }}
app: {{ template "superset.name" $ }}-worker
chart: {{ template "superset.chart" $ }}
release: {{ $.Release.Name }}
heritage: {{ $.Release.Service }}
{{- if $.Values.extraLabels }}
{{- toYaml $.Values.extraLabels | nindent 4 }}
{{- end }}
spec:
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}

View File

@@ -27,7 +27,13 @@ kind: PodDisruptionBudget
metadata:
name: {{ include "superset.fullname" $ }}-ws-pdb
labels:
{{- include "superset.componentLabels" (dict "component" "websocket" "root" $) | nindent 4 }}
app: {{ template "superset.name" $ }}-ws
chart: {{ template "superset.chart" $ }}
release: {{ $.Release.Name }}
heritage: {{ $.Release.Service }}
{{- if $.Values.extraLabels }}
{{- toYaml $.Values.extraLabels | nindent 4 }}
{{- end }}
spec:
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}

View File

@@ -27,7 +27,13 @@ kind: PodDisruptionBudget
metadata:
name: {{ include "superset.fullname" $ }}-pdb
labels:
{{- include "superset.componentLabels" (dict "component" "web" "root" $) | nindent 4 }}
app: {{ template "superset.name" $ }}
chart: {{ template "superset.chart" $ }}
release: {{ $.Release.Name }}
heritage: {{ $.Release.Service }}
{{- if $.Values.extraLabels }}
{{- toYaml $.Values.extraLabels | nindent 4 }}
{{- end }}
spec:
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}

View File

@@ -23,7 +23,13 @@ metadata:
name: {{ template "superset.fullname" . }}-env
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.labels" . | nindent 4 }}
app: {{ template "superset.fullname" . }}
chart: {{ template "superset.chart" . }}
release: "{{ .Release.Name }}"
heritage: "{{ .Release.Service }}"
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
type: Opaque
stringData:
REDIS_HOST: {{ tpl .Values.supersetNode.connections.redis_host . | quote }}

View File

@@ -23,7 +23,13 @@ metadata:
name: {{ template "superset.fullname" . }}-config
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.labels" . | nindent 4 }}
app: {{ template "superset.fullname" . }}
chart: {{ template "superset.chart" . }}
release: "{{ .Release.Name }}"
heritage: "{{ .Release.Service }}"
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
type: Opaque
stringData:
superset_config.py: |

View File

@@ -24,7 +24,13 @@ metadata:
name: "{{ template "superset.fullname" . }}-ws-config"
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.labels" . | nindent 4 }}
app: {{ template "superset.fullname" . }}
chart: {{ template "superset.chart" . }}
release: "{{ .Release.Name }}"
heritage: "{{ .Release.Service }}"
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
type: Opaque
stringData:
config.json: |

View File

@@ -24,7 +24,13 @@ metadata:
name: "{{ template "superset.fullname" . }}-flower"
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.componentLabels" (dict "component" "flower" "root" .) | nindent 4 }}
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- with .Values.supersetCeleryFlower.service.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
@@ -39,7 +45,8 @@ spec:
nodePort: {{ .Values.supersetCeleryFlower.service.nodePort.http }}
{{- end }}
selector:
{{- include "supersetCeleryFlower.selectorLabels" . | nindent 4 }}
app: {{ template "superset.name" . }}-flower
release: {{ .Release.Name }}
{{- if .Values.supersetCeleryFlower.service.loadBalancerIP }}
loadBalancerIP: {{ .Values.supersetCeleryFlower.service.loadBalancerIP }}
{{- end }}

View File

@@ -24,7 +24,13 @@ metadata:
name: "{{ template "superset.fullname" . }}-ws"
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.componentLabels" (dict "component" "websocket" "root" .) | nindent 4 }}
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- with .Values.supersetWebsockets.service.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
@@ -39,7 +45,8 @@ spec:
nodePort: {{ .Values.supersetWebsockets.service.nodePort.http }}
{{- end }}
selector:
{{- include "supersetWebsockets.selectorLabels" . | nindent 4 }}
app: "{{ template "superset.name" . }}-ws"
release: {{ .Release.Name }}
{{- if .Values.supersetWebsockets.service.loadBalancerIP }}
loadBalancerIP: {{ .Values.supersetWebsockets.service.loadBalancerIP }}
{{- end }}

View File

@@ -23,7 +23,13 @@ metadata:
name: {{ template "superset.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.componentLabels" (dict "component" "web" "root" .) | nindent 4 }}
app: {{ template "superset.name" . }}
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- with .Values.service.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
@@ -38,7 +44,8 @@ spec:
nodePort: {{ .Values.service.nodePort.http }}
{{- end }}
selector:
{{- include "supersetNode.selectorLabels" . | nindent 4 }}
app: {{ template "superset.name" . }}
release: {{ .Release.Name }}
{{- if .Values.service.loadBalancerIP }}
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
{{- end }}

View File

@@ -24,11 +24,17 @@ metadata:
name: {{ include "superset.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "superset.labels" . | nindent 4 }}
app.kubernetes.io/name: {{ include "superset.name" . }}
helm.sh/chart: {{ include "superset.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- if semverCompare "> 1.6" .Capabilities.KubeVersion.GitVersion }}
kubernetes.io/cluster-service: "true"
{{- end }}
addonmanager.kubernetes.io/mode: Reconcile
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- if .Values.serviceAccount.annotations }}
annotations: {{- toYaml .Values.serviceAccount.annotations | nindent 4 }}
{{- end }}

View File

@@ -1,520 +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.
#
suite: Label Consistency Tests
templates:
- deployment.yaml
- deployment-worker.yaml
- deployment-beat.yaml
- deployment-flower.yaml
- deployment-ws.yaml
- service.yaml
- service-ws.yaml
- service-flower.yaml
- init-job.yaml
- ingress.yaml
- configmap-superset.yaml
- secret-superset-config.yaml
- secret-ws.yaml
- pdb.yaml
- pdb-worker.yaml
- pdb-beat.yaml
- pdb-flower.yaml
- pdb-ws.yaml
# These tests validate that Kubernetes recommended labels are consistently applied
# across all chart resources per https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
#
# Required Labels (app.kubernetes.io/):
# - name: The name of the application
# - instance: A unique name identifying the instance of an application
# - version: The current version of the application
# - component: The component within the architecture
# - part-of: The name of a higher level application this one is part of
# - managed-by: The tool being used to manage the operation of an application
#
# Helm-specific Labels:
# - helm.sh/chart: The chart name and version
tests:
# =============================================================================
# Main Deployment Labels
# =============================================================================
- it: should have all recommended labels on main deployment
template: deployment.yaml
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/version"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/managed-by"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/part-of"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- isNotNull:
path: metadata.labels["helm.sh/chart"]
- it: should have correct component label on main deployment
template: deployment.yaml
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: web
- it: should have part-of label set to superset on main deployment
template: deployment.yaml
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/part-of"]
value: superset
# =============================================================================
# Worker Deployment Labels
# =============================================================================
- it: should have all recommended labels on worker deployment
template: deployment-worker.yaml
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/version"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/managed-by"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/part-of"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on worker deployment
template: deployment-worker.yaml
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: worker
# =============================================================================
# Celery Beat Deployment Labels
# =============================================================================
- it: should have all recommended labels on celerybeat deployment
template: deployment-beat.yaml
set:
supersetCeleryBeat.enabled: true
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on celerybeat deployment
template: deployment-beat.yaml
set:
supersetCeleryBeat.enabled: true
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: celerybeat
# =============================================================================
# Flower Deployment Labels
# =============================================================================
- it: should have all recommended labels on flower deployment
template: deployment-flower.yaml
set:
supersetCeleryFlower.enabled: true
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on flower deployment
template: deployment-flower.yaml
set:
supersetCeleryFlower.enabled: true
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: flower
# =============================================================================
# WebSocket Deployment Labels
# =============================================================================
- it: should have all recommended labels on websocket deployment
template: deployment-ws.yaml
set:
supersetWebsockets.enabled: true
supersetWebsockets.config.jwtSecret: "test-secret-for-unit-test"
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on websocket deployment
template: deployment-ws.yaml
set:
supersetWebsockets.enabled: true
supersetWebsockets.config.jwtSecret: "test-secret-for-unit-test"
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: websocket
# =============================================================================
# Service Labels
# =============================================================================
- it: should have all recommended labels on main service
template: service.yaml
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on main service
template: service.yaml
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: web
- it: should have all recommended labels on websocket service
template: service-ws.yaml
set:
supersetWebsockets.enabled: true
supersetWebsockets.config.jwtSecret: "test-secret-for-unit-test"
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on websocket service
template: service-ws.yaml
set:
supersetWebsockets.enabled: true
supersetWebsockets.config.jwtSecret: "test-secret-for-unit-test"
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: websocket
- it: should have all recommended labels on flower service
template: service-flower.yaml
set:
supersetCeleryFlower.enabled: true
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on flower service
template: service-flower.yaml
set:
supersetCeleryFlower.enabled: true
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: flower
# =============================================================================
# Init Job Labels
# =============================================================================
- it: should have all recommended labels on init job
template: init-job.yaml
set:
init.enabled: true
init.createAdmin: true
init.adminUser.password: "test-password"
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on init job
template: init-job.yaml
set:
init.enabled: true
init.createAdmin: true
init.adminUser.password: "test-password"
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: init
# =============================================================================
# Ingress Labels
# =============================================================================
- it: should have all recommended labels on ingress
template: ingress.yaml
set:
ingress.enabled: true
ingress.hosts:
- host: superset.example.com
paths:
- path: /
pathType: Prefix
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/instance"]
- isNotNull:
path: metadata.labels["app.kubernetes.io/component"]
- it: should have correct component label on ingress
template: ingress.yaml
set:
ingress.enabled: true
ingress.hosts:
- host: superset.example.com
paths:
- path: /
pathType: Prefix
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: ingress
# =============================================================================
# Selector Label Consistency
#
# These use value assertions (not isNotNull) on purpose: a missing/misscoped
# release name renders as the string "<no value>", which is non-null and would
# silently pass isNotNull. Asserting the concrete value catches that class of
# bug, and asserting the pod template labels equal the selector guards the
# immutable spec.selector.matchLabels <-> pod label invariant.
# =============================================================================
- it: should set selector matchLabels to concrete values on main deployment
template: deployment.yaml
asserts:
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/name"]
value: superset
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/component"]
value: web
- it: should match pod template labels to the selector on main deployment
template: deployment.yaml
asserts:
- equal:
path: spec.template.metadata.labels["app.kubernetes.io/name"]
value: superset
- equal:
path: spec.template.metadata.labels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: spec.template.metadata.labels["app.kubernetes.io/component"]
value: web
- it: should set selector matchLabels to concrete values on worker deployment
template: deployment-worker.yaml
asserts:
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/component"]
value: worker
- it: should match pod template labels to the selector on worker deployment
template: deployment-worker.yaml
asserts:
- equal:
path: spec.template.metadata.labels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: spec.template.metadata.labels["app.kubernetes.io/component"]
value: worker
# =============================================================================
# Extra Labels Support
# =============================================================================
- it: should include extraLabels when specified
template: deployment.yaml
set:
extraLabels:
custom-label: custom-value
environment: production
asserts:
- equal:
path: metadata.labels.custom-label
value: custom-value
- equal:
path: metadata.labels.environment
value: production
- it: should include extraLabels in service
template: service.yaml
set:
extraLabels:
custom-label: custom-value
asserts:
- equal:
path: metadata.labels.custom-label
value: custom-value
# =============================================================================
# ConfigMap / Secret Labels
# =============================================================================
- it: should have recommended labels on extra-config configmap
template: configmap-superset.yaml
set:
extraConfigs:
custom.py: "FOO = 1"
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- equal:
path: metadata.labels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- isNotNull:
path: metadata.labels["app.kubernetes.io/managed-by"]
- equal:
path: metadata.labels["app.kubernetes.io/part-of"]
value: superset
- it: should have recommended labels on superset config secret
template: secret-superset-config.yaml
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- equal:
path: metadata.labels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: metadata.labels["app.kubernetes.io/part-of"]
value: superset
- it: should have recommended labels on websocket config secret
template: secret-ws.yaml
set:
supersetWebsockets.enabled: true
asserts:
- isNotNull:
path: metadata.labels["app.kubernetes.io/name"]
- equal:
path: metadata.labels["app.kubernetes.io/instance"]
value: RELEASE-NAME
# =============================================================================
# PodDisruptionBudget Labels (metadata must match the selector)
# =============================================================================
- it: should have recommended labels and matching selector on main pdb
template: pdb.yaml
set:
supersetNode.podDisruptionBudget.enabled: true
supersetNode.podDisruptionBudget.maxUnavailable: null
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: web
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/component"]
value: web
- it: should set correct component on worker pdb
template: pdb-worker.yaml
set:
supersetWorker.podDisruptionBudget.enabled: true
supersetWorker.podDisruptionBudget.maxUnavailable: null
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: worker
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/component"]
value: worker
- it: should set correct component on celerybeat pdb
template: pdb-beat.yaml
set:
supersetCeleryBeat.podDisruptionBudget.enabled: true
supersetCeleryBeat.podDisruptionBudget.maxUnavailable: null
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: celerybeat
- it: should set correct component on flower pdb
template: pdb-flower.yaml
set:
supersetCeleryFlower.podDisruptionBudget.enabled: true
supersetCeleryFlower.podDisruptionBudget.maxUnavailable: null
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: flower
- it: should set correct component on websocket pdb
template: pdb-ws.yaml
set:
supersetWebsockets.enabled: true
supersetWebsockets.podDisruptionBudget.enabled: true
supersetWebsockets.podDisruptionBudget.maxUnavailable: null
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/component"]
value: websocket
- equal:
path: spec.selector.matchLabels["app.kubernetes.io/component"]
value: websocket
- it: should use recommended labels on init job pod template
template: init-job.yaml
set:
init.enabled: true
asserts:
- equal:
path: spec.template.metadata.labels["app.kubernetes.io/instance"]
value: RELEASE-NAME
- equal:
path: spec.template.metadata.labels["app.kubernetes.io/component"]
value: init
- isNotNull:
path: spec.template.metadata.labels.job

View File

@@ -182,7 +182,7 @@
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.61.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.4.6",
"@storybook/addon-docs": "10.4.5",
"@storybook/addon-links": "10.4.4",
"@storybook/react-webpack5": "10.4.4",
"@storybook/test-runner": "0.24.4",
@@ -9718,16 +9718,16 @@
"license": "MIT"
},
"node_modules/@storybook/addon-docs": {
"version": "10.4.6",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.6.tgz",
"integrity": "sha512-aWAfP5JMiT5a3zBJizwroCRzOCqZwDTJmvsYvwMD3ilIEa/kT1vhf6Xrbk4XIPhDwbh8Hpb/Gfnka1xBYEISWg==",
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.5.tgz",
"integrity": "sha512-9mIV0maIxixfuvdpNhr3QMeU/gbJKeaBcWhPYuf176cqDZAG9EUhZ50TIinxeFRbyEGRJqaLPoiYwIu4GJu3jA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mdx-js/react": "^3.0.0",
"@storybook/csf-plugin": "10.4.6",
"@storybook/csf-plugin": "10.4.5",
"@storybook/icons": "^2.0.2",
"@storybook/react-dom-shim": "10.4.6",
"@storybook/react-dom-shim": "10.4.5",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"ts-dedent": "^2.0.0"
@@ -9738,7 +9738,7 @@
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.4.6"
"storybook": "^10.4.5"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -9747,9 +9747,9 @@
}
},
"node_modules/@storybook/addon-docs/node_modules/@storybook/react-dom-shim": {
"version": "10.4.6",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.6.tgz",
"integrity": "sha512-iGNmKzrq9vgl2PDrYAnZKI+yvac3Ym+lJXXuQaqlFRS23zA5MNm4EBX+rAG7WulqchoK6NaZ0KQOs2mAgEpTMg==",
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.5.tgz",
"integrity": "sha512-fKdikHC7cDgSuaBirPwvgFBmfO//3cln0y3GmDEQchUV2VFDrZ7ZL1/iH7dA21XuiFFhQcDRRkArJmvMAGG5Cg==",
"dev": true,
"license": "MIT",
"funding": {
@@ -9761,7 +9761,7 @@
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.4.6"
"storybook": "^10.4.5"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -9800,9 +9800,9 @@
}
},
"node_modules/@storybook/csf-plugin": {
"version": "10.4.6",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.6.tgz",
"integrity": "sha512-NILLxDqpA/JR/AazGWpsz+4fadJwRU4uhHephGtYpVOWnQA/DkJfKT6zpcJVq8+QA8A2zKMLX3GVKsXIrxjuDA==",
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.5.tgz",
"integrity": "sha512-OsSsSLulBmdKTz7MIKLgoWADZB8bjYaAjZZy/THdI50G/TTd6FVSXQMCM7GO7xQZ/EguRY1PmjOVCLbgcnXsDA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9815,7 +9815,7 @@
"peerDependencies": {
"esbuild": "*",
"rollup": "*",
"storybook": "^10.4.6",
"storybook": "^10.4.5",
"vite": "*",
"webpack": "*"
},

View File

@@ -265,7 +265,7 @@
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.61.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.4.6",
"@storybook/addon-docs": "10.4.5",
"@storybook/addon-links": "10.4.4",
"@storybook/react-webpack5": "10.4.4",
"@storybook/test-runner": "0.24.4",

View File

@@ -62,6 +62,14 @@ export interface Tab {
*/
id: string;
/**
* The stable backend-assigned identifier for this tab. Exposed as an opaque
* string so the public extension API does not leak the backend's internal
* numeric tab id. Set once the tab has been persisted to the backend;
* undefined for new tabs before the first backend sync.
*/
backendId?: string;
/**
* The display title of the tab.
* This is what users see in the tab header.

View File

@@ -713,5 +713,4 @@ export interface DataColumnMeta {
isChildColumn?: boolean;
description?: string;
currencyCodeColumn?: string;
isFilterable?: boolean;
}

View File

@@ -67,7 +67,7 @@
"rison": "^0.1.1",
"seedrandom": "^3.0.5",
"xss": "^1.0.15",
"lodash-es": "^4.18.1"
"lodash-es": "^4.17.21"
},
"devDependencies": {
"@emotion/styled": "^11.14.1",

View File

@@ -29,7 +29,6 @@ import { waitForPost } from '../../helpers/api/intercepts';
import { expectStatusOneOf } from '../../helpers/api/assertions';
import { getDatabaseByName } from '../../helpers/api/database';
import { apiExecuteSql } from '../../helpers/api/sqllab';
import { TIMEOUT } from '../../utils/constants';
interface ExamplesSetupResult {
tableName: string;
@@ -117,7 +116,7 @@ async function dropTempTable(
// Uses test.describe only because Playwright's serial mode API requires it -
// (Deviation from "avoid describe" guideline is necessary for functional reasons)
test.describe('create dataset wizard', () => {
test.describe.configure({ mode: 'serial', timeout: TIMEOUT.SLOW_TEST });
test.describe.configure({ mode: 'serial' });
test('should create a dataset via wizard', async ({ page, testAssets }) => {
const { tableName, dbId, createDatasetPage } = await setupExamplesDataset(

View File

@@ -33,9 +33,8 @@ import { getLayerConfig } from '../util/controlPanelUtil';
export default class CartodiagramPlugin extends ChartPlugin {
constructor(opts: CartodiagramPluginConstructorOpts) {
const metadata = new ChartMetadata({
description: t(
description:
'Display charts on a map. For using this plugin, users first have to create any other chart that can then be placed on the map.',
),
name: t('Cartodiagram'),
thumbnail,
thumbnailDark,

View File

@@ -28,9 +28,8 @@ export default class PopKPIPlugin extends ChartPlugin {
constructor() {
const metadata = new ChartMetadata({
category: t('KPI'),
description: t(
description:
'Showcases a metric along with a comparison of value, change, and percent change for a selected time period.',
),
name: t('Big Number with Time Period Comparison'),
tags: [
t('Comparison'),

View File

@@ -1204,11 +1204,8 @@ export default function TableChart<D extends DataRecord = DataRecord>(
onClick:
emitCrossFilters && !valueRange && !isMetric
? () => {
const isFilterable = columnsMeta.find(
(cm: DataColumnMeta) => cm.key === key,
)?.isFilterable;
// allow selecting text in a cell
if (!getSelectedText() && isFilterable !== false) {
if (!getSelectedText()) {
toggleFilter(key, value);
}
}

View File

@@ -232,9 +232,6 @@ const processColumns = memoizeOne(function processColumns(
const metricsSet = new Set(metrics);
const percentMetricsSet = new Set(percentMetrics);
const rawPercentMetricsSet = new Set(rawPercentMetrics);
const columnsByName = new Map(
(props.datasource.columns ?? []).map(col => [col.column_name, col]),
);
const columns: DataColumnMeta[] = (colnames || [])
.filter(
@@ -247,7 +244,6 @@ const processColumns = memoizeOne(function processColumns(
const config = columnConfig[key] || {};
// for the purpose of presentation, only numeric values are treated as metrics
// because users can also add things like `MAX(str_col)` as a metric.
const isFilterable = columnsByName.get(key)?.filterable;
const isMetric = metricsSet.has(key) && isNumeric(key, records);
const isPercentMetric = percentMetricsSet.has(key);
const label =
@@ -330,7 +326,6 @@ const processColumns = memoizeOne(function processColumns(
isPercentMetric,
formatter,
config,
isFilterable,
description,
currencyCodeColumn,
};

View File

@@ -2534,33 +2534,3 @@ test('sorts genuinely string columns alphanumerically', () => {
const values = Array.from(cells).map(td => td.textContent);
expect(values).toEqual(['apple', 'banana', 'cherry']);
});
test('TableChart should NOT emit cross-filter when clicking a cell in a not-filterable column', () => {
const setDataMask = jest.fn();
const props = transformProps({
...testData.basic,
datasource: {
...testData.basic.datasource,
columns: [{ column_name: 'name', filterable: false } as any],
},
hooks: { setDataMask },
emitCrossFilters: true,
});
render(
<ProviderWrapper>
<TableChart
{...props}
emitCrossFilters
setDataMask={setDataMask}
sticky={false}
/>
</ProviderWrapper>,
);
fireEvent.click(screen.getByText('Michael'));
const crossFilterCall = setDataMask.mock.calls.find(
(call: any[]) => call[0]?.filterState?.filters,
);
expect(crossFilterCall).toBeUndefined();
});

View File

@@ -41,6 +41,8 @@ import {
import ResultSet from 'src/SqlLab/components/ResultSet';
import { api } from 'src/hooks/apiResources/queryApi';
import setupCodeOverrides from 'src/setup/setupCodeOverrides';
import { views } from 'src/core';
import { ViewLocations } from 'src/SqlLab/contributions';
import type { Action, Middleware, Store } from 'redux';
import SqlEditor, { Props } from '.';
@@ -348,6 +350,29 @@ describe('SqlEditor', () => {
).toBeInTheDocument();
});
test('renders a registered northPane view in place of the editor', async () => {
const { queryEditor } = mockedProps;
// The fixture has no tabViewId, so the component falls back to the id;
// mirror that here to derive the same persistence key.
const storageKey = `sqllab.northPaneView.${queryEditor.id}`;
localStorage.setItem(storageKey, 'test.northPane');
const disposable = views.registerView(
{ id: 'test.northPane', name: 'Test North Pane' },
ViewLocations.sqllab.northPane,
() => <div data-test="np-view">NorthPane content</div>,
);
try {
const { findByTestId, queryByTestId } = setup(mockedProps, store);
expect(await findByTestId('np-view')).toBeInTheDocument();
// The default SQL editor pane is replaced, not rendered alongside.
expect(queryByTestId('react-ace')).not.toBeInTheDocument();
} finally {
disposable.dispose();
localStorage.removeItem(storageKey);
}
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('with EstimateQueryCost enabled', () => {
beforeEach(() => {

View File

@@ -55,6 +55,7 @@ import {
Button,
Divider,
EmptyState,
Flex,
Input,
Modal,
} from '@superset-ui/core/components';
@@ -121,6 +122,37 @@ import KeyboardShortcutButton, {
KeyboardShortcut,
} from '../KeyboardShortcutButton';
import SqlEditorTopBar from '../SqlEditorTopBar';
import {
ViewLocations,
PENDING_NORTH_PANE_VIEW_KEY,
} from 'src/SqlLab/contributions';
import { resolveView, useViews } from 'src/core/views';
/** Per-tab localStorage key storing the active northPane view ID. */
const NORTH_PANE_VIEW_KEY = (tabId: string) => `sqllab.northPaneView.${tabId}`;
// The northPane keys are dynamic per-tab strings rather than members of the
// typed LocalStorageKeys enum, so the typed helpers don't apply. Guard the raw
// access here so a storage-restricted browser can't crash the editor mount.
const readNorthPaneStorage = (key: string): string | null => {
try {
return localStorage.getItem(key);
} catch {
return null;
}
};
const writeNorthPaneStorage = (key: string, value: string | null): void => {
try {
if (value === null) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, value);
}
} catch {
// localStorage may be unavailable (blocked/quota/private mode); ignore.
}
};
const bootstrapData = getBootstrapData();
const scheduledQueriesConf = bootstrapData?.common?.conf?.SCHEDULED_QUERIES;
@@ -271,6 +303,48 @@ const SqlEditor: FC<Props> = ({
const logAction = useLogAction({ queryEditorId: queryEditor.id });
const isActive = currentQueryEditorId === queryEditor.id;
// Re-renders when an extension registers a northPane view after async load.
const northPaneViews = useViews(ViewLocations.sqllab.northPane) || [];
// Resolve the per-tab localStorage key the same way every other SQL Lab
// consumer does (`tabViewId ?? id`), so the value written, read back, and
// observed via the `storage` event all agree once a tab is backend-persisted.
const northPaneStorageId = queryEditor.tabViewId ?? queryEditor.id;
// ID of the northPane view active for this tab, or null for the default
// SQL editor layout. Set by an extension via PENDING_NORTH_PANE_VIEW_KEY
// before calling createTab(); persisted per-tab in localStorage.
const [northPaneViewId, setNorthPaneViewId] = useState<string | null>(() => {
const pendingViewId = readNorthPaneStorage(PENDING_NORTH_PANE_VIEW_KEY);
if (pendingViewId) {
writeNorthPaneStorage(PENDING_NORTH_PANE_VIEW_KEY, null);
writeNorthPaneStorage(
NORTH_PANE_VIEW_KEY(northPaneStorageId),
pendingViewId,
);
return pendingViewId;
}
return readNorthPaneStorage(NORTH_PANE_VIEW_KEY(northPaneStorageId));
});
useEffect(() => {
writeNorthPaneStorage(
NORTH_PANE_VIEW_KEY(northPaneStorageId),
northPaneViewId,
);
}, [northPaneStorageId, northPaneViewId]);
useEffect(() => {
const handler = (e: StorageEvent) => {
if (e.key === NORTH_PANE_VIEW_KEY(northPaneStorageId)) {
setNorthPaneViewId(e.newValue || null);
}
};
window.addEventListener('storage', handler);
return () => window.removeEventListener('storage', handler);
}, [northPaneStorageId]);
const [autorun, setAutorun] = useState(queryEditor.autorun);
const [ctas, setCtas] = useState('');
const [northPercent, setNorthPercent] = useState(
@@ -1046,6 +1120,29 @@ const SqlEditor: FC<Props> = ({
'Choose one of the available databases from the panel on the left.',
)}
/>
) : northPaneViewId &&
northPaneViews.some(v => v.id === northPaneViewId) ? (
<Flex
vertical
css={css`
height: 100%;
`}
>
<SqlEditorTopBar
queryEditorId={queryEditor.id}
defaultPrimaryActions={null}
defaultSecondaryActions={[]}
/>
<div
css={css`
flex: 1;
overflow: auto;
padding: 0 ${theme.sizeUnit * 4}px;
`}
>
{resolveView(northPaneViewId)}
</div>
</Flex>
) : (
queryPane()
)}

View File

@@ -29,6 +29,8 @@ import { newQueryTabName } from 'src/SqlLab/utils/newQueryTabName';
import { Store } from 'redux';
import { RootState } from 'src/views/store';
import { QueryEditor } from 'src/SqlLab/types';
import { commands, menus } from 'src/core';
import { ViewLocations } from 'src/SqlLab/contributions';
jest.mock('src/SqlLab/components/SqlEditor', () =>
// eslint-disable-next-line react/display-name
@@ -172,3 +174,94 @@ test('should have an empty state when query editors is empty', async () => {
expect(getByText('Add a new tab to create SQL Query')).toBeInTheDocument(),
);
});
// The new-tab "+" button (NewTabButton) opens a dropdown of contributed
// actions when an extension registers something under sqllab.newTab, and
// otherwise falls back to adding a SQL editor tab directly. These tests cover
// that branching plus the resilience to a contributed-but-unregistered command.
const newTabDisposables: ReturnType<typeof menus.registerMenuItem>[] = [];
afterEach(() => {
while (newTabDisposables.length) {
newTabDisposables.pop()?.dispose();
}
});
const contributeNewTabItem = (command: string) =>
newTabDisposables.push(
menus.registerMenuItem(
{ view: 'builtin.editor', command },
ViewLocations.sqllab.newTab,
'primary',
),
);
test('new tab button opens a dropdown listing SQL Editor and the contributed item', async () => {
contributeNewTabItem('ext.newTab');
newTabDisposables.push(
commands.registerCommand(
{ id: 'ext.newTab', title: 'Contributed Tab' },
jest.fn(),
),
);
setup(undefined, initialState);
fireEvent.click(screen.getAllByLabelText('Add tab')[0]);
expect(await screen.findByText('SQL Editor')).toBeInTheDocument();
expect(screen.getByText('Contributed Tab')).toBeInTheDocument();
});
test('new tab button runs the contributed command when its menu item is clicked', async () => {
const handler = jest.fn();
contributeNewTabItem('ext.newTab');
newTabDisposables.push(
commands.registerCommand(
{ id: 'ext.newTab', title: 'Contributed Tab' },
handler,
),
);
setup(undefined, initialState);
fireEvent.click(screen.getAllByLabelText('Add tab')[0]);
fireEvent.click(await screen.findByText('Contributed Tab'));
await waitFor(() => expect(handler).toHaveBeenCalledTimes(1));
});
test('new tab button adds a tab directly when there are no contributions', async () => {
const { getAllByLabelText, getAllByRole, queryByText } = setup(
undefined,
initialState,
);
const tabCount = getAllByRole('tab').filter(
tab => !tab.classList.contains('ant-tabs-tab-remove'),
).length;
fireEvent.click(getAllByLabelText('Add tab')[0]);
// No dropdown appears; a new editor tab is created immediately.
expect(queryByText('SQL Editor')).not.toBeInTheDocument();
await waitFor(() =>
expect(
getAllByRole('tab').filter(
tab => !tab.classList.contains('ant-tabs-tab-remove'),
).length,
).toEqual(tabCount + 1),
);
});
test('new tab button skips a contributed item whose command is not registered', async () => {
// Menu item registered, but its command never is — the item must be dropped
// rather than throwing "Command not found" when the dropdown renders.
contributeNewTabItem('ext.missing');
setup(undefined, initialState);
fireEvent.click(screen.getAllByLabelText('Add tab')[0]);
expect(await screen.findByText('SQL Editor')).toBeInTheDocument();
expect(screen.queryByText('ext.missing')).not.toBeInTheDocument();
});

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useEffect, useCallback, useMemo, useRef } from 'react';
import { useEffect, useCallback, useMemo, useRef, useState } from 'react';
import { EditableTabs } from '@superset-ui/core/components/Tabs';
import { connect } from 'react-redux';
import type { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
@@ -24,12 +24,15 @@ import { t } from '@apache-superset/core/translation';
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
import { styled, css } from '@apache-superset/core/theme';
import { Logger } from 'src/logger/LogUtils';
import { EmptyState, Tooltip } from '@superset-ui/core/components';
import { Dropdown, EmptyState, Tooltip } from '@superset-ui/core/components';
import { MenuItemType } from '@superset-ui/core/components/Menu';
import { ErrorBoundary } from 'src/components/ErrorBoundary';
import { detectOS } from 'src/utils/common';
import * as Actions from 'src/SqlLab/actions/sqlLab';
import { Icons } from '@superset-ui/core/components/Icons';
import { SQLLAB_TAB_OVERFLOW_POPUP_CLASS } from 'src/SqlLab/SqlLabGlobalStyles';
import { menus, commands } from 'src/core';
import { ViewLocations } from 'src/SqlLab/contributions';
import SqlEditor from '../SqlEditor';
import SqlEditorTabHeader from '../SqlEditorTabHeader';
@@ -94,6 +97,114 @@ const TabTitle = styled.span`
// Get the user's OS
const userOS = detectOS();
const newTabTooltip =
userOS === 'Windows' ? t('New tab (Ctrl + q)') : t('New tab (Ctrl + t)');
const PlusIcon = (
<Icons.PlusOutlined
iconSize="l"
css={css`
vertical-align: middle;
`}
data-test="add-tab-icon"
/>
);
function NewTabButton({ onAddSqlEditor }: { onAddSqlEditor: () => void }) {
const [open, setOpen] = useState(false);
const dropdownItems = useMemo<MenuItemType[]>(() => {
if (!open) return [];
const primaryItems =
menus.getMenu(ViewLocations.sqllab.newTab)?.primary ?? [];
return [
{
key: 'sql-editor',
label: t('SQL Editor'),
icon: <Icons.TableOutlined iconSize="m" />,
onClick: () => {
setOpen(false);
onAddSqlEditor();
},
},
...primaryItems.flatMap(item => {
const command = commands.getCommand(item.command);
if (!command) {
// An extension contributed this menu item but its command isn't
// registered (load is still pending or failed). Skip it so clicking
// can't throw "Command not found" and break the add-tab flow.
return [];
}
const Icon = command.icon
? ((Icons as Record<string, typeof Icons.FileOutlined>)[
command.icon
] ?? Icons.FileOutlined)
: Icons.FileOutlined;
return [
{
key: command.id,
label: command.title ?? item.command,
icon: <Icon iconSize="m" />,
onClick: () => {
setOpen(false);
commands.executeCommand(item.command);
},
} as MenuItemType,
];
}),
];
}, [open, onAddSqlEditor]);
const activate = useCallback(() => {
const primaryItems =
menus.getMenu(ViewLocations.sqllab.newTab)?.primary ?? [];
if (primaryItems.length === 0) {
onAddSqlEditor();
} else {
setOpen(prev => !prev);
}
}, [onAddSqlEditor]);
const anchorRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
// Antd's Tabs wraps addIcon in its own <button onClick={() => onEdit('add')}>,
// and that button is the element that actually receives focus and activation.
// Intercept on the button itself in the capture phase so the extension
// dropdown is reached before antd's default add-tab path runs. A native button
// synthesizes a click for both mouse and keyboard (Enter/Space) activation, so
// a single capture-phase click listener keeps keyboard and mouse behavior in
// sync — a handler on the inner span only fires when the span is the event
// target and is bypassed when the button is activated via the keyboard.
const button = anchorRef.current?.closest('button');
if (!button) {
return undefined;
}
const handleActivate = (e: Event) => {
e.preventDefault();
e.stopPropagation();
activate();
};
button.addEventListener('click', handleActivate, true);
return () => {
button.removeEventListener('click', handleActivate, true);
};
}, [activate]);
return (
<Tooltip id="add-tab" placement="left" title={newTabTooltip}>
<Dropdown
open={open}
onOpenChange={setOpen}
menu={{ items: dropdownItems }}
trigger={[]}
>
<span ref={anchorRef}>{PlusIcon}</span>
</Dropdown>
</Tooltip>
);
}
type TabbedSqlEditorsProps = ReturnType<typeof mergeProps>;
function TabbedSqlEditors({
@@ -140,6 +251,10 @@ function TabbedSqlEditors({
}, [queries, activeQueryEditor, actions, displayLimit]);
const newQueryEditor = useCallback(() => {
// Mark the timing origin for add-tab performance telemetry. Centralized here
// so every add-tab entry point (the "+" button, its dropdown, and antd's
// onEdit) records it consistently.
Logger.markTimeOrigin();
actions.addNewQueryEditor();
}, [actions]);
@@ -173,7 +288,6 @@ function TabbedSqlEditors({
}
}
if (action === 'add') {
Logger.markTimeOrigin();
newQueryEditor();
}
},
@@ -265,25 +379,7 @@ function TabbedSqlEditors({
onEdit={handleEdit}
popupClassName={SQLLAB_TAB_OVERFLOW_POPUP_CLASS}
type={queryEditors?.length === 0 ? 'card' : 'editable-card'}
addIcon={
<Tooltip
id="add-tab"
placement="left"
title={
userOS === 'Windows'
? t('New tab (Ctrl + q)')
: t('New tab (Ctrl + t)')
}
>
<Icons.PlusOutlined
iconSize="l"
css={css`
vertical-align: middle;
`}
data-test="add-tab-icon"
/>
</Tooltip>
}
addIcon={<NewTabButton onAddSqlEditor={() => newQueryEditor()} />}
items={tabItems}
/>
);

View File

@@ -46,5 +46,27 @@ export const ViewLocations = {
statusBar: 'sqllab.statusBar',
results: 'sqllab.results',
queryHistory: 'sqllab.queryHistory',
// Extensions can register a full-pane replacement here. SqlEditor renders
// the registered view instead of the default editor+SouthPane split when
// a tab was opened in that mode.
northPane: 'sqllab.northPane',
// Extensions register tab-type commands here. When any are present the
// "+" new-tab button becomes a dropdown listing all registered tab types
// plus the built-in SQL Editor option.
newTab: 'sqllab.newTab',
},
} as const;
/**
* localStorage key an extension sets before calling createTab() to declare
* which northPane view the new tab should open with. The value must be the
* view ID passed to views.registerView() (e.g. "my-ext.northPane"). SqlEditor
* consumes and removes this key during initialization, then persists the chosen
* view ID under a per-tab key so the mode survives page reloads.
*
* @example
* // In an extension's newTab command handler:
* localStorage.setItem(PENDING_NORTH_PANE_VIEW_KEY, 'my-ext.northPane');
* sqlLab.createTab({ title: 'My View' });
*/
export const PENDING_NORTH_PANE_VIEW_KEY = 'sqllab.pendingNorthPaneView';

View File

@@ -161,19 +161,29 @@ const makeTab = (
catalog: string | null = null,
schema: string | null = null,
closed: boolean = false,
backendId?: string,
): Tab => {
const panels: Panel[] = []; // TODO: Populate panels
const editorGetter = closed
? () => Promise.reject(new Error(`Tab ${id} has been closed`))
: () => getEditorAsync(id);
return new Tab(id, name, dbId, catalog, schema, editorGetter, panels);
return new Tab(
id,
name,
dbId,
catalog,
schema,
editorGetter,
panels,
backendId,
);
};
const getTab = (id: string): Tab | undefined => {
const queryEditor = findQueryEditor(id);
if (queryEditor?.dbId !== undefined) {
const { name, dbId, catalog, schema } = queryEditor;
return makeTab(id, name, dbId, catalog, schema);
const { name, dbId, catalog, schema, tabViewId } = queryEditor;
return makeTab(id, name, dbId, catalog, schema, false, tabViewId);
}
return undefined;
};
@@ -441,6 +451,7 @@ const onDidCloseTab: typeof sqlLabApi.onDidCloseTab = (
action.queryEditor.catalog,
action.queryEditor.schema,
true, // closed
action.queryEditor.tabViewId,
),
thisArgs,
);
@@ -507,6 +518,8 @@ const onDidCreateTab: typeof sqlLabApi.onDidCreateTab = (
action.queryEditor.dbId ?? 0,
action.queryEditor.catalog,
action.queryEditor.schema ?? undefined,
false,
action.queryEditor.tabViewId,
),
thisArgs,
);
@@ -574,6 +587,8 @@ const createTab: typeof sqlLabApi.createTab = async (
newTab.dbId ?? 0,
newTab.catalog,
newTab.schema ?? undefined,
false,
newTab.tabViewId,
);
};

View File

@@ -34,6 +34,8 @@ export class Panel implements sqlLabType.Panel {
export class Tab implements sqlLabType.Tab {
id: string;
backendId?: string;
title: string;
databaseId: number;
@@ -54,6 +56,7 @@ export class Tab implements sqlLabType.Tab {
schema: string | null = null,
editorGetter: () => Promise<sqlLabType.Editor>,
panels: Panel[] = [],
backendId?: string,
) {
this.id = id;
this.title = title;
@@ -62,6 +65,7 @@ export class Tab implements sqlLabType.Tab {
this.schema = schema;
this.editorGetter = editorGetter;
this.panels = panels;
this.backendId = backendId;
}
getEditor(): Promise<sqlLabType.Editor> {

View File

@@ -561,10 +561,29 @@ test('createTab dispatches ADD_QUERY_EDITOR and returns the new tab', async () =
expect(tab).toBeDefined();
expect(tab.title).toBe('Custom Tab');
// A freshly created tab has no backend identifier until it syncs.
expect(tab.backendId).toBeUndefined();
const tabs = sqlLab.getTabs();
expect(tabs.length).toBeGreaterThanOrEqual(2);
});
test('getTabs leaves backendId undefined when the editor has no tabViewId', () => {
// The preloaded editor has no tabViewId, so its backendId stays undefined.
const [tab] = sqlLab.getTabs();
expect(tab.id).toBe(EDITOR_ID);
expect(tab.backendId).toBeUndefined();
});
test('getTabs surfaces the editor tabViewId as the tab backendId', () => {
// Stamp a backend id onto the editor and confirm it flows through to the tab.
(mockStore.getState().sqlLab.queryEditors[0] as QueryEditor).tabViewId =
'backend-42';
const tab = sqlLab.getCurrentTab();
expect(tab).toBeDefined();
expect(tab!.backendId).toBe('backend-42');
});
test('setActiveTab switches the active tab', async () => {
// Create a second tab first
await sqlLab.createTab({ title: 'Second Tab' });

View File

@@ -22,7 +22,6 @@ import {
createStore,
render,
screen,
userEvent,
waitFor,
} from 'spec/helpers/testing-library';
import reducerIndex from 'spec/helpers/reducerIndex';
@@ -31,7 +30,7 @@ import {
useDashboardCharts,
useDashboardDatasets,
} from 'src/hooks/apiResources';
import { SupersetApiError, SupersetClient } from '@superset-ui/core';
import { SupersetClient } from '@superset-ui/core';
import CrudThemeProvider from 'src/components/CrudThemeProvider';
import { hydrateDashboard } from 'src/dashboard/actions/hydrate';
import {
@@ -560,48 +559,6 @@ test('does not overwrite filterState when modern native_filters URL format is us
).toBeUndefined();
});
test('renders a not-found state instead of throwing when the dashboard 404s', async () => {
mockUseDashboard.mockReturnValue({
result: null,
error: new SupersetApiError({ status: 404, message: 'Not found' }),
});
mockUseDashboardCharts.mockReturnValue({
result: null,
error: new SupersetApiError({ status: 404, message: 'Not found' }),
});
mockUseDashboardDatasets.mockReturnValue({
result: null,
error: new SupersetApiError({ status: 404, message: 'Not found' }),
status: 'error',
});
render(
<Suspense fallback="loading">
<DashboardPage idOrSlug="404" />
</Suspense>,
{
useRedux: true,
useRouter: true,
initialState: {
dashboardInfo: {},
dashboardState: { sliceIds: [] },
nativeFilters: { filters: {} },
dataMask: {},
},
},
);
expect(
await screen.findByText('This dashboard does not exist'),
).toBeInTheDocument();
expect(screen.queryByTestId('dashboard-builder')).not.toBeInTheDocument();
await userEvent.click(
screen.getByRole('button', { name: 'See all dashboards' }),
);
expect(window.location.pathname).toBe('/dashboard/list/');
});
test('clears undo history after hydrating the dashboard', async () => {
render(
<Suspense fallback="loading">

View File

@@ -24,7 +24,7 @@ import { useTheme } from '@apache-superset/core/theme';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from '@reduxjs/toolkit';
import { useToasts } from 'src/components/MessageToasts/withToasts';
import { EmptyState, Loading } from '@superset-ui/core/components';
import { Loading } from '@superset-ui/core/components';
import {
useDashboard,
useDashboardCharts,
@@ -67,8 +67,7 @@ import SyncDashboardState, {
getDashboardContextLocalStorage,
} from '../components/SyncDashboardState';
import { AutoRefreshProvider } from '../contexts/AutoRefreshContext';
import { Filter, PartialFilters, SupersetApiError } from '@superset-ui/core';
import { RoutePaths } from 'src/views/routePaths';
import { Filter, PartialFilters } from '@superset-ui/core';
import {
parseRisonFilters,
risonFiltersToExtraFormDataFilters,
@@ -152,9 +151,6 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
const isDashboardHydrated = useRef(false);
const error = dashboardApiError || chartsApiError;
// Only 404 gets a graceful not-found state; a 403 (access denied) still
// surfaces through the error boundary.
const isNotFoundError = (error as SupersetApiError | null)?.status === 404;
const readyToRender = Boolean(dashboard && charts);
const { dashboard_title, id = 0 } = dashboard || {};
@@ -369,21 +365,18 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
useEffect(() => {
if (datasetsApiError) {
// A missing dashboard also 404s its datasets; the not-found state covers it.
if (!isNotFoundError) {
addDangerToast(
t('Error loading chart datasources. Filters may not work correctly.'),
);
}
addDangerToast(
t('Error loading chart datasources. Filters may not work correctly.'),
);
} else {
dispatch(setDatasources(datasets));
}
}, [addDangerToast, datasets, datasetsApiError, dispatch, isNotFoundError]);
}, [addDangerToast, datasets, datasetsApiError, dispatch]);
const relevantDataMask = useSelector(selectRelevantDatamask);
const activeFilters = useSelector(selectActiveFilters);
if (error && !isNotFoundError) throw error; // caught in error boundary
if (error) throw error; // caught in error boundary
const globalStyles = useMemo(
() => [
@@ -396,25 +389,9 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
[theme],
);
if (error && !isNotFoundError) throw error; // caught in error boundary
if (error) throw error; // caught in error boundary
const DashboardBuilderComponent = useMemo(() => <DashboardBuilder />, []);
if (isNotFoundError) {
return (
<EmptyState
size="large"
image="empty-dashboard.svg"
title={t('This dashboard does not exist')}
description={t(
'The dashboard you are looking for may have been deleted or moved.',
)}
buttonText={t('See all dashboards')}
buttonAction={() => history.push(RoutePaths.DASHBOARD_LIST)}
/>
);
}
return (
<>
<Global styles={globalStyles} />

View File

@@ -34,40 +34,3 @@ test('Render a FilterInput', async () => {
expect(onChangeHandler).toHaveBeenCalledTimes(4);
});
test('FilterInput auto-focuses when a non-editable element (e.g. a tab) has focus', () => {
const onChangeHandler = jest.fn();
const button = document.createElement('button');
document.body.appendChild(button);
try {
button.focus();
expect(document.activeElement).toBe(button);
render(<FilterInput onChangeHandler={onChangeHandler} shouldFocus />);
const filterInput = screen.getByPlaceholderText('Search');
// Auto-focus should fire — a button is not an editable element
expect(document.activeElement).toBe(filterInput);
} finally {
document.body.removeChild(button);
}
});
test('FilterInput does not steal focus when another input already has focus', () => {
const onChangeHandler = jest.fn();
const otherInput = document.createElement('input');
document.body.appendChild(otherInput);
try {
otherInput.focus();
expect(document.activeElement).toBe(otherInput);
render(<FilterInput onChangeHandler={onChangeHandler} shouldFocus />);
const filterInput = screen.getByPlaceholderText('Search');
// FilterInput should not have stolen focus from the already-focused input
expect(document.activeElement).not.toBe(filterInput);
expect(document.activeElement).toBe(otherInput);
} finally {
document.body.removeChild(otherInput);
}
});

View File

@@ -98,20 +98,9 @@ export const FilterInput = ({
const inputRef: RefObject<any> = useRef(null);
useEffect(() => {
// Focus the input element when the component mounts
if (inputRef.current && shouldFocus) {
// Skip auto-focus only when an editable element already has focus (e.g.
// user is typing in a form control when this pane remounts after a data
// refresh). Non-editable focused elements like tabs/buttons still allow
// auto-focus so the search box focuses on first open.
const activeEl = document.activeElement;
const editableFocused =
activeEl instanceof HTMLElement &&
(activeEl.tagName === 'INPUT' ||
activeEl.tagName === 'TEXTAREA' ||
activeEl.isContentEditable);
if (!editableFocused) {
inputRef.current.focus();
}
inputRef.current.focus();
}
}, []);

View File

@@ -234,13 +234,13 @@ test('passes an absolute remoteEntry URL through unchanged', async () => {
script.restore();
});
test('logs error when initializeExtensions fails', async () => {
test('logs error and rejects when initializeExtensions fails', async () => {
const loader = ExtensionsLoader.getInstance();
const errorSpy = jest.spyOn(logging, 'error').mockImplementation();
const fetchError = new Error('Network error');
jest.spyOn(SupersetClient, 'get').mockRejectedValue(fetchError);
await loader.initializeExtensions();
await expect(loader.initializeExtensions()).rejects.toThrow('Network error');
expect(errorSpy).toHaveBeenCalledWith(
'Error setting up extensions:',

View File

@@ -74,7 +74,12 @@ class ExtensionsLoader {
);
logging.info('Extensions initialized successfully.');
} catch (error) {
// Reset so a later call can retry, and rethrow so callers (e.g.
// ExtensionsStartup) can surface the failure instead of it being
// swallowed here and the success path running regardless.
this.initializationPromise = null;
logging.error('Error setting up extensions:', error);
throw error;
}
})();
return this.initializationPromise;

View File

@@ -16,7 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
import { render, waitFor } from 'spec/helpers/testing-library';
import { render, waitFor, createStore } from 'spec/helpers/testing-library';
import reducerIndex from 'spec/helpers/reducerIndex';
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
import fetchMock from 'fetch-mock';
import ExtensionsStartup from './ExtensionsStartup';
@@ -260,26 +261,23 @@ test('does not initialize ExtensionsLoader when EnableExtensions feature flag is
initializeSpy.mockRestore();
});
test('continues rendering children even when ExtensionsLoader initialization fails', async () => {
test('renders children and surfaces a warning toast when init fails', async () => {
// Ensure feature flag is enabled
mockIsFeatureEnabled.mockReturnValue(true);
// Mock the initializeExtensions method to reject — ExtensionsLoader handles
// its own error logging internally
// Mock the initializeExtensions method to reject so the caller's .catch runs.
const originalInitialize = ExtensionsLoader.prototype.initializeExtensions;
ExtensionsLoader.prototype.initializeExtensions = jest
.fn()
.mockImplementation(() => Promise.resolve());
.mockRejectedValue(new Error('boom'));
const store = createStore(mockInitialState, reducerIndex);
const { container } = render(
<ExtensionsStartup>
<div data-testid="child" />
</ExtensionsStartup>,
{
useRedux: true,
useRouter: true,
initialState: mockInitialState,
},
{ store, useRouter: true },
);
await waitFor(() => {
@@ -291,6 +289,17 @@ test('continues rendering children even when ExtensionsLoader initialization fai
).toBeInTheDocument();
});
// The failure must reach the user as a warning toast rather than being
// swallowed silently.
await waitFor(() => {
const { messageToasts } = store.getState() as unknown as {
messageToasts: { text: string }[];
};
expect(
messageToasts.some(toast => /Extensions failed to load/.test(toast.text)),
).toBe(true);
});
// Restore original method
ExtensionsLoader.prototype.initializeExtensions = originalInitialize;
});

View File

@@ -20,6 +20,7 @@ import { useEffect } from 'react';
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
// eslint-disable-next-line no-restricted-syntax
import * as supersetCore from '@apache-superset/core';
import { t } from '@apache-superset/core/translation';
import {
authentication,
chat,
@@ -33,8 +34,9 @@ import {
sqlLab,
views,
} from 'src/core';
import { useSelector } from 'react-redux';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from 'src/views/store';
import { addWarningToast } from 'src/components/MessageToasts/actions';
import ExtensionsLoader from './ExtensionsLoader';
import 'src/extensions/Namespaces';
@@ -43,6 +45,7 @@ const ExtensionsStartup: React.FC<{ children?: React.ReactNode }> = ({
}) => {
useNavigationTracker();
const dispatch = useDispatch();
const userId = useSelector<RootState, number | undefined>(
({ user }) => user.userId,
);
@@ -67,9 +70,28 @@ const ExtensionsStartup: React.FC<{ children?: React.ReactNode }> = ({
views,
};
// Load extensions without blocking the initial render (see #40915);
// surface any load failure as a warning toast instead of failing silently.
if (isFeatureEnabled(FeatureFlag.EnableExtensions)) {
ExtensionsLoader.getInstance().initializeExtensions();
ExtensionsLoader.getInstance()
.initializeExtensions()
.then(() =>
supersetCore.utils.logging.info(
'Extensions initialized successfully.',
),
)
.catch((error: unknown) => {
supersetCore.utils.logging.error(
'Error setting up extensions:',
error,
);
dispatch(
addWarningToast(t('Extensions failed to load: %s', String(error))),
);
});
}
// dispatch is stable; intentionally only re-run when the user changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId]);
return <>{children}</>;

View File

@@ -541,9 +541,8 @@ function SavedQueryList({
key: 'search',
input: 'search',
operator: FilterOperator.AllText,
toolTipDescription: t(
toolTipDescription:
'Searches all text fields: Name, Description, Database & Schema',
),
},
{
Header: t('Database'),

View File

@@ -133,9 +133,8 @@ function TagList(props: TagListProps) {
const emptyState = {
title: t('No Tags created'),
image: 'dashboard.svg',
description: t(
description:
'Create a new tag and assign it to existing entities like charts or dashboards',
),
buttonAction: () => setShowTagModal(true),
buttonIcon: <Icons.PlusOutlined iconSize="m" data-test="add-rule-empty" />,
buttonText: t('Create a new Tag'),

View File

@@ -8250,16 +8250,16 @@ msgid "List"
msgstr "Auflisten"
msgid "List Groups"
msgstr "Gruppen auflisten"
msgstr "Gruppen"
msgid "List Roles"
msgstr "Rollen auflisten"
msgstr "Rollen"
msgid "List Unique Values"
msgstr "Eindeutige Werte auflisten"
msgid "List Users"
msgstr "Benutzer*innen auflisten"
msgstr "Benutzer*innen"
msgid "List of extra columns made available in JavaScript functions"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,6 @@
"""Unit tests for the MCP get_dashboard_datasets tool."""
from importlib import import_module
from unittest.mock import Mock, patch
import pytest
@@ -30,10 +29,6 @@ from superset.mcp_service.utils.sanitization import (
)
from superset.utils import json
get_dashboard_datasets_module = import_module(
"superset.mcp_service.dashboard.tool.get_dashboard_datasets"
)
def _wrapped(value: str) -> str:
return f"{LLM_CONTEXT_OPEN_DELIMITER}\n{value}\n{LLM_CONTEXT_CLOSE_DELIMITER}"
@@ -147,8 +142,8 @@ def mock_dataset_access():
@pytest.fixture(autouse=True)
def allow_data_model_metadata():
"""Keep tests in the metadata-allowed path unless a test overrides it."""
with patch.object(
get_dashboard_datasets_module,
with patch(
"superset.mcp_service.dashboard.tool.get_dashboard_datasets."
"user_can_view_data_model_metadata",
return_value=True,
) as mock_allow:

View File

@@ -15,7 +15,6 @@
# specific language governing permissions and limitations
# under the License.
from datetime import datetime
from importlib import import_module
from importlib.util import find_spec
from unittest.mock import patch
@@ -27,8 +26,6 @@ from superset.utils.core import DTTM_ALIAS
from superset.utils.pandas_postprocessing import prophet
from tests.unit_tests.fixtures.dataframes import prophet_df
prophet_module = import_module("superset.utils.pandas_postprocessing.prophet")
def test_prophet_valid():
df = prophet(df=prophet_df, time_grain="P1M", periods=3, confidence_interval=0.9)
@@ -210,7 +207,9 @@ def test_prophet_fit_error():
if find_spec("prophet") is None:
pytest.skip("prophet not installed")
with patch.object(prophet_module, "_prophet_fit_and_predict") as mock_fit:
with patch(
"superset.utils.pandas_postprocessing.prophet._prophet_fit_and_predict"
) as mock_fit:
mock_fit.side_effect = InvalidPostProcessingError(
"Unable to generate forecast: Dataframe has fewer than 2 non-NaN rows."
)

View File

@@ -2983,9 +2983,8 @@ def test_coerce_integer_rejects_non_integer_float() -> None:
def test_coerce_integer_rejects_other_types() -> None:
raw: Any = [1]
with pytest.raises(ValueError, match="Invalid integer value"):
_coerce_scalar_filter_value(raw, _dim(pa.int64()))
_coerce_scalar_filter_value([1], _dim(pa.int64()))
@pytest.mark.parametrize(
@@ -3009,9 +3008,8 @@ def test_coerce_floating_invalid_string_raises() -> None:
def test_coerce_floating_rejects_other_types() -> None:
raw: Any = [1.0]
with pytest.raises(ValueError, match="Invalid numeric value"):
_coerce_scalar_filter_value(raw, _dim(pa.float64()))
_coerce_scalar_filter_value([1.0], _dim(pa.float64()))
def test_coerce_date_from_datetime() -> None: