Compare commits

...

23 Commits

Author SHA1 Message Date
Elizabeth Thompson
98212189b8 fix(docker): bind webpack dev server to all interfaces
Change WEBPACK_DEVSERVER_HOST from 127.0.0.1 to 0.0.0.0 to make the
frontend accessible from outside the Docker container. This fixes an
issue where the frontend was only accessible from inside the container.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-23 17:03:49 -07:00
SBIN2010
1e4bc6ee78 fix: bug in tooltip timeseries chart in calculated total with annotation layer (#35179) 2025-09-19 10:30:42 -07:00
Pat Buxton
db178cf527 fix: Bump pandas to 2.1.4 for python 3.12 (#34999) 2025-09-19 09:18:00 -07:00
Alexandru Soare
5901320933 feat(database): Adding per-user caching option in Security tab (#34842) 2025-09-19 19:15:31 +03:00
SBIN2010
23bb4f88c0 fix(Funnel): onInit overridden row_limit to default value on save chart (#35076) 2025-09-19 09:13:45 -07:00
Levis Mbote
4130b92966 fix(gantt-chart): fix Y-axis label visibility in dark theme (#35189) 2025-09-19 12:33:53 +03:00
Mehmet Salih Yavuz
38297edc6b chore(matrixify): Remove leftover option (#35195) 2025-09-19 00:47:47 +03:00
sha174n
0c8f326258 docs: Add security warning for ENABLE_TEMPLATE_PROCESSING (#35192) 2025-09-18 17:36:41 -04:00
Joe Li
127f6b3d66 fix(tests): migrate Cypress control tests to React Testing Library (#35181)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-18 14:23:33 -07:00
Maxime Beauchemin
ea519a77b5 fix: only block showtime for unauthorized users on push (#35184)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-18 12:28:28 -07:00
Michael S. Molina
6cb3ef9f5d chore: TypeScript Configuration Modernization and Cleanup (#35159) 2025-09-18 16:27:57 -03:00
Kamil Gabryjelski
a889ae75fc chore: Bump ag grid to 34.2.0 (#35193) 2025-09-18 19:09:22 +02:00
Mehmet Salih Yavuz
b60be9655f feat(TimeTable): add other sparkline type options (#35180) 2025-09-18 16:41:05 +03:00
lc-4918
fd6da21ce0 chore(i18n): update French translations (#35070)
Co-authored-by: l.clement <l.clement@altereo.fr>
2025-09-17 21:05:15 -07:00
marun
1bf112a57a fix(CrudThemeProvider): Optimized theme loading logic (#35155) 2025-09-17 20:49:26 -07:00
marun
1f530d45cb fix(embedded): resolve theme context error in Loading component (#35168)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-17 20:44:04 -07:00
Joe Li
1187902e68 feat(playwright): Add Playwright CI Integration for Cypress Migration (SIP-178) (#35110)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-17 17:13:47 -07:00
Maxime Beauchemin
ad3eff9e90 feat(matrixify): replace single toggle with separate horizontal/vertical layout controls (#35067)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Mehmet Salih Yavuz <salih.yavuz@proton.me>
2025-09-17 22:57:06 +03:00
SBIN2010
3e554674ff feat(waterfall): add changes label series and grouping customize settings (#34847) 2025-09-17 08:24:45 -03:00
Amin Ghadersohi
dced2f8564 feat: Add BaseDAO improvements and test reorganization (#35018)
Co-authored-by: bito-code-review[bot] <188872107+bito-code-review[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-16 18:15:16 -07:00
Maxime Beauchemin
05c6a1bf20 fix(viz): resolve dark mode compatibility issues in BigNumber and Heatmap (#35151)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-16 10:21:47 -07:00
SBIN2010
c193d6d6a1 fix: import bug template params (#35144) 2025-09-16 10:21:29 -07:00
Joe Li
fb840b8e71 fix(deck.gl): restore legend display for Polygon charts with linear palette and fixed color schemes (#35142)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-16 20:20:42 +03:00
150 changed files with 7854 additions and 1518 deletions

View File

@@ -182,6 +182,76 @@ cypress-run-all() {
kill $flaskProcessId
}
playwright-install() {
cd "$GITHUB_WORKSPACE/superset-frontend"
say "::group::Install Playwright browsers"
npx playwright install --with-deps chromium
# Create output directories for test results and debugging
mkdir -p playwright-results
mkdir -p test-results
say "::endgroup::"
}
playwright-run() {
local APP_ROOT=$1
# Start Flask from the project root (same as Cypress)
cd "$GITHUB_WORKSPACE"
local flasklog="${HOME}/flask-playwright.log"
local port=8081
PLAYWRIGHT_BASE_URL="http://localhost:${port}"
if [ -n "$APP_ROOT" ]; then
export SUPERSET_APP_ROOT=$APP_ROOT
PLAYWRIGHT_BASE_URL=${PLAYWRIGHT_BASE_URL}${APP_ROOT}/
fi
export PLAYWRIGHT_BASE_URL
nohup flask run --no-debugger -p $port >"$flasklog" 2>&1 </dev/null &
local flaskProcessId=$!
# Ensure cleanup on exit
trap "kill $flaskProcessId 2>/dev/null || true" EXIT
# Wait for server to be ready with health check
local timeout=60
say "Waiting for Flask server to start on port $port..."
while [ $timeout -gt 0 ]; do
if curl -f ${PLAYWRIGHT_BASE_URL}/health >/dev/null 2>&1; then
say "Flask server is ready"
break
fi
sleep 1
timeout=$((timeout - 1))
done
if [ $timeout -eq 0 ]; then
echo "::error::Flask server failed to start within 60 seconds"
echo "::group::Flask startup log"
cat "$flasklog"
echo "::endgroup::"
return 1
fi
# Change to frontend directory for Playwright execution
cd "$GITHUB_WORKSPACE/superset-frontend"
say "::group::Run Playwright tests"
echo "Running Playwright with baseURL: ${PLAYWRIGHT_BASE_URL}"
npx playwright test auth/login --reporter=github --output=playwright-results
local status=$?
say "::endgroup::"
# After job is done, print out Flask log for debugging
echo "::group::Flask log for Playwright run"
cat "$flasklog"
echo "::endgroup::"
# make sure the program exits
kill $flaskProcessId
return $status
}
eyes-storybook-dependencies() {
say "::group::install eyes-storyook dependencies"
sudo apt-get update -y && sudo apt-get -y install gconf-service ca-certificates libxshmfence-dev fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libglib2.0-0 libgdk-pixbuf2.0-0 libgtk-3-0 libnspr4 libnss3 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release xdg-utils libappindicator1

View File

@@ -61,17 +61,8 @@ jobs:
console.log(`📊 Permission level for ${actor}: ${permission.permission}`);
const authorized = ['write', 'admin'].includes(permission.permission);
if (!authorized) {
console.log(`🚨 Unauthorized user ${actor} - skipping all operations`);
core.setOutput('authorized', 'false');
return;
}
console.log(`✅ Authorized maintainer: ${actor}`);
core.setOutput('authorized', 'true');
// If this is a synchronize event, check if Showtime is active and set blocked label
if (context.eventName === 'pull_request_target' && context.payload.action === 'synchronize') {
// If this is a synchronize event from unauthorized user, check if Showtime is active and set blocked label
if (!authorized && context.eventName === 'pull_request_target' && context.payload.action === 'synchronize') {
console.log(`🔒 Synchronize event detected - checking if Showtime is active`);
// Check if PR has any circus tent labels (Showtime is in use)
@@ -99,6 +90,15 @@ jobs:
}
}
if (!authorized) {
console.log(`🚨 Unauthorized user ${actor} - skipping all operations`);
core.setOutput('authorized', 'false');
return;
}
console.log(`✅ Authorized maintainer: ${actor}`);
core.setOutput('authorized', 'true');
- name: Install Superset Showtime
if: steps.auth.outputs.authorized == 'true'
run: |

View File

@@ -143,7 +143,7 @@ jobs:
- name: tsc
run: |
docker run --rm $TAG bash -c \
"npm run type"
"npm run plugins:build && npm run type"
validate-frontend:
needs: frontend-build

View File

@@ -0,0 +1,141 @@
name: Playwright E2E Tests
on:
push:
branches:
- "master"
- "[0-9].[0-9]*"
pull_request:
types: [synchronize, opened, reopened, ready_for_review]
workflow_dispatch:
inputs:
ref:
description: 'The branch or tag to checkout'
required: false
default: ''
pr_id:
description: 'The pull request ID to checkout'
required: false
default: ''
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
jobs:
playwright-tests:
runs-on: ubuntu-22.04
# Allow workflow to succeed even if tests fail during shadow mode
continue-on-error: true
permissions:
contents: read
pull-requests: read
strategy:
fail-fast: false
matrix:
browser: ["chromium"]
app_root: ["", "/app/prefix"]
env:
SUPERSET_ENV: development
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
SUPERSET__SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://superset:superset@127.0.0.1:15432/superset
PYTHONPATH: ${{ github.workspace }}
REDIS_PORT: 16379
GITHUB_TOKEN: ${{ github.token }}
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: superset
POSTGRES_PASSWORD: superset
ports:
- 15432:5432
redis:
image: redis:7-alpine
ports:
- 16379:6379
steps:
# -------------------------------------------------------
# Conditional checkout based on context (same as Cypress workflow)
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@v5
with:
persist-credentials: false
submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@v5
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
submodules: recursive
- name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@v5
with:
persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
submodules: recursive
# -------------------------------------------------------
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Python
uses: ./.github/actions/setup-backend/
if: steps.check.outputs.python || steps.check.outputs.frontend
- name: Setup postgres
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: setup-postgres
- name: Import test data
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: testdata
- name: Setup Node.js
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: actions/setup-node@v4
with:
node-version-file: './superset-frontend/.nvmrc'
- name: Install npm dependencies
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: npm-install
- name: Build javascript packages
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: build-instrumented-assets
- name: Install Playwright
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
with:
run: playwright-install
- name: Run Playwright
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
env:
NODE_OPTIONS: "--max-old-space-size=4096"
with:
run: playwright-run ${{ matrix.app_root }}
- name: Set safe app root
if: failure()
id: set-safe-app-root
run: |
APP_ROOT="${{ matrix.app_root }}"
SAFE_APP_ROOT=${APP_ROOT//\//_}
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
- name: Upload Playwright Artifacts
uses: actions/upload-artifact@v4
if: failure()
with:
path: |
${{ github.workspace }}/superset-frontend/playwright-results/
${{ github.workspace }}/superset-frontend/test-results/
name: playwright-artifact-${{ github.run_id }}-${{ github.job }}-${{ matrix.browser }}--${{ steps.set-safe-app-root.outputs.safe_app_root }}

17
LLMS.md
View File

@@ -15,8 +15,9 @@ Apache Superset is a data visualization platform with Flask/Python backend and R
### Testing Strategy Migration
- **Prefer unit tests** over integration tests
- **Prefer integration tests** over Cypress end-to-end tests
- **Cypress is last resort** - Actively moving away from Cypress
- **Prefer integration tests** over end-to-end tests
- **Use Playwright for E2E tests** - Migrating from Cypress
- **Cypress is deprecated** - Will be removed once migration is completed
- **Use Jest + React Testing Library** for component testing
- **Use `test()` instead of `describe()`** - Follow [avoid nesting when testing](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing) principles
@@ -107,6 +108,18 @@ superset/
npm run test # All tests
npm run test -- filename.test.tsx # Single file
# E2E Tests (Playwright - NEW)
npm run playwright:test # All Playwright tests
npm run playwright:ui # Interactive UI mode
npm run playwright:headed # See browser during tests
npx playwright test tests/auth/login.spec.ts # Single file
npm run playwright:debug tests/auth/login.spec.ts # Debug specific file
# E2E Tests (Cypress - DEPRECATED)
cd superset-frontend/cypress-base
npm run cypress-run-chrome # All Cypress tests (headless)
npm run cypress-debug # Interactive Cypress UI
# Backend
pytest # All tests
pytest tests/unit_tests/specific_test.py # Single file

View File

@@ -163,7 +163,7 @@ services:
# configuring the dev-server to use the host.docker.internal to connect to the backend
superset: "http://superset-light:8088"
# Webpack dev server configuration
WEBPACK_DEVSERVER_HOST: "${WEBPACK_DEVSERVER_HOST:-127.0.0.1}"
WEBPACK_DEVSERVER_HOST: "${WEBPACK_DEVSERVER_HOST:-0.0.0.0}"
WEBPACK_DEVSERVER_PORT: "${WEBPACK_DEVSERVER_PORT:-9000}"
ports:
- "${NODE_PORT:-9001}:9000" # Parameterized port, accessible on all interfaces

View File

@@ -10,8 +10,15 @@ version: 1
## Jinja Templates
SQL Lab and Explore supports [Jinja templating](https://jinja.palletsprojects.com/en/2.11.x/) in queries.
To enable templating, the `ENABLE_TEMPLATE_PROCESSING` [feature flag](/docs/configuration/configuring-superset#feature-flags) needs to be enabled in
`superset_config.py`. When templating is enabled, python code can be embedded in virtual datasets and
To enable templating, the `ENABLE_TEMPLATE_PROCESSING` [feature flag](/docs/configuration/configuring-superset#feature-flags) needs to be enabled in `superset_config.py`.
> #### ⚠️ Security Warning
>
> While powerful, this feature executes template code on the server. Within the Superset security model, this is **intended functionality**, as users with permissions to edit charts and virtual datasets are considered **trusted users**.
>
> If you grant these permissions to untrusted users, this feature can be exploited as a **Server-Side Template Injection (SSTI)** vulnerability. Do not enable `ENABLE_TEMPLATE_PROCESSING` unless you fully understand and accept the associated security risks.
When templating is enabled, python code can be embedded in virtual datasets and
in Custom SQL in the filter and metric controls in Explore. By default, the following variables are
made available in the Jinja context:

View File

@@ -631,7 +631,7 @@ can find all of the workflows and other assets under the `.github/` folder. This
- running the backend unit test suites (`tests/`)
- running the frontend test suites (`superset-frontend/src/**.*.test.*`)
- running our Cypress end-to-end tests (`superset-frontend/cypress-base/`)
- running our Playwright end-to-end tests (`superset-frontend/playwright/`) and legacy Cypress tests (`superset-frontend/cypress-base/`)
- linting the codebase, including all Python, Typescript and Javascript, yaml and beyond
- checking for all sorts of other rules conventions

View File

@@ -225,21 +225,57 @@ npm run test -- path/to/file.js
### E2E Integration Testing
For E2E testing, we recommend that you use a `docker compose` backend
**Note: We are migrating from Cypress to Playwright. Use Playwright for new tests.**
#### Playwright (Recommended - NEW)
For E2E testing with Playwright, use the same `docker compose` backend:
```bash
CYPRESS_CONFIG=true docker compose up --build
```
`docker compose` will get to work and expose a Cypress-ready Superset app.
This app uses a different database schema (`superset_cypress`) to keep it isolated from
your other dev environment(s), a specific set of examples, and a set of configurations that
aligns with the expectations within the end-to-end tests. Also note that it's served on a
different port than the default port for the backend (`8088`).
Now in another terminal, let's get ready to execute some Cypress commands. First, tell cypress
to connect to the Cypress-ready Superset backend.
The backend setup is identical - this exposes a test-ready Superset app on port 8081 with isolated database schema (`superset_cypress`), test data, and configurations.
Now in another terminal, run Playwright tests:
```bash
# Navigate to frontend directory (Playwright config is here)
cd superset-frontend
# Run all Playwright tests
npm run playwright:test
# or: npx playwright test
# Run with interactive UI for debugging
npm run playwright:ui
# or: npx playwright test --ui
# Run in headed mode (see browser)
npm run playwright:headed
# or: npx playwright test --headed
# Run specific test file
npx playwright test tests/auth/login.spec.ts
# Run with debug mode (step through tests)
npm run playwright:debug tests/auth/login.spec.ts
# or: npx playwright test --debug tests/auth/login.spec.ts
# Generate test report
npx playwright show-report
```
Configuration is in `superset-frontend/playwright.config.ts`. Base URL is automatically set to `http://localhost:8088` but will use `PLAYWRIGHT_BASE_URL` if provided.
#### Cypress (DEPRECATED - will be removed in Phase 5)
:::warning
Cypress is being phased out in favor of Playwright. Use Playwright for all new tests.
:::
```bash
# Set base URL for Cypress
CYPRESS_BASE_URL=http://localhost:8081
```

View File

@@ -76,7 +76,7 @@ dependencies = [
"packaging",
# --------------------------
# pandas and related (wanting pandas[performance] without numba as it's 100+MB and not needed)
"pandas[excel]>=2.0.3, <2.1",
"pandas[excel]>=2.0.3, <2.2",
"bottleneck", # recommended performance dependency for pandas, see https://pandas.pydata.org/docs/getting_started/install.html#performance-dependencies-recommended
# --------------------------
"parsedatetime",
@@ -84,6 +84,7 @@ dependencies = [
"pgsanity",
"Pillow>=11.0.0, <12",
"polyline>=2.0.0, <3.0",
"pydantic>=2.8.0",
"pyparsing>=3.0.6, <4",
"python-dateutil",
"python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies

View File

@@ -6,6 +6,8 @@ alembic==1.15.2
# via flask-migrate
amqp==5.3.1
# via kombu
annotated-types==0.7.0
# via pydantic
apispec==6.6.1
# via
# -r requirements/base.in
@@ -115,7 +117,9 @@ flask==2.3.3
# flask-sqlalchemy
# flask-wtf
flask-appbuilder==5.0.0
# via apache-superset (pyproject.toml)
# via
# apache-superset (pyproject.toml)
# apache-superset-core
flask-babel==3.1.0
# via flask-appbuilder
flask-caching==2.3.1
@@ -156,6 +160,7 @@ greenlet==3.1.1
# via
# apache-superset (pyproject.toml)
# shillelagh
# sqlalchemy
gunicorn==23.0.0
# via apache-superset (pyproject.toml)
h11==0.16.0
@@ -262,7 +267,7 @@ packaging==25.0
# limits
# marshmallow
# shillelagh
pandas==2.0.3
pandas==2.1.4
# via apache-superset (pyproject.toml)
paramiko==3.5.1
# via
@@ -294,6 +299,10 @@ pyasn1-modules==0.4.2
# via google-auth
pycparser==2.22
# via cffi
pydantic==2.11.7
# via apache-superset (pyproject.toml)
pydantic-core==2.33.2
# via pydantic
pygments==2.19.1
# via rich
pyjwt==2.10.1
@@ -404,10 +413,15 @@ typing-extensions==4.14.0
# alembic
# cattrs
# limits
# pydantic
# pydantic-core
# pyopenssl
# referencing
# selenium
# shillelagh
# typing-inspection
typing-inspection==0.4.1
# via pydantic
tzdata==2025.2
# via
# kombu

View File

@@ -18,6 +18,10 @@ amqp==5.3.1
# via
# -c requirements/base-constraint.txt
# kombu
annotated-types==0.7.0
# via
# -c requirements/base-constraint.txt
# pydantic
apispec==6.6.1
# via
# -c requirements/base-constraint.txt
@@ -212,6 +216,7 @@ flask-appbuilder==5.0.0
# via
# -c requirements/base-constraint.txt
# apache-superset
# apache-superset-core
flask-babel==3.1.0
# via
# -c requirements/base-constraint.txt
@@ -326,6 +331,7 @@ greenlet==3.1.1
# apache-superset
# gevent
# shillelagh
# sqlalchemy
grpcio==1.71.0
# via
# apache-superset
@@ -531,7 +537,7 @@ packaging==25.0
# pytest
# shillelagh
# sqlalchemy-bigquery
pandas==2.0.3
pandas==2.1.4
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -631,6 +637,14 @@ pycparser==2.22
# via
# -c requirements/base-constraint.txt
# cffi
pydantic==2.11.7
# via
# -c requirements/base-constraint.txt
# apache-superset
pydantic-core==2.33.2
# via
# -c requirements/base-constraint.txt
# pydantic
pydata-google-auth==1.9.0
# via pandas-gbq
pydruid==0.6.9
@@ -874,10 +888,17 @@ typing-extensions==4.14.0
# apache-superset
# cattrs
# limits
# pydantic
# pydantic-core
# pyopenssl
# referencing
# selenium
# shillelagh
# typing-inspection
typing-inspection==0.4.1
# via
# -c requirements/base-constraint.txt
# pydantic
tzdata==2025.2
# via
# -c requirements/base-constraint.txt

View File

@@ -323,6 +323,7 @@ module.exports = {
'*.stories.tsx',
'*.stories.jsx',
'fixtures.*',
'playwright/**/*',
],
excludedFiles: 'cypress-base/cypress/**/*',
plugins: ['jest', 'jest-dom', 'no-only-tests', 'testing-library'],
@@ -397,6 +398,13 @@ module.exports = {
'react/no-void-elements': 0,
},
},
{
files: ['playwright/**/*'],
rules: {
'import/no-unresolved': 0, // Playwright is not installed in main build
'import/no-extraneous-dependencies': 0, // Playwright is not installed in main build
},
},
],
// eslint-disable-next-line no-dupe-keys
rules: {

View File

@@ -175,7 +175,7 @@ describe('Charts list', () => {
interceptDelete();
cy.getBySel('sort-header').contains('Name').click();
// Modal closes immediatly without this
// Modal closes immediately without this
cy.wait(2000);
cy.getBySel('table-row').eq(0).contains('3 - Sample chart');

View File

@@ -1,193 +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.
*/
// ***********************************************
// Tests for setting controls in the UI
// ***********************************************
import { interceptChart, setSelectSearchInput } from 'cypress/utils';
describe('Datasource control', () => {
const newMetricName = `abc${Date.now()}`;
it('should allow edit dataset', () => {
interceptChart({ legacy: false }).as('chartData');
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@chartData' });
cy.get('[data-test="datasource-menu-trigger"]').click();
cy.get('[data-test="edit-dataset"]').click();
cy.get('[data-test="edit-dataset-tabs"]').within(() => {
cy.contains('Metrics').click();
});
// create new metric
cy.get('[data-test="crud-add-table-item"]', { timeout: 10000 }).click();
cy.wait(1000);
cy.get('.ant-table-body [data-test="textarea-editable-title-input"]')
.first()
.click();
cy.get('.ant-table-body [data-test="textarea-editable-title-input"]')
.first()
.focus();
cy.focused().clear({ force: true });
cy.focused().type(`${newMetricName}{enter}`, { force: true });
cy.get('[data-test="datasource-modal-save"]').click();
cy.get('.ant-modal-confirm-btns button').contains('OK').click();
// select new metric
cy.get('[data-test=metrics]')
.contains('Drop columns/metrics here or click')
.click();
cy.get('input[aria-label="Select saved metrics"]')
.should('exist')
.then($input => {
setSelectSearchInput($input, newMetricName);
});
// delete metric
cy.get('[data-test="datasource-menu-trigger"]').click();
cy.get('[data-test="edit-dataset"]').click();
cy.get('.ant-modal-content').within(() => {
cy.get('[data-test="collection-tab-Metrics"]')
.contains('Metrics')
.click();
});
cy.get(`[data-test="textarea-editable-title-input"]`)
.contains(newMetricName)
.closest('tr')
.find('[data-test="crud-delete-icon"]')
.click();
cy.get('[data-test="datasource-modal-save"]').click();
cy.get('.ant-modal-confirm-btns button').contains('OK').click();
cy.get('[data-test="metrics"]').contains(newMetricName).should('not.exist');
});
});
describe('Color scheme control', () => {
beforeEach(() => {
interceptChart({ legacy: false }).as('chartData');
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@chartData' });
});
it('should show color options with and without tooltips', () => {
cy.get('#controlSections-tab-CUSTOMIZE').click();
cy.get('.ant-select-selection-item .color-scheme-label').contains(
'Superset Colors',
);
cy.get('.ant-select-selection-item .color-scheme-label').trigger(
'mouseover',
);
cy.get('.color-scheme-tooltip').should('be.visible');
cy.get('.color-scheme-tooltip').contains('Superset Colors');
cy.get('.Control[data-test="color_scheme"]').scrollIntoView();
cy.get('.Control[data-test="color_scheme"] input[type="search"]').focus();
cy.get('.color-scheme-label')
.contains('Superset Colors')
.trigger('mouseover');
cy.get('.color-scheme-label')
.contains('Superset Colors')
.trigger('mouseout');
cy.focused().type('lyftColors');
cy.getBySel('lyftColors').should('exist');
cy.getBySel('lyftColors').trigger('mouseover', { force: true });
cy.get('.color-scheme-tooltip').should('not.be.visible');
});
});
describe('VizType control', () => {
beforeEach(() => {
interceptChart({ legacy: false }).as('tableChartData');
interceptChart({ legacy: false }).as('bigNumberChartData');
});
it('Can change vizType', () => {
cy.visitChartByName('Daily Totals').then(() => {
cy.get('.slice_container').should('be.visible');
});
cy.verifySliceSuccess({ waitAlias: '@tableChartData' });
cy.contains('View all charts').should('be.visible').click();
cy.get('.ant-modal-content').within(() => {
cy.get('button').contains('KPI').click(); // change categories
cy.get('[role="button"]').contains('Big Number').click();
cy.get('button').contains('Select').click();
});
cy.get('button[data-test="run-query-button"]').click();
cy.verifySliceSuccess({
waitAlias: '@bigNumberChartData',
});
});
});
describe('Test datatable', () => {
beforeEach(() => {
interceptChart({ legacy: false }).as('tableChartData');
interceptChart({ legacy: false }).as('lineChartData');
cy.visitChartByName('Daily Totals');
});
it('Data Pane opens and loads results', () => {
cy.contains('Results').click();
cy.get('[data-test="row-count-label"]').contains('26 rows');
cy.get('.ant-empty-description').should('not.exist');
});
it('Datapane loads view samples', () => {
cy.intercept(
'**/datasource/samples?force=false&datasource_type=table&datasource_id=*',
).as('Samples');
cy.contains('Samples').click();
cy.wait('@Samples');
cy.get('.ant-tabs-tab-active').contains('Samples');
cy.get('[data-test="row-count-label"]').contains('1k rows');
cy.get('.ant-empty-description').should('not.exist');
});
});
describe('Groupby control', () => {
it('Set groupby', () => {
interceptChart({ legacy: false }).as('chartData');
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@chartData' });
cy.get('[data-test=groupby]')
.contains('Drop columns here or click')
.click();
cy.get('[id="adhoc-metric-edit-tabs-tab-simple"]').click();
cy.get('input[aria-label="Columns and metrics"]', { timeout: 10000 })
.should('be.visible')
.click();
cy.get('input[aria-label="Columns and metrics"]').type('state{enter}');
cy.get('[data-test="ColumnEdit#save"]').contains('Save').click();
cy.get('button[data-test="run-query-button"]').click();
cy.verifySliceSuccess({ waitAlias: '@chartData' });
});
});

View File

@@ -33,6 +33,7 @@ module.exports = {
'^@superset-ui/([^/]+)$': '<rootDir>/node_modules/@superset-ui/$1/src',
// mapping @apache-superset/core to local package
'^@apache-superset/core$': '<rootDir>/packages/superset-core/src',
'^@apache-superset/core/(.*)$': '<rootDir>/packages/superset-core/src/$1',
},
testEnvironment: '<rootDir>/spec/helpers/jsDomWithFetchAPI.ts',
modulePathIgnorePatterns: ['<rootDir>/packages/generator-superset'],

View File

@@ -54,6 +54,8 @@
"@visx/scale": "^3.5.0",
"@visx/tooltip": "^3.0.0",
"@visx/xychart": "^3.5.1",
"ag-grid-community": "34.2.0",
"ag-grid-react": "34.2.0",
"antd": "^5.24.6",
"chrono-node": "^2.7.8",
"classnames": "^2.2.5",
@@ -159,6 +161,7 @@
"@hot-loader/react-dom": "^17.0.2",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@mihkeleidast/storybook-addon-source": "^1.0.1",
"@playwright/test": "^1.49.1",
"@storybook/addon-actions": "8.1.11",
"@storybook/addon-controls": "8.1.11",
"@storybook/addon-essentials": "8.1.11",
@@ -10109,6 +10112,22 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/@playwright/test": {
"version": "1.55.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz",
"integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.55.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@pnpm/config.env-replace": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz",
@@ -18696,27 +18715,27 @@
}
},
"node_modules/ag-charts-types": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-12.0.2.tgz",
"integrity": "sha512-AWM1Y+XW+9VMmV3AbzdVEnreh/I2C9Pmqpc2iLmtId3Xbvmv7O56DqnuDb9EXjK5uPxmyUerTP+utL13UGcztw==",
"version": "12.2.0",
"resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-12.2.0.tgz",
"integrity": "sha512-d2qQrQirt9wP36YW5HPuOvXsiajyiFnr1CTsoCbs02bavPDz7Lk2jHp64+waM4YKgXb3GN7gafbBI9Qgk33BmQ==",
"license": "MIT"
},
"node_modules/ag-grid-community": {
"version": "34.0.2",
"resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-34.0.2.tgz",
"integrity": "sha512-hVJp5vrmwHRB10YjfSOVni5YJkO/v+asLjT72S4YnIFSx8lAgyPmByNJgtojk1aJ5h6Up93jTEmGDJeuKiWWLA==",
"version": "34.2.0",
"resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-34.2.0.tgz",
"integrity": "sha512-peS7THEMYwpIrwLQHmkRxw/TlOnddD/F5A88RqlBxf8j+WqVYRWMOOhU5TqymGcha7z2oZ8IoL9ROl3gvtdEjg==",
"license": "MIT",
"dependencies": {
"ag-charts-types": "12.0.2"
"ag-charts-types": "12.2.0"
}
},
"node_modules/ag-grid-react": {
"version": "34.0.2",
"resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-34.0.2.tgz",
"integrity": "sha512-1KBXkTvwtZiYVlSuDzBkiqfHjZgsATOmpLZdAtdmsCSOOOEWai0F9zHHgBuHfyciAE4nrbQWfojkx8IdnwsKFw==",
"version": "34.2.0",
"resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-34.2.0.tgz",
"integrity": "sha512-dLKFw6hz75S0HLuZvtcwjm+gyiI4gXVzHEu7lWNafWAX0mb8DhogEOP5wbzAlsN6iCfi7bK/cgZImZFjenlqwg==",
"license": "MIT",
"dependencies": {
"ag-grid-community": "34.0.2",
"ag-grid-community": "34.2.0",
"prop-types": "^15.8.1"
},
"peerDependencies": {
@@ -45517,6 +45536,53 @@
"dev": true,
"license": "MIT"
},
"node_modules/playwright": {
"version": "1.55.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz",
"integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.55.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.55.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz",
"integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/png-async": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/png-async/-/png-async-0.9.4.tgz",
@@ -60624,7 +60690,7 @@
},
"packages/superset-core": {
"name": "@apache-superset/core",
"version": "0.0.1-rc3",
"version": "0.0.1-rc4",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.26.4",
@@ -63321,6 +63387,7 @@
"version": "0.20.3",
"license": "Apache-2.0",
"dependencies": {
"@apache-superset/core": "*",
"@react-icons/all-files": "^4.1.0",
"@types/react": "*",
"lodash": "^4.17.21"
@@ -63348,14 +63415,15 @@
"license": "Apache-2.0",
"dependencies": {
"@ant-design/icons": "^5.2.6",
"@apache-superset/core": "*",
"@babel/runtime": "^7.28.2",
"@fontsource/fira-code": "^5.2.6",
"@fontsource/inter": "^5.2.6",
"@types/json-bigint": "^1.0.4",
"@visx/responsive": "^3.12.0",
"ace-builds": "^1.43.1",
"ag-grid-community": "^34.0.2",
"ag-grid-react": "34.0.2",
"ag-grid-community": "34.2.0",
"ag-grid-react": "34.2.0",
"brace": "^0.11.1",
"classnames": "^2.2.5",
"core-js": "^3.38.1",
@@ -65394,6 +65462,7 @@
},
"peerDependencies": {
"@ant-design/icons": "^5.2.6",
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@testing-library/dom": "^8.20.1",
@@ -65445,6 +65514,7 @@
"lodash": "^4.17.21"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"echarts": "*",
@@ -66622,6 +66692,7 @@
},
"peerDependencies": {
"@ant-design/icons": "^5.2.6",
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"lodash": "^4.17.11",
@@ -67753,6 +67824,7 @@
},
"peerDependencies": {
"@ant-design/icons": "^5.2.6",
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@testing-library/dom": "^8.20.1",

View File

@@ -63,6 +63,11 @@
"plugins:release-conventional": "npm run prune && npm run plugins:build && lerna publish --conventional-commits --create-release github --yes",
"plugins:release-from-tag": "npm run prune && npm run plugins:build && lerna publish from-package --yes",
"plugins:storybook": "cd packages/superset-ui-demo && npm run storybook",
"playwright:test": "playwright test",
"playwright:ui": "playwright test --ui",
"playwright:headed": "playwright test --headed",
"playwright:debug": "playwright test --debug",
"playwright:report": "playwright show-report",
"prettier": "npm run _prettier -- --write",
"prettier-check": "npm run _prettier -- --check",
"prod": "npm run build",
@@ -122,6 +127,8 @@
"@visx/scale": "^3.5.0",
"@visx/tooltip": "^3.0.0",
"@visx/xychart": "^3.5.1",
"ag-grid-community": "34.2.0",
"ag-grid-react": "34.2.0",
"antd": "^5.24.6",
"chrono-node": "^2.7.8",
"classnames": "^2.2.5",
@@ -227,6 +234,7 @@
"@hot-loader/react-dom": "^17.0.2",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@mihkeleidast/storybook-addon-source": "^1.0.1",
"@playwright/test": "^1.49.1",
"@storybook/addon-actions": "8.1.11",
"@storybook/addon-controls": "8.1.11",
"@storybook/addon-essentials": "8.1.11",

View File

@@ -22,19 +22,6 @@ To add the package to Superset, go to the `superset-frontend` subdirectory in yo
npm i -S ../../<%= packageName %>
```
If your Superset plugin exists in the `superset-frontend` directory and you wish to resolve TypeScript errors about `@superset-ui/core` not being resolved correctly, add the following to your `tsconfig.json` file:
```
"references": [
{
"path": "../../packages/superset-ui-chart-controls"
},
{
"path": "../../packages/superset-ui-core"
}
]
```
You may also wish to add the following to the `include` array in `tsconfig.json` to make Superset types available to your plugin:
```

View File

@@ -1,44 +1,19 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"declaration": true,
"declarationDir": "lib",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": false,
"jsx": "react",
"lib": [
"dom",
"esnext"
],
"module": "esnext",
"moduleResolution": "node",
"noEmitOnError": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"outDir": "lib",
"pretty": true,
"removeComments": false,
"strict": true,
"target": "es2015",
"useDefineForClassFields": false,
"composite": true,
"declarationMap": true,
"rootDir": "src",
"skipLibCheck": true,
"emitDeclarationOnly": true,
"resolveJsonModule": true,
"types": ["jest"],
"typeRoots": [
"./node_modules/@types"
]
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"lib",
"test"
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"include": [
"src/**/*",
"types/**/*"
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]
}

View File

@@ -1,19 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"declaration": true,
"declarationDir": "lib",
"outDir": "lib",
"strict": true,
"rootDir": "src",
"jsx": "preserve",
"baseUrl": ".",
"module": "esnext",
"moduleResolution": "node",
"skipLibCheck": true,
"target": "es2020",
"esModuleInterop": true
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*.ts*"],
"exclude": ["lib"]
"include": ["src/**/*", "types/**/*"],
"exclude": ["src/**/*.test.*", "src/**/*.stories.*"]
}

View File

@@ -24,6 +24,7 @@
"lib"
],
"dependencies": {
"@apache-superset/core": "*",
"@react-icons/all-files": "^4.1.0",
"@types/react": "*",
"lodash": "^4.17.21"

View File

@@ -20,20 +20,32 @@ import { t } from '@superset-ui/core';
import { ControlPanelSectionConfig } from '../types';
export const matrixifyEnableSection: ControlPanelSectionConfig = {
label: t('Enable matrixify'),
label: t('Matrixify'),
expanded: true,
controlSetRows: [
[
{
name: 'matrixify_enabled',
name: 'matrixify_enable_horizontal_layout',
config: {
type: 'CheckboxControl',
label: t('Enable matrixify'),
label: t('Enable horizontal layout (columns)'),
description: t(
'Create matrix columns by placing charts side-by-side',
),
default: false,
renderTrigger: true,
},
},
],
[
{
name: 'matrixify_enable_vertical_layout',
config: {
type: 'CheckboxControl',
label: t('Enable vertical layout (rows)'),
description: t('Create matrix rows by stacking charts vertically'),
default: false,
renderTrigger: true,
description: t(
'Transform this chart into a matrix/grid of charts based on dimensions or metrics',
),
},
},
],
@@ -42,9 +54,11 @@ export const matrixifyEnableSection: ControlPanelSectionConfig = {
};
export const matrixifySection: ControlPanelSectionConfig = {
label: t('Matrixify'),
label: t('Cell layout & styling'),
expanded: false,
visibility: ({ controls }) => controls?.matrixify_enabled?.value === true,
visibility: ({ controls }) =>
controls?.matrixify_enable_vertical_layout?.value === true ||
controls?.matrixify_enable_horizontal_layout?.value === true,
controlSetRows: [
[
{
@@ -105,9 +119,10 @@ export const matrixifySection: ControlPanelSectionConfig = {
};
export const matrixifyRowSection: ControlPanelSectionConfig = {
label: t('Vertical layout'),
label: t('Vertical layout (rows)'),
expanded: false,
visibility: ({ controls }) => controls?.matrixify_enabled?.value === true,
visibility: ({ controls }) =>
controls?.matrixify_enable_vertical_layout?.value === true,
controlSetRows: [
['matrixify_show_row_labels'],
['matrixify_mode_rows'],
@@ -118,13 +133,14 @@ export const matrixifyRowSection: ControlPanelSectionConfig = {
['matrixify_topn_metric_rows'],
['matrixify_topn_order_rows'],
],
tabOverride: 'data',
tabOverride: 'matrixify',
};
export const matrixifyColumnSection: ControlPanelSectionConfig = {
label: t('Horizontal layout'),
label: t('Horizontal layout (columns)'),
expanded: false,
visibility: ({ controls }) => controls?.matrixify_enabled?.value === true,
visibility: ({ controls }) =>
controls?.matrixify_enable_horizontal_layout?.value === true,
controlSetRows: [
['matrixify_show_column_headers'],
['matrixify_mode_columns'],
@@ -135,5 +151,5 @@ export const matrixifyColumnSection: ControlPanelSectionConfig = {
['matrixify_topn_metric_columns'],
['matrixify_topn_order_columns'],
],
tabOverride: 'data',
tabOverride: 'matrixify',
};

View File

@@ -18,21 +18,49 @@
* under the License.
*/
import { t } from '@superset-ui/core';
import { t, validateNonEmpty } from '@superset-ui/core';
import { SharedControlConfig } from '../types';
import { dndAdhocMetricControl } from './dndControls';
import { defineSavedMetrics } from '../utils';
/**
* Matrixify control definitions
* Controls for transforming charts into matrix/grid layouts
*/
// Utility function to check if matrixify controls should be visible
const isMatrixifyVisible = (
controls: any,
axis: 'rows' | 'columns',
mode?: 'metrics' | 'dimensions',
selectionMode?: 'members' | 'topn',
) => {
const layoutControl = `matrixify_enable_${axis === 'rows' ? 'vertical' : 'horizontal'}_layout`;
const modeControl = `matrixify_mode_${axis}`;
const selectionModeControl = `matrixify_dimension_selection_mode_${axis}`;
const isLayoutEnabled = controls?.[layoutControl]?.value === true;
if (!isLayoutEnabled) return false;
if (mode) {
const isModeMatch = controls?.[modeControl]?.value === mode;
if (!isModeMatch) return false;
if (selectionMode && mode === 'dimensions') {
return controls?.[selectionModeControl]?.value === selectionMode;
}
}
return true;
};
// Initialize the controls object that will be populated dynamically
const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
// Dynamically add axis-specific controls (rows and columns)
['columns', 'rows'].forEach(axisParam => {
const axis = axisParam; // Capture the value in a local variable
(['columns', 'rows'] as const).forEach(axisParam => {
const axis: 'rows' | 'columns' = axisParam;
matrixifyControls[`matrixify_mode_${axis}`] = {
type: 'RadioButtonControl',
@@ -43,17 +71,18 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
['dimensions', t('Dimension members')],
],
renderTrigger: true,
tabOverride: 'matrixify',
visibility: ({ controls }) => isMatrixifyVisible(controls, axis),
};
matrixifyControls[`matrixify_${axis}`] = {
...dndAdhocMetricControl,
label: t(`Metrics`),
multi: true,
validators: [], // Not required
// description: t(`Select metrics for ${axis}`),
validators: [], // No validation - rely on visibility
renderTrigger: true,
visibility: ({ controls }) =>
controls?.[`matrixify_mode_${axis}`]?.value === 'metrics',
tabOverride: 'matrixify',
visibility: ({ controls }) => isMatrixifyVisible(controls, axis, 'metrics'),
};
// Combined dimension and values control
@@ -62,8 +91,9 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
label: t(`Dimension selection`),
description: t(`Select dimension and values`),
default: { dimension: '', values: [] },
validators: [], // Not required
validators: [], // No validation - rely on visibility
renderTrigger: true,
tabOverride: 'matrixify',
shouldMapStateToProps: (prevState, state) => {
// Recalculate when any relevant form_data field changes
const fieldsToCheck = [
@@ -82,24 +112,40 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
const getValue = (key: string, defaultValue?: any) =>
form_data?.[key] ?? controls?.[key]?.value ?? defaultValue;
const selectionMode = getValue(
`matrixify_dimension_selection_mode_${axis}`,
'members',
);
const isVisible = isMatrixifyVisible(controls, axis, 'dimensions');
// Validate dimension is selected when visible
const dimensionValidator = (value: any) => {
if (!value?.dimension) {
return t('Dimension is required');
}
return false;
};
// Additional validation for topN mode
const validators = isVisible
? [dimensionValidator, validateNonEmpty]
: [];
return {
datasource,
selectionMode: getValue(
`matrixify_dimension_selection_mode_${axis}`,
'members',
),
selectionMode,
topNMetric: getValue(`matrixify_topn_metric_${axis}`),
topNValue: getValue(`matrixify_topn_value_${axis}`),
topNOrder: getValue(`matrixify_topn_order_${axis}`),
formData: form_data,
validators,
};
},
visibility: ({ controls }) =>
controls?.[`matrixify_mode_${axis}`]?.value === 'dimensions',
isMatrixifyVisible(controls, axis, 'dimensions'),
};
// Dimension picker for TopN mode (just dimension, no values)
// NOTE: This is now handled by matrixify_dimension control, so hiding it
matrixifyControls[`matrixify_topn_dimension_${axis}`] = {
type: 'SelectControl',
label: t('Dimension'),
@@ -127,33 +173,67 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
['topn', t('Top n')],
],
renderTrigger: true,
tabOverride: 'matrixify',
visibility: ({ controls }) =>
controls?.[`matrixify_mode_${axis}`]?.value === 'dimensions',
isMatrixifyVisible(controls, axis, 'dimensions'),
};
// TopN controls
matrixifyControls[`matrixify_topn_value_${axis}`] = {
type: 'TextControl',
type: 'NumberControl',
label: t(`Number of top values`),
description: t(`How many top values to select`),
default: 10,
isInt: true,
validators: [],
renderTrigger: true,
tabOverride: 'matrixify',
visibility: ({ controls }) =>
controls?.[`matrixify_mode_${axis}`]?.value === 'dimensions' &&
controls?.[`matrixify_dimension_selection_mode_${axis}`]?.value ===
isMatrixifyVisible(controls, axis, 'dimensions', 'topn'),
mapStateToProps: ({ controls }) => {
const isVisible = isMatrixifyVisible(
controls,
axis,
'dimensions',
'topn',
);
return {
validators: isVisible ? [validateNonEmpty] : [],
};
},
};
matrixifyControls[`matrixify_topn_metric_${axis}`] = {
...dndAdhocMetricControl,
label: t(`Metric for ordering`),
multi: false,
validators: [], // Not required
validators: [],
description: t(`Metric to use for ordering Top N values`),
tabOverride: 'matrixify',
visibility: ({ controls }) =>
controls?.[`matrixify_mode_${axis}`]?.value === 'dimensions' &&
controls?.[`matrixify_dimension_selection_mode_${axis}`]?.value ===
isMatrixifyVisible(controls, axis, 'dimensions', 'topn'),
mapStateToProps: (state, controlState) => {
const { controls, datasource } = state;
const isVisible = isMatrixifyVisible(
controls,
axis,
'dimensions',
'topn',
);
const originalProps =
dndAdhocMetricControl.mapStateToProps?.(state, controlState) || {};
return {
...originalProps,
columns: datasource?.columns || [],
savedMetrics: defineSavedMetrics(datasource),
datasource,
datasourceType: datasource?.type,
validators: isVisible ? [validateNonEmpty] : [],
};
},
};
matrixifyControls[`matrixify_topn_order_${axis}`] = {
@@ -164,10 +244,10 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
['asc', t('Ascending')],
['desc', t('Descending')],
],
renderTrigger: true,
tabOverride: 'matrixify',
visibility: ({ controls }) =>
controls?.[`matrixify_mode_${axis}`]?.value === 'dimensions' &&
controls?.[`matrixify_dimension_selection_mode_${axis}`]?.value ===
'topn',
isMatrixifyVisible(controls, axis, 'dimensions', 'topn'),
};
});
@@ -213,15 +293,22 @@ matrixifyControls.matrixify_charts_per_row = {
!controls?.matrixify_fit_columns_dynamically?.value,
};
// Main enable control
matrixifyControls.matrixify_enabled = {
matrixifyControls.matrixify_enable_vertical_layout = {
type: 'CheckboxControl',
label: t('Enable matrixify'),
description: t(
'Transform this chart into a matrix/grid of charts based on dimensions or metrics',
),
label: t('Enable vertical layout (rows)'),
description: t('Create matrix rows by stacking charts vertically'),
default: false,
renderTrigger: true,
tabOverride: 'matrixify',
};
matrixifyControls.matrixify_enable_horizontal_layout = {
type: 'CheckboxControl',
label: t('Enable horizontal layout (columns)'),
description: t('Create matrix columns by placing charts side-by-side'),
default: false,
renderTrigger: true,
tabOverride: 'matrixify',
};
// Cell title control for Matrixify
@@ -234,8 +321,8 @@ matrixifyControls.matrixify_cell_title_template = {
default: '',
renderTrigger: true,
visibility: ({ controls }) =>
(controls?.matrixify_mode_rows?.value ||
controls?.matrixify_mode_columns?.value) !== undefined,
controls?.matrixify_enable_vertical_layout?.value === true ||
controls?.matrixify_enable_horizontal_layout?.value === true,
};
// Matrix display controls
@@ -245,9 +332,9 @@ matrixifyControls.matrixify_show_row_labels = {
description: t('Display labels for each row on the left side of the matrix'),
default: true,
renderTrigger: true,
tabOverride: 'matrixify',
visibility: ({ controls }) =>
(controls?.matrixify_mode_rows?.value ||
controls?.matrixify_mode_columns?.value) !== undefined,
controls?.matrixify_enable_vertical_layout?.value === true,
};
matrixifyControls.matrixify_show_column_headers = {
@@ -256,9 +343,9 @@ matrixifyControls.matrixify_show_column_headers = {
description: t('Display headers for each column at the top of the matrix'),
default: true,
renderTrigger: true,
tabOverride: 'matrixify',
visibility: ({ controls }) =>
(controls?.matrixify_mode_rows?.value ||
controls?.matrixify_mode_columns?.value) !== undefined,
controls?.matrixify_enable_horizontal_layout?.value === true,
};
export { matrixifyControls };

View File

@@ -17,11 +17,7 @@
* under the License.
*/
import { ensureIsArray, GenericDataType, ValueOf } from '@superset-ui/core';
import {
ControlPanelState,
isDataset,
isQueryResponse,
} from '@superset-ui/chart-controls';
import { ControlPanelState, isDataset, isQueryResponse } from '../types';
export function checkColumnType(
columnName: string,

View File

@@ -2,18 +2,8 @@
"compilerOptions": {
"composite": false,
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "."
},
"extends": "../../../tsconfig.json",
"include": [
"**/*",
"../types/**/*",
"../../../types/**/*"
],
"references": [
{
"path": ".."
}
]
"include": ["**/*", "../types/**/*", "../../../types/**/*"]
}

View File

@@ -1,22 +1,13 @@
{
"compilerOptions": {
"declarationDir": "lib",
"outDir": "lib",
"rootDir": "src"
},
"exclude": [
"lib",
"test"
],
"extends": "../../tsconfig.json",
"include": [
"src/**/*",
"types/**/*",
"../../types/**/*"
],
"compilerOptions": {
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["src/**/*.test.*", "src/**/*.stories.*"],
"references": [
{
"path": "../superset-ui-core"
}
{ "path": "../superset-core" },
{ "path": "../superset-ui-core" }
]
}

View File

@@ -24,14 +24,15 @@
"lib"
],
"dependencies": {
"@apache-superset/core": "*",
"@ant-design/icons": "^5.2.6",
"@babel/runtime": "^7.28.2",
"@fontsource/fira-code": "^5.2.6",
"@fontsource/inter": "^5.2.6",
"@types/json-bigint": "^1.0.4",
"ace-builds": "^1.43.1",
"ag-grid-community": "^34.0.2",
"ag-grid-react": "34.0.2",
"ag-grid-community": "34.2.0",
"ag-grid-react": "34.2.0",
"brace": "^0.11.1",
"classnames": "^2.2.5",
"csstype": "^3.1.3",

View File

@@ -276,10 +276,10 @@ export function generateMatrixifyGrid(
const cellFormData = generateCellFormData(
formData,
rowCount > 1 ? config.rows : null,
colCount > 1 ? config.columns : null,
rowCount > 1 ? row : null,
colCount > 1 ? col : null,
rowCount > 0 ? config.rows : null,
colCount > 0 ? config.columns : null,
rowCount > 0 ? row : null,
colCount > 0 ? col : null,
);
// Generate title using template if provided

View File

@@ -74,7 +74,8 @@ test('should create single group when fitting columns dynamically', () => {
const formData = {
viz_type: 'test_chart',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_enable_horizontal_layout: true,
matrixify_fit_columns_dynamically: true,
matrixify_charts_per_row: 3,
matrixify_show_row_labels: true,
@@ -123,7 +124,8 @@ test('should create multiple groups when not fitting columns dynamically', () =>
const formData = {
viz_type: 'test_chart',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_enable_horizontal_layout: true,
matrixify_fit_columns_dynamically: false,
matrixify_charts_per_row: 3,
matrixify_show_row_labels: true,
@@ -158,7 +160,8 @@ test('should handle exact division of columns', () => {
const formData = {
viz_type: 'test_chart',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_enable_horizontal_layout: true,
matrixify_fit_columns_dynamically: false,
matrixify_charts_per_row: 2,
matrixify_show_row_labels: true,
@@ -186,7 +189,8 @@ test('should handle case where charts_per_row exceeds total columns', () => {
const formData = {
viz_type: 'test_chart',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_enable_horizontal_layout: true,
matrixify_fit_columns_dynamically: false,
matrixify_charts_per_row: 5,
matrixify_show_row_labels: true,
@@ -216,7 +220,8 @@ test('should show headers for each group when wrapping occurs', () => {
const formData = {
viz_type: 'test_chart',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_enable_horizontal_layout: true,
matrixify_fit_columns_dynamically: false,
matrixify_charts_per_row: 2,
matrixify_show_row_labels: true,
@@ -250,7 +255,8 @@ test('should show headers only on first row when not wrapping', () => {
const formData = {
viz_type: 'test_chart',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_enable_horizontal_layout: true,
matrixify_fit_columns_dynamically: true, // No wrapping
matrixify_show_row_labels: true,
matrixify_show_column_headers: true,
@@ -279,7 +285,8 @@ test('should hide headers when disabled', () => {
const formData = {
viz_type: 'test_chart',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_enable_horizontal_layout: true,
matrixify_show_row_labels: false,
matrixify_show_column_headers: false,
};
@@ -306,7 +313,8 @@ test('should place cells correctly in wrapped layout', () => {
const formData = {
viz_type: 'test_chart',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_enable_horizontal_layout: true,
matrixify_fit_columns_dynamically: false,
matrixify_charts_per_row: 2,
matrixify_show_row_labels: true,
@@ -336,7 +344,8 @@ test('should handle null grid gracefully', () => {
const formData = {
viz_type: 'test_chart',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_enable_horizontal_layout: true,
};
const { container } = renderWithTheme(
@@ -357,7 +366,8 @@ test('should handle empty grid gracefully', () => {
const formData = {
viz_type: 'test_chart',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_enable_horizontal_layout: true,
};
const { container } = renderWithTheme(
@@ -381,7 +391,8 @@ test('should use default values for missing configuration', () => {
const formData = {
viz_type: 'test_chart',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_enable_horizontal_layout: true,
// Missing optional configurations
};

View File

@@ -128,9 +128,13 @@ function MatrixifyGridRenderer({
[formData],
);
// Determine layout parameters
const showRowLabels = formData.matrixify_show_row_labels ?? true;
const showColumnHeaders = formData.matrixify_show_column_headers ?? true;
// Determine layout parameters - only show headers/labels if layout is enabled
const showRowLabels =
formData.matrixify_enable_vertical_layout === true &&
(formData.matrixify_show_row_labels ?? true);
const showColumnHeaders =
formData.matrixify_enable_horizontal_layout === true &&
(formData.matrixify_show_column_headers ?? true);
const rowHeight = formData.matrixify_row_height || DEFAULT_ROW_HEIGHT;
const fitColumnsDynamically =
formData.matrixify_fit_columns_dynamically ?? true;

View File

@@ -37,10 +37,11 @@ test('isMatrixifyEnabled should return false when no matrixify configuration exi
expect(isMatrixifyEnabled(formData)).toBe(false);
});
test('isMatrixifyEnabled should return false when matrixify_enabled is false', () => {
test('isMatrixifyEnabled should return false when layout controls are false', () => {
const formData = {
viz_type: 'table',
matrixify_enabled: false,
matrixify_enable_vertical_layout: false,
matrixify_enable_horizontal_layout: false,
matrixify_mode_rows: 'metrics',
matrixify_rows: [createMetric('Revenue')],
} as MatrixifyFormData;
@@ -51,7 +52,7 @@ test('isMatrixifyEnabled should return false when matrixify_enabled is false', (
test('isMatrixifyEnabled should return true for valid metrics mode configuration', () => {
const formData = {
viz_type: 'table',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_mode_rows: 'metrics',
matrixify_mode_columns: 'metrics',
matrixify_rows: [createMetric('Revenue')],
@@ -64,7 +65,7 @@ test('isMatrixifyEnabled should return true for valid metrics mode configuration
test('isMatrixifyEnabled should return true for valid dimensions mode configuration', () => {
const formData = {
viz_type: 'table',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_mode_rows: 'dimensions',
matrixify_mode_columns: 'dimensions',
matrixify_dimension_rows: { dimension: 'country', values: ['USA'] },
@@ -77,7 +78,7 @@ test('isMatrixifyEnabled should return true for valid dimensions mode configurat
test('isMatrixifyEnabled should return true for mixed mode configuration', () => {
const formData = {
viz_type: 'table',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_mode_rows: 'metrics',
matrixify_mode_columns: 'dimensions',
matrixify_rows: [createMetric('Revenue')],
@@ -90,7 +91,7 @@ test('isMatrixifyEnabled should return true for mixed mode configuration', () =>
test('isMatrixifyEnabled should return true for topn dimension selection mode', () => {
const formData = {
viz_type: 'table',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_mode_rows: 'dimensions',
matrixify_mode_columns: 'dimensions',
matrixify_dimension_rows: {
@@ -109,7 +110,7 @@ test('isMatrixifyEnabled should return true for topn dimension selection mode',
test('isMatrixifyEnabled should return false when both axes have empty metrics arrays', () => {
const formData = {
viz_type: 'table',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_mode_rows: 'metrics',
matrixify_mode_columns: 'metrics',
matrixify_rows: [],
@@ -122,7 +123,7 @@ test('isMatrixifyEnabled should return false when both axes have empty metrics a
test('isMatrixifyEnabled should return false when both dimensions have empty values and no topn mode', () => {
const formData = {
viz_type: 'table',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_mode_rows: 'dimensions',
matrixify_mode_columns: 'dimensions',
matrixify_dimension_rows: { dimension: 'country', values: [] },
@@ -140,7 +141,7 @@ test('getMatrixifyConfig should return null when no matrixify configuration exis
test('getMatrixifyConfig should return valid config for metrics mode', () => {
const formData = {
viz_type: 'table',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_mode_rows: 'metrics',
matrixify_mode_columns: 'metrics',
matrixify_rows: [createMetric('Revenue')],
@@ -158,7 +159,7 @@ test('getMatrixifyConfig should return valid config for metrics mode', () => {
test('getMatrixifyConfig should return valid config for dimensions mode', () => {
const formData = {
viz_type: 'table',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_mode_rows: 'dimensions',
matrixify_mode_columns: 'dimensions',
matrixify_dimension_rows: { dimension: 'country', values: ['USA'] },
@@ -182,7 +183,7 @@ test('getMatrixifyConfig should return valid config for dimensions mode', () =>
test('getMatrixifyConfig should handle topn selection mode', () => {
const formData = {
viz_type: 'table',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_mode_rows: 'dimensions',
matrixify_mode_columns: 'dimensions',
matrixify_dimension_rows: {
@@ -203,7 +204,8 @@ test('getMatrixifyConfig should handle topn selection mode', () => {
test('getMatrixifyValidationErrors should return empty array when matrixify is not enabled', () => {
const formData = {
viz_type: 'table',
matrixify_enabled: false,
matrixify_enable_vertical_layout: false,
matrixify_enable_horizontal_layout: false,
} as MatrixifyFormData;
expect(getMatrixifyValidationErrors(formData)).toEqual([]);
@@ -212,7 +214,7 @@ test('getMatrixifyValidationErrors should return empty array when matrixify is n
test('getMatrixifyValidationErrors should return empty array when properly configured', () => {
const formData = {
viz_type: 'table',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_mode_rows: 'metrics',
matrixify_mode_columns: 'metrics',
matrixify_rows: [createMetric('Revenue')],
@@ -225,7 +227,7 @@ test('getMatrixifyValidationErrors should return empty array when properly confi
test('getMatrixifyValidationErrors should return error when enabled but no configuration exists', () => {
const formData = {
viz_type: 'table',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
} as MatrixifyFormData;
const errors = getMatrixifyValidationErrors(formData);
@@ -235,7 +237,7 @@ test('getMatrixifyValidationErrors should return error when enabled but no confi
test('getMatrixifyValidationErrors should return error when metrics mode has no metrics', () => {
const formData = {
viz_type: 'table',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_mode_rows: 'metrics',
matrixify_rows: [],
matrixify_columns: [],
@@ -261,7 +263,7 @@ test('should handle empty form data object', () => {
test('should handle partial configuration with one axis only', () => {
const formData = {
viz_type: 'table',
matrixify_enabled: true,
matrixify_enable_vertical_layout: true,
matrixify_mode_rows: 'metrics',
matrixify_rows: [createMetric('Revenue')],
// No columns configuration

View File

@@ -96,8 +96,9 @@ export interface MatrixifyAxisConfig {
* Complete Matrixify configuration in form data
*/
export interface MatrixifyFormData {
// Enable/disable matrixify functionality
matrixify_enabled?: boolean;
// Layout enable controls
matrixify_enable_vertical_layout?: boolean;
matrixify_enable_horizontal_layout?: boolean;
// Row axis configuration
matrixify_mode_rows?: MatrixifyMode;
@@ -177,8 +178,12 @@ export function getMatrixifyConfig(
* Check if Matrixify is enabled and properly configured
*/
export function isMatrixifyEnabled(formData: MatrixifyFormData): boolean {
// First check if matrixify is explicitly enabled via checkbox
if (!formData.matrixify_enabled) {
// Check if either vertical or horizontal layout is enabled
const hasVerticalLayout = formData.matrixify_enable_vertical_layout === true;
const hasHorizontalLayout =
formData.matrixify_enable_horizontal_layout === true;
if (!hasVerticalLayout && !hasHorizontalLayout) {
return false;
}
@@ -216,7 +221,11 @@ export function getMatrixifyValidationErrors(
const errors: string[] = [];
// Only validate if matrixify is enabled
if (!formData.matrixify_enabled) {
const hasVerticalLayout = formData.matrixify_enable_vertical_layout === true;
const hasHorizontalLayout =
formData.matrixify_enable_horizontal_layout === true;
if (!hasVerticalLayout && !hasHorizontalLayout) {
return errors;
}

View File

@@ -19,8 +19,10 @@
import { useEffect, useState, FunctionComponent } from 'react';
import { t, styled, css, useTheme } from '@superset-ui/core';
import dayjs from 'dayjs';
import { Dayjs } from 'dayjs';
import { extendedDayjs } from '../../utils/dates';
import 'dayjs/plugin/updateLocale';
import 'dayjs/plugin/calendar';
import { Icons } from '../Icons';
import type { LastUpdatedProps } from './types';
@@ -46,9 +48,7 @@ export const LastUpdated: FunctionComponent<LastUpdatedProps> = ({
update,
}) => {
const theme = useTheme();
const [timeSince, setTimeSince] = useState<dayjs.Dayjs>(
extendedDayjs(updatedAt),
);
const [timeSince, setTimeSince] = useState<Dayjs>(extendedDayjs(updatedAt));
useEffect(() => {
setTimeSince(() => extendedDayjs(updatedAt));

View File

@@ -43,6 +43,7 @@ dayjs.updateLocale('en', {
});
export const extendedDayjs = dayjs;
export type { Dayjs };
export const fDuration = function (
t1: number,

View File

@@ -2,14 +2,8 @@
"compilerOptions": {
"composite": false,
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "."
},
"extends": "../../../tsconfig.json",
"include": ["**/*", "../types/**/*", "../../../types/**/*"],
"references": [
{
"path": ".."
}
]
"include": ["**/*", "../types/**/*", "../../../types/**/*"]
}

View File

@@ -1,24 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"declarationDir": "lib",
"outDir": "lib",
"rootDir": "src",
"baseUrl": ".",
"paths": {
"src/*": ["./src/*"],
"@superset-ui/core": ["src"],
"@superset-ui/core/*": ["src/*"]
}
"baseUrl": "../..",
"outDir": "lib"
},
"exclude": [
"lib",
"test"
],
"include": [
"src/**/*",
"spec/**/*",
"types/**/*"
],
"references": []
"include": ["src/**/*", "types/**/*"],
"exclude": ["src/**/*.test.*", "src/**/*.stories.*"],
"references": [{ "path": "../superset-core" }]
}

View File

@@ -19,3 +19,5 @@
declare module '*.gif';
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';

View File

@@ -1,18 +1,9 @@
{
"compilerOptions": {
"declarationDir": "lib",
"outDir": "lib",
"rootDir": "src"
},
"exclude": [
"lib",
"src/**/*.test.ts"
],
"extends": "../../tsconfig.json",
"include": [
"src/**/*",
"types/**/*",
"../../types/**/*"
],
"references": []
"compilerOptions": {
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["src/**/*.test.*", "src/**/*.stories.*"]
}

View File

@@ -0,0 +1,90 @@
/**
* 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.
*/
/// <reference types="node" />
// eslint-disable-next-line import/no-extraneous-dependencies
import { defineConfig } from '@playwright/test';
export default defineConfig({
// Test directory
testDir: './playwright/tests',
// Timeout settings
timeout: 30000,
expect: { timeout: 8000 },
// Parallel execution
fullyParallel: true,
workers: process.env.CI ? 2 : 1,
// Retry logic - 2 retries in CI, 0 locally
retries: process.env.CI ? 2 : 0,
// Reporter configuration - multiple reporters for better visibility
reporter: process.env.CI
? [
['github'], // GitHub Actions annotations
['list'], // Detailed output with summary table
['html', { outputFolder: 'playwright-report', open: 'never' }], // Interactive report
['json', { outputFile: 'test-results/results.json' }], // Machine-readable
]
: [
['list'], // Shows summary table locally
['html', { outputFolder: 'playwright-report', open: 'on-failure' }], // Auto-open on failure
],
// Global test setup
use: {
// Use environment variable for base URL in CI, default to localhost:8088 for local
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8088',
// Browser settings
headless: !!process.env.CI,
viewport: { width: 1280, height: 1024 },
// Screenshots and videos on failure
screenshot: 'only-on-failure',
video: 'retain-on-failure',
// Trace collection for debugging
trace: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: {
browserName: 'chromium',
testIdAttribute: 'data-test',
},
},
],
// Web server setup - disabled in CI (Flask started separately in workflow)
webServer: process.env.CI
? undefined
: {
command: 'curl -f http://localhost:8088/health',
url: 'http://localhost:8088/health',
reuseExistingServer: true,
timeout: 5000,
},
});

View File

@@ -0,0 +1,218 @@
<!--
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.
-->
# Playwright E2E Tests for Superset
This directory contains Playwright end-to-end tests for Apache Superset, designed as a replacement for the existing Cypress tests during the migration to Playwright.
## Architecture
```
playwright/
├── components/core/ # Reusable UI components
├── pages/ # Page Object Models
├── tests/ # Test files organized by feature
├── utils/ # Shared constants and utilities
└── README.md # This file
```
## Design Principles
We follow **YAGNI** (You Aren't Gonna Need It), **DRY** (Don't Repeat Yourself), and **KISS** (Keep It Simple, Stupid) principles:
- Build only what's needed now
- Reuse existing patterns and components
- Keep solutions simple and maintainable
## Component Architecture
### Core Components (`components/core/`)
Reusable UI interaction classes for common elements:
- **Form**: Container with properly scoped child element access
- **Input**: Supports `fill()`, `type()`, and `pressSequentially()` methods
- **Button**: Standard click, hover, focus interactions
**Usage Example:**
```typescript
import { Form } from '../components/core';
const loginForm = new Form(page, '[data-test="login-form"]');
const usernameInput = loginForm.getInput('[data-test="username-input"]');
await usernameInput.fill('admin');
```
### Page Objects (`pages/`)
Each page object encapsulates:
- **Actions**: What you can do on the page
- **Queries**: Information you can get from the page
- **Selectors**: Centralized in private static SELECTORS constant
- **NO Assertions**: Keep assertions in test files
**Page Object Pattern:**
```typescript
export class AuthPage {
// Selectors centralized in the page object
private static readonly SELECTORS = {
LOGIN_FORM: '[data-test="login-form"]',
USERNAME_INPUT: '[data-test="username-input"]',
} as const;
// Actions - what you can do
async loginWithCredentials(username: string, password: string) { }
// Queries - information you can get
async getCurrentUrl(): Promise<string> { }
// NO assertions - those belong in tests
}
```
### Tests (`tests/`)
Organized by feature/area (auth, dashboard, charts, etc.):
- Use page objects for actions
- Keep assertions in test files
- Import shared constants from `utils/`
**Test Pattern:**
```typescript
import { test, expect } from '@playwright/test';
import { AuthPage } from '../../pages/AuthPage';
import { LOGIN } from '../../utils/urls';
test('should login with correct credentials', async ({ page }) => {
const authPage = new AuthPage(page);
await authPage.goto();
await authPage.loginWithCredentials('admin', 'general');
// Assertions belong in tests, not page objects
expect(await authPage.getCurrentUrl()).not.toContain(LOGIN);
});
```
### Utilities (`utils/`)
Shared constants and utilities:
- **urls.ts**: URL paths and request patterns
- Keep flat exports (no premature namespacing)
## Contributing Guidelines
### Adding New Tests
1. **Check existing components** before creating new ones
2. **Use page objects** for page interactions
3. **Keep assertions in tests**, not page objects
4. **Follow naming conventions**: `feature.spec.ts`
### Adding New Components
1. **Follow YAGNI**: Only build what's immediately needed
2. **Use Locator-based scoping** for proper element isolation
3. **Support both string selectors and Locator objects** via constructor overloads
4. **Add to `components/core/index.ts`** for easy importing
### Adding New Page Objects
1. **Centralize selectors** in private static SELECTORS constant
2. **Import shared constants** from `utils/urls.ts`
3. **Actions and queries only** - no assertions
4. **Use existing components** for DOM interactions
## Running Tests
```bash
# Run all tests
npm run playwright:test
# or: npx playwright test
# Run specific test file
npx playwright test tests/auth/login.spec.ts
# Run with UI mode for debugging
npm run playwright:ui
# or: npx playwright test --ui
# Run in headed mode (see browser)
npm run playwright:headed
# or: npx playwright test --headed
# Debug specific test file
npm run playwright:debug tests/auth/login.spec.ts
# or: npx playwright test --debug tests/auth/login.spec.ts
```
## Test Reports
Playwright generates multiple reports for better visibility:
```bash
# View interactive HTML report (opens automatically on failure)
npm run playwright:report
# or: npx playwright show-report
# View test trace for debugging failures
npx playwright show-trace test-results/[test-name]/trace.zip
```
### Report Types
- **List Reporter**: Shows progress and summary table in terminal
- **HTML Report**: Interactive web interface with screenshots, videos, and traces
- **JSON Report**: Machine-readable format in `test-results/results.json`
- **GitHub Actions**: Annotations in CI for failed tests
### Debugging Failed Tests
When tests fail, Playwright automatically captures:
- **Screenshots** at the point of failure
- **Videos** of the entire test run
- **Traces** with timeline and network activity
- **Error context** with detailed debugging information
All debugging artifacts are available in the HTML report for easy analysis.
## Configuration
- **Config**: `playwright.config.ts` - matches Cypress settings
- **Base URL**: `http://localhost:8088` (assumes Superset running)
- **Browsers**: Chrome only for Phase 1 (YAGNI)
- **Retries**: 2 in CI, 0 locally (matches Cypress)
## Migration from Cypress
When porting Cypress tests:
1. **Port the logic**, not the implementation
2. **Use page objects** instead of inline selectors
3. **Replace `cy.intercept/cy.wait`** with `page.waitForRequest()`
4. **Use shared constants** from `utils/urls.ts`
5. **Follow the established patterns** shown in `tests/auth/login.spec.ts`
## Best Practices
- **Centralize selectors** in page objects
- **Centralize URLs** in `utils/urls.ts`
- **Use meaningful test descriptions**
- **Keep page objects action-focused**
- **Put assertions in tests, not page objects**
- **Follow the existing patterns** for consistency

View File

@@ -0,0 +1,119 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Locator, Page } from '@playwright/test';
export class Button {
private readonly locator: Locator;
constructor(page: Page, selector: string);
constructor(page: Page, locator: Locator);
constructor(page: Page, selectorOrLocator: string | Locator) {
if (typeof selectorOrLocator === 'string') {
this.locator = page.locator(selectorOrLocator);
} else {
this.locator = selectorOrLocator;
}
}
/**
* Gets the button element locator
*/
get element(): Locator {
return this.locator;
}
/**
* Clicks the button
* @param options - Optional click options
*/
async click(options?: {
timeout?: number;
force?: boolean;
delay?: number;
button?: 'left' | 'right' | 'middle';
}): Promise<void> {
await this.element.click(options);
}
/**
* Gets the button text content
*/
async getText(): Promise<string> {
return (await this.element.textContent()) ?? '';
}
/**
* Gets a specific attribute value from the button
* @param attribute - The attribute name to retrieve
*/
async getAttribute(attribute: string): Promise<string | null> {
return this.element.getAttribute(attribute);
}
/**
* Checks if the button is visible
*/
async isVisible(): Promise<boolean> {
return this.element.isVisible();
}
/**
* Checks if the button is enabled
*/
async isEnabled(): Promise<boolean> {
return this.element.isEnabled();
}
/**
* Checks if the button is disabled
*/
async isDisabled(): Promise<boolean> {
return this.element.isDisabled();
}
/**
* Hovers over the button
* @param options - Optional hover options
*/
async hover(options?: { timeout?: number; force?: boolean }): Promise<void> {
await this.element.hover(options);
}
/**
* Focuses on the button
*/
async focus(): Promise<void> {
await this.element.focus();
}
/**
* Double clicks the button
* @param options - Optional click options
*/
async doubleClick(options?: {
timeout?: number;
force?: boolean;
delay?: number;
}): Promise<void> {
await this.element.dblclick(options);
}
}

View File

@@ -0,0 +1,110 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Locator, Page } from '@playwright/test';
import { Input } from './Input';
import { Button } from './Button';
export class Form {
private readonly page: Page;
private readonly locator: Locator;
constructor(page: Page, selector: string);
constructor(page: Page, locator: Locator);
constructor(page: Page, selectorOrLocator: string | Locator) {
this.page = page;
if (typeof selectorOrLocator === 'string') {
this.locator = page.locator(selectorOrLocator);
} else {
this.locator = selectorOrLocator;
}
}
/**
* Gets the form element locator
*/
get element(): Locator {
return this.locator;
}
/**
* Gets an input field within the form (properly scoped)
* @param inputSelector - Selector for the input field
*/
getInput(inputSelector: string): Input {
const scopedLocator = this.locator.locator(inputSelector);
return new Input(this.page, scopedLocator);
}
/**
* Gets a button within the form (properly scoped)
* @param buttonSelector - Selector for the button
*/
getButton(buttonSelector: string): Button {
const scopedLocator = this.locator.locator(buttonSelector);
return new Button(this.page, scopedLocator);
}
/**
* Checks if the form is visible
*/
async isVisible(): Promise<boolean> {
return this.locator.isVisible();
}
/**
* Submits the form (triggers submit event)
*/
async submit(): Promise<void> {
await this.locator.evaluate((form: HTMLElement) => {
if (form instanceof HTMLFormElement) {
form.submit();
}
});
}
/**
* Waits for the form to be visible
* @param options - Optional wait options
*/
async waitForVisible(options?: { timeout?: number }): Promise<void> {
await this.locator.waitFor({ state: 'visible', ...options });
}
/**
* Gets all form data as key-value pairs
* Useful for validation and debugging
*/
async getFormData(): Promise<Record<string, string>> {
return this.locator.evaluate((form: HTMLElement) => {
if (form instanceof HTMLFormElement) {
const formData = new FormData(form);
const result: Record<string, string> = {};
formData.forEach((value, key) => {
result[key] = value.toString();
});
return result;
}
return {};
});
}
}

View File

@@ -0,0 +1,111 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Locator, Page } from '@playwright/test';
export class Input {
private readonly locator: Locator;
constructor(page: Page, selector: string);
constructor(page: Page, locator: Locator);
constructor(page: Page, selectorOrLocator: string | Locator) {
if (typeof selectorOrLocator === 'string') {
this.locator = page.locator(selectorOrLocator);
} else {
this.locator = selectorOrLocator;
}
}
/**
* Gets the input element locator
*/
get element(): Locator {
return this.locator;
}
/**
* Fast fill - clears the input and sets the value directly
* @param value - The value to fill
* @param options - Optional fill options
*/
async fill(
value: string,
options?: { timeout?: number; force?: boolean },
): Promise<void> {
await this.element.fill(value, options);
}
/**
* Types text character by character (simulates real typing)
* @param text - The text to type
* @param options - Optional typing options
*/
async type(text: string, options?: { delay?: number }): Promise<void> {
await this.element.type(text, options);
}
/**
* Types text sequentially with more control over timing
* @param text - The text to type
* @param options - Optional sequential typing options
*/
async pressSequentially(
text: string,
options?: { delay?: number },
): Promise<void> {
await this.element.pressSequentially(text, options);
}
/**
* Gets the current value of the input
*/
async getValue(): Promise<string> {
return this.element.inputValue();
}
/**
* Clears the input field
*/
async clear(): Promise<void> {
await this.element.clear();
}
/**
* Checks if the input is visible
*/
async isVisible(): Promise<boolean> {
return this.element.isVisible();
}
/**
* Checks if the input is enabled
*/
async isEnabled(): Promise<boolean> {
return this.element.isEnabled();
}
/**
* Focuses on the input field
*/
async focus(): Promise<void> {
await this.element.focus();
}
}

View File

@@ -0,0 +1,23 @@
/**
* 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.
*/
// Core Playwright Components for Superset
export { Button } from './Button';
export { Form } from './Form';
export { Input } from './Input';

View File

@@ -0,0 +1,122 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Page, Response } from '@playwright/test';
import { Form } from '../components/core';
import { URL } from '../utils/urls';
export class AuthPage {
private readonly page: Page;
private readonly loginForm: Form;
// Selectors specific to the auth/login page
private static readonly SELECTORS = {
LOGIN_FORM: '[data-test="login-form"]',
USERNAME_INPUT: '[data-test="username-input"]',
PASSWORD_INPUT: '[data-test="password-input"]',
LOGIN_BUTTON: '[data-test="login-button"]',
ERROR_SELECTORS: [
'[role="alert"]',
'.ant-form-item-explain-error',
'.ant-form-item-explain.ant-form-item-explain-error',
'.alert-danger',
],
} as const;
constructor(page: Page) {
this.page = page;
this.loginForm = new Form(page, AuthPage.SELECTORS.LOGIN_FORM);
}
/**
* Navigate to the login page
*/
async goto(): Promise<void> {
await this.page.goto(URL.LOGIN);
}
/**
* Wait for login form to be visible
*/
async waitForLoginForm(): Promise<void> {
await this.loginForm.waitForVisible({ timeout: 5000 });
}
/**
* Login with provided credentials
* @param username - Username to enter
* @param password - Password to enter
*/
async loginWithCredentials(
username: string,
password: string,
): Promise<void> {
const usernameInput = this.loginForm.getInput(
AuthPage.SELECTORS.USERNAME_INPUT,
);
const passwordInput = this.loginForm.getInput(
AuthPage.SELECTORS.PASSWORD_INPUT,
);
const loginButton = this.loginForm.getButton(
AuthPage.SELECTORS.LOGIN_BUTTON,
);
await usernameInput.fill(username);
await passwordInput.fill(password);
await loginButton.click();
}
/**
* Get current page URL
*/
async getCurrentUrl(): Promise<string> {
return this.page.url();
}
/**
* Get the session cookie specifically
*/
async getSessionCookie(): Promise<{ name: string; value: string } | null> {
const cookies = await this.page.context().cookies();
return cookies.find((c: any) => c.name === 'session') || null;
}
/**
* Check if login form has validation errors
*/
async hasLoginError(): Promise<boolean> {
const visibilityPromises = AuthPage.SELECTORS.ERROR_SELECTORS.map(
selector => this.page.locator(selector).isVisible(),
);
const visibilityResults = await Promise.all(visibilityPromises);
return visibilityResults.some((isVisible: any) => isVisible);
}
/**
* Wait for a login request to be made and return the response
*/
async waitForLoginRequest(): Promise<Response> {
return this.page.waitForResponse(
(response: any) =>
response.url().includes('/login/') &&
response.request().method() === 'POST',
);
}
}

View File

@@ -0,0 +1,88 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { test, expect } from '@playwright/test';
import { AuthPage } from '../../pages/AuthPage';
import { URL } from '../../utils/urls';
test.describe('Login view', () => {
let authPage: AuthPage;
test.beforeEach(async ({ page }: any) => {
authPage = new AuthPage(page);
await authPage.goto();
await authPage.waitForLoginForm();
});
test('should redirect to login with incorrect username and password', async ({
page,
}: any) => {
// Setup request interception before login attempt
const loginRequestPromise = authPage.waitForLoginRequest();
// Attempt login with incorrect credentials
await authPage.loginWithCredentials('admin', 'wrongpassword');
// Wait for login request and verify response
const loginResponse = await loginRequestPromise;
// Failed login returns 401 Unauthorized or 302 redirect to login
expect([401, 302]).toContain(loginResponse.status());
// Wait for redirect to complete before checking URL
await page.waitForURL((url: any) => url.pathname.endsWith('login/'), {
timeout: 10000,
});
// Verify we stay on login page
const currentUrl = await authPage.getCurrentUrl();
expect(currentUrl).toContain(URL.LOGIN);
// Verify error message is shown
const hasError = await authPage.hasLoginError();
expect(hasError).toBe(true);
});
test('should login with correct username and password', async ({
page,
}: any) => {
// Setup request interception before login attempt
const loginRequestPromise = authPage.waitForLoginRequest();
// Login with correct credentials
await authPage.loginWithCredentials('admin', 'general');
// Wait for login request and verify response
const loginResponse = await loginRequestPromise;
// Successful login returns 302 redirect
expect(loginResponse.status()).toBe(302);
// Wait for successful redirect to welcome page
await page.waitForURL(
(url: any) => url.pathname.endsWith('superset/welcome/'),
{
timeout: 10000,
},
);
// Verify specific session cookie exists
const sessionCookie = await authPage.getSessionCookie();
expect(sessionCookie).not.toBeNull();
expect(sessionCookie?.value).toBeTruthy();
});
});

View File

@@ -0,0 +1,23 @@
/**
* 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.
*/
export const URL = {
LOGIN: 'login/',
WELCOME: 'superset/welcome/',
} as const;

View File

@@ -1,14 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": "."
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -1,14 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": "."
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -1,14 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": "."
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -1,14 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": "."
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -2,18 +2,8 @@
"compilerOptions": {
"composite": false,
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "."
},
"extends": "../../../tsconfig.json",
"include": [
"**/*",
"../types/**/*",
"../../../types/**/*"
],
"references": [
{
"path": ".."
}
]
"include": ["**/*", "../types/**/*", "../../../types/**/*"]
}

View File

@@ -1,14 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": "."
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -1,14 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": "."
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -1,19 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": ".",
"paths": {
"d3v3": ["./types/d3v3"]
}
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -2,18 +2,8 @@
"compilerOptions": {
"composite": false,
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "."
},
"extends": "../../../tsconfig.json",
"include": [
"**/*",
"../types/**/*",
"../../../types/**/*"
],
"references": [
{
"path": ".."
}
]
"include": ["**/*", "../types/**/*", "../../../types/**/*"]
}

View File

@@ -1,17 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": ".",
"paths": {
"@superset-ui/core/components": ["../../packages/superset-ui-core/src/components"]
}
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -1,14 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": "."
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -1,14 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": "."
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -0,0 +1,355 @@
/**
* 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.
*/
// eslint-disable-next-line import/no-extraneous-dependencies
import { render, screen } from '@testing-library/react';
// eslint-disable-next-line import/no-extraneous-dependencies
import '@testing-library/jest-dom';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
import DeckGLPolygon, { getPoints } from './Polygon';
import { COLOR_SCHEME_TYPES } from '../../utilities/utils';
import * as utils from '../../utils';
// Mock the utils functions
const mockGetBuckets = jest.spyOn(utils, 'getBuckets');
const mockGetColorBreakpointsBuckets = jest.spyOn(
utils,
'getColorBreakpointsBuckets',
);
// Mock DeckGL container and Legend
jest.mock('../../DeckGLContainer', () => ({
DeckGLContainerStyledWrapper: ({ children }: any) => (
<div data-testid="deckgl-container">{children}</div>
),
}));
jest.mock('../../components/Legend', () => ({ categories, position }: any) => (
<div
data-testid="legend"
data-categories={JSON.stringify(categories)}
data-position={position}
>
Legend Mock
</div>
));
const mockProps = {
formData: {
// Required QueryFormData properties
datasource: 'test_datasource',
viz_type: 'deck_polygon',
// Polygon-specific properties
metric: { label: 'population' },
color_scheme_type: COLOR_SCHEME_TYPES.linear_palette,
legend_position: 'tr',
legend_format: '.2f',
autozoom: false,
mapbox_style: 'mapbox://styles/mapbox/light-v9',
opacity: 80,
filled: true,
stroked: true,
extruded: false,
line_width: 1,
line_width_unit: 'pixels',
multiplier: 1,
break_points: [],
num_buckets: '5',
linear_color_scheme: 'blue_white_yellow',
},
payload: {
data: {
features: [
{
population: 100000,
polygon: [
[0, 0],
[1, 0],
[1, 1],
[0, 1],
],
},
{
population: 200000,
polygon: [
[2, 2],
[3, 2],
[3, 3],
[2, 3],
],
},
],
mapboxApiKey: 'test-key',
},
form_data: {},
},
setControlValue: jest.fn(),
viewport: { longitude: 0, latitude: 0, zoom: 1 },
onAddFilter: jest.fn(),
width: 800,
height: 600,
onContextMenu: jest.fn(),
setDataMask: jest.fn(),
filterState: undefined,
emitCrossFilters: false,
};
describe('DeckGLPolygon bucket generation logic', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGetBuckets.mockReturnValue({
'100000 - 150000': { color: [0, 100, 200], enabled: true },
'150000 - 200000': { color: [50, 150, 250], enabled: true },
});
mockGetColorBreakpointsBuckets.mockReturnValue({});
});
const renderWithTheme = (component: React.ReactElement) =>
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
test('should use getBuckets for linear_palette color scheme', () => {
const propsWithLinearPalette = {
...mockProps,
formData: {
...mockProps.formData,
color_scheme_type: COLOR_SCHEME_TYPES.linear_palette,
},
};
renderWithTheme(<DeckGLPolygon {...propsWithLinearPalette} />);
// Should call getBuckets, not getColorBreakpointsBuckets
expect(mockGetBuckets).toHaveBeenCalled();
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
});
test('should use getBuckets for fixed_color color scheme', () => {
const propsWithFixedColor = {
...mockProps,
formData: {
...mockProps.formData,
color_scheme_type: COLOR_SCHEME_TYPES.fixed_color,
},
};
renderWithTheme(<DeckGLPolygon {...propsWithFixedColor} />);
// Should call getBuckets, not getColorBreakpointsBuckets
expect(mockGetBuckets).toHaveBeenCalled();
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
});
test('should use getColorBreakpointsBuckets for color_breakpoints scheme', () => {
const propsWithBreakpoints = {
...mockProps,
formData: {
...mockProps.formData,
color_scheme_type: COLOR_SCHEME_TYPES.color_breakpoints,
color_breakpoints: [
{
minValue: 0,
maxValue: 100000,
color: { r: 255, g: 0, b: 0, a: 100 },
},
{
minValue: 100001,
maxValue: 200000,
color: { r: 0, g: 255, b: 0, a: 100 },
},
],
},
};
mockGetColorBreakpointsBuckets.mockReturnValue({
'0 - 100000': { color: [255, 0, 0], enabled: true },
'100001 - 200000': { color: [0, 255, 0], enabled: true },
});
renderWithTheme(<DeckGLPolygon {...propsWithBreakpoints} />);
// Should call getColorBreakpointsBuckets, not getBuckets
expect(mockGetColorBreakpointsBuckets).toHaveBeenCalled();
expect(mockGetBuckets).not.toHaveBeenCalled();
});
test('should use getBuckets when color_scheme_type is undefined (backward compatibility)', () => {
const propsWithUndefinedScheme = {
...mockProps,
formData: {
...mockProps.formData,
color_scheme_type: undefined,
},
};
renderWithTheme(<DeckGLPolygon {...propsWithUndefinedScheme} />);
// Should call getBuckets for backward compatibility
expect(mockGetBuckets).toHaveBeenCalled();
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
});
test('should use getBuckets for unsupported color schemes (categorical_palette)', () => {
const propsWithUnsupportedScheme = {
...mockProps,
formData: {
...mockProps.formData,
color_scheme_type: COLOR_SCHEME_TYPES.categorical_palette,
},
};
renderWithTheme(<DeckGLPolygon {...propsWithUnsupportedScheme} />);
// Should fall back to getBuckets for unsupported color schemes
expect(mockGetBuckets).toHaveBeenCalled();
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
});
});
describe('DeckGLPolygon Error Handling and Edge Cases', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGetBuckets.mockReturnValue({});
mockGetColorBreakpointsBuckets.mockReturnValue({});
});
const renderWithTheme = (component: React.ReactElement) =>
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
test('handles empty features data gracefully', () => {
const propsWithEmptyData = {
...mockProps,
payload: {
...mockProps.payload,
data: {
...mockProps.payload.data,
features: [],
},
},
};
renderWithTheme(<DeckGLPolygon {...propsWithEmptyData} />);
// Should still call getBuckets with empty data
expect(mockGetBuckets).toHaveBeenCalled();
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
});
test('handles missing color_breakpoints for color_breakpoints scheme', () => {
const propsWithMissingBreakpoints = {
...mockProps,
formData: {
...mockProps.formData,
color_scheme_type: COLOR_SCHEME_TYPES.color_breakpoints,
color_breakpoints: undefined,
},
};
renderWithTheme(<DeckGLPolygon {...propsWithMissingBreakpoints} />);
// Should call getColorBreakpointsBuckets even with undefined breakpoints
expect(mockGetColorBreakpointsBuckets).toHaveBeenCalledWith(undefined);
expect(mockGetBuckets).not.toHaveBeenCalled();
});
test('handles null legend_position correctly', () => {
const propsWithNullLegendPosition = {
...mockProps,
formData: {
...mockProps.formData,
legend_position: null,
},
};
renderWithTheme(<DeckGLPolygon {...propsWithNullLegendPosition} />);
// Legend should not be rendered when position is null
expect(screen.queryByTestId('legend')).not.toBeInTheDocument();
});
});
describe('DeckGLPolygon Legend Integration', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGetBuckets.mockReturnValue({
'100000 - 150000': { color: [0, 100, 200], enabled: true },
'150000 - 200000': { color: [50, 150, 250], enabled: true },
});
});
const renderWithTheme = (component: React.ReactElement) =>
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
test('renders legend with non-empty categories when metric and linear_palette are defined', () => {
const { container } = renderWithTheme(<DeckGLPolygon {...mockProps} />);
// Verify the component renders and calls the correct bucket function
expect(mockGetBuckets).toHaveBeenCalled();
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
// Verify the legend mock was rendered with non-empty categories
const legendElement = container.querySelector('[data-testid="legend"]');
expect(legendElement).toBeTruthy();
const categoriesAttr = legendElement?.getAttribute('data-categories');
const categoriesData = JSON.parse(categoriesAttr || '{}');
expect(Object.keys(categoriesData)).toHaveLength(2);
});
test('does not render legend when metric is null', () => {
const propsWithoutMetric = {
...mockProps,
formData: {
...mockProps.formData,
metric: null,
},
};
renderWithTheme(<DeckGLPolygon {...propsWithoutMetric} />);
// Legend should not be rendered when no metric is defined
expect(screen.queryByTestId('legend')).not.toBeInTheDocument();
});
});
describe('getPoints utility', () => {
test('extracts points from polygon data', () => {
const data = [
{
polygon: [
[0, 0],
[1, 0],
[1, 1],
[0, 1],
],
},
{
polygon: [
[2, 2],
[3, 2],
[3, 3],
[2, 3],
],
},
];
const points = getPoints(data);
expect(points).toHaveLength(8); // 4 points per polygon * 2 polygons
expect(points[0]).toEqual([0, 0]);
expect(points[4]).toEqual([2, 2]);
});
});

View File

@@ -335,9 +335,10 @@ const DeckGLPolygon = (props: DeckGLPolygonProps) => {
const accessor = (d: JsonObject) => d[metricLabel];
const colorSchemeType = formData.color_scheme_type;
const buckets = colorSchemeType
? getColorBreakpointsBuckets(formData.color_breakpoints)
: getBuckets(formData, payload.data.features, accessor);
const buckets =
colorSchemeType === COLOR_SCHEME_TYPES.color_breakpoints
? getColorBreakpointsBuckets(formData.color_breakpoints)
: getBuckets(formData, payload.data.features, accessor);
return (
<div style={{ position: 'relative' }}>

View File

@@ -18,9 +18,9 @@
*/
import { useEffect, useState, memo } from 'react';
import { styled, t } from '@superset-ui/core';
import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
import { SafeMarkdown } from '@superset-ui/core/components';
import Handlebars from 'handlebars';
import dayjs from 'dayjs';
import { isPlainObject } from 'lodash';
export interface HandlebarsRendererProps {

View File

@@ -1,17 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": ".",
"paths": {
"@superset-ui/core/components": ["../../packages/superset-ui-core/src/components"]
}
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"exclude": ["src/**/*.test.*", "src/**/*.stories.*"],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -19,7 +19,6 @@
*/
import { kebabCase, throttle } from 'lodash';
import d3 from 'd3';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import nv from 'nvd3-fork';
import PropTypes from 'prop-types';
@@ -34,6 +33,7 @@ import {
t,
VizType,
} from '@superset-ui/core';
import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
import 'nvd3-fork/build/nv.d3.css';

View File

@@ -2,18 +2,8 @@
"compilerOptions": {
"composite": false,
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "."
},
"extends": "../../../tsconfig.json",
"include": [
"**/*",
"../types/**/*",
"../../../types/**/*"
],
"references": [
{
"path": ".."
}
]
"include": ["**/*", "../types/**/*", "../../../types/**/*"]
}

View File

@@ -1,14 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": "."
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"exclude": ["src/**/*.test.*", "src/**/*.stories.*"],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -36,6 +36,7 @@
"xss": "^1.0.15"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@ant-design/icons": "^5.2.6",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",

View File

@@ -30,7 +30,7 @@ export const useIsDark = () => {
return tinycolor(theme.colorBgContainer).isDark();
};
const useTableTheme = () => {
const useTableTheme = (): ReturnType<typeof themeQuartz.withPart> => {
const baseTheme = themeQuartz;
const isDarkTheme = useIsDark();
const tableTheme = isDarkTheme

View File

@@ -1,18 +1,19 @@
{
"compilerOptions": {
"declarationDir": "lib",
"outDir": "lib",
"rootDir": "src"
},
"exclude": ["lib", "test"],
"extends": "../../tsconfig.json",
"include": ["src/**/*", "types/**/*", "../../types/**/*"],
"compilerOptions": {
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{
"path": "../../packages/superset-ui-chart-controls"
},
{
"path": "../../packages/superset-ui-core"
}
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]
}

View File

@@ -2,21 +2,8 @@
"compilerOptions": {
"composite": false,
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "."
},
"extends": "../../../tsconfig.json",
"include": [
"**/*",
"../types/**/*",
"../../../types/**/*"
],
"references": [
{
"path": "../../../packages/superset-ui-chart-controls"
},
{
"path": "../../../packages/superset-ui-core"
},
]
"include": ["**/*", "../types/**/*", "../../../types/**/*"]
}

View File

@@ -1,14 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": "."
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -30,6 +30,7 @@
"lodash": "^4.17.21"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"echarts": "*",

View File

@@ -16,8 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import { Metric } from '@superset-ui/chart-controls';
import {
ChartProps,
@@ -27,6 +25,8 @@ import {
SimpleAdhocFilter,
ensureIsArray,
} from '@superset-ui/core';
import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
import 'dayjs/plugin/utc';
import {
getComparisonFontSize,
getHeaderFontSize,
@@ -35,8 +35,6 @@ import {
import { getOriginalLabel } from '../utils';
dayjs.extend(utc);
export const parseMetricValue = (metricValue: number | string | null) => {
if (typeof metricValue === 'string') {
const dateObject = dayjs.utc(metricValue, undefined, true);

View File

@@ -25,7 +25,6 @@ import {
getXAxisLabel,
Metric,
getValueFormatter,
supersetTheme,
t,
tooltipHtml,
} from '@superset-ui/core';
@@ -281,7 +280,7 @@ export default function transformProps(
},
{
offset: 1,
color: supersetTheme.colorBgContainer,
color: 'transparent',
},
]),
},

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import {
getTimeFormatter,
@@ -29,6 +28,7 @@ import {
SMART_DATE_ID,
TimeGranularity,
} from '@superset-ui/core';
import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
dayjs.extend(utc);

View File

@@ -19,7 +19,6 @@
import { t } from '@superset-ui/core';
import {
ControlPanelConfig,
ControlStateMapping,
ControlSubSectionHeader,
D3_FORMAT_DOCS,
D3_FORMAT_OPTIONS,
@@ -197,15 +196,6 @@ const config: ControlPanelConfig = {
],
},
],
onInit(state: ControlStateMapping) {
return {
...state,
row_limit: {
...state.row_limit,
value: state.row_limit.default,
},
};
},
formDataOverrides: formData => ({
...formData,
metric: getStandardizedControls().shiftMetric(),

View File

@@ -33,8 +33,8 @@ import {
t,
tooltipHtml,
} from '@superset-ui/core';
import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
import { CallbackDataParams } from 'echarts/types/src/util/types';
import dayjs from 'dayjs';
import {
Cartesian2dCoordSys,
EchartsGanttChartProps,
@@ -325,6 +325,7 @@ export default function transformProps(chartProps: EchartsGanttChartProps) {
show: true,
position: 'start',
formatter: '{b}',
color: theme.colorText,
},
data: categoryLines,
},

View File

@@ -27,7 +27,6 @@ import {
getValueFormatter,
rgbToHex,
addAlpha,
supersetTheme,
tooltipHtml,
} from '@superset-ui/core';
import memoizeOne from 'memoize-one';
@@ -78,7 +77,8 @@ export default function transformProps(
chartProps: HeatmapChartProps,
): HeatmapTransformedProps {
const refs: Refs = {};
const { width, height, formData, queriesData, datasource } = chartProps;
const { width, height, formData, queriesData, datasource, theme } =
chartProps;
const {
bottomMargin,
xAxis,
@@ -176,9 +176,9 @@ export default function transformProps(
},
emphasis: {
itemStyle: {
borderColor: supersetTheme.colorBgContainer,
borderColor: 'transparent',
shadowBlur: 10,
shadowColor: supersetTheme.colorTextBase,
shadowColor: addAlpha(theme.colorText, 0.3),
},
},
},

View File

@@ -47,7 +47,10 @@ import {
isDerivedSeries,
} from '@superset-ui/chart-controls';
import type { EChartsCoreOption } from 'echarts/core';
import type { LineStyleOption } from 'echarts/types/src/util/types';
import type {
LineStyleOption,
CallbackDataParams,
} from 'echarts/types/src/util/types';
import type { SeriesOption } from 'echarts';
import {
EchartsTimeseriesChartProps,
@@ -575,16 +578,31 @@ export default function transformProps(
const xValue: number = richTooltip
? params[0].value[xIndex]
: params.value[xIndex];
const forecastValue: any[] = richTooltip ? params : [params];
const forecastValue: CallbackDataParams[] = richTooltip
? params
: [params];
const sortedKeys = extractTooltipKeys(
forecastValue,
yIndex,
richTooltip,
tooltipSortByMetric,
);
const filteredForecastValue = forecastValue.filter(
(item: CallbackDataParams) =>
!annotationLayers.some(
(annotation: AnnotationLayer) =>
item.seriesName === annotation.name,
),
);
const forecastValues: Record<string, ForecastValue> =
extractForecastValuesFromTooltipParams(forecastValue, isHorizontal);
const filteredForecastValues: Record<string, ForecastValue> =
extractForecastValuesFromTooltipParams(
filteredForecastValue,
isHorizontal,
);
const isForecast = Object.values(forecastValues).some(
value =>
value.forecastTrend || value.forecastLower || value.forecastUpper,
@@ -595,7 +613,7 @@ export default function transformProps(
: (getCustomFormatter(customFormatters, metrics) ?? defaultFormatter);
const rows: string[][] = [];
const total = Object.values(forecastValues).reduce(
const total = Object.values(filteredForecastValues).reduce(
(acc, value) =>
value.observation !== undefined ? acc + value.observation : acc,
0,
@@ -617,7 +635,16 @@ export default function transformProps(
seriesName: key,
formatter,
});
if (showPercentage && value.observation !== undefined) {
const annotationRow = annotationLayers.some(
item => item.name === key,
);
if (
showPercentage &&
value.observation !== undefined &&
!annotationRow
) {
row.push(
percentFormatter.format(value.observation / (total || 1)),
);

View File

@@ -58,41 +58,109 @@ const config: ControlPanelConfig = {
},
},
],
],
},
{
label: t('Series settings'),
expanded: true,
controlSetRows: [
[
<ControlSubSectionHeader>
{t('Series colors')}
{t('Series increase setting')}
</ControlSubSectionHeader>,
],
[
{
name: 'increase_color',
config: {
label: t('Increase'),
label: t('Increase color'),
type: 'ColorPickerControl',
default: { r: 90, g: 193, b: 137, a: 1 },
renderTrigger: true,
description: t(
'Select the color used for values that indicate an increase in the chart',
),
},
},
{
name: 'decrease_color',
name: 'increase_label',
config: {
label: t('Decrease'),
type: 'ColorPickerControl',
default: { r: 224, g: 67, b: 85, a: 1 },
renderTrigger: true,
},
},
{
name: 'total_color',
config: {
label: t('Total'),
type: 'ColorPickerControl',
default: { r: 102, g: 102, b: 102, a: 1 },
label: t('Increase label'),
type: 'TextControl',
renderTrigger: true,
description: t(
'Customize the label displayed for increasing values in the chart tooltips and legend.',
),
},
},
],
[<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],
[
<ControlSubSectionHeader>
{t('Series decrease setting')}
</ControlSubSectionHeader>,
],
[
{
name: 'decrease_color',
config: {
label: t('Decrease color'),
type: 'ColorPickerControl',
default: { r: 224, g: 67, b: 85, a: 1 },
renderTrigger: true,
description: t(
'Select the color used for values that indicate a decrease in the chart.',
),
},
},
{
name: 'decrease_label',
config: {
label: t('Decrease label'),
type: 'TextControl',
renderTrigger: true,
description: t(
'Customize the label displayed for decreasing values in the chart tooltips and legend.',
),
},
},
],
[
<ControlSubSectionHeader>
{t('Series total setting')}
</ControlSubSectionHeader>,
],
[
{
name: 'total_color',
config: {
label: t('Total color'),
type: 'ColorPickerControl',
default: { r: 102, g: 102, b: 102, a: 1 },
renderTrigger: true,
description: t(
'Select the color used for values that represent total bars in the chart',
),
},
},
{
name: 'total_label',
config: {
label: t('Total label'),
type: 'TextControl',
renderTrigger: true,
description: t(
'Customize the label displayed for total values in the chart tooltips, legend, and chart axis.',
),
},
},
],
],
},
{
label: t('X Axis'),
expanded: true,
controlSetRows: [
[
{
name: 'x_axis_label',
@@ -134,7 +202,12 @@ const config: ControlPanelConfig = {
},
},
],
[<ControlSubSectionHeader>{t('Y Axis')}</ControlSubSectionHeader>],
],
},
{
label: t('Y Axis'),
expanded: true,
controlSetRows: [
[
{
name: 'y_axis_label',

View File

@@ -51,11 +51,13 @@ function formatTooltip({
breakdownName,
defaultFormatter,
xAxisFormatter,
totalMark,
}: {
params: ICallbackDataParams[];
breakdownName?: string;
defaultFormatter: NumberFormatter | CurrencyFormatter;
xAxisFormatter: (value: number | string, index: number) => string;
totalMark: string;
}) {
const series = params.find(
param => param.seriesName !== ASSIST_MARK && param.data.value !== TOKEN,
@@ -66,7 +68,7 @@ function formatTooltip({
return '';
}
const isTotal = series?.seriesName === LEGEND.TOTAL;
const isTotal = series?.seriesName === totalMark;
if (!series) {
return NULL_STRING;
}
@@ -82,7 +84,7 @@ function formatTooltip({
defaultFormatter(series.data.originalValue),
]);
}
rows.push([TOTAL_MARK, defaultFormatter(series.data.totalSum)]);
rows.push([totalMark, defaultFormatter(series.data.totalSum)]);
return tooltipHtml(rows, title);
}
@@ -91,11 +93,13 @@ function transformer({
xAxis,
metric,
breakdown,
totalMark,
}: {
data: DataRecord[];
xAxis: string;
metric: string;
breakdown?: string;
totalMark: string;
}) {
// Group by series (temporary map)
const groupedData = data.reduce((acc, cur) => {
@@ -119,7 +123,7 @@ function transformer({
// Push total per period to the end of period values array
tempValue.push({
[xAxis]: key,
[breakdown]: TOTAL_MARK,
[breakdown]: totalMark,
[metric]: sum,
});
transformedData.push(...tempValue);
@@ -138,7 +142,7 @@ function transformer({
total += sum;
});
transformedData.push({
[xAxis]: TOTAL_MARK,
[xAxis]: totalMark,
[metric]: total,
});
}
@@ -179,11 +183,21 @@ export default function transformProps(
xAxisLabel,
yAxisFormat,
showValue,
totalLabel,
increaseLabel,
decreaseLabel,
} = formData;
const defaultFormatter = currencyFormat?.symbol
? new CurrencyFormatter({ d3Format: yAxisFormat, currency: currencyFormat })
: getNumberFormatter(yAxisFormat);
const totalMark = totalLabel || TOTAL_MARK;
const legendNames = {
INCREASE: increaseLabel || LEGEND.INCREASE,
DECREASE: decreaseLabel || LEGEND.DECREASE,
TOTAL: totalLabel || LEGEND.TOTAL,
};
const seriesformatter = (params: ICallbackDataParams) => {
const { data } = params;
const { originalValue } = data;
@@ -205,6 +219,7 @@ export default function transformProps(
breakdown: breakdownName,
xAxis: xAxisName,
metric: metricLabel,
totalMark,
});
const assistData: ISeriesData[] = [];
@@ -217,18 +232,18 @@ export default function transformProps(
transformedData.forEach((datum, index, self) => {
const totalSum = self.slice(0, index + 1).reduce((prev, cur, i) => {
if (breakdownName) {
if (cur[breakdownName] !== TOTAL_MARK || i === 0) {
if (cur[breakdownName] !== totalMark || i === 0) {
return prev + ((cur[metricLabel] as number) ?? 0);
}
} else if (cur[xAxisName] !== TOTAL_MARK) {
} else if (cur[xAxisName] !== totalMark) {
return prev + ((cur[metricLabel] as number) ?? 0);
}
return prev;
}, 0);
const isTotal =
(breakdownName && datum[breakdownName] === TOTAL_MARK) ||
datum[xAxisName] === TOTAL_MARK;
(breakdownName && datum[breakdownName] === totalMark) ||
datum[xAxisName] === totalMark;
const originalValue = datum[metricLabel] as number;
let value = originalValue;
@@ -270,9 +285,9 @@ export default function transformProps(
: 'transparent';
let opacity = 1;
if (legendState?.[LEGEND.INCREASE] === false && value > 0) {
if (legendState?.[legendNames.INCREASE] === false && value > 0) {
opacity = 0;
} else if (legendState?.[LEGEND.DECREASE] === false && value < 0) {
} else if (legendState?.[legendNames.DECREASE] === false && value < 0) {
opacity = 0;
}
@@ -301,7 +316,7 @@ export default function transformProps(
const xAxisData = transformedData.map(row => {
let column = xAxisName;
let value = row[xAxisName];
if (breakdownName && row[breakdownName] !== TOTAL_MARK) {
if (breakdownName && row[breakdownName] !== totalMark) {
column = breakdownName;
value = row[breakdownName];
}
@@ -316,8 +331,8 @@ export default function transformProps(
});
const xAxisFormatter = (value: number | string, index: number) => {
if (value === TOTAL_MARK) {
return TOTAL_MARK;
if (value === totalMark) {
return totalMark;
}
if (coltypeMapping[xAxisColumns[index]] === GenericDataType.Temporal) {
if (typeof value === 'string') {
@@ -370,7 +385,7 @@ export default function transformProps(
},
{
...seriesProps,
name: LEGEND.INCREASE,
name: legendNames.INCREASE,
label: {
...labelProps,
position: 'top',
@@ -382,7 +397,7 @@ export default function transformProps(
},
{
...seriesProps,
name: LEGEND.DECREASE,
name: legendNames.DECREASE,
label: {
...labelProps,
position: 'bottom',
@@ -394,7 +409,7 @@ export default function transformProps(
},
{
...seriesProps,
name: LEGEND.TOTAL,
name: legendNames.TOTAL,
label: {
...labelProps,
position: 'top',
@@ -417,7 +432,7 @@ export default function transformProps(
legend: {
show: showLegend,
selected: legendState,
data: [LEGEND.INCREASE, LEGEND.DECREASE, LEGEND.TOTAL],
data: [legendNames.INCREASE, legendNames.DECREASE, legendNames.TOTAL],
},
xAxis: {
data: xAxisData,
@@ -450,6 +465,7 @@ export default function transformProps(
breakdownName,
defaultFormatter,
xAxisFormatter,
totalMark,
}),
},
series: barSeries,

View File

@@ -57,6 +57,9 @@ export type EchartsWaterfallFormData = QueryFormData &
xTicksLayout?: WaterfallFormXTicksLayout;
yAxisLabel: string;
yAxisFormat: string;
increaseLabel?: string;
decreaseLabel?: string;
totalLabel?: string;
};
export const DEFAULT_FORM_DATA: Partial<EchartsWaterfallFormData> = {

View File

@@ -128,7 +128,7 @@ export const showValueControl: ControlSetItem = {
name: 'show_value',
config: {
type: 'CheckboxControl',
label: t('Show Value'),
label: t('Show value'),
default: false,
renderTrigger: true,
description: t('Show series values on the chart'),

View File

@@ -23,8 +23,7 @@ import {
SqlaFormData,
supersetTheme,
} from '@superset-ui/core';
import { EchartsBubbleChartProps } from 'plugins/plugin-chart-echarts/src/Bubble/types';
import { EchartsBubbleChartProps } from '../../src/Bubble/types';
import transformProps, { formatTooltip } from '../../src/Bubble/transformProps';
const defaultFormData: SqlaFormData = {

View File

@@ -257,6 +257,7 @@ describe('Gantt transformProps', () => {
show: true,
position: 'start',
formatter: '{b}',
color: 'rgba(0,0,0,0.88)',
},
lineStyle: expect.objectContaining({
color: '#00000000',

View File

@@ -31,6 +31,14 @@ const extractSeries = (props: WaterfallChartTransformedProps) => {
return series.map(item => item.data).map(item => item.map(i => i.value));
};
const extractSeriesName = (props: WaterfallChartTransformedProps) => {
const { echartOptions } = props;
const { series } = echartOptions as unknown as {
series: [{ name: string }];
};
return series.map(item => item.name);
};
describe('Waterfall tranformProps', () => {
const data = [
{ year: '2019', name: 'Sylvester', sum: 10 },
@@ -94,4 +102,44 @@ describe('Waterfall tranformProps', () => {
['-', '-', 13, '-', '-', 8],
]);
});
it('renaming series names, checking legend and X axis labels', () => {
const chartProps = new ChartProps({
formData: {
...formData,
increaseLabel: 'sale increase',
decreaseLabel: 'sale decrease',
totalLabel: 'sale total',
},
width: 800,
height: 600,
queriesData: [
{
data,
},
],
theme: supersetTheme,
});
const transformedProps = transformProps(
chartProps as unknown as EchartsWaterfallChartProps,
);
expect((transformedProps.echartOptions.legend as any).data).toEqual([
'sale increase',
'sale decrease',
'sale total',
]);
expect((transformedProps.echartOptions.xAxis as any).data).toEqual([
'2019',
'2020',
'sale total',
]);
expect(extractSeriesName(transformedProps)).toEqual([
'Assist',
'sale increase',
'sale decrease',
'sale total',
]);
});
});

View File

@@ -2,21 +2,8 @@
"compilerOptions": {
"composite": false,
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "."
},
"extends": "../../../tsconfig.json",
"include": [
"**/*",
"../types/**/*",
"../../../types/**/*"
],
"references": [
{
"path": "../../../packages/superset-ui-chart-controls"
},
{
"path": "../../../packages/superset-ui-core"
}
]
"include": ["**/*", "../types/**/*", "../../../types/**/*"]
}

View File

@@ -1,17 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": ".",
"paths": {
"@superset-ui/core/components": ["../../packages/superset-ui-core/src/components"]
}
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -18,8 +18,8 @@
*/
import { styled, t } from '@superset-ui/core';
import { SafeMarkdown } from '@superset-ui/core/components';
import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
import Handlebars from 'handlebars';
import dayjs from 'dayjs';
import { useMemo, useState } from 'react';
import { isPlainObject } from 'lodash';
import Helpers from 'just-handlebars-helpers';

View File

@@ -1,17 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": ".",
"paths": {
"@superset-ui/core/components": ["../../packages/superset-ui-core/src/components"]
}
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -27,6 +27,7 @@
"access": "public"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@ant-design/icons": "^5.2.6",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",

View File

@@ -1,14 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": "."
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"exclude": ["src/**/*.test.*", "src/**/*.stories.*"],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -36,6 +36,7 @@
"xss": "^1.0.15"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@ant-design/icons": "^5.2.6",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",

View File

@@ -2,18 +2,8 @@
"compilerOptions": {
"composite": false,
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "../../../"
},
"extends": "../../../tsconfig.json",
"include": [
"**/*",
"../types/**/*",
"../../../types/**/*"
],
"references": [
{
"path": ".."
}
]
"include": ["**/*", "../types/**/*", "../../../types/**/*"]
}

View File

@@ -1,17 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": ".",
"paths": {
"@superset-ui/core/components": ["../../packages/superset-ui-core/src/components"]
}
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -2,18 +2,8 @@
"compilerOptions": {
"composite": false,
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "."
},
"extends": "../../../tsconfig.json",
"include": [
"**/*",
"../types/**/*",
"../../../types/**/*"
],
"references": [
{
"path": ".."
}
]
"include": ["**/*", "../types/**/*", "../../../types/**/*"]
}

View File

@@ -1,14 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib",
"baseUrl": "."
"baseUrl": "../..",
"outDir": "lib"
},
"include": ["src/**/*", "types/**/*"],
"exclude": ["lib", "test"],
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
"exclude": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.test.*",
"src/**/*.stories.*"
],
"references": [
{ "path": "../../packages/superset-core" },
{ "path": "../../packages/superset-ui-core" },
{ "path": "../../packages/superset-ui-chart-controls" }
]

View File

@@ -42,8 +42,8 @@ import {
FeatureFlag,
isFeatureEnabled,
} from '@superset-ui/core';
import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
import { useSelector, useDispatch } from 'react-redux';
import dayjs from 'dayjs';
import rison from 'rison';
import { createDatasource } from 'src/SqlLab/actions/sqlLab';
import { addDangerToast } from 'src/components/MessageToasts/actions';

View File

@@ -188,7 +188,10 @@ const ChartContextMenu = (
isFeatureEnabled(FeatureFlag.DrillBy) &&
canDrillBy &&
isDisplayed(ContextMenuItem.DrillBy) &&
!formData.matrixify_enabled; // Disable drill by when matrixify is enabled
!(
formData.matrixify_enable_vertical_layout === true ||
formData.matrixify_enable_horizontal_layout === true
); // Disable drill by when matrixify is enabled
const datasetResource = useDatasetDrillInfo(
formData.datasource,

Some files were not shown because too many files have changed in this diff Show More