mirror of
https://github.com/apache/superset.git
synced 2026-07-02 21:05:36 +00:00
Compare commits
12 Commits
codex/fix-
...
feat/sqlla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29d8f9da8a | ||
|
|
8b86bbc95d | ||
|
|
27f05d7639 | ||
|
|
e3b4ac215a | ||
|
|
a17472bbb1 | ||
|
|
1181df7fd6 | ||
|
|
276cae3ba7 | ||
|
|
d817a3be77 | ||
|
|
7af82a9041 | ||
|
|
0378c85ed9 | ||
|
|
0c7b5d34f2 | ||
|
|
72e2340715 |
7
.github/workflows/showtime-trigger.yml
vendored
7
.github/workflows/showtime-trigger.yml
vendored
@@ -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'
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
13
UPDATING.md
13
UPDATING.md
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
1583
docs/yarn.lock
1583
docs/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
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 |
|
||||
|
||||
@@ -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" . }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}: |
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
30
superset-frontend/package-lock.json
generated
30
superset-frontend/package-lock.json
generated
@@ -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": "*"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -713,5 +713,4 @@ export interface DataColumnMeta {
|
||||
isChildColumn?: boolean;
|
||||
description?: string;
|
||||
currencyCodeColumn?: string;
|
||||
isFilterable?: boolean;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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()
|
||||
)}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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} />
|
||||
|
||||
37
superset-frontend/src/explore/components/DataTableControl/FilterInput.test.tsx
Executable file → Normal file
37
superset-frontend/src/explore/components/DataTableControl/FilterInput.test.tsx
Executable file → Normal 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);
|
||||
}
|
||||
});
|
||||
|
||||
15
superset-frontend/src/explore/components/DataTableControl/index.tsx
Executable file → Normal file
15
superset-frontend/src/explore/components/DataTableControl/index.tsx
Executable file → Normal 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();
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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:',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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}</>;
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user