Compare commits

..

132 Commits

Author SHA1 Message Date
Maxime Beauchemin
9a82c2015c trying further 2025-08-14 13:17:43 -07:00
Maxime Beauchemin
489d8a4b83 refactor: Consolidate applyDatasetChartDefaults tests into hydrateExplore.test.ts
Move all 15 test cases for applyDatasetChartDefaults from separate test file
into the main hydrateExplore.test.ts file where the function is defined.

This follows standard practice of keeping tests close to their source code
for better maintainability and discoverability.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 10:43:19 -07:00
Maxime Beauchemin
cb993639ce feat(explore): Add temporal column support and comprehensive test coverage for chart defaults
## Changes
- Added default_temporal_column property to backend model for X-axis defaults
- Added temporal column selector in Dataset Editor UI (datetime columns only)
- Integrated temporal column application in chart creation flow
- Added comprehensive test coverage for all new functionality

## Test Coverage Added

### Backend Tests (7 new tests)
- Properties correctly parse JSON from extra field
- Invalid/null JSON handling
- set_default_chart_metadata functionality
- Preservation of other extra field data

### Frontend Tests (15+ new tests)
- applyDatasetChartDefaults function unit tests
- Validation of metrics, dimensions, and temporal columns
- Error handling for malformed data
- DatasourceEditor component tests
- Integration with existing hydrateExplore tests

## Test Results
 All backend tests passing (pytest)
 All frontend tests passing (Jest)
 Pre-commit checks passing

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 10:43:19 -07:00
Maxime Beauchemin
031938edb8 refactor(explore): Extract chart defaults logic into testable function
- Extract applyDatasetChartDefaults function for better testability
- Add comprehensive test coverage for all edge cases
- Handle extra field as both string and object types
- Ensure malformed JSON doesn't break the application

This refactoring makes the chart defaults logic easier to test in isolation
and improves code maintainability.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 10:43:00 -07:00
Maxime Beauchemin
6e735d40a8 feat(datasets): Replace chart defaults with Explore controls
- Replace time range Select with DateFilterControl for rich time selection UI
- Make time grains database-aware, fetching options from database engine
- Add AdhocFilterControl for setting default filters on datasets
- Update hydrateExplore to apply default filters for new charts

These changes give users the same powerful controls they use in Explore
when setting chart defaults, ensuring consistency and familiarity.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 10:43:00 -07:00
Maxime Beauchemin
377bccd464 fix: Move Chart Defaults outside Fieldset to prevent onChange override
CRITICAL FIX: The Fieldset component was also overriding our onChange
handlers, preventing chart defaults from working.

Root cause: Fieldset uses recurseReactClone to inject its own onChange
handler that expects simple fields like item.fieldKey, but our chart
defaults are nested inside item.extra.default_chart_metadata.

Solution:
- Moved Chart Defaults section outside of Fieldset component
- Used Form.Item with Typography.Title for the section header
- This allows Field components to use our custom onChange handlers

Now the data flow works correctly:
Field onChange → onChartDefaultChange → parse/update extra JSON →
onDatasourcePropChange → state update → modal notification

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 10:43:00 -07:00
Maxime Beauchemin
8bdb41674c fix: Chart Defaults onChange handlers not being called
MAJOR BUG FIX: The Field component was overriding our manual onChange
handlers, preventing chart defaults from being saved.

Root cause: Field component uses cloneElement to override the control's
onChange prop with its own onControlChange handler.

Solution:
- Added onChartDefaultChange helper method to handle chart defaults logic
- Updated Default Metric field to use proper Field pattern:
  - Set value prop on Field (not control)
  - Set onChange prop on Field (not control)
  - Let Field component handle the control binding

This allows the Field component to properly call our chart defaults
update logic when users interact with the form controls.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 10:43:00 -07:00
Maxime Beauchemin
51b887901f debug: Add comprehensive logging for Chart Defaults data flow
Added debug logs to trace how chart defaults flow through the system:

DatasourceEditor.jsx:
- Log onDatasourcePropChange calls with attr and value
- Log updated datasource.extra after setState
- Log what gets sent to modal via onDatasourceChange

DatasourceModal.tsx:
- Log what data.extra is received in onDatasourceChange callback
- Log what currentDatasource.extra gets set to
- Log currentDatasource.extra right before save

This will help identify exactly where chart defaults data gets lost
in the DatasourceEditor → DatasourceModal → API save flow.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 10:43:00 -07:00
Maxime Beauchemin
46924ad89e refactor: Simplify Chart Defaults extra field parsing
Replaced complex IIFE pattern with a clean helper function for parsing
the datasource.extra field in Chart Defaults form controls.

Changes:
- Added parseExtra() helper function to handle string/object parsing
- Simplified all value props to use parseExtra(datasource.extra).default_chart_metadata?.field
- Streamlined onChange handlers to use the same helper consistently
- Improved code readability and maintainability
- Fixed issue with saved defaults not displaying when reopening DatasetEditor

The helper function handles all edge cases (null, string, object) and provides
safe fallbacks, making the Chart Defaults section more robust.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 10:43:00 -07:00
Maxime Beauchemin
1d3c186fec fix: DatasetEditor Chart Defaults not displaying saved values
Fixed an issue where the Chart Defaults section in the DatasetEditor
would not display previously saved values when reopening the editor.

The problem was that the component assumed `datasource.extra` would
always be a string that needed JSON parsing, but it could also be an
already-parsed object depending on the component's state.

Changes:
- Added proper type checking for `datasource.extra` (string vs object)
- Used IIFE with try-catch for safe value extraction in Select components
- Fixed onChange handlers to handle both string and object types
- Ensured saved chart defaults now properly display on editor reload

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 10:43:00 -07:00
Maxime Beauchemin
03ba5b6949 feat(dataset): Add chart creation defaults to improve UX
## Summary
Introduces dataset-level chart defaults that automatically pre-populate common settings when creating new charts, significantly improving the user experience by reducing repetitive configuration steps.

## Features Added

### Backend
- Added computed properties to `SqlaTable` model for reading/writing chart defaults from the `extra` JSON field
- Properties: `default_metric`, `default_dimension`, `default_time_grain`, `default_time_range`, `default_row_limit`
- Added `set_default_chart_metadata()` method for updating defaults

### Frontend UI
- Added new "Chart Defaults" section in Dataset Editor (Settings tab)
- Includes dropdowns for:
  - Default Metric (from available metrics)
  - Default Dimension (from groupable columns)
  - Default Time Grain (common intervals)
  - Default Time Range (common presets)
  - Default Row Limit (numeric input)

### Chart Creation Integration
- Modified `hydrateExplore` action to apply defaults when creating new charts
- Only applies to new charts (no existing slice_id)
- Validates that referenced metrics/columns exist before applying
- Falls back gracefully if defaults can't be applied

### Metadata Sync Protection
- Added `cleanupChartDefaults()` to handle column removal during metadata refresh
- Shows warning if default dimension is removed
- Preserves all other defaults safely

## Technical Details

### Storage
- Uses existing `extra` JSON column - no database migration needed
- Structure: `{"default_chart_metadata": {"default_metric": "count", ...}}`

### Best-Effort Resolution
- String-based references (not foreign keys) for flexibility
- Graceful degradation if referenced objects are deleted
- No cascading failures

### Validation
- Backend: Type hints and property methods ensure data consistency
- Frontend: Dropdowns only show valid options
- Chart creation: Validates existence before applying

## Benefits
- **Improved UX**: Users don't need to repeatedly select the same metric/dimension
- **Time Savings**: Especially helpful for datasets with many metrics/columns
- **Consistency**: Encourages use of primary metrics across charts
- **Flexibility**: Optional and can be overridden per chart

## Future Opportunities
- SQL Lab → Explore flow integration
- Dashboard quick chart creation
- API exposure for external tools
- Viz-type specific defaults

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 10:42:59 -07:00
sha174n
2403d8d584 docs: CVEs added to 5.0.0 and 4.1.3 documentation (#34701) 2025-08-14 10:34:16 -07:00
Kamil Gabryjelski
47874318df fix: Invalid error tooltip if control label is function (#34698) 2025-08-14 16:52:50 +02:00
Kamil Gabryjelski
f6353bd1e8 fix: Bar chart crash when switching from Big Number (#34671) 2025-08-14 15:24:23 +02:00
JUST.in DO IT
1101182654 fix(bootstrapData): Missing application_root data throws an error (#34680) 2025-08-14 08:59:19 -03:00
Maxime Beauchemin
d79fc92a1a fix(theming): Fix ag-grid theming regression in SQL Lab (#34675)
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-13 16:21:47 -07:00
JUST.in DO IT
bce476c4a2 feat(extension): Add extension for chart header (#34678) 2025-08-13 14:40:42 -07:00
Mehmet Salih Yavuz
ecfb9f7d7c fix(row_level_security): Correct api response code for update (#34672) 2025-08-13 23:51:10 +03:00
Kasia
58ebc57285 feat(sqllab): improve SaveDatasetModal design with proper theme spacing (#34658)
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-13 20:50:54 +02:00
JUST.in DO IT
1a57e50bd6 refactor: Migrate ExploreChartPanel to typescript (#34606) 2025-08-13 09:35:01 -07:00
Mehmet Salih Yavuz
f3884a2db8 fix(theming): Theming visual fixes p5 (#34585) 2025-08-13 15:03:01 +03:00
Mehmet Salih Yavuz
cb899f691b fix(csv_tests): Import from utils (#34664) 2025-08-12 15:55:53 -07:00
Evan Rusackas
b25722ee2b fix(sqllab): show actual execution duration in Query History (#34511)
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-12 14:34:34 -07:00
Martyn Gigg
34e10f5972 fix(superset-ui-core): Include appRoot in endpoint of SupersetClientClass.postForm action (#34395) 2025-08-12 14:26:04 -07:00
Beto Dealmeida
e88096f75c fix(presto): return proper data type for column (#34304) 2025-08-12 14:13:59 -07:00
Le Xich Long
6d827cf905 fix(security): grant TableSchemaView to only sql_lab role (#32340) 2025-08-12 13:45:36 -07:00
natilehrer
ab13166e41 fix: activity table delta time (#33503) 2025-08-12 13:39:31 -07:00
Dog foot ruler
89f09ea57c fix(open-api): Add missing FormatQueryPayloadSchema and DashboardScreenshotPostSchema to open-api component schemas (#33202) 2025-08-12 13:33:23 -07:00
Eugene Bikkinin
baec438be9 feat(filter_state): Added @api and @has_access_api to all methods of filter_state API. (#27086) 2025-08-12 13:30:39 -07:00
Maxime Beauchemin
5309edf3a5 feat: Implement UI-based system theme administration (#34560)
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-12 13:27:48 -07:00
Maxime Beauchemin
f50cbd7958 feat: add @sadpandajoe to migrations CODEOWNERS (#34663) 2025-08-12 13:26:33 -07:00
Elizabeth Thompson
2465ab4a98 chore: add more csv tests (#32663) 2025-08-12 13:26:10 -07:00
Đỗ Trọng Hải
1947d4da76 fix(daos/tag): prevent non-unique tags getting created along with unique ones (#32405)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2025-08-12 13:21:42 -07:00
Häbu
e452f5b70d fix(install): set SUPERSET_VERSION_RC at the right time (#21083)
Co-authored-by: Joel Häberli <habej2@bfh.ch>
Co-authored-by: Evan Rusackas <evan@preset.io>
2025-08-12 13:11:12 -07:00
Kasia
698de7a38d feat(dashboard): change chart background option from "White" to "Solid" (#34655)
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-12 10:21:23 -07:00
Kamil Gabryjelski
e2a9f2dead chore: Increase memory limit on webpack ts checker plugin (#34653) 2025-08-12 18:55:38 +02:00
dependabot[bot]
1f80725b0e chore(deps-dev): bump eslint-import-resolver-typescript from 3.7.0 to 4.4.4 in /superset-frontend (#34460)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 09:04:04 -07:00
dependabot[bot]
c3cb5c7e99 chore(deps): bump tmp from 0.2.1 to 0.2.4 in /superset-frontend/cypress-base (#34581)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 09:00:34 -07:00
dependabot[bot]
f7dd0659bf chore(deps): bump tmp and inquirer in /superset-frontend (#34646)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 09:00:10 -07:00
Geido
3c17ff8445 chore: Refactor Menu.Item and cleanup console errors (#34536) 2025-08-12 16:57:23 +03:00
Kamil Gabryjelski
57d0e78d40 feat: Tiled screenshots in Playwright reports (#34561) 2025-08-12 08:09:01 +02:00
Gabriel Torres Ruiz
ae986903b3 fix(webpack): webpack warnings (#34645) 2025-08-11 22:40:11 -07:00
dependabot[bot]
6964f9bdbf chore(deps): bump googleapis from 130.0.0 to 154.1.0 in /superset-frontend (#34481)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-11 22:39:54 -07:00
PolinaFam
9efa9898ff fix: update Russian translations (#34005)
Co-authored-by: Polina Fam <pfam@ptsecurity.com>
2025-08-11 22:39:38 -07:00
Vitor Avila
22b44421a4 fix: Fix Slice import on has_drill_by_access (#34644) 2025-08-11 19:51:15 -03:00
Vitor Avila
02924b3c74 fix: Slack channels and Color Palettes search (#34641) 2025-08-11 15:53:28 -03:00
Elizabeth Thompson
99539c786e fix(initialization): prevent startup failures when database tables don't exist (#34584) 2025-08-11 10:49:52 -07:00
Evan Rusackas
5e621ceb34 fix: Remove deprecated @types/classnames package (#34625)
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-11 10:23:05 -07:00
Rafael Benitez
370a24da81 fix(Dashboards): Tabs highlight and dataset contrast in darkmode issues (#34602) 2025-08-11 18:42:17 +02:00
Vitor Avila
732506b3fa fix: Use labels in Drill to Detail (#34620) 2025-08-11 10:25:25 -03:00
Mehmet Salih Yavuz
1af9c8dba2 fix(DatabaseModal): Don't set activeKey to undefined repeatedly (#34636) 2025-08-11 16:07:47 +03:00
Joe Li
1dc22a9002 chore: add tests to DatabaseConnectionForm/EncryptedField (#34442)
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-08 14:09:30 -07:00
Abhinav Kumar
ad592c717e fix: Reset description height to zero when chart is not expanded (#33843) 2025-08-07 12:51:46 -07:00
SBIN2010
38e15196f2 fix(Heatmap): addin x axis label rotation (#34239) 2025-08-07 12:27:35 -07:00
dependabot[bot]
8131c24acd chore(deps): bump ws and @types/ws in /superset-websocket (#34450)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-07 12:16:26 -07:00
dependabot[bot]
952b620465 chore(deps-dev): bump @types/node from 22.10.3 to 24.1.0 in /superset-websocket (#34448)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-07 12:16:12 -07:00
Oliver Schlüter
f3e3bd0348 fix(db_engine_specs): generate correct boolean filter SQL syntax for Athena compatibility (#34598) 2025-08-07 18:39:31 +03:00
Brandon Kaplan
1e1310dbd8 chore(helm): bump app version to 5.0.0 (#33889) 2025-08-07 07:32:01 -07:00
Mehmet Salih Yavuz
adaae8ba15 fix(Timeshift): Determine temporal column correctly (#34582) 2025-08-07 15:20:34 +03:00
Joe Li
a66b7e98e0 feat: Add ESLint rule to enforce sentence case in button text (#34434)
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-06 13:13:36 -07:00
JUST.in DO IT
3e12d97e8e fix(echart): broken aggregator due to bigint value (#34580) 2025-08-06 15:22:04 -03:00
dependabot[bot]
00304f77e1 chore(deps-dev): bump globals from 16.0.0 to 16.3.0 in /superset-websocket (#34452)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 09:21:18 -07:00
dependabot[bot]
e88db9f403 chore(deps): update re-resizable requirement from ^6.10.1 to ^6.11.2 in /superset-frontend/packages/superset-ui-core (#34453)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 09:12:41 -07:00
JUST.in DO IT
53e9cf6d17 fix: navigate to SQL Lab due to router api updates (#34569) 2025-08-06 11:54:10 -03:00
Damian Pendrak
5a004590e0 feat(deckgl): add selected cross-filter indication (#34322) 2025-08-06 17:53:54 +03:00
Levis Mbote
53503e32ae fix(Table chart): fix percentage metric column (#34175) 2025-08-06 17:51:02 +03:00
Maxime Beauchemin
246181a546 feat(docker): Add pytest support to docker-compose-light.yml (#34373)
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-06 00:17:50 -04:00
JUST.in DO IT
6f5d9c989a fix(echarts): rename time series shifted without dimensions (#34541) 2025-08-05 18:29:55 -07:00
dependabot[bot]
8515792b04 chore(deps): update @deck.gl/aggregation-layers requirement from ^9.1.13 to ^9.1.14 in /superset-frontend/plugins/legacy-preset-chart-deckgl (#34468)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 17:51:03 -07:00
dependabot[bot]
923b2b1d77 chore(deps-dev): bump @babel/runtime-corejs3 from 7.26.7 to 7.28.2 in /superset-frontend (#34464)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 17:46:27 -07:00
dependabot[bot]
486b0122d0 chore(deps-dev): update jest requirement from ^30.0.4 to ^30.0.5 in /superset-frontend/plugins/plugin-chart-pivot-table (#34462)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 17:46:01 -07:00
dependabot[bot]
ae090fa74c chore(deps-dev): update @types/prop-types requirement from ^15.7.2 to ^15.7.15 in /superset-frontend/packages/superset-ui-core (#34451)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 17:45:40 -07:00
dependabot[bot]
35ec6d308a chore(deps-dev): update jest requirement from ^30.0.4 to ^30.0.5 in /superset-frontend/packages/generator-superset (#34457)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 16:43:18 -07:00
dependabot[bot]
c62a6f5cee chore(deps): bump @deck.gl/react from 9.1.13 to 9.1.14 in /superset-frontend (#34461)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 16:42:48 -07:00
dependabot[bot]
cdd140b3cc chore(deps-dev): update jest requirement from ^30.0.4 to ^30.0.5 in /superset-frontend/plugins/plugin-chart-handlebars (#34501)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 16:31:23 -07:00
dependabot[bot]
09cf49c2ba chore(deps): bump @babel/runtime from 7.26.10 to 7.28.2 in /superset-frontend (#34472)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 16:30:21 -07:00
dependabot[bot]
ac4b4c7646 chore(deps-dev): bump eslint-config-prettier from 10.1.5 to 10.1.8 in /superset-websocket (#34454)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 16:28:28 -07:00
dependabot[bot]
d0a6c78966 chore(deps): bump react-draggable from 4.4.6 to 4.5.0 in /superset-frontend (#34474)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 16:19:42 -07:00
dependabot[bot]
65f2071aa4 chore(deps): bump react-lines-ellipsis from 0.15.4 to 0.16.1 in /superset-frontend (#34483)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 16:09:17 -07:00
dependabot[bot]
e8f37a3f89 chore(deps-dev): bump eslint from 9.31.0 to 9.32.0 in /docs (#34492)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 16:04:26 -07:00
dependabot[bot]
19d229ea12 chore(deps-dev): bump typescript-eslint from 8.37.0 to 8.38.0 in /docs (#34493)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 16:03:50 -07:00
dependabot[bot]
622a62d7a1 chore(deps): update react requirement from ^19.1.0 to ^19.1.1 in /superset-frontend/plugins/legacy-plugin-chart-chord (#34502)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 16:01:19 -07:00
yousoph
4a556f4ac4 fix: update copy text for better capitalization and abbreviation standards (#34508)
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-05 14:40:52 -07:00
dependabot[bot]
7a1839ba1b chore(deps): bump @rjsf/validator-ajv8 from 5.24.9 to 5.24.12 in /superset-frontend (#34487)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 14:05:42 -07:00
dependabot[bot]
8f2afb8f4d chore(deps-dev): bump @babel/preset-react from 7.26.3 to 7.27.1 in /superset-frontend (#34489)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 14:03:11 -07:00
dependabot[bot]
02586981da chore(deps-dev): bump eslint-plugin-prettier from 5.5.1 to 5.5.3 in /docs (#34496)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 13:59:30 -07:00
JUST.in DO IT
8700a0b939 fix(table chart): render bigint value in a raw mode (#34556) 2025-08-05 13:11:28 -07:00
Mehmet Salih Yavuz
d843fef2ce fix(theming): More theming bugs/regressions (#34507) 2025-08-05 23:07:35 +03:00
Vitor Avila
f45654c03c chore: Rename dataset creation buttons (#34544) 2025-08-05 15:24:51 -03:00
Mehmet Salih Yavuz
761daec53d feat(timeshift): Add support for date range timeshifts (#34375) 2025-08-05 19:31:40 +03:00
Vitor Avila
407fb67f1e fix: Avoid null scrollLeft in VirtualTable (#34545) 2025-08-05 09:25:47 -03:00
Vitor Avila
49689eec6c feat: Enable drilling in embedded (#34319) 2025-08-05 02:23:00 -03:00
Maxime Beauchemin
791ea9860d fix(explore): Fix missing await for async buildV1ChartDataPayload calls (#34528) 2025-08-04 15:08:34 -07:00
JUST.in DO IT
2f8939d229 fix(native filters): throws an error when a chart containing a bigint value (#34539) 2025-08-04 16:17:06 -03:00
JUST.in DO IT
ccf6290120 chore(core): Add drawer to core ui components (#34515) 2025-08-04 11:06:29 -07:00
dependabot[bot]
96a1aa60e8 chore(deps): update gh-pages requirement from ^6.2.0 to ^6.3.0 in /superset-frontend/packages/superset-ui-demo (#34444)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-03 00:38:12 -07:00
dependabot[bot]
2ea0368c2d chore(deps-dev): bump @types/classnames from 2.3.0 to 2.3.4 in /superset-frontend (#34478)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-03 00:36:27 -07:00
dependabot[bot]
9e407e4e80 chore(deps): bump dom-to-image-more from 3.5.0 to 3.6.0 in /superset-frontend (#34482)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-03 00:11:29 -07:00
dependabot[bot]
360e58c181 chore(deps): bump @deck.gl/core from 9.1.13 to 9.1.14 in /superset-frontend (#34480)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-03 00:11:13 -07:00
dependabot[bot]
22d5eb7835 chore(deps-dev): bump tsx from 4.19.4 to 4.20.3 in /superset-frontend (#34484)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-03 00:10:12 -07:00
dependabot[bot]
7c4a77a909 chore(deps-dev): bump @babel/compat-data from 7.27.2 to 7.28.0 in /superset-frontend (#34485)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-03 00:09:55 -07:00
Evan Rusackas
4e209e51d0 fix(sqllab): prevent strings with angle brackets from being hidden (#34512)
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-02 22:53:17 -07:00
Ville Brofeldt
7191ae55c8 fix: docs eslint command (#34520) 2025-08-02 16:49:23 -07:00
Ville Brofeldt
17725ebc83 chore: use logger on all migrations (#34521) 2025-08-02 12:19:50 -07:00
JUST.in DO IT
1a7a381bd5 fix(echart): initial chart animation (#34516) 2025-08-02 08:41:53 -03:00
dependabot[bot]
daf207e5c2 chore(deps): bump less from 4.3.0 to 4.4.0 in /docs (#34494)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-01 17:10:59 -07:00
dependabot[bot]
72294c569f chore(deps): bump antd from 5.26.3 to 5.26.7 in /docs (#34495)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-01 17:10:39 -07:00
dependabot[bot]
792dd08d38 chore(deps-dev): bump @eslint/js from 9.31.0 to 9.32.0 in /docs (#34497)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-01 17:09:58 -07:00
dependabot[bot]
1e40e7d02b chore(deps): bump swagger-ui-react from 5.26.0 to 5.27.1 in /docs (#34498)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-01 17:09:29 -07:00
dependabot[bot]
7e98c75f01 chore(deps-dev): bump eslint-config-prettier from 10.1.5 to 10.1.8 in /docs (#34499)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-01 17:08:40 -07:00
dependabot[bot]
b18de05ea4 chore(deps-dev): bump webpack from 5.99.9 to 5.101.0 in /docs (#34500)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-01 17:08:09 -07:00
dependabot[bot]
9300652277 chore(deps): bump actions/first-interaction from 1 to 2 (#34459)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-01 14:02:25 -07:00
yousoph
7c2ec4ca5f fix: Update table chart configuration labels to sentence case (#34438)
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-01 12:02:42 -07:00
Evan Rusackas
6a83b6fd87 fix(pie chart): Total now positioned correctly with all Legend positions, and respects theming (#34435) 2025-08-01 12:00:23 -07:00
Evan Rusackas
659cd33749 fix(echarts): resolve bar chart X-axis time formatting stuck on adaptive (#34436)
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-01 09:55:20 -07:00
Maxime Beauchemin
cb27d5fe8d chore: proper current_app.config proxy usage (#34345)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-31 19:27:42 -07:00
Joe Li
6c9cda758a chore: update chart list e2e and component tests (#34393)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-31 17:12:55 -07:00
Mehmet Salih Yavuz
967134f540 fix(theming): Visual bugs p-3 (#34424) 2025-08-01 00:26:38 +03:00
dependabot[bot]
25bb353f9d chore(deps-dev): update jest requirement from ^30.0.2 to ^30.0.4 in /superset-frontend/packages/generator-superset (#34039)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2025-07-31 13:24:18 -07:00
Beto Dealmeida
9cf2472291 fix: time grain and DB dropdowns (#34431) 2025-07-31 16:10:04 -04:00
ObservabilityTeam
cf5b976659 fix(dashboard): adds dependent filter select first value fixes (#34137)
Co-authored-by: Muhammad Musfir <muhammad.musfir@de-cix.net>
2025-07-31 12:39:30 -07:00
yousoph
70394e79ef feat: Add configurable query identifiers for Mixed Timeseries charts (#34406)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-31 12:16:12 -07:00
Kasia
ea64f3122e chore: Change button labels to sentence case (#34432)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-31 12:04:33 -07:00
Kasia
50197fc33e chore: Add bottom border to top navigation menu (#34429)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-31 12:03:38 -07:00
Maxime Beauchemin
c480fa7fcf fix(migrations): prevent theme seeding before themes table exists (#34433)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-31 11:35:34 -07:00
Beto Dealmeida
6fc734da51 fix: prevent anonymous code in Postgres (#34412) 2025-07-31 08:33:34 -04:00
JUST.in DO IT
762a11b0bb fix(sqllab): access legacy kv record (#34411) 2025-07-31 08:58:10 -03:00
Michael Gerber
f168dd69a8 fix(sunburst): Fix sunburst chart cross-filter logic (#31495) 2025-07-30 18:47:02 -07:00
Maxime Beauchemin
becd0b8883 feat: add runtime custom font loading via configuration (#34416) 2025-07-30 18:01:37 -07:00
Maxime Beauchemin
fd4570625a fix(theme-list): reorder buttons to place import leftmost (#34389)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-30 14:17:23 -07:00
Maxime Beauchemin
54a5b58e40 feat(codespaces): auto-setup Python venv with dependencies (#34409)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-30 13:57:54 -07:00
Mehmet Salih Yavuz
a611278e04 fix: Console errors from various sources (#34178)
Co-authored-by: Diego Pucci <diegopucci.me@gmail.com>
2025-07-30 23:32:32 +03:00
641 changed files with 26878 additions and 37305 deletions

20
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
# Keep this in sync with the base image in the main Dockerfile (ARG PY_VER)
FROM python:3.11.13-bookworm AS base
# Install system dependencies that Superset needs
# This layer will be cached across Codespace sessions
RUN apt-get update && apt-get install -y \
libsasl2-dev \
libldap2-dev \
libpq-dev \
tmux \
gh \
&& rm -rf /var/lib/apt/lists/*
# Install uv for fast Python package management
# This will also be cached in the image
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
echo 'export PATH="/root/.cargo/bin:$PATH"' >> /etc/bash.bashrc
# Set the cargo/bin directory in PATH for all users
ENV PATH="/root/.cargo/bin:${PATH}"

View File

@@ -3,3 +3,14 @@
For complete documentation on using GitHub Codespaces with Apache Superset, please see:
**[Setting up a Development Environment - GitHub Codespaces](https://superset.apache.org/docs/contributing/development#github-codespaces-cloud-development)**
## Pre-installed Development Environment
When you create a new Codespace from this repository, it automatically:
1. **Creates a Python virtual environment** using `uv venv`
2. **Installs all development dependencies** via `uv pip install -r requirements/development.txt`
3. **Sets up pre-commit hooks** with `pre-commit install`
4. **Activates the virtual environment** automatically in all terminals
The virtual environment is located at `/workspaces/{repository-name}/.venv` and is automatically activated through environment variables set in the devcontainer configuration.

View File

@@ -0,0 +1,62 @@
# Superset Codespaces environment setup
# This file is appended to ~/.bashrc during Codespace setup
# Find the workspace directory (handles both 'superset' and 'superset-2' names)
WORKSPACE_DIR=$(find /workspaces -maxdepth 1 -name "superset*" -type d | head -1)
if [ -n "$WORKSPACE_DIR" ]; then
# Check if virtual environment exists
if [ -d "$WORKSPACE_DIR/.venv" ]; then
# Activate the virtual environment
source "$WORKSPACE_DIR/.venv/bin/activate"
echo "✅ Python virtual environment activated"
# Verify pre-commit is installed and set up
if command -v pre-commit &> /dev/null; then
echo "✅ pre-commit is available ($(pre-commit --version))"
# Install git hooks if not already installed
if [ -d "$WORKSPACE_DIR/.git" ] && [ ! -f "$WORKSPACE_DIR/.git/hooks/pre-commit" ]; then
echo "🪝 Installing pre-commit hooks..."
cd "$WORKSPACE_DIR" && pre-commit install
fi
else
echo "⚠️ pre-commit not found. Run: pip install pre-commit"
fi
else
echo "⚠️ Python virtual environment not found at $WORKSPACE_DIR/.venv"
echo " Run: cd $WORKSPACE_DIR && .devcontainer/setup-dev.sh"
fi
# Always cd to the workspace directory for convenience
cd "$WORKSPACE_DIR"
fi
# Add helpful aliases for Superset development
alias start-superset="$WORKSPACE_DIR/.devcontainer/start-superset.sh"
alias setup-dev="$WORKSPACE_DIR/.devcontainer/setup-dev.sh"
# Show helpful message on login
echo ""
echo "🚀 Superset Codespaces Environment"
echo "=================================="
# Check if Superset is running
if docker ps 2>/dev/null | grep -q "superset"; then
echo "✅ Superset is running!"
echo " - Check the 'Ports' tab for your live Superset URL"
echo " - Initial startup takes 10-20 minutes"
echo " - Login: admin/admin"
else
echo "⚠️ Superset is not running. Use: start-superset"
# Check if there's a startup log
if [ -f "/tmp/superset-startup.log" ]; then
echo " 📋 Startup log found: cat /tmp/superset-startup.log"
fi
fi
echo ""
echo "Quick commands:"
echo " start-superset - Start Superset with Docker Compose"
echo " setup-dev - Set up Python environment (if not already done)"
echo " pre-commit run - Run pre-commit checks on staged files"
echo ""

View File

@@ -0,0 +1,20 @@
#!/bin/bash
# Script to build and push the devcontainer image to GitHub Container Registry
# This allows caching the image between Codespace sessions
# You'll need to run this with appropriate GitHub permissions
# gh auth login --scopes write:packages
REGISTRY="ghcr.io"
OWNER="apache"
REPO="superset"
TAG="devcontainer-base"
echo "Building devcontainer image..."
docker build -t $REGISTRY/$OWNER/$REPO:$TAG .devcontainer/
echo "Pushing to GitHub Container Registry..."
docker push $REGISTRY/$OWNER/$REPO:$TAG
echo "Done! Update .devcontainer/devcontainer.json to use:"
echo " \"image\": \"$REGISTRY/$OWNER/$REPO:$TAG\""

View File

@@ -1,19 +0,0 @@
{
// Extend the base configuration
"extends": "../devcontainer-base.json",
"name": "Apache Superset Development (Default)",
// Forward ports for development
"forwardPorts": [9001],
"portsAttributes": {
"9001": {
"label": "Superset (via Webpack Dev Server)",
"onAutoForward": "notify",
"visibility": "public"
}
},
// Auto-start Superset on Codespace resume
"postStartCommand": ".devcontainer/start-superset.sh"
}

View File

@@ -1,39 +0,0 @@
{
"name": "Apache Superset Development",
// Keep this in sync with the base image in Dockerfile (ARG PY_VER)
// Using the same base as Dockerfile, but non-slim for dev tools
"image": "python:3.11.13-bookworm",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"moby": true,
"dockerDashComposeVersion": "v2"
},
"ghcr.io/devcontainers/features/node:1": {
"version": "20"
},
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/common-utils:2": {
"configureZshAsDefaultShell": true
},
"ghcr.io/devcontainers/features/sshd:1": {
"version": "latest"
}
},
// Run commands after container is created
"postCreateCommand": "chmod +x .devcontainer/setup-dev.sh && .devcontainer/setup-dev.sh",
// VS Code customizations
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"charliermarsh.ruff",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
}
}

View File

@@ -1,8 +1,15 @@
{
"name": "Apache Superset Development",
// Keep this in sync with the base image in Dockerfile (ARG PY_VER)
// Using the same base as Dockerfile, but non-slim for dev tools
"image": "python:3.11.13-bookworm",
// Option 1: Use pre-built image directly
// "image": "ghcr.io/apache/superset:devcontainer-base",
// Option 2: Build from Dockerfile with cache (current approach)
"build": {
"dockerfile": "Dockerfile",
"context": ".",
// Cache from the Apache registry image
"cacheFrom": ["ghcr.io/apache/superset:devcontainer-base"]
},
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
@@ -32,10 +39,17 @@
},
// Run commands after container is created
"postCreateCommand": "chmod +x .devcontainer/setup-dev.sh && .devcontainer/setup-dev.sh",
"postCreateCommand": "bash .devcontainer/setup-dev.sh || echo '⚠️ Setup had issues - run .devcontainer/setup-dev.sh manually'",
// Auto-start Superset on Codespace resume
"postStartCommand": ".devcontainer/start-superset.sh",
// Auto-start Superset after ensuring Docker is ready
// Run in foreground to see any errors, but don't block on failures
"postStartCommand": "bash -c 'echo \"Waiting 30s for services to initialize...\"; sleep 30; .devcontainer/start-superset.sh || echo \"⚠️ Auto-start failed - run start-superset manually\"'",
// Set environment variables
"remoteEnv": {
// Removed automatic venv activation to prevent startup issues
// The setup script will handle this
},
// VS Code customizations
"customizations": {

View File

@@ -3,30 +3,76 @@
echo "🔧 Setting up Superset development environment..."
# The universal image has most tools, just need Superset-specific libs
echo "📦 Installing Superset-specific dependencies..."
sudo apt-get update
sudo apt-get install -y \
libsasl2-dev \
libldap2-dev \
libpq-dev \
tmux \
gh
# System dependencies and uv are now pre-installed in the Docker image
# This speeds up Codespace creation significantly!
# Install uv for fast Python package management
echo "📦 Installing uv..."
curl -LsSf https://astral.sh/uv/install.sh | sh
# Create virtual environment using uv
echo "🐍 Creating Python virtual environment..."
if ! uv venv; then
echo "❌ Failed to create virtual environment"
exit 1
fi
# Add cargo/bin to PATH for uv
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.zshrc
# Install Python dependencies
echo "📦 Installing Python dependencies..."
if ! uv pip install -r requirements/development.txt; then
echo "❌ Failed to install Python dependencies"
echo "💡 You may need to run this manually after the Codespace starts"
exit 1
fi
# Install pre-commit hooks
echo "🪝 Installing pre-commit hooks..."
if source .venv/bin/activate && pre-commit install; then
echo "✅ Pre-commit hooks installed"
else
echo "⚠️ Pre-commit hooks installation failed (non-critical)"
fi
# Install Claude Code CLI via npm
echo "🤖 Installing Claude Code..."
npm install -g @anthropic-ai/claude-code
if npm install -g @anthropic-ai/claude-code; then
echo "✅ Claude Code installed"
else
echo "⚠️ Claude Code installation failed (non-critical)"
fi
# Make the start script executable
chmod +x .devcontainer/start-superset.sh
# Add bashrc additions for automatic venv activation
echo "🔧 Setting up automatic environment activation..."
if [ -f ~/.bashrc ]; then
# Check if we've already added our additions
if ! grep -q "Superset Codespaces environment setup" ~/.bashrc; then
echo "" >> ~/.bashrc
cat .devcontainer/bashrc-additions >> ~/.bashrc
echo "✅ Added automatic venv activation to ~/.bashrc"
else
echo "✅ Bashrc additions already present"
fi
else
# Create bashrc if it doesn't exist
cat .devcontainer/bashrc-additions > ~/.bashrc
echo "✅ Created ~/.bashrc with automatic venv activation"
fi
# Also add to zshrc since that's the default shell
if [ -f ~/.zshrc ] || [ -n "$ZSH_VERSION" ]; then
if ! grep -q "Superset Codespaces environment setup" ~/.zshrc; then
echo "" >> ~/.zshrc
cat .devcontainer/bashrc-additions >> ~/.zshrc
echo "✅ Added automatic venv activation to ~/.zshrc"
fi
fi
echo "✅ Development environment setup complete!"
echo "🚀 Run '.devcontainer/start-superset.sh' to start Superset"
echo ""
echo "📝 The virtual environment will be automatically activated in new terminals"
echo ""
echo "🔄 To activate in this terminal, run:"
echo " source ~/.bashrc"
echo ""
echo "🚀 To start Superset:"
echo " start-superset"
echo ""

View File

@@ -1,14 +1,14 @@
#!/bin/bash
# Startup script for Superset in Codespaces
# Log to a file for debugging
LOG_FILE="/tmp/superset-startup.log"
echo "[$(date)] Starting Superset startup script" >> "$LOG_FILE"
echo "[$(date)] User: $(whoami), PWD: $(pwd)" >> "$LOG_FILE"
echo "🚀 Starting Superset in Codespaces..."
echo "🌐 Frontend will be available at port 9001"
# Check if MCP is enabled
if [ "$ENABLE_MCP" = "true" ]; then
echo "🤖 MCP Service will be available at port 5008"
fi
# Find the workspace directory (Codespaces clones as 'superset', not 'superset-2')
WORKSPACE_DIR=$(find /workspaces -maxdepth 1 -name "superset*" -type d | head -1)
if [ -n "$WORKSPACE_DIR" ]; then
@@ -18,32 +18,71 @@ else
echo "📁 Using current directory: $(pwd)"
fi
# Check if docker is running
if ! docker info > /dev/null 2>&1; then
echo " Waiting for Docker to start..."
sleep 5
# Wait for Docker to be available
echo "⏳ Waiting for Docker to start..."
echo "[$(date)] Waiting for Docker..." >> "$LOG_FILE"
max_attempts=30
attempt=0
while ! docker info > /dev/null 2>&1; do
if [ $attempt -eq $max_attempts ]; then
echo "❌ Docker failed to start after $max_attempts attempts"
echo "[$(date)] Docker failed to start after $max_attempts attempts" >> "$LOG_FILE"
echo "🔄 Please restart the Codespace or run this script manually later"
exit 1
fi
echo " Attempt $((attempt + 1))/$max_attempts..."
echo "[$(date)] Docker check attempt $((attempt + 1))/$max_attempts" >> "$LOG_FILE"
sleep 2
attempt=$((attempt + 1))
done
echo "✅ Docker is ready!"
echo "[$(date)] Docker is ready" >> "$LOG_FILE"
# Check if Superset containers are already running
if docker ps | grep -q "superset"; then
echo "✅ Superset containers are already running!"
echo ""
echo "🌐 To access Superset:"
echo " 1. Click the 'Ports' tab at the bottom of VS Code"
echo " 2. Find port 9001 and click the globe icon to open"
echo " 3. Wait 10-20 minutes for initial startup"
echo ""
echo "📝 Login credentials: admin/admin"
exit 0
fi
# Clean up any existing containers
echo "🧹 Cleaning up existing containers..."
docker-compose -f docker-compose-light.yml --profile mcp down
docker-compose -f docker-compose-light.yml down
# Start services
echo "🏗️ Building and starting services..."
echo "🏗️ Starting Superset in background (daemon mode)..."
echo ""
echo "📝 Once started, login with:"
echo " Username: admin"
echo " Password: admin"
echo ""
echo "📋 Running in foreground with live logs (Ctrl+C to stop)..."
# Run docker-compose and capture exit code
if [ "$ENABLE_MCP" = "true" ]; then
echo "🤖 Starting with MCP Service enabled..."
docker-compose -f docker-compose-light.yml --profile mcp up
else
docker-compose -f docker-compose-light.yml up
fi
# Start in detached mode
docker-compose -f docker-compose-light.yml up -d
echo ""
echo "✅ Docker Compose started successfully!"
echo ""
echo "📋 Important information:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "⏱️ Initial startup takes 10-20 minutes"
echo "🌐 Check the 'Ports' tab for your Superset URL (port 9001)"
echo "👤 Login: admin / admin"
echo ""
echo "📊 Useful commands:"
echo " docker-compose -f docker-compose-light.yml logs -f # Follow logs"
echo " docker-compose -f docker-compose-light.yml ps # Check status"
echo " docker-compose -f docker-compose-light.yml down # Stop services"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "💤 Keeping terminal open for 60 seconds to test persistence..."
sleep 60
echo "✅ Test complete - check if this terminal is still visible!"
# Show final status
docker-compose -f docker-compose-light.yml ps
EXIT_CODE=$?
# If it failed, provide helpful instructions

View File

@@ -1,29 +0,0 @@
{
// Extend the base configuration
"extends": "../devcontainer-base.json",
"name": "Apache Superset Development with MCP",
// Forward ports for development
"forwardPorts": [9001, 5008],
"portsAttributes": {
"9001": {
"label": "Superset (via Webpack Dev Server)",
"onAutoForward": "notify",
"visibility": "public"
},
"5008": {
"label": "MCP Service (Model Context Protocol)",
"onAutoForward": "notify",
"visibility": "private"
}
},
// Auto-start Superset with MCP on Codespace resume
"postStartCommand": "ENABLE_MCP=true .devcontainer/start-superset.sh",
// Environment variables
"containerEnv": {
"ENABLE_MCP": "true"
}
}

2
.github/CODEOWNERS vendored
View File

@@ -2,7 +2,7 @@
# https://github.com/apache/superset/issues/13351
/superset/migrations/ @mistercrunch @michael-s-molina @betodealmeida @eschutho
/superset/migrations/ @mistercrunch @michael-s-molina @betodealmeida @eschutho @sadpandajoe
# Notify some committers of changes in the components

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- name: Welcome Message
uses: actions/first-interaction@v1
uses: actions/first-interaction@v2
continue-on-error: true
with:
repo-token: ${{ github.token }}

3
.gitignore vendored
View File

@@ -131,3 +131,6 @@ superset/static/stats/statistics.html
# LLM-related
CLAUDE.local.md
.aider*
.claude_rc*
.env.local
PROJECT.md

View File

@@ -1,215 +0,0 @@
# Chart Metadata API Reference
The Superset MCP service provides rich metadata alongside chart generation to enable better UI integration and user experiences.
## Background & Design Philosophy
Modern chart systems need to provide more than just visual output. Inspired by contemporary web standards and LLM integration patterns, this metadata system addresses several key needs:
**Accessibility-First Design**: Following WCAG guidelines and `aria-*` attribute patterns, charts include semantic descriptions and accessibility metadata to ensure inclusive experiences.
**Rich Context for AI Systems**: Similar to how platforms like social media generate rich previews (OpenGraph, Twitter Cards), charts provide semantic understanding beyond just visual representation - enabling AI agents to reason about and describe visualizations meaningfully.
**Performance-Aware Integration**: Modern web APIs emphasize performance transparency (Core Web Vitals, etc.). Charts include execution metrics and optimization suggestions to help UIs make informed decisions about rendering and user feedback.
**Capability-Driven UX**: Rather than requiring UIs to hardcode chart type behaviors, the system exposes what each chart can actually do - enabling dynamic, contextual interfaces that adapt to chart capabilities.
## Overview
When generating charts via `generate_chart`, the response includes structured metadata that helps UIs:
- Present appropriate controls and interactions
- Generate accessible descriptions
- Optimize rendering performance
- Guide user workflows
## Metadata Types
### ChartCapabilities
Describes what interactions and features the chart supports.
```python
{
"supports_interaction": bool, # User can interact (zoom, pan, hover)
"supports_real_time": bool, # Chart can update with live data
"supports_drill_down": bool, # Can navigate to more detailed views
"supports_export": bool, # Can be exported to other formats
"optimal_formats": [ # Recommended preview formats
"url", # Static image URL
"interactive", # HTML with JavaScript controls
"ascii", # Text-based representation
"vega_lite" # Vega-Lite specification
],
"data_types": [ # Types of data visualized
"time_series", # Time-based data
"categorical", # Discrete categories
"metric" # Numeric measurements
]
}
```
**UI Integration:**
- Show/hide interaction controls based on `supports_interaction`
- Enable real-time updates if `supports_real_time`
- Display drill-down options for `supports_drill_down`
- Choose optimal preview format from `optimal_formats`
### ChartSemantics
Provides semantic understanding of what the chart represents and reveals.
```python
{
"primary_insight": "Shows trends and changes over time",
"data_story": "This line chart analyzes sales, revenue over Q1-Q4",
"recommended_actions": [
"Review data patterns and trends",
"Consider filtering for more detail",
"Export chart for reporting"
],
"anomalies": [], # Notable outliers (future enhancement)
"statistical_summary": {} # Key statistics (future enhancement)
}
```
**UI Integration:**
- Display `primary_insight` as chart description
- Use `data_story` for accessibility and tooltips
- Show `recommended_actions` as suggested next steps
- Highlight `anomalies` in the visualization
### AccessibilityMetadata
Information for creating inclusive, accessible chart experiences.
```python
{
"color_blind_safe": bool, # Uses colorblind-friendly palette
"alt_text": "Chart showing Sales Data over time",
"high_contrast_available": bool # High contrast version available
}
```
**UI Integration:**
- Use `alt_text` for screen readers
- Show accessibility indicators if `color_blind_safe`
- Offer high contrast mode if available
### PerformanceMetadata
Performance information for optimization and user feedback.
```python
{
"query_duration_ms": 1250, # Time to generate chart data
"cache_status": "hit|miss|error", # Whether data came from cache
"optimization_suggestions": [ # Performance improvement tips
"Consider adding date filters to reduce data volume",
"Chart complexity may impact load time"
]
}
```
**UI Integration:**
- Show loading indicators based on `query_duration_ms`
- Display cache status for debugging
- Present `optimization_suggestions` to users
- Warn about slow queries
## Example Response
```json
{
"chart": {
"id": 123,
"slice_name": "Sales Trends Q1-Q4",
"viz_type": "echarts_timeseries_line",
"url": "/explore/?slice_id=123"
},
"capabilities": {
"supports_interaction": true,
"supports_real_time": false,
"supports_drill_down": false,
"supports_export": true,
"optimal_formats": ["url", "interactive", "ascii"],
"data_types": ["time_series", "metric"]
},
"semantics": {
"primary_insight": "Shows trends and changes over time",
"data_story": "This line chart analyzes sales over Q1-Q4",
"recommended_actions": [
"Review seasonal patterns",
"Export for quarterly report"
]
},
"accessibility": {
"color_blind_safe": true,
"alt_text": "Line chart showing sales trends from Q1 to Q4",
"high_contrast_available": false
},
"performance": {
"query_duration_ms": 450,
"cache_status": "miss",
"optimization_suggestions": []
}
}
```
## Usage Examples
### React Component Integration
```jsx
function ChartComponent({ chartData }) {
const { capabilities, semantics, accessibility, performance } = chartData;
return (
<div>
{/* Accessibility */}
<img
src={chartData.chart.url}
alt={accessibility.alt_text}
aria-describedby="chart-description"
/>
{/* Semantic description */}
<p id="chart-description">{semantics.primary_insight}</p>
{/* Conditional controls based on capabilities */}
{capabilities.supports_interaction && (
<InteractiveControls />
)}
{capabilities.supports_export && (
<ExportButton />
)}
{/* Performance feedback */}
{performance.query_duration_ms > 2000 && (
<SlowQueryWarning suggestions={performance.optimization_suggestions} />
)}
{/* Recommended actions */}
<ActionSuggestions actions={semantics.recommended_actions} />
</div>
);
}
```
## Chart Type Mapping
Different chart types provide different capabilities:
| Chart Type | Interaction | Real-time | Drill-down | Optimal Formats |
|------------|------------|-----------|------------|-----------------|
| `echarts_timeseries_line` | ✅ | ✅ | ❌ | url, interactive, ascii |
| `echarts_timeseries_bar` | ✅ | ✅ | ❌ | url, interactive, ascii |
| `table` | ❌ | ❌ | ✅ | url, table, ascii |
| `pie` | ✅ | ❌ | ❌ | url, interactive |
## Future Enhancements
- **Statistical Summary**: Automatic calculation of mean, median, trends
- **Anomaly Detection**: Identification of outliers and unusual patterns
- **Smart Recommendations**: ML-powered suggestions for chart improvements
- **Accessibility Scoring**: Automated accessibility compliance checking

View File

@@ -180,7 +180,6 @@ pre-commit run eslint # Frontend linting
## Platform-Specific Instructions
- **[LLMS.md](LLMS.md)** - General LLM development guide (READ THIS FIRST)
- **[CLAUDE.md](CLAUDE.md)** - For Claude/Anthropic tools
- **[.github/copilot-instructions.md](.github/copilot-instructions.md)** - For GitHub Copilot
- **[GEMINI.md](GEMINI.md)** - For Google Gemini tools

View File

@@ -32,11 +32,10 @@ else
SUPERSET_VERSION="${1}"
SUPERSET_RC="${2}"
SUPERSET_PGP_FULLNAME="${3}"
SUPERSET_VERSION_RC="${SUPERSET_VERSION}rc${SUPERSET_RC}"
SUPERSET_RELEASE_RC_TARBALL="apache_superset-${SUPERSET_VERSION_RC}-source.tar.gz"
fi
SUPERSET_VERSION_RC="${SUPERSET_VERSION}rc${SUPERSET_RC}"
if [ -z "${SUPERSET_SVN_DEV_PATH}" ]; then
SUPERSET_SVN_DEV_PATH="$HOME/svn/superset_dev"
fi

View File

@@ -28,6 +28,7 @@ These features are considered **unfinished** and should only be used on developm
[//]: # "PLEASE KEEP THE LIST SORTED ALPHABETICALLY"
- ALERT_REPORT_TABS
- DATE_RANGE_TIMESHIFTS_ENABLED
- ENABLE_ADVANCED_DATA_TYPES
- PRESTO_EXPAND_DATA
- SHARE_QUERIES_VIA_KV_STORE

View File

@@ -94,9 +94,9 @@ under the License.
| can available domains on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can request access on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can dashboard on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can post on TableSchemaView |:heavy_check_mark:|:heavy_check_mark:|O|O|
| can expanded on TableSchemaView |:heavy_check_mark:|:heavy_check_mark:|O|O|
| can delete on TableSchemaView |:heavy_check_mark:|:heavy_check_mark:|O|O|
| can post on TableSchemaView |:heavy_check_mark:|O|O|:heavy_check_mark:|
| can expanded on TableSchemaView |:heavy_check_mark:|O|O|:heavy_check_mark:|
| can delete on TableSchemaView |:heavy_check_mark:|O|O|:heavy_check_mark:|
| can get on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can post on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can delete query on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|

View File

@@ -23,6 +23,13 @@ This file documents any backwards-incompatible changes in Superset and
assists people when migrating to a new version.
## Next
- [34536](https://github.com/apache/superset/pull/34536): The `ENVIRONMENT_TAG_CONFIG` color values have changed to support only Ant Design semantic colors. Update your `superset_config.py`:
- Change `"error.base"` to just `"error"` after this PR
- Change any hex color values to one of: `"success"`, `"processing"`, `"error"`, `"warning"`, `"default"`
- Custom colors are no longer supported to maintain consistency with Ant Design components
- [34561](https://github.com/apache/superset/pull/34561) Added tiled screenshot functionality for Playwright-based reports to handle large dashboards more efficiently. When enabled (default: `SCREENSHOT_TILED_ENABLED = True`), dashboards with 20+ charts or height exceeding 5000px will be captured using multiple viewport-sized tiles and combined into a single image. This improves report generation performance and reliability for large dashboards.
Note: Pillow is now a required dependency (previously optional) to support image processing for tiled screenshots.
`thumbnails` optional dependency is now deprecated and will be removed in the next major release (7.0).
- [33084](https://github.com/apache/superset/pull/33084) The DISALLOWED_SQL_FUNCTIONS configuration now includes additional potentially sensitive database functions across PostgreSQL, MySQL, SQLite, MS SQL Server, and ClickHouse. Existing queries using these functions may now be blocked. Review your SQL Lab queries and dashboards if you encounter "disallowed function" errors after upgrading
- [34235](https://github.com/apache/superset/pull/34235) CSV exports now use `utf-8-sig` encoding by default to include a UTF-8 BOM, improving compatibility with Excel.
- [34258](https://github.com/apache/superset/pull/34258) changing the default in Dockerfile to INCLUDE_CHROMIUM="false" (from "true") in the past. This ensures the `lean` layer is lean by default, and people can opt-in to the `chromium` layer by setting the build arg `INCLUDE_CHROMIUM=true`. This is a breaking change for anyone using the `lean` layer, as it will no longer include Chromium by default.
@@ -32,6 +39,7 @@ assists people when migrating to a new version.
- [32317](https://github.com/apache/superset/pull/32317) The horizontal filter bar feature is now out of testing/beta development and its feature flag `HORIZONTAL_FILTER_BAR` has been removed.
- [31590](https://github.com/apache/superset/pull/31590) Marks the begining of intricate work around supporting dynamic Theming, and breaks support for [THEME_OVERRIDES](https://github.com/apache/superset/blob/732de4ac7fae88e29b7f123b6cbb2d7cd411b0e4/superset/config.py#L671) in favor of a new theming system based on AntD V5. Likely this will be in disrepair until settling over the 5.x lifecycle.
- [32432](https://github.com/apache/superset/pull/31260) Moves the List Roles FAB view to the frontend and requires `FAB_ADD_SECURITY_API` to be enabled in the configuration and `superset init` to be executed.
- [34319](https://github.com/apache/superset/pull/34319) Drill to Detail and Drill By is now supported in Embedded mode, and also with the `DASHBOARD_RBAC` FF. If you don't want to expose these features in Embedded / `DASHBOARD_RBAC`, make sure the roles used for Embedded / `DASHBOARD_RBAC`don't have the required permissions to perform D2D actions.
## 5.0.0

View File

@@ -17,22 +17,47 @@
# -----------------------------------------------------------------------
# Lightweight docker-compose for running multiple Superset instances
# This includes only essential services: database, Redis, and Superset app
# This includes only essential services: database and Superset app (no Redis)
#
# IMPORTANT: To run multiple instances in parallel:
# RUNNING SUPERSET:
# 1. Start services: docker-compose -f docker-compose-light.yml up
# 2. Access at: http://localhost:9001 (or NODE_PORT if specified)
#
# RUNNING MULTIPLE INSTANCES:
# - Use different project names: docker-compose -p project1 -f docker-compose-light.yml up
# - Use different NODE_PORT values: NODE_PORT=9002 docker-compose -p project2 -f docker-compose-light.yml up
# - Volumes are isolated by project name (e.g., project1_db_home_light, project2_db_home_light)
# - Database name is intentionally different (superset_light) to prevent accidental cross-connections
#
# MCP Service (Model Context Protocol):
# - Optional service for LLM agent integration, available under 'mcp' profile
# - To include MCP: docker-compose -f docker-compose-light.yml --profile mcp up
# - MCP runs on port 5008 by default (customize with MCP_PORT=5009)
# - Enable SQL debugging with MCP_SQL_DEBUG=true
# RUNNING TESTS WITH PYTEST:
# Tests run in an isolated environment with a separate test database.
# The pytest-runner service automatically creates and initializes the test database on first use.
#
# For verbose logging during development:
# - Set SUPERSET_LOG_LEVEL=debug in docker/.env-local for detailed Superset logs
# Basic usage:
# docker-compose -f docker-compose-light.yml run --rm pytest-runner pytest tests/unit_tests/
#
# Run specific test file:
# docker-compose -f docker-compose-light.yml run --rm pytest-runner pytest tests/unit_tests/test_foo.py
#
# Run with pytest options:
# docker-compose -f docker-compose-light.yml run --rm pytest-runner pytest -v -s -x tests/
#
# Force reload test database and run tests (when tests are failing due to bad state):
# docker-compose -f docker-compose-light.yml run --rm -e FORCE_RELOAD=true pytest-runner pytest tests/
#
# Run any command in test environment:
# docker-compose -f docker-compose-light.yml run --rm pytest-runner bash
# docker-compose -f docker-compose-light.yml run --rm pytest-runner pytest --collect-only
#
# For parallel test execution with different projects:
# docker-compose -p project1 -f docker-compose-light.yml run --rm pytest-runner pytest tests/
#
# DEVELOPMENT TIPS:
# - First test run takes ~20-30 seconds (database creation + initialization)
# - Subsequent runs are fast (~2-3 seconds startup)
# - Use FORCE_RELOAD=true when you need a clean test database
# - Tests use SimpleCache instead of Redis (no Redis required)
# - Set SUPERSET_LOG_LEVEL=debug in docker/.env-local for detailed logs
# -----------------------------------------------------------------------
x-superset-user: &superset-user root
x-superset-volumes: &superset-volumes
@@ -62,13 +87,14 @@ services:
required: false
image: postgres:16
restart: unless-stopped
# No host port mapping - only accessible within Docker network
volumes:
- db_home_light:/var/lib/postgresql/data
- ./docker/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
environment:
# Override database name to avoid conflicts
POSTGRES_DB: superset_light
# Increase max connections for test runs
command: postgres -c max_connections=200
superset-light:
env_file:
@@ -156,36 +182,33 @@ services:
required: false
volumes: *superset-volumes
superset-mcp-light:
profiles:
- mcp
pytest-runner:
build:
<<: *common-build
command: ["/app/docker/docker-bootstrap.sh", "mcp"]
restart: unless-stopped
ports:
- "127.0.0.1:${MCP_PORT:-5008}:5008" # Parameterized port
extra_hosts:
- "host.docker.internal:host-gateway"
user: *superset-user
depends_on:
superset-init-light:
condition: service_completed_successfully
volumes: *superset-volumes
entrypoint: ["/app/docker/docker-pytest-entrypoint.sh"]
env_file:
- path: docker/.env # default
required: true
- path: docker/.env-local # optional override
required: false
profiles:
- test # Only starts when --profile test is used
depends_on:
db-light:
condition: service_started
user: *superset-user
volumes: *superset-volumes
environment:
# Override DB connection for light service
# Test-specific database configuration
DATABASE_HOST: db-light
DATABASE_DB: superset_light
POSTGRES_DB: superset_light
# Use light-specific config that disables Redis
SUPERSET_CONFIG_PATH: /app/docker/pythonpath_dev/superset_config_docker_light.py
# Enable SQL debugging for MCP if needed
SQLALCHEMY_DEBUG: ${MCP_SQL_DEBUG:-false}
DATABASE_DB: test
POSTGRES_DB: test
# Point to test database
SUPERSET__SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://superset:superset@db-light:5432/test
# Use the light test config that doesn't require Redis
SUPERSET_CONFIG: superset_test_config_light
# Python path includes test directory
PYTHONPATH: /app/pythonpath:/app/docker/pythonpath_dev:/app
volumes:
superset_home_light:

View File

@@ -78,10 +78,6 @@ case "${1}" in
echo "Starting web app..."
/usr/bin/run-server.sh
;;
mcp)
echo "Starting MCP service..."
superset mcp run --host 0.0.0.0 --port ${MCP_PORT:-5008} --debug
;;
*)
echo "Unknown Operation!!!"
;;

View File

@@ -0,0 +1,152 @@
#!/bin/bash
#
# 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.
#
set -e
# Wait for PostgreSQL to be ready
echo "Waiting for database to be ready..."
for i in {1..30}; do
if python3 -c "
import psycopg2
try:
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='superset_light')
conn.close()
print('Database is ready!')
except:
exit(1)
" 2>/dev/null; then
echo "Database connection established!"
break
fi
echo "Waiting for database... ($i/30)"
if [ $i -eq 30 ]; then
echo "Database connection timeout after 30 seconds"
exit 1
fi
sleep 1
done
# Handle database setup based on FORCE_RELOAD
if [ "${FORCE_RELOAD}" = "true" ]; then
echo "Force reload requested - resetting test database"
# Drop and recreate the test database using Python
python3 -c "
import psycopg2
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
# Connect to default database
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='superset_light')
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
cur = conn.cursor()
# Drop and recreate test database
try:
cur.execute('DROP DATABASE IF EXISTS test')
except:
pass
cur.execute('CREATE DATABASE test')
conn.close()
# Connect to test database to create schemas
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='test')
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
cur = conn.cursor()
cur.execute('CREATE SCHEMA sqllab_test_db')
cur.execute('CREATE SCHEMA admin_database')
cur.close()
conn.close()
print('Test database reset successfully')
"
# Use --no-reset-db since we already reset it
FLAGS="--no-reset-db"
else
echo "Using existing test database (set FORCE_RELOAD=true to reset)"
FLAGS="--no-reset-db"
# Ensure test database exists using Python
python3 -c "
import psycopg2
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
# Check if test database exists
try:
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='test')
conn.close()
print('Test database already exists')
except:
print('Creating test database...')
# Connect to default database to create test database
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='superset_light')
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
cur = conn.cursor()
# Create test database
cur.execute('CREATE DATABASE test')
conn.close()
# Connect to test database to create schemas
conn = psycopg2.connect(host='db-light', user='superset', password='superset', database='test')
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
cur = conn.cursor()
cur.execute('CREATE SCHEMA IF NOT EXISTS sqllab_test_db')
cur.execute('CREATE SCHEMA IF NOT EXISTS admin_database')
cur.close()
conn.close()
print('Test database created successfully')
"
fi
# Always run database migrations to ensure schema is up to date
echo "Running database migrations..."
cd /app
superset db upgrade
# Initialize test environment if needed
if [ "${FORCE_RELOAD}" = "true" ] || [ ! -f "/app/superset_home/.test_initialized" ]; then
echo "Initializing test environment..."
# Run initialization commands
superset init
echo "Loading test users..."
superset load-test-users
# Mark as initialized
touch /app/superset_home/.test_initialized
else
echo "Test environment already initialized (skipping init and load-test-users)"
echo "Tip: Use FORCE_RELOAD=true to reinitialize the test database"
fi
# Create missing scripts needed for tests
if [ ! -f "/app/scripts/tag_latest_release.sh" ]; then
echo "Creating missing tag_latest_release.sh script for tests..."
cp /app/docker/tag_latest_release.sh /app/scripts/tag_latest_release.sh 2>/dev/null || true
fi
# Install pip module for Shillelagh compatibility (aligns with CI environment)
echo "Installing pip module for Shillelagh compatibility..."
uv pip install pip
# If arguments provided, execute them
if [ $# -gt 0 ]; then
exec "$@"
fi

View File

@@ -23,25 +23,57 @@ MIN_MEM_FREE_GB=3
MIN_MEM_FREE_KB=$(($MIN_MEM_FREE_GB*1000000))
echo_mem_warn() {
MEM_FREE_KB=$(awk '/MemFree/ { printf "%s \n", $2 }' /proc/meminfo)
MEM_FREE_GB=$(awk '/MemFree/ { printf "%s \n", $2/1024/1024 }' /proc/meminfo)
# Check if running in Codespaces first
if [[ -n "${CODESPACES}" ]]; then
echo "Memory available: Codespaces managed"
return
fi
if [[ "${MEM_FREE_KB}" -lt "${MIN_MEM_FREE_KB}" ]]; then
# Check platform and get memory accordingly
if [[ -f /proc/meminfo ]]; then
# Linux
if grep -q MemAvailable /proc/meminfo; then
MEM_AVAIL_KB=$(awk '/MemAvailable/ { printf "%s \n", $2 }' /proc/meminfo)
MEM_AVAIL_GB=$(awk '/MemAvailable/ { printf "%s \n", $2/1024/1024 }' /proc/meminfo)
else
MEM_AVAIL_KB=$(awk '/MemFree/ { printf "%s \n", $2 }' /proc/meminfo)
MEM_AVAIL_GB=$(awk '/MemFree/ { printf "%s \n", $2/1024/1024 }' /proc/meminfo)
fi
elif [[ "$(uname)" == "Darwin" ]]; then
# macOS - use vm_stat to get free memory
# vm_stat reports in pages, typically 4096 bytes per page
PAGE_SIZE=$(pagesize)
FREE_PAGES=$(vm_stat | awk '/Pages free:/ {print $3}' | tr -d '.')
INACTIVE_PAGES=$(vm_stat | awk '/Pages inactive:/ {print $3}' | tr -d '.')
# Free + inactive pages give us available memory (similar to MemAvailable on Linux)
AVAIL_PAGES=$((FREE_PAGES + INACTIVE_PAGES))
MEM_AVAIL_KB=$((AVAIL_PAGES * PAGE_SIZE / 1024))
MEM_AVAIL_GB=$(echo "scale=2; $MEM_AVAIL_KB / 1024 / 1024" | bc)
else
# Other platforms
echo "Memory available: Unable to determine"
return
fi
if [[ "${MEM_AVAIL_KB}" -lt "${MIN_MEM_FREE_KB}" ]]; then
cat <<EOF
===============================================
======== Memory Insufficient Warning =========
===============================================
It looks like you only have ${MEM_FREE_GB}GB of
memory free. Please increase your Docker
It looks like you only have ${MEM_AVAIL_GB}GB of
memory ${MEM_TYPE}. Please increase your Docker
resources to at least ${MIN_MEM_FREE_GB}GB
Note: During builds, available memory may be
temporarily low due to caching and compilation.
===============================================
======== Memory Insufficient Warning =========
===============================================
EOF
else
echo "Memory check Ok [${MEM_FREE_GB}GB free]"
echo "Memory available: ${MEM_AVAIL_GB} GB"
fi
}

View File

@@ -0,0 +1,55 @@
# 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.
#
# Test configuration for docker-compose-light.yml - uses SimpleCache instead of Redis
# Import all settings from the main test config first
import os
import sys
# Add the tests directory to the path to import the test config
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
from tests.integration_tests.superset_test_config import * # noqa: F403
# Override Redis-based caching to use simple in-memory cache
CACHE_CONFIG = {
"CACHE_TYPE": "SimpleCache",
"CACHE_DEFAULT_TIMEOUT": 300,
"CACHE_KEY_PREFIX": "superset_test_",
}
DATA_CACHE_CONFIG = {
**CACHE_CONFIG,
"CACHE_DEFAULT_TIMEOUT": 30,
"CACHE_KEY_PREFIX": "superset_test_data_",
}
# Keep SimpleCache for these as they're already using it
# FILTER_STATE_CACHE_CONFIG - already SimpleCache in parent
# EXPLORE_FORM_DATA_CACHE_CONFIG - already SimpleCache in parent
# Disable Celery for lightweight testing
CELERY_CONFIG = None
# Use FileSystemCache for SQL Lab results instead of Redis
from flask_caching.backends.filesystemcache import FileSystemCache # noqa: E402
RESULTS_BACKEND = FileSystemCache("/app/superset_home/sqllab_test")
# Override WEBDRIVER_BASEURL for tests to match expected values
WEBDRIVER_BASEURL = "http://0.0.0.0:8080/"
WEBDRIVER_BASEURL_USER_FRIENDLY = WEBDRIVER_BASEURL

190
docker/tag_latest_release.sh Executable file
View File

@@ -0,0 +1,190 @@
#! /bin/bash
# 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.
run_git_tag () {
if [[ "$DRY_RUN" == "false" ]] && [[ "$SKIP_TAG" == "false" ]]
then
git tag -a -f latest "${GITHUB_TAG_NAME}" -m "latest tag"
echo "${GITHUB_TAG_NAME} has been tagged 'latest'"
fi
exit 0
}
###
# separating out git commands into functions so they can be mocked in unit tests
###
git_show_ref () {
if [[ "$TEST_ENV" == "true" ]]
then
if [[ "$GITHUB_TAG_NAME" == "does_not_exist" ]]
# mock return for testing only
then
echo ""
else
echo "2817aebd69dc7d199ec45d973a2079f35e5658b6 refs/tags/${GITHUB_TAG_NAME}"
fi
fi
result=$(git show-ref "${GITHUB_TAG_NAME}")
echo "${result}"
}
get_latest_tag_list () {
if [[ "$TEST_ENV" == "true" ]]
then
echo "(tag: 2.1.0, apache/2.1test)"
else
result=$(git show-ref --tags --dereference latest | awk '{print $2}' | xargs git show --pretty=tformat:%d -s | grep tag:)
echo "${result}"
fi
}
###
split_string () {
local version="$1"
local delimiter="$2"
local components=()
local tmp=""
for (( i=0; i<${#version}; i++ )); do
local char="${version:$i:1}"
if [[ "$char" != "$delimiter" ]]; then
tmp="$tmp$char"
elif [[ -n "$tmp" ]]; then
components+=("$tmp")
tmp=""
fi
done
if [[ -n "$tmp" ]]; then
components+=("$tmp")
fi
echo "${components[@]}"
}
DRY_RUN=false
# get params passed in with script when it was run
# --dry-run is optional and returns the value of SKIP_TAG, but does not run the git tag statement
# A tag name is required as a param. A SHA won't work. You must first tag a sha with a release number
# and then run this script
while [[ $# -gt 0 ]]
do
key="$1"
case $key in
--dry-run)
DRY_RUN=true
shift # past value
;;
*) # this should be the tag name
GITHUB_TAG_NAME=$key
shift # past value
;;
esac
done
if [ -z "${GITHUB_TAG_NAME}" ]; then
echo "Missing tag parameter, usage: ./scripts/tag_latest_release.sh <GITHUB_TAG_NAME>"
echo "SKIP_TAG=true" >> $GITHUB_OUTPUT
exit 1
fi
if [ -z "$(git_show_ref)" ]; then
echo "The tag ${GITHUB_TAG_NAME} does not exist. Please use a different tag."
echo "SKIP_TAG=true" >> $GITHUB_OUTPUT
exit 0
fi
# check that this tag only contains a proper semantic version
if ! [[ ${GITHUB_TAG_NAME} =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
then
echo "This tag ${GITHUB_TAG_NAME} is not a valid release version. Not tagging."
echo "SKIP_TAG=true" >> $GITHUB_OUTPUT
exit 1
fi
## split the current GITHUB_TAG_NAME into an array at the dot
THIS_TAG_NAME=$(split_string "${GITHUB_TAG_NAME}" ".")
# look up the 'latest' tag on git
LATEST_TAG_LIST=$(get_latest_tag_list) || echo 'not found'
# if 'latest' tag doesn't exist, then set this commit to latest
if [[ -z "$LATEST_TAG_LIST" ]]
then
echo "there are no latest tags yet, so I'm going to start by tagging this sha as the latest"
run_git_tag
exit 0
fi
# remove parenthesis and tag: from the list of tags
LATEST_TAGS_STRINGS=$(echo "$LATEST_TAG_LIST" | sed 's/tag: \([^,]*\)/\1/g' | tr -d '()')
LATEST_TAGS=$(split_string "$LATEST_TAGS_STRINGS" ",")
TAGS=($(split_string "$LATEST_TAGS" " "))
# Initialize a flag for comparison result
compare_result=""
# Iterate through the tags of the latest release
for tag in $TAGS
do
if [[ $tag == "latest" ]]; then
continue
else
## extract just the version from this tag
LATEST_RELEASE_TAG="$tag"
echo "LATEST_RELEASE_TAG: ${LATEST_RELEASE_TAG}"
# check that this only contains a proper semantic version
if ! [[ ${LATEST_RELEASE_TAG} =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
then
echo "'Latest' has been associated with tag ${LATEST_RELEASE_TAG} which is not a valid release version. Looking for another."
continue
fi
echo "The current release with the latest tag is version ${LATEST_RELEASE_TAG}"
# Split the version strings into arrays
THIS_TAG_NAME_ARRAY=($(split_string "$THIS_TAG_NAME" "."))
LATEST_RELEASE_TAG_ARRAY=($(split_string "$LATEST_RELEASE_TAG" "."))
# Iterate through the components of the version strings
for (( j=0; j<${#THIS_TAG_NAME_ARRAY[@]}; j++ )); do
echo "Comparing ${THIS_TAG_NAME_ARRAY[$j]} to ${LATEST_RELEASE_TAG_ARRAY[$j]}"
if [[ $((THIS_TAG_NAME_ARRAY[$j])) > $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
compare_result="greater"
break
elif [[ $((THIS_TAG_NAME_ARRAY[$j])) < $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
compare_result="lesser"
break
fi
done
fi
done
# Determine the result based on the comparison
if [[ -z "$compare_result" ]]; then
echo "Versions are equal"
echo "SKIP_TAG=true" >> $GITHUB_OUTPUT
elif [[ "$compare_result" == "greater" ]]; then
echo "This release tag ${GITHUB_TAG_NAME} is newer than the latest."
echo "SKIP_TAG=false" >> $GITHUB_OUTPUT
# Add other actions you want to perform for a newer version
elif [[ "$compare_result" == "lesser" ]]; then
echo "This release tag ${GITHUB_TAG_NAME} is older than the latest."
echo "This release tag ${GITHUB_TAG_NAME} is not the latest. Not tagging."
# if you've gotten this far, then we don't want to run any tags in the next step
echo "SKIP_TAG=true" >> $GITHUB_OUTPUT
fi

View File

@@ -13,9 +13,9 @@ apache-superset>=6.0
Superset now rides on **Ant Design v5's token-based theming**.
Every Antd token works, plus a handful of Superset-specific ones for charts and dashboard chrome.
## Managing Themes via CRUD Interface
## Managing Themes via UI
Superset now includes a built-in **Theme Management** interface accessible from the admin menu under **Settings > Themes**.
Superset includes a built-in **Theme Management** interface accessible from the admin menu under **Settings > Themes**.
### Creating a New Theme
@@ -29,22 +29,38 @@ Superset now includes a built-in **Theme Management** interface accessible from
You can also extend with Superset-specific tokens (documented in the default theme object) before you import.
### System Theme Administration
When `ENABLE_UI_THEME_ADMINISTRATION = True` is configured, administrators can manage system-wide themes directly from the UI:
#### Setting System Themes
- **System Default Theme**: Click the sun icon on any theme to set it as the system-wide default
- **System Dark Theme**: Click the moon icon on any theme to set it as the system dark mode theme
- **Automatic OS Detection**: When both default and dark themes are set, Superset automatically detects and applies the appropriate theme based on OS preferences
#### Managing System Themes
- System themes are indicated with special badges in the theme list
- Only administrators with write permissions can modify system theme settings
- Removing a system theme designation reverts to configuration file defaults
### Applying Themes to Dashboards
Once created, themes can be applied to individual dashboards:
- Edit any dashboard and select your custom theme from the theme dropdown
- Each dashboard can have its own theme, allowing for branded or context-specific styling
## Alternative: Instance-wide Configuration
## Configuration Options
For system-wide theming, you can configure default themes via Python configuration:
### Python Configuration
### Setting Default Themes
Configure theme behavior via `superset_config.py`:
```python
# superset_config.py
# Enable UI-based theme administration for admins
ENABLE_UI_THEME_ADMINISTRATION = True
# Default theme (light mode)
# Optional: Set initial default themes via configuration
# These can be overridden via the UI when ENABLE_UI_THEME_ADMINISTRATION = True
THEME_DEFAULT = {
"token": {
"colorPrimary": "#2893B3",
@@ -53,7 +69,7 @@ THEME_DEFAULT = {
}
}
# Dark theme configuration
# Optional: Dark theme configuration
THEME_DARK = {
"algorithm": "dark",
"token": {
@@ -62,23 +78,28 @@ THEME_DARK = {
}
}
# Theme behavior settings
THEME_SETTINGS = {
"enforced": False, # If True, forces default theme always
"allowSwitching": True, # Allow users to switch between themes
"allowOSPreference": True, # Auto-detect system theme preference
}
# To force a single theme on all users, set THEME_DARK = None
# When both themes are defined (via UI or config):
# - Users can manually switch between themes
# - OS preference detection is automatically enabled
```
### Copying Themes from CRUD Interface
### Migration from Configuration to UI
To use a theme created via the CRUD interface as your system default:
When `ENABLE_UI_THEME_ADMINISTRATION = True`:
1. Navigate to **Settings > Themes** and edit your desired theme
2. Copy the complete JSON configuration from the theme definition field
3. Paste it directly into your `superset_config.py` as shown above
1. System themes set via the UI take precedence over configuration file settings
2. The UI shows which themes are currently set as system defaults
3. Administrators can change system themes without restarting Superset
4. Configuration file themes serve as fallbacks when no UI themes are set
Restart Superset to apply changes.
### Copying Themes Between Systems
To export a theme for use in configuration files or another instance:
1. Navigate to **Settings > Themes** and click the export icon on your desired theme
2. Extract the JSON configuration from the exported YAML file
3. Use this JSON in your `superset_config.py` or import it into another Superset instance
## Theme Development Workflow
@@ -87,8 +108,85 @@ Restart Superset to apply changes.
3. **Apply**: Assign themes to specific dashboards or configure instance-wide
4. **Iterate**: Modify theme JSON directly in the CRUD interface or re-import from the theme editor
## Custom Fonts
Superset supports custom fonts through runtime configuration, allowing you to use branded or custom typefaces without rebuilding the application.
### Configuring Custom Fonts
Add font URLs to your `superset_config.py`:
```python
# Load fonts from Google Fonts, Adobe Fonts, or self-hosted sources
CUSTOM_FONT_URLS = [
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap",
"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap",
]
# Update CSP to allow font sources
TALISMAN_CONFIG = {
"content_security_policy": {
"font-src": ["'self'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
"style-src": ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
}
}
```
### Using Custom Fonts in Themes
Once configured, reference the fonts in your theme configuration:
```python
THEME_DEFAULT = {
"token": {
"fontFamily": "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
"fontFamilyCode": "JetBrains Mono, Monaco, monospace",
# ... other theme tokens
}
}
```
Or in the CRUD interface theme JSON:
```json
{
"token": {
"fontFamily": "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
"fontFamilyCode": "JetBrains Mono, Monaco, monospace"
}
}
```
### Font Sources
- **Google Fonts**: Free, CDN-hosted fonts with wide variety
- **Adobe Fonts**: Premium fonts (requires subscription and kit ID)
- **Self-hosted**: Place font files in `/static/assets/fonts/` and reference via CSS
This feature works with the stock Docker image - no custom build required!
## Advanced Features
- **System Themes**: Superset includes built-in light and dark themes
- **System Themes**: Manage system-wide default and dark themes via UI or configuration
- **Per-Dashboard Theming**: Each dashboard can have its own visual identity
- **JSON Editor**: Edit theme configurations directly within Superset's interface
- **Custom Fonts**: Load external fonts via configuration without rebuilding
- **OS Dark Mode Detection**: Automatically switches themes based on system preferences
- **Theme Import/Export**: Share themes between instances via YAML files
## API Access
For programmatic theme management, Superset provides REST endpoints:
- `GET /api/v1/theme/` - List all themes
- `POST /api/v1/theme/` - Create a new theme
- `PUT /api/v1/theme/{id}` - Update a theme
- `DELETE /api/v1/theme/{id}` - Delete a theme
- `PUT /api/v1/theme/{id}/set_system_default` - Set as system default theme (admin only)
- `PUT /api/v1/theme/{id}/set_system_dark` - Set as system dark theme (admin only)
- `DELETE /api/v1/theme/unset_system_default` - Remove system default designation
- `DELETE /api/v1/theme/unset_system_dark` - Remove system dark designation
- `GET /api/v1/theme/export/` - Export themes as YAML
- `POST /api/v1/theme/import/` - Import themes from YAML
These endpoints require appropriate permissions and are subject to RBAC controls.

View File

@@ -137,7 +137,7 @@ contributing to Apache Superset more accessible to developers worldwide.
1. **Create a Codespace**: Use this pre-configured link that sets up everything you need:
[**Launch Superset Codespace →**](https://github.com/codespaces/new?skip_quickstart=true&machine=standardLinux32gb&repo=39464018&ref=codespaces&geo=UsWest&devcontainer_path=.devcontainer%2Fdevcontainer.json)
[**Launch Superset Codespace →**](https://github.com/codespaces/new?skip_quickstart=true&machine=standardLinux32gb&repo=39464018&ref=master&devcontainer_path=.devcontainer%2Fdevcontainer.json&geo=UsWest)
:::caution
**Important**: You must select at least the **4 CPU / 16GB RAM** machine type (pre-selected in the link above).
@@ -421,14 +421,6 @@ Then make sure you run your WSGI server using the right worker type:
gunicorn "superset.app:create_app()" -k "geventwebsocket.gunicorn.workers.GeventWebSocketWorker" -b 127.0.0.1:8088 --reload
```
You can log anything to the browser console, including objects:
```python
from superset import app
app.logger.error('An exception occurred!')
app.logger.info(form_data)
```
### Frontend
Frontend assets (TypeScript, JavaScript, CSS, and images) must be compiled in order to properly display the web UI. The `superset-frontend` directory contains all NPM-managed frontend assets. Note that for some legacy pages there are additional frontend assets bundled with Flask-Appbuilder (e.g. jQuery and bootstrap). These are not managed by NPM and may be phased out in the future.

View File

@@ -1,741 +0,0 @@
---
title: API Reference
sidebar_position: 3
version: 1
---
# MCP Tools API Reference
Complete reference for all 16 MCP tools with request/response examples.
> 🚀 **First time here?** Start with [Dashboard Tools](#dashboard-tools) or [Chart Tools](#chart-tools) to see the most commonly used features.
>
> 🔐 **Need authentication?** See the [Authentication Guide](./authentication) for JWT setup.
>
> 🔧 **Want to add tools?** Check the [Development Guide](./development#adding-new-tools) for step-by-step instructions.
## Dashboard Tools
### list_dashboards
List dashboards with search, filtering, and pagination support.
**Request Schema:**
```json
{
"search": "sales", // Optional: Search term
"filters": [ // Optional: Advanced filters
{
"col": "published",
"opr": "eq",
"value": true
}
],
"page": 1, // Optional: Page number (default: 1)
"page_size": 20, // Optional: Items per page (default: 20)
"select_columns": [ // Optional: Specific columns
"id", "dashboard_title", "uuid"
],
"use_cache": true // Optional: Use cached data (default: true)
}
```
**Response Example:**
```json
{
"dashboards": [
{
"id": 1,
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"dashboard_title": "Sales Performance",
"url": "/superset/dashboard/1/",
"published": true,
"owners": ["admin"],
"created_on": "2024-01-15T10:30:00Z",
"changed_on": "2024-01-20T14:15:00Z"
}
],
"total_count": 45,
"page": 1,
"page_size": 20,
"cache_status": {
"cache_hit": true,
"cache_age_seconds": 300
}
}
```
### get_dashboard_info
Get detailed information about a specific dashboard.
**Request Schema:**
```json
{
"identifier": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", // ID, UUID, or slug
"use_cache": true
}
```
**Response Example:**
```json
{
"dashboard_id": 1,
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"dashboard_title": "Sales Performance Dashboard",
"slug": "sales-performance",
"url": "/superset/dashboard/1/",
"published": true,
"owners": ["admin", "analyst"],
"roles": ["Sales Team"],
"charts": [
{
"id": 10,
"slice_name": "Monthly Revenue",
"viz_type": "line"
},
{
"id": 11,
"slice_name": "Regional Sales",
"viz_type": "bar"
}
],
"filters": [
{
"column": "region",
"type": "select"
}
],
"created_on": "2024-01-15T10:30:00Z",
"changed_on": "2024-01-20T14:15:00Z"
}
```
### generate_dashboard
Create a new dashboard with multiple charts.
**Request Schema:**
```json
{
"chart_ids": [10, 11, 12, 13],
"dashboard_title": "Q4 Performance Dashboard",
"description": "Quarterly performance metrics and KPIs",
"published": true,
"layout_type": "grid" // Optional: "grid" or "tabs"
}
```
**Response Example:**
```json
{
"dashboard_id": 25,
"uuid": "new-dash-uuid-here",
"dashboard_title": "Q4 Performance Dashboard",
"url": "/superset/dashboard/25/",
"charts_added": 4,
"layout": {
"type": "grid",
"columns": 2,
"rows": 2
},
"created_on": "2024-01-25T16:45:00Z"
}
```
## Chart Tools
### list_charts
List charts with advanced filtering and search capabilities.
**Request Schema:**
```json
{
"search": "revenue",
"filters": [
{
"col": "viz_type",
"opr": "in",
"value": ["line", "bar", "area"]
}
],
"page": 1,
"page_size": 25,
"select_columns": ["id", "slice_name", "viz_type", "uuid"],
"use_cache": true
}
```
**Response Example:**
```json
{
"charts": [
{
"id": 10,
"uuid": "chart-uuid-1",
"slice_name": "Monthly Revenue Trend",
"viz_type": "line",
"datasource_name": "sales_data",
"owners": ["admin"],
"created_on": "2024-01-10T09:15:00Z"
}
],
"total_count": 125,
"page": 1,
"page_size": 25
}
```
### get_chart_info
Get comprehensive chart information including configuration.
**Request Schema:**
```json
{
"identifier": 10, // ID or UUID
"include_form_data": true, // Include chart configuration
"use_cache": true
}
```
**Response Example:**
```json
{
"chart_id": 10,
"uuid": "chart-uuid-1",
"slice_name": "Monthly Revenue Trend",
"viz_type": "line",
"datasource_id": 5,
"datasource_name": "sales_data",
"datasource_type": "table",
"form_data": {
"viz_type": "line",
"x_axis": "month",
"metrics": ["sum__revenue"],
"time_range": "Last 12 months"
},
"query_context": {
"datasource": {"id": 5, "type": "table"},
"queries": [{"columns": [], "metrics": ["sum__revenue"]}]
},
"explore_url": "/superset/explore/?form_data=%7B%22slice_id%22%3A10%7D",
"owners": ["admin"],
"created_on": "2024-01-10T09:15:00Z",
"changed_on": "2024-01-15T11:30:00Z"
}
```
### generate_chart
Create a new chart with specified configuration.
**Request Schema:**
```json
{
"dataset_id": "5",
"config": {
"chart_type": "xy",
"x": {"name": "month", "label": "Month"},
"y": [
{
"name": "revenue",
"aggregate": "SUM",
"label": "Total Revenue"
},
{
"name": "orders",
"aggregate": "COUNT",
"label": "Order Count"
}
],
"kind": "line",
"x_axis": {
"title": "Month",
"format": "smart_date"
},
"y_axis": {
"title": "Revenue ($)",
"format": "$,.0f"
},
"legend": {
"show": true,
"position": "top"
}
},
"slice_name": "Revenue and Orders Trend",
"description": "Monthly revenue and order count comparison",
"save_chart": true,
"generate_preview": true,
"preview_formats": ["url", "ascii"]
}
```
**Response Example:**
```json
{
"chart_id": 45,
"uuid": "new-chart-uuid",
"slice_name": "Revenue and Orders Trend",
"viz_type": "echarts_timeseries_line",
"datasource_id": 5,
"explore_url": "/superset/explore/?form_data=%7B%22slice_id%22%3A45%7D",
"query_executed": true,
"query_result": {
"status": "success",
"row_count": 12,
"execution_time": 0.145
},
"preview": {
"url": {
"preview_url": "http://localhost:5008/screenshot/chart/45.png",
"width": 800,
"height": 600
},
"ascii": {
"ascii_content": "Revenue Trend\n==============\nJan |████████████████ $125K\nFeb |██████████████████ $140K\n...",
"width": 80,
"height": 20
}
},
"created_on": "2024-01-25T14:20:00Z"
}
```
### get_chart_data
Export chart data in multiple formats.
**Request Schema:**
```json
{
"identifier": 10,
"format": "json", // "json", "csv", "excel"
"limit": 1000, // Optional: Row limit
"offset": 0, // Optional: Row offset
"filters": [ // Optional: Additional filters
{
"column": "region",
"op": "=",
"value": "US"
}
],
"use_cache": true,
"force_refresh": false
}
```
**Response Example:**
```json
{
"data": [
{
"month": "2024-01",
"revenue": 125000,
"orders": 450
},
{
"month": "2024-02",
"revenue": 140000,
"orders": 520
}
],
"total_rows": 12,
"columns": [
{"name": "month", "type": "DATE"},
{"name": "revenue", "type": "BIGINT"},
{"name": "orders", "type": "BIGINT"}
],
"query": {
"sql": "SELECT month, SUM(revenue) as revenue, COUNT(*) as orders FROM sales_data GROUP BY month ORDER BY month",
"execution_time": 0.089
},
"cache_status": {
"cache_hit": false,
"cache_type": "query",
"refreshed": true
}
}
```
### get_chart_preview
Generate chart previews in multiple formats.
**Request Schema:**
```json
{
"identifier": 10,
"format": "url", // "url", "base64", "ascii", "table"
"width": 800, // For image formats
"height": 600, // For image formats
"ascii_width": 80, // For ASCII format
"ascii_height": 20, // For ASCII format
"use_cache": true
}
```
**Response Examples:**
**URL Format:**
```json
{
"format": "url",
"preview_url": "http://localhost:5008/screenshot/chart/10.png",
"width": 800,
"height": 600,
"supports_interaction": false,
"expires_at": "2024-01-26T14:20:00Z"
}
```
**ASCII Format:**
```json
{
"format": "ascii",
"ascii_content": "Monthly Revenue Trend\n=====================\n\nJan |████████████████████ $125K\nFeb |██████████████████████ $140K\nMar |███████████████████ $135K\nApr |█████████████████████████ $155K\n\nRange: $125K to $155K\n▁▃▂▅▇▆▄▃▂▄▅▆▇▅▃▂",
"width": 80,
"height": 20,
"supports_color": false
}
```
**Table Format:**
```json
{
"format": "table",
"table_data": "Monthly Revenue Data\n====================\n\nMonth | Revenue | Orders\n---------|----------|--------\nJan 2024 | $125,000 | 450\nFeb 2024 | $140,000 | 520\nMar 2024 | $135,000 | 495\n\nTotal: 12 rows × 3 columns",
"row_count": 12,
"supports_sorting": true
}
```
## Dataset Tools
### list_datasets
List available datasets with columns and metrics.
**Request Schema:**
```json
{
"search": "sales",
"filters": [
{
"col": "is_active",
"opr": "eq",
"value": true
}
],
"include_columns": true, // Include column metadata
"include_metrics": true, // Include metric metadata
"page": 1,
"page_size": 15
}
```
**Response Example:**
```json
{
"datasets": [
{
"id": 1,
"uuid": "dataset-uuid-1",
"table_name": "sales_data",
"database_name": "main_warehouse",
"schema": "public",
"owners": ["admin"],
"columns": [
{
"column_name": "region",
"type": "VARCHAR",
"is_active": true,
"is_dttm": false
},
{
"column_name": "revenue",
"type": "DECIMAL",
"is_active": true,
"is_dttm": false
}
],
"metrics": [
{
"metric_name": "sum__revenue",
"expression": "SUM(revenue)",
"metric_type": "sum"
}
],
"created_on": "2024-01-05T08:00:00Z"
}
],
"total_count": 23,
"page": 1,
"page_size": 15
}
```
### get_dataset_info
Get detailed dataset information with full column/metric metadata.
**Request Schema:**
```json
{
"identifier": "dataset-uuid-1", // ID or UUID
"include_columns": true,
"include_metrics": true,
"use_cache": true
}
```
**Response Example:**
```json
{
"dataset_id": 1,
"uuid": "dataset-uuid-1",
"table_name": "sales_data",
"database_name": "main_warehouse",
"database_id": 1,
"schema": "public",
"sql": null,
"is_active": true,
"owners": ["admin", "data_team"],
"columns": [
{
"id": 101,
"column_name": "region",
"type": "VARCHAR",
"is_active": true,
"is_dttm": false,
"groupby": true,
"filterable": true,
"description": "Geographic region"
},
{
"id": 102,
"column_name": "order_date",
"type": "DATE",
"is_active": true,
"is_dttm": true,
"groupby": true,
"filterable": true
}
],
"metrics": [
{
"id": 201,
"metric_name": "sum__revenue",
"expression": "SUM(revenue)",
"metric_type": "sum",
"is_active": true,
"description": "Total revenue"
}
],
"created_on": "2024-01-05T08:00:00Z",
"changed_on": "2024-01-18T12:30:00Z"
}
```
## System Tools
### get_superset_instance_info
Get Superset instance information and statistics.
**Request Schema:**
```json
{
"include_statistics": true, // Include usage statistics
"include_tools": true, // Include available MCP tools
"use_cache": true
}
```
**Response Example:**
```json
{
"version": "4.1.0",
"build": "apache-superset-4.1.0",
"mcp_service_version": "1.0.0",
"authentication": {
"enabled": true,
"type": "jwt_bearer",
"required_scopes": ["dashboard:read", "chart:read"]
},
"statistics": {
"dashboards": {
"total": 45,
"published": 32
},
"charts": {
"total": 125,
"by_viz_type": {
"line": 35,
"bar": 28,
"table": 42,
"pie": 20
}
},
"datasets": {
"total": 23,
"active": 18
},
"users": {
"total": 15,
"active": 12
}
},
"mcp_tools": [
{
"name": "list_dashboards",
"description": "List dashboards with search and filtering",
"category": "dashboard"
},
{
"name": "generate_chart",
"description": "Create new charts programmatically",
"category": "chart"
}
],
"database_connections": [
{
"id": 1,
"database_name": "main_warehouse",
"backend": "postgresql",
"status": "healthy"
}
],
"cache_status": {
"enabled": true,
"backend": "redis",
"hit_rate": 0.85
}
}
```
### generate_explore_link
Generate Superset explore URLs with pre-configured chart settings.
**Request Schema:**
```json
{
"dataset_id": "1",
"chart_config": {
"viz_type": "line",
"x_axis": "month",
"metrics": ["sum__revenue"],
"time_range": "Last 6 months"
},
"title": "Revenue Analysis",
"cache_form_data": true
}
```
**Response Example:**
```json
{
"explore_url": "/superset/explore/?form_data_key=abc123def456",
"full_url": "http://localhost:8088/superset/explore/?form_data_key=abc123def456",
"form_data_key": "abc123def456",
"expires_at": "2024-01-26T16:45:00Z",
"chart_config": {
"viz_type": "line",
"datasource": "1__table",
"x_axis": "month",
"metrics": ["sum__revenue"],
"time_range": "Last 6 months"
}
}
```
## SQL Lab Tools
### open_sql_lab_with_context
Open SQL Lab with pre-configured database, schema, and SQL.
**Request Schema:**
```json
{
"database_connection_id": 1,
"schema": "public",
"dataset_in_context": "sales_data",
"sql": "SELECT region, SUM(revenue) as total_revenue\nFROM sales_data \nWHERE order_date >= '2024-01-01'\nGROUP BY region\nORDER BY total_revenue DESC",
"title": "Regional Sales Analysis"
}
```
**Response Example:**
```json
{
"sql_lab_url": "/superset/sqllab/?dbid=1&schema=public&sql_template=encoded_sql_here",
"full_url": "http://localhost:8088/superset/sqllab/?dbid=1&schema=public&sql_template=encoded_sql_here",
"database_connection": {
"id": 1,
"database_name": "main_warehouse",
"backend": "postgresql"
},
"schema": "public",
"sql_template": "SELECT region, SUM(revenue) as total_revenue...",
"context": {
"dataset": "sales_data",
"title": "Regional Sales Analysis"
}
}
```
## Error Responses
All tools can return error responses with this structure:
```json
{
"error": "Chart not found with identifier: 999",
"error_type": "NotFound",
"suggestions": [
"Verify the chart ID exists",
"Check if you have permission to access this chart",
"Try using the chart UUID instead of ID"
],
"details": {
"identifier": 999,
"identifier_type": "id"
}
}
```
## Cache Status
Many responses include cache status information:
```json
{
"cache_status": {
"cache_hit": true, // Data served from cache
"cache_type": "query", // Type: query, metadata, form_data
"cache_age_seconds": 300, // Age of cached data
"refreshed": false // Whether cache was refreshed
}
}
```
This API reference provides complete documentation for integrating with the Superset MCP service, including all request schemas, response formats, and error handling patterns.
## What's Next?
### 🔐 **Ready for Production?**
Set up authentication and security with the [Authentication Guide](./authentication).
### 🔧 **Want to Add More Tools?**
Learn how to extend the MCP service in the [Development Guide](./development).
### 🏗️ **Need Architecture Details?**
Understand the system design in the [Architecture Overview](./architecture).
### 🏢 **Enterprise Features?**
Explore advanced capabilities in the [Preset Integration Guide](./preset-integration).
> 📖 **Back to Documentation Index**: [MCP Service](./intro)

View File

@@ -1,191 +0,0 @@
---
title: Architecture Overview
sidebar_position: 5
version: 1
---
# Architecture Overview
The Superset Model Context Protocol (MCP) service provides a modular, schema-driven interface for programmatic access to Superset dashboards, charts, datasets, and instance metadata. Built on FastMCP for LLM agents and automation tools.
**Status:** Phase 1 Complete. Core functionality stable, authentication production-ready. See [SIP-171](https://github.com/apache/superset/issues/33870) for roadmap.
## Core Architecture
### Tool Structure
- **16 MCP tools** organized by domain: `dashboard/`, `chart/`, `dataset/`, `system/`
- All tools decorated with `@mcp.tool` and `@mcp_auth_hook`
- **Import inside functions**: All Superset DAOs/commands imported in function body to ensure proper app context
- Pydantic v2 schemas with LLM/OpenAPI-compatible field descriptions
### Request Schema Pattern
Eliminates LLM parameter validation issues using structured request objects:
```python
# New approach - single request object
get_dataset_info(request={"identifier": 123}) # ID
get_dataset_info(request={"identifier": "uuid-string"}) # UUID
# Old approach - replaced
get_dataset_info(dataset_id=123)
```
### Multi-Identifier Support
- **Charts/Datasets**: ID (numeric) or UUID (string)
- **Dashboards**: ID (numeric), UUID (string), or slug (string)
- Validation prevents conflicting parameters (search + filters)
## Available Tools
### Dashboard Tools (5)
- `list_dashboards` - List with search/filters/pagination
- `get_dashboard_info` - Get by ID/UUID/slug
- `get_dashboard_available_filters` - Discover filterable columns
- `generate_dashboard` - Create dashboards with multiple charts
- `add_chart_to_existing_dashboard` - Add charts to existing dashboards
### Chart Tools (8)
- `list_charts` - List with search/filters/pagination
- `get_chart_info` - Get by ID/UUID
- `get_chart_available_filters` - Discover filterable columns
- `generate_chart` - Create charts (table, line, bar, area, scatter)
- `update_chart` - Update saved charts
- `update_chart_preview` - Update cached previews
- `get_chart_data` - Export data (JSON/CSV/Excel)
- `get_chart_preview` - Screenshots, ASCII art, table previews
### Dataset Tools (3)
- `list_datasets` - List with columns/metrics
- `get_dataset_info` - Get by ID/UUID with metadata
- `get_dataset_available_filters` - Discover filterable columns
### System Tools (2)
- `get_superset_instance_info` - Instance statistics and version
- `generate_explore_link` - Generate chart exploration URLs
### SQL Lab Tools (1)
- `open_sql_lab_with_context` - Pre-configured SQL Lab sessions
## Authentication & Security
### JWT Bearer Authentication
Production-ready authentication with configurable factory pattern:
```python
# In superset_config.py
MCP_AUTH_ENABLED = True
MCP_JWKS_URI = "https://auth.company.com/.well-known/jwks.json"
MCP_JWT_ISSUER = "https://auth.company.com/"
MCP_JWT_AUDIENCE = "superset-mcp-api"
```
### Scope-Based Authorization
| Tool Category | Required Scope |
|---------------|----------------|
| Dashboard ops | `dashboard:read` |
| Chart ops | `chart:read` / `chart:write` |
| Dataset ops | `dataset:read` |
| System ops | `instance:read` |
### Audit Logging
All operations logged with MCP context:
- User impersonation tracking
- Tool execution details
- Sanitized payloads (sensitive data redacted)
## Cache Control
Leverages Superset's existing cache layers with comprehensive control:
### Cache Types
1. **Query Result Cache** - Database query results
2. **Metadata Cache** - Table schemas, columns, metrics
3. **Form Data Cache** - Chart configurations
4. **Dashboard Cache** - Rendered components
### Cache Parameters
Tools support cache control through request schemas:
- `use_cache`: Enable/disable caching (default: true)
- `force_refresh`: Force cache refresh (default: false)
- `cache_timeout`: Override timeout in seconds
- `refresh_metadata`: Force metadata refresh
### Cache Status Reporting
```json
{
"cache_status": {
"cache_hit": true,
"cache_type": "query",
"cache_age_seconds": 300,
"refreshed": false
}
}
```
## Tool Abstractions
### Generic Base Classes
- **ModelListTool**: Handles list/search/filter operations with pagination
- **ModelGetInfoTool**: Single object retrieval by multiple identifier types
- **ModelGetAvailableFiltersTool**: Returns filterable columns/operators
### Implementation Pattern
```python
@mcp.tool
@mcp_auth_hook
def my_tool(request: MyRequest) -> MyResponse:
# Import Superset modules inside function
from superset.daos.dashboard import DashboardDAO
from superset.commands.chart.create import CreateChartCommand
# Tool implementation
return response
```
## Configuration
### URL Configuration
Centralized URL management for consistent link generation:
```python
# In superset_config.py
SUPERSET_WEBSERVER_ADDRESS = "http://localhost:8088" # Development
SUPERSET_WEBSERVER_ADDRESS = "https://superset.company.com" # Production
```
### Schema Design Principles
- **Minimal columns** in list responses
- **Optional fields** in info schemas for missing data handling
- **Null exclusion** for cleaner JSON responses
- **Type safety** with clear Pydantic validation
## Adding New Tools
1. **Choose domain folder**: `dashboard/`, `chart/`, `dataset/`, or `system/`
2. **Define schemas**: Use Pydantic with field descriptions
3. **Implement tool**:
- Decorate with `@mcp.tool` and `@mcp_auth_hook`
- Import Superset modules inside function body
- Use generic abstractions where applicable
4. **Register**: Add to appropriate `__init__.py`
5. **Test**: Add unit tests in `tests/unit_tests/mcp_service/`
## Current Status
### ✅ Phase 1 Complete
- FastMCP server with CLI
- JWT authentication with RBAC
- All 16 core tools implemented
- Request schema pattern
- Cache control system
- Audit logging
- 194+ unit tests
### 🎯 Future Enhancements
- Demo notebooks and video examples
- OAuth integration for user impersonation
- Enhanced chart rendering formats
- Advanced security features
**Production Ready**: Core functionality stable with comprehensive testing and authentication.
---
For setup and usage, see the [MCP Service overview](./intro).

View File

@@ -1,434 +0,0 @@
---
title: Authentication & Security
sidebar_position: 4
version: 1
---
# Authentication & Security
The MCP service provides enterprise-grade JWT Bearer authentication with flexible configuration options and comprehensive security controls.
## Quick Start
### Development Mode (Default)
:::tip
Authentication is **disabled by default** for local development - no configuration needed.
:::
```bash
# No configuration needed - service runs without authentication
superset mcp run --port 5008 --debug
```
### Production Mode
:::warning
Always enable authentication for production deployments to secure your Superset instance.
:::
Enable JWT authentication in your Superset configuration:
```python
# In superset_config.py
MCP_AUTH_ENABLED = True
MCP_JWKS_URI = "https://auth.company.com/.well-known/jwks.json"
MCP_JWT_ISSUER = "https://auth.company.com/"
MCP_JWT_AUDIENCE = "superset-mcp-api"
MCP_REQUIRED_SCOPES = ["dashboard:read", "chart:read", "dataset:read"]
```
## Configuration Options
### Option 1: Simple Configuration
Add to your `superset_config.py`:
```python
# Enable authentication
MCP_AUTH_ENABLED = True
# JWT settings
MCP_JWKS_URI = "https://auth.company.com/.well-known/jwks.json"
MCP_JWT_ISSUER = "https://auth.company.com/"
MCP_JWT_AUDIENCE = "superset-mcp-api"
MCP_REQUIRED_SCOPES = ["dashboard:read", "chart:read"]
# Optional: User resolution
MCP_JWT_USER_CLAIM = "sub" # JWT claim for username (default: "sub")
MCP_JWT_EMAIL_CLAIM = "email" # JWT claim for email (default: "email")
MCP_FALLBACK_USER = "admin" # Fallback user if JWT user not found
```
### Option 2: Custom Factory
For advanced authentication requirements:
```python
def create_custom_mcp_auth(app):
"""Custom auth factory for enterprise environments."""
from fastmcp.server.auth.providers.bearer import BearerAuthProvider
return BearerAuthProvider(
jwks_uri=app.config["MCP_JWKS_URI"],
issuer=app.config["MCP_JWT_ISSUER"],
audience=app.config["MCP_JWT_AUDIENCE"],
required_scopes=app.config.get("MCP_REQUIRED_SCOPES", []),
user_resolver=custom_user_resolver,
cache_ttl=300 # Cache JWKS for 5 minutes
)
MCP_AUTH_FACTORY = create_custom_mcp_auth
```
### Option 3: Environment Variables
For containerized deployments:
```bash
# Environment variables
export MCP_AUTH_ENABLED=true
export MCP_JWKS_URI=https://auth.company.com/.well-known/jwks.json
export MCP_JWT_ISSUER=https://auth.company.com/
export MCP_JWT_AUDIENCE=superset-mcp-api
export MCP_REQUIRED_SCOPES=dashboard:read,chart:read,dataset:read
```
## Identity Provider Integration
### Auth0
```python
# Auth0 configuration
MCP_JWKS_URI = "https://your-tenant.auth0.com/.well-known/jwks.json"
MCP_JWT_ISSUER = "https://your-tenant.auth0.com/"
MCP_JWT_AUDIENCE = "superset-mcp-api"
```
### Okta
```python
# Okta configuration
MCP_JWKS_URI = "https://your-org.okta.com/oauth2/default/v1/keys"
MCP_JWT_ISSUER = "https://your-org.okta.com/oauth2/default"
MCP_JWT_AUDIENCE = "api://superset-mcp"
```
### AWS Cognito
```python
# Cognito configuration
MCP_JWKS_URI = "https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json"
MCP_JWT_ISSUER = "https://cognito-idp.{region}.amazonaws.com/{userPoolId}"
MCP_JWT_AUDIENCE = "your-app-client-id"
```
### Azure AD
```python
# Azure AD configuration
MCP_JWKS_URI = "https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys"
MCP_JWT_ISSUER = "https://login.microsoftonline.com/{tenant}/v2.0"
MCP_JWT_AUDIENCE = "api://superset-mcp"
```
## Scope-Based Authorization
### Standard Scopes
The MCP service defines these standard scopes:
| Scope | Description | Required For |
|-------|-------------|--------------|
| `dashboard:read` | Read dashboard information | `list_dashboards`, `get_dashboard_info` |
| `dashboard:write` | Create/modify dashboards | `generate_dashboard`, `add_chart_to_existing_dashboard` |
| `chart:read` | Read chart information | `list_charts`, `get_chart_info`, `get_chart_data` |
| `chart:write` | Create/modify charts | `generate_chart`, `update_chart` |
| `dataset:read` | Read dataset information | `list_datasets`, `get_dataset_info` |
| `instance:read` | Read instance information | `get_superset_instance_info` |
### Custom Scopes
Define custom scopes for specific use cases:
```python
# Custom scope definitions
CUSTOM_MCP_SCOPES = {
"analytics:export": "Export analytical data",
"reports:generate": "Generate automated reports",
"admin:config": "Access administrative configuration"
}
# Map tools to custom scopes
def get_custom_required_scopes(tool_name: str) -> List[str]:
scope_map = {
"get_chart_data": ["chart:read", "analytics:export"],
"generate_dashboard": ["dashboard:write", "reports:generate"],
"get_superset_instance_info": ["instance:read", "admin:config"]
}
return scope_map.get(tool_name, [])
MCP_SCOPE_RESOLVER = get_custom_required_scopes
```
## JWT Token Format
### Required Claims
Your JWT tokens must include these standard claims:
```json
{
"iss": "https://auth.company.com/", // Issuer
"aud": "superset-mcp-api", // Audience
"sub": "user@company.com", // Subject (username)
"exp": 1704118800, // Expiration timestamp
"iat": 1704115200, // Issued at timestamp
"scope": "dashboard:read chart:read" // Space-separated scopes
}
```
### Optional Claims
Additional claims for enhanced functionality:
```json
{
"email": "user@company.com", // User email
"name": "John Doe", // Full name
"groups": ["analysts", "sales_team"], // User groups
"tenant_id": "company_123", // Multi-tenant ID
"role": "analyst" // User role
}
```
## Client Integration
### API Client Usage
```python
import requests
# Get JWT token from your identity provider
token = get_jwt_token()
# Call MCP service with Bearer authentication
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
response = requests.post(
"http://localhost:5008/call_tool",
headers=headers,
json={
"tool": "list_dashboards",
"arguments": {"search": "sales"}
}
)
data = response.json()
```
### Claude Desktop with Authentication
For Claude Desktop, the proxy script handles authentication:
```bash
#!/bin/bash
# run_proxy_with_auth.sh
# Get token from environment or file
if [ -f ~/.superset_mcp_token ]; then
TOKEN=$(cat ~/.superset_mcp_token)
else
TOKEN=${SUPERSET_MCP_TOKEN}
fi
# Export token for proxy
export MCP_AUTH_TOKEN="$TOKEN"
cd /path/to/superset
source venv/bin/activate
exec fastmcp proxy http://localhost:5008 --auth-header "Authorization: Bearer $TOKEN"
```
## User Resolution
### Default User Resolution
The service maps JWT claims to Superset users:
```python
def default_user_resolver(claims: Dict[str, Any]) -> User:
"""Default user resolution from JWT claims."""
# Extract username from configurable claim
username = claims.get(app.config.get("MCP_JWT_USER_CLAIM", "sub"))
# Find Superset user
user = security_manager.find_user(username=username)
if not user:
# Try email lookup
email = claims.get(app.config.get("MCP_JWT_EMAIL_CLAIM", "email"))
if email:
user = security_manager.find_user(email=email)
if not user and app.config.get("MCP_FALLBACK_USER"):
# Use fallback user for development
user = security_manager.find_user(username=app.config["MCP_FALLBACK_USER"])
return user
```
### Custom User Resolution
Implement custom user resolution logic:
```python
def custom_user_resolver(claims: Dict[str, Any]) -> User:
"""Custom user resolution for enterprise environments."""
# Extract custom claims
employee_id = claims.get("employee_id")
tenant_id = claims.get("tenant_id")
# Multi-tenant user lookup
user = find_user_by_employee_id(employee_id, tenant_id)
if user:
# Set additional context
user.mcp_tenant_id = tenant_id
user.mcp_groups = claims.get("groups", [])
return user
# Use custom resolver
MCP_USER_RESOLVER = custom_user_resolver
```
## Security Features
### Token Validation
Comprehensive JWT validation:
- **Signature verification**: RS256 with JWKS key rotation support
- **Expiration checking**: Automatic token expiry validation
- **Audience validation**: Prevents token reuse across services
- **Issuer validation**: Ensures tokens from trusted sources only
- **Scope validation**: Enforces tool-level permissions
### Request Security
- **HTTPS enforcement**: Production deployments should use HTTPS
- **Rate limiting**: Configurable per-user rate limits
- **Request logging**: All authenticated requests logged with user context
- **Input validation**: Comprehensive request schema validation
### Audit Logging
Every tool call is logged with security context:
```json
{
"timestamp": "2024-01-25T14:30:00Z",
"user_id": "user@company.com",
"tool_name": "get_chart_data",
"source": "mcp",
"jwt_subject": "user@company.com",
"jwt_scopes": ["chart:read", "analytics:export"],
"tenant_id": "company_123",
"request_id": "req_12345",
"execution_time": 0.145,
"status": "success"
}
```
## Testing Authentication
### Generate Test Tokens
For development and testing:
```python
from fastmcp.server.auth.providers.bearer import RSAKeyPair
# Generate test keypair
keypair = RSAKeyPair.generate()
print("Public key:", keypair.public_key)
# Create test token
token = keypair.create_token(
subject="test@example.com",
issuer="https://test.example.com",
audience="superset-mcp-api",
scopes=["dashboard:read", "chart:read", "dataset:read"],
expires_in=3600 # 1 hour
)
print("Test token:", token)
```
### Test Configuration
```python
# Test configuration with generated keypair
MCP_AUTH_ENABLED = True
MCP_JWT_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----"""
MCP_JWT_ISSUER = "https://test.example.com"
MCP_JWT_AUDIENCE = "superset-mcp-api"
MCP_FALLBACK_USER = "admin"
```
### Manual Testing
```bash
# Test with curl
curl -X POST http://localhost:5008/call_tool \
-H "Authorization: Bearer $TEST_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tool": "get_superset_instance_info",
"arguments": {"include_statistics": true}
}'
```
## Troubleshooting
### Common Issues
**Token Validation Errors:**
```
Error: Invalid JWT signature
Solution: Verify JWKS_URI is accessible and contains correct keys
```
**User Not Found:**
```
Error: User not found for JWT subject
Solution: Check MCP_JWT_USER_CLAIM configuration and user exists in Superset
```
**Insufficient Scopes:**
```
Error: Missing required scope 'chart:read'
Solution: Update JWT token to include required scopes
```
### Debug Configuration
Enable debug logging for authentication issues:
```python
# Enhanced logging for auth debugging
import logging
logging.getLogger('superset.mcp_service.auth').setLevel(logging.DEBUG)
# Log all JWT validation steps
MCP_AUTH_DEBUG = True
```
This authentication guide provides comprehensive coverage for securing the MCP service in production environments while maintaining development flexibility.

View File

@@ -1,705 +0,0 @@
---
title: Development Guide
sidebar_position: 2
version: 1
---
# MCP Service Development Guide
This guide covers the internal architecture, development workflows, and patterns for extending the Superset MCP service.
> 🚀 **New to MCP?** Start with the [Overview](./overview) to understand what the service does before diving into development.
>
> 📚 **Need API examples?** Check the [API Reference](./api-reference) to see how existing tools work.
>
> 🔐 **Planning production use?** Review [Authentication](./authentication) for security considerations.
## Internal Architecture
### Component Overview
The MCP service follows a layered architecture with clear separation of concerns:
```mermaid
graph TB
subgraph "Transport Layer"
HTTP[HTTP Server :5008]
FastMCP[FastMCP Protocol Handler]
end
subgraph "Auth & Middleware Layer"
AuthHook[Auth Hook Decorator]
JWT[JWT Validator]
RBAC[RBAC Engine]
Audit[Audit Logger]
end
subgraph "Tool Layer"
Tools[16 MCP Tools<br/>Tool Decorated]
Schemas[Pydantic Schemas]
Validation[Request Validation]
end
subgraph "Business Logic Layer"
Generic[Generic Tool Abstractions]
ModelList[ModelListTool]
ModelGet[ModelGetInfoTool]
ModelFilter[ModelGetAvailableFiltersTool]
end
subgraph "Data Access Layer"
DAOs[Superset DAOs]
Commands[Superset Commands]
Cache[Cache Manager]
end
subgraph "Storage Layer"
MetaDB[(Metadata DB)]
DataWH[(Data Warehouse)]
Redis[(Redis Cache)]
end
HTTP --> FastMCP
FastMCP --> AuthHook
AuthHook --> JWT
JWT --> RBAC
RBAC --> Audit
Audit --> Tools
Tools --> Schemas
Schemas --> Validation
Validation --> Generic
Generic --> ModelList
Generic --> ModelGet
Generic --> ModelFilter
ModelList --> DAOs
ModelGet --> DAOs
ModelFilter --> DAOs
Tools --> Commands
Commands --> Cache
DAOs --> MetaDB
Commands --> MetaDB
Commands --> DataWH
Cache --> Redis
```
### Request Flow
Every MCP tool call follows this execution pattern:
```mermaid
sequenceDiagram
participant Client as LLM Client
participant MCP as FastMCP Server
participant Auth as Auth Hook
participant Tool as MCP Tool
participant Generic as Generic Abstraction
participant DAO as Superset DAO
participant DB as Database
Client->>+MCP: tool_call(request)
MCP->>+Auth: validate_and_authorize()
Auth->>Auth: Validate JWT token
Auth->>Auth: Check required scopes
Auth->>Auth: Set Flask g.user context
Auth->>Auth: Log audit event
Auth->>+Tool: execute_tool(validated_request)
Tool->>Tool: Parse Pydantic request schema
Tool->>+Generic: Use generic abstraction
Generic->>+DAO: Query Superset data
DAO->>+DB: Execute SQL
DB-->>-DAO: Return results
DAO-->>-Generic: Return objects
Generic->>Generic: Apply pagination/filtering
Generic-->>-Tool: Return formatted data
Tool->>Tool: Build Pydantic response schema
Tool-->>-Auth: Return response
Auth->>Auth: Log success audit event
Auth-->>-MCP: Return validated response
MCP-->>-Client: JSON response
```
### Tool Registration System
Tools are automatically discovered and registered through the decorator pattern:
```python
# superset/mcp_service/mcp_app.py
from fastmcp import FastMCP
# Global MCP instance
mcp = FastMCP("Superset MCP Service")
# Tools register themselves via decorators
@mcp.tool
@mcp_auth_hook(['chart:read'])
def get_chart_info(request: GetChartInfoRequest) -> GetChartInfoResponse:
# Tool implementation
pass
# All tool modules imported to trigger registration
from superset.mcp_service.chart.tool import *
from superset.mcp_service.dashboard.tool import *
from superset.mcp_service.dataset.tool import *
from superset.mcp_service.system.tool import *
```
## Development Patterns
### Tool Implementation Pattern
All tools follow this standardized pattern:
```python
# Example: superset/mcp_service/chart/tool/get_chart_info.py
from superset.mcp_service.auth import mcp_auth_hook
from superset.mcp_service.mcp_app import mcp
from superset.mcp_service.schemas.chart_schemas import (
GetChartInfoRequest,
GetChartInfoResponse,
ChartError
)
@mcp.tool
@mcp_auth_hook(['chart:read'])
def get_chart_info(request: GetChartInfoRequest) -> GetChartInfoResponse:
"""
Get detailed information about a specific chart.
Supports lookup by ID or UUID with comprehensive metadata.
"""
try:
# CRITICAL: Import Superset modules inside function
from superset.daos.chart import ChartDAO
from superset.models.slice import Slice
# Use generic abstraction for common operations
from superset.mcp_service.generic_tools import ModelGetInfoTool
tool = ModelGetInfoTool(
dao=ChartDAO,
model=Slice,
response_schema=GetChartInfoResponse,
identifier_field_map={
'id': 'id',
'uuid': 'uuid'
}
)
return tool.execute(request)
except Exception as e:
return ChartError(
error=f"Failed to get chart info: {str(e)}",
error_type="ChartInfoError"
)
```
### Schema Design Patterns
Pydantic schemas follow these conventions:
```python
# Request Schema Pattern
class GetChartInfoRequest(BaseModel):
"""Request to get detailed chart information."""
identifier: Union[int, str] = Field(
...,
description="Chart ID (numeric) or UUID (string)"
)
include_form_data: bool = Field(
default=True,
description="Whether to include chart configuration"
)
use_cache: bool = Field(
default=True,
description="Whether to use cached data"
)
# Response Schema Pattern
class GetChartInfoResponse(BaseModel):
"""Detailed chart information response."""
chart_id: int = Field(description="Chart numeric ID")
uuid: Optional[str] = Field(description="Chart UUID")
slice_name: str = Field(description="Chart display name")
viz_type: str = Field(description="Visualization type")
datasource_id: Optional[int] = Field(description="Dataset ID")
form_data: Optional[Dict[str, Any]] = Field(description="Chart configuration")
explore_url: Optional[str] = Field(description="Explore URL for editing")
# Cache status for transparency
cache_status: Optional[CacheStatus] = Field(description="Cache hit information")
# Error Schema Pattern
class ChartError(BaseModel):
"""Chart operation error response."""
error: str = Field(description="Error message")
error_type: str = Field(description="Error type identifier")
suggestions: Optional[List[str]] = Field(description="Suggested fixes")
```
### Generic Tool Abstractions
Common operations are abstracted into reusable classes:
```python
# superset/mcp_service/generic_tools.py
from typing import Type, Dict, Any, List, Optional
from pydantic import BaseModel
class ModelListTool:
"""Generic tool for list operations with pagination and filtering."""
def __init__(self,
dao: Type,
model: Type,
response_schema: Type[BaseModel],
default_columns: List[str] = None,
searchable_columns: List[str] = None):
self.dao = dao
self.model = model
self.response_schema = response_schema
self.default_columns = default_columns or []
self.searchable_columns = searchable_columns or []
def execute(self, request: BaseModel) -> BaseModel:
"""Execute list operation with pagination and filtering."""
# Build query with filters
query = self.dao.find_all()
# Apply search if provided
if hasattr(request, 'search') and request.search:
query = self._apply_search(query, request.search)
# Apply filters if provided
if hasattr(request, 'filters') and request.filters:
query = self._apply_filters(query, request.filters)
# Apply pagination
total = query.count()
if hasattr(request, 'page') and hasattr(request, 'page_size'):
offset = (request.page - 1) * request.page_size
query = query.offset(offset).limit(request.page_size)
# Execute query and serialize
results = query.all()
serialized = [self._serialize_model(obj) for obj in results]
return self.response_schema(
results=serialized,
total_count=total,
page=getattr(request, 'page', 1),
page_size=getattr(request, 'page_size', len(serialized))
)
class ModelGetInfoTool:
"""Generic tool for getting single object by multiple identifier types."""
def __init__(self,
dao: Type,
model: Type,
response_schema: Type[BaseModel],
identifier_field_map: Dict[str, str]):
self.dao = dao
self.model = model
self.response_schema = response_schema
self.identifier_field_map = identifier_field_map
def execute(self, request: BaseModel) -> BaseModel:
"""Execute get operation with multi-identifier support."""
identifier = request.identifier
# Determine identifier type and field
if isinstance(identifier, int):
field = self.identifier_field_map.get('id', 'id')
obj = self.dao.find_by_id(identifier)
elif isinstance(identifier, str):
if len(identifier) == 36 and '-' in identifier: # UUID format
field = self.identifier_field_map.get('uuid', 'uuid')
obj = self.dao.find_by_uuid(identifier)
else: # Assume slug
field = self.identifier_field_map.get('slug', 'slug')
obj = getattr(self.dao, 'find_by_slug', lambda x: None)(identifier)
if not obj:
raise ValueError(f"Object not found with identifier: {identifier}")
# Serialize and return
serialized = self._serialize_model(obj)
return self.response_schema(**serialized)
```
## Adding New Tools
### Step-by-Step Process
1. **Define the Domain**
Choose the appropriate domain folder:
- `dashboard/` - Dashboard operations
- `chart/` - Chart operations
- `dataset/` - Dataset operations
- `system/` - System-level operations
2. **Create Schemas**
```bash
# Create schema file
touch superset/mcp_service/schemas/my_domain_schemas.py
```
```python
# Define request/response schemas
class MyToolRequest(BaseModel):
param1: str = Field(description="Parameter description")
param2: Optional[int] = Field(default=None, description="Optional parameter")
class MyToolResponse(BaseModel):
result: str = Field(description="Result description")
metadata: Dict[str, Any] = Field(description="Additional metadata")
```
3. **Implement the Tool**
```bash
# Create tool file
touch superset/mcp_service/my_domain/tool/my_tool.py
```
```python
@mcp.tool
@mcp_auth_hook(['required:scope'])
def my_tool(request: MyToolRequest) -> MyToolResponse:
"""Tool description for LLM."""
# Import Superset modules inside function
from superset.daos.my_dao import MyDAO
# Implement business logic
result = MyDAO.do_something(request.param1)
return MyToolResponse(
result=result,
metadata={"processed_at": datetime.utcnow()}
)
```
4. **Register the Tool**
```python
# Add to superset/mcp_service/my_domain/tool/__init__.py
from .my_tool import my_tool
__all__ = ['my_tool']
```
```python
# Import in superset/mcp_service/mcp_app.py
from superset.mcp_service.my_domain.tool import *
```
5. **Add Tests**
```bash
# Create test file
touch tests/unit_tests/mcp_service/test_my_tool.py
```
```python
import pytest
from superset.mcp_service.my_domain.tool.my_tool import my_tool
from superset.mcp_service.schemas.my_domain_schemas import MyToolRequest
class TestMyTool:
def test_my_tool_success(self):
request = MyToolRequest(param1="test")
response = my_tool(request)
assert response.result == "expected_result"
```
### Tool Best Practices
1. **Import Inside Functions**
```python
# ❌ DON'T: Import at module level
from superset.daos.chart import ChartDAO
@mcp.tool
def my_tool():
# Tool implementation
pass
# ✅ DO: Import inside function
@mcp.tool
def my_tool():
from superset.daos.chart import ChartDAO
# Tool implementation
pass
```
2. **Use Generic Abstractions**
```python
# ✅ Leverage existing patterns
@mcp.tool
def list_my_objects(request):
from superset.mcp_service.generic_tools import ModelListTool
tool = ModelListTool(
dao=MyDAO,
model=MyModel,
response_schema=ListMyObjectsResponse
)
return tool.execute(request)
```
3. **Comprehensive Error Handling**
```python
@mcp.tool
def my_tool(request):
try:
# Tool implementation
return success_response
except PermissionError as e:
return MyToolError(
error="Permission denied",
error_type="PermissionError",
suggestions=["Check user permissions"]
)
except Exception as e:
return MyToolError(
error=f"Unexpected error: {str(e)}",
error_type="InternalError"
)
```
## Testing Patterns
### Unit Test Structure
```python
# tests/unit_tests/mcp_service/test_chart_tools.py
import pytest
from unittest.mock import Mock, patch
from superset.mcp_service.chart.tool.get_chart_info import get_chart_info
from superset.mcp_service.schemas.chart_schemas import GetChartInfoRequest
class TestGetChartInfo:
"""Test suite for get_chart_info tool."""
@patch('superset.mcp_service.chart.tool.get_chart_info.ChartDAO')
def test_get_chart_info_by_id_success(self, mock_dao):
"""Test successful chart lookup by ID."""
# Setup mock
mock_chart = Mock()
mock_chart.id = 1
mock_chart.slice_name = "Test Chart"
mock_chart.viz_type = "line"
mock_dao.find_by_id.return_value = mock_chart
# Execute
request = GetChartInfoRequest(identifier=1)
response = get_chart_info(request)
# Verify
assert response.chart_id == 1
assert response.slice_name == "Test Chart"
mock_dao.find_by_id.assert_called_once_with(1)
@patch('superset.mcp_service.chart.tool.get_chart_info.ChartDAO')
def test_get_chart_info_not_found(self, mock_dao):
"""Test chart not found scenario."""
# Setup mock
mock_dao.find_by_id.return_value = None
# Execute
request = GetChartInfoRequest(identifier=999)
response = get_chart_info(request)
# Verify error response
assert hasattr(response, 'error')
assert "not found" in response.error.lower()
```
### Integration Test Patterns
```python
# tests/integration_tests/mcp_service/test_chart_integration.py
import pytest
from superset.app import create_app
from superset.mcp_service.mcp_app import mcp
from tests.integration_tests.base_tests import SupersetTestCase
class TestChartIntegration(SupersetTestCase):
"""Integration tests for chart tools."""
def setUp(self):
super().setUp()
self.app = create_app()
self.app_context = self.app.app_context()
self.app_context.push()
def tearDown(self):
self.app_context.pop()
super().tearDown()
def test_chart_workflow_integration(self):
"""Test complete chart workflow."""
# Create chart
create_request = {
"dataset_id": "1",
"config": {
"chart_type": "table",
"columns": [{"name": "region"}]
}
}
create_response = mcp.call_tool("generate_chart", create_request)
chart_id = create_response["chart_id"]
# Get chart info
info_request = {"identifier": chart_id}
info_response = mcp.call_tool("get_chart_info", info_request)
assert info_response["chart_id"] == chart_id
assert info_response["viz_type"] == "table"
# Get chart data
data_request = {"identifier": chart_id, "limit": 10}
data_response = mcp.call_tool("get_chart_data", data_request)
assert "data" in data_response
assert len(data_response["data"]) <= 10
```
## Performance Considerations
### Caching Strategy
The MCP service leverages Superset's existing cache layers:
```python
# Cache control in tools
@mcp.tool
def get_chart_data(request: GetChartDataRequest):
"""Tool with cache control."""
cache_config = {
'use_cache': request.use_cache,
'force_refresh': request.force_refresh,
'cache_timeout': request.cache_timeout
}
# Use Superset's cache infrastructure
result = execute_with_cache(query, cache_config)
return ChartDataResponse(
data=result.data,
cache_status=result.cache_status
)
```
### Query Optimization
```python
# Efficient pagination
def list_objects(query, page, page_size):
"""Optimized pagination pattern."""
# Count query optimization
total = query.count()
# Limit columns for list operations
query = query.options(load_only('id', 'name', 'created_on'))
# Apply pagination
offset = (page - 1) * page_size
results = query.offset(offset).limit(page_size).all()
return results, total
```
## Security Considerations
### Authentication Flow
```python
# JWT validation and user context
@mcp_auth_hook(['chart:read'])
def secure_tool(request):
"""Tool with proper security context."""
# g.user is set by auth hook
user_id = g.user.id
# Apply user-specific filtering
query = ChartDAO.find_all().filter(
Chart.owners.contains(g.user)
)
return execute_query(query)
```
### Input Validation
```python
# Comprehensive request validation
class CreateChartRequest(BaseModel):
"""Validated chart creation request."""
dataset_id: Union[int, str] = Field(
...,
description="Dataset ID or UUID"
)
config: ChartConfig = Field(
...,
description="Chart configuration"
)
@validator('dataset_id')
def validate_dataset_id(cls, v):
"""Validate dataset exists and user has access."""
# Validation logic
return v
@validator('config')
def validate_chart_config(cls, v):
"""Validate chart configuration."""
# Configuration validation
return v
```
This development guide provides comprehensive coverage of the MCP service's internal architecture and development patterns, enabling team members to effectively extend and maintain the system.
## Related Documentation
### 📚 **Ready to Use Your New Tools?**
Test your implementations with examples from the [API Reference](./api-reference).
### 🔐 **Securing Your Extensions?**
Add authentication to your tools using the [Authentication Guide](./authentication).
### 🏗️ **Understanding the Big Picture?**
See the complete system design in the [Architecture Overview](./architecture).
### 🏢 **Building Enterprise Features?**
Explore advanced patterns in the [Preset Integration Guide](./preset-integration).
> 📖 **Back to Documentation Index**: [MCP Service](./intro)

View File

@@ -1,124 +0,0 @@
---
title: MCP Service
sidebar_position: 1
version: 1
---
# Superset MCP Service
The Superset Model Context Protocol (MCP) service provides programmatic access to Superset dashboards, charts, datasets, and instance metadata. Built for LLM agents and automation tools.
## What is MCP?
The Model Context Protocol (MCP) is an open standard that allows AI assistants to securely connect to data sources and tools. Superset's MCP service exposes **16 production-ready tools** that enable:
- 📊 **Data Exploration**: List and query dashboards, charts, and datasets
- 🔧 **Chart Creation**: Generate visualizations programmatically
- 📈 **Data Export**: Extract data in multiple formats (JSON, CSV, Excel)
- 🔗 **Navigation**: Generate explore links and SQL Lab sessions
## Quick Start
### Installation
:::note
The MCP service is included with Superset development setup. FastMCP dependencies are installed automatically with `make install`.
:::
```bash
# MCP service is included with Superset development setup
git clone https://github.com/apache/superset.git
cd superset
make venv && source venv/bin/activate
make install
# Start Superset
superset run -p 8088 --with-threads --reload --debugger
# Start MCP service (separate terminal)
source venv/bin/activate
superset mcp run --port 5008 --debug
```
### Claude Desktop Integration
```json
{
"mcpServers": {
"Superset MCP": {
"command": "/path/to/superset/superset/mcp_service/run_proxy.sh",
"args": [],
"env": {}
}
}
}
```
## Key Features
### 🔧 **16 Production Tools**
| Category | Tools | Purpose |
|----------|-------|---------|
| **Dashboard** (5) | List, get info, create, add charts | Dashboard management |
| **Chart** (8) | Full CRUD, data export, previews | Chart operations |
| **Dataset** (3) | List, get info, discover filters | Dataset exploration |
| **System** (2) | Instance info, explore links | System integration |
| **SQL Lab** (1) | Pre-configured sessions | SQL development |
### 🔐 **Enterprise Security**
- **JWT Bearer Authentication**: Production-ready with configurable factory pattern
- **RBAC Integration**: Scope-based permissions with Superset's security model
- **Audit Logging**: Comprehensive MCP context tracking
### 📊 **Advanced Capabilities**
- **Multi-format Export**: JSON, CSV, Excel data export
- **Chart Previews**: Screenshots, ASCII art, table representations
- **Cache Control**: Leverage Superset's existing cache infrastructure
- **Request Schemas**: Eliminates LLM parameter validation issues
## Example Usage
```python
# List dashboards
dashboards = client.call_tool("list_dashboards", {
"search": "sales",
"page_size": 10
})
# Create a chart
chart = client.call_tool("generate_chart", {
"dataset_id": "1",
"config": {
"chart_type": "line",
"x": {"name": "date"},
"y": [{"name": "revenue", "aggregate": "SUM"}]
}
})
# Export chart data
data = client.call_tool("get_chart_data", {
"identifier": chart["chart_id"],
"format": "json",
"limit": 1000
})
```
## Status
✅ **Phase 1 Complete** - Core functionality stable, authentication production-ready, comprehensive testing coverage.
## Documentation Structure
### Getting Started
- **[Overview](./overview)** - Features, use cases, and examples
- **[API Reference](./api-reference)** - Complete tool documentation
### Development
- **[Development Guide](./development)** - Internal architecture and adding tools
- **[Architecture](./architecture)** - System design and patterns
### Production
- **[Authentication](./authentication)** - JWT setup and security
- **[Preset Integration](./preset-integration)** - Enterprise features
> 🚀 **Ready to start?** Continue with the [Overview](./overview) for detailed examples and use cases.

View File

@@ -1,196 +0,0 @@
---
title: MCP Service Overview
sidebar_position: 1
version: 1
---
# Superset MCP Service
The Superset Model Context Protocol (MCP) service provides a modular, schema-driven interface for programmatic access to Superset dashboards, charts, datasets, and instance metadata. Built on FastMCP, it's designed for LLM agents and automation tools.
**Status:** ✅ Phase 1 Complete. Core functionality stable, authentication production-ready, comprehensive testing coverage.
## What is MCP?
The Model Context Protocol (MCP) is an open standard for connecting AI assistants to data sources and tools. Superset's MCP service exposes 16 tools that allow LLM agents to:
- **Explore data**: List and query dashboards, charts, and datasets
- **Create visualizations**: Generate charts and dashboards programmatically
- **Export data**: Extract chart data in multiple formats
- **Navigate interfaces**: Generate explore links and SQL Lab sessions
## Key Features
### 🔧 **16 Production-Ready Tools**
- **Dashboard Tools (5)**: List, get info, create dashboards, add charts
- **Chart Tools (8)**: Full CRUD operations, data export, screenshot previews
- **Dataset Tools (3)**: List, get info, discover filterable columns
- **System Tools (2)**: Instance info, explore link generation
- **SQL Lab Tools (1)**: Pre-configured SQL sessions
### 🔐 **Enterprise Authentication**
- **JWT Bearer Authentication**: Production-ready with configurable factory pattern
- **RBAC Integration**: Scope-based permissions with Superset's security model
- **Audit Logging**: Comprehensive MCP context tracking with impersonation support
### 📊 **Advanced Capabilities**
- **Multi-format Export**: JSON, CSV, Excel data export
- **Chart Previews**: Screenshots, ASCII art, and table representations
- **Cache Control**: Comprehensive control over Superset's cache layers
- **Request Schema Pattern**: Eliminates LLM parameter validation issues
## Architecture Overview
```mermaid
graph TB
subgraph "Client Layer"
LLM[LLM/Agent Client]
Claude[Claude Desktop]
SDK[Custom SDK]
end
subgraph "MCP Service Layer"
FastMCP[FastMCP Server<br/>Port 5008]
Auth[JWT Auth Hook]
Tools[16 MCP Tools]
end
subgraph "Superset Integration"
DAOs[Superset DAOs]
Commands[Superset Commands]
Cache[Cache Layer]
end
subgraph "Data Layer"
DB[(Superset Database)]
DataWarehouse[(Data Warehouse)]
end
LLM --> FastMCP
Claude --> FastMCP
SDK --> FastMCP
FastMCP --> Auth
Auth --> Tools
Tools --> DAOs
Tools --> Commands
Tools --> Cache
DAOs --> DB
Commands --> DB
Commands --> DataWarehouse
```
## Getting Started
### Quick Setup
```bash
# Clone and install Superset
git clone https://github.com/apache/superset.git
cd superset
make venv && source venv/bin/activate
make install
# Start Superset
superset run -p 8088 --with-threads --reload --debugger
# Start MCP service (in separate terminal)
source venv/bin/activate
superset mcp run --port 5008 --debug
```
### Connect to Claude Desktop
:::note
The MCP service runs on HTTP and requires a proxy for Claude Desktop integration.
:::
```bash
# Install FastMCP proxy
pip install fastmcp
```
Configure Claude Desktop (`~/.config/Claude/claude_desktop_config.json`):
```json
{
"mcpServers": {
"Superset MCP": {
"command": "/path/to/superset/superset/mcp_service/run_proxy.sh",
"args": [],
"env": {}
}
}
}
```
## Use Cases
### Data Exploration
- "List all dashboards related to sales"
- "Show me the charts in the Q4 Performance dashboard"
- "What datasets are available for customer analysis?"
### Chart Creation
- "Create a line chart showing revenue trends by month"
- "Generate a table showing top 10 products by sales"
- "Build a bar chart comparing regional performance"
### Data Export
- "Export the sales data from this chart as CSV"
- "Get the underlying data for this dashboard as JSON"
- "Show me a preview of this chart as ASCII art"
### Dashboard Management
- "Create a new dashboard with these 4 charts"
- "Add this revenue chart to the executive dashboard"
- "Generate an explore link for this chart configuration"
## Example Workflow
```python
# List available dashboards
dashboards = client.call_tool("list_dashboards", {
"search": "sales",
"page_size": 10
})
# Get detailed dashboard info
dashboard = client.call_tool("get_dashboard_info", {
"identifier": dashboards["dashboards"][0]["id"]
})
# Create a new chart
chart = client.call_tool("generate_chart", {
"dataset_id": "1",
"config": {
"chart_type": "line",
"x": {"name": "date"},
"y": [{"name": "revenue", "aggregate": "SUM"}]
}
})
# Export chart data
data = client.call_tool("get_chart_data", {
"identifier": chart["chart_id"],
"format": "json",
"limit": 1000
})
```
## Next Steps
### Ready to Use MCP?
- **[📚 API Reference](./api-reference)** - Try all 16 tools with request/response examples
- **[🔐 Authentication](./authentication)** - Set up JWT security for production use
### Want to Extend MCP?
- **[🔧 Development Guide](./development)** - Learn internal architecture and add new tools
- **[🏗️ Architecture](./architecture)** - Understand system design and deployment patterns
### Enterprise Deployment?
- **[🏢 Preset Integration](./preset-integration)** - RBAC extensions and OIDC integration for enterprise
> 💡 **Getting started?** Return to the [MCP Service intro](./intro) for a complete overview.

View File

@@ -1,483 +0,0 @@
---
title: Preset.io Integration
sidebar_position: 6
version: 1
---
# Preset.io Integration Guide
This document outlines integration points for the Preset.io team to extend the Superset MCP service with enterprise features, RBAC customizations, and OIDC integration.
## RBAC Extension Points
### Custom Authorization Factory
The MCP service supports custom authorization logic through the factory pattern:
```python
# In preset_config.py or superset_config.py
def create_preset_mcp_auth(app):
"""Custom auth factory for Preset.io environments."""
from superset.mcp_service.auth import create_auth_provider
from preset.auth.mcp import PresetMCPAuthProvider
return PresetMCPAuthProvider(
jwks_uri=app.config["PRESET_JWKS_URI"],
issuer=app.config["PRESET_JWT_ISSUER"],
audience=app.config["PRESET_JWT_AUDIENCE"],
tenant_resolver=preset_tenant_resolver,
rbac_manager=app.security_manager,
)
MCP_AUTH_FACTORY = create_preset_mcp_auth
```
### Multi-Tenant RBAC
Extend the base auth hook for tenant-aware permissions:
```python
# preset/mcp/auth.py
from superset.mcp_service.auth import mcp_auth_hook
from functools import wraps
def preset_tenant_auth_hook(required_permissions=None):
"""Preset-specific auth hook with tenant isolation."""
def decorator(func):
@wraps(func)
@mcp_auth_hook(required_permissions)
def wrapper(*args, **kwargs):
# Extract tenant from JWT claims
tenant_id = g.user.tenant_id if hasattr(g.user, 'tenant_id') else None
# Inject tenant context
g.mcp_tenant_id = tenant_id
g.mcp_tenant_context = get_tenant_context(tenant_id)
return func(*args, **kwargs)
return wrapper
return decorator
```
### Custom Permission Scopes
Define Preset-specific permission scopes:
```python
# preset/mcp/permissions.py
PRESET_MCP_SCOPES = {
# Tenant-level permissions
"tenant:admin": "Full tenant administration",
"tenant:read": "Read tenant resources",
# Workspace-level permissions
"workspace:admin": "Full workspace administration",
"workspace:read": "Read workspace resources",
# Enhanced dashboard permissions
"dashboard:publish": "Publish dashboards to marketplace",
"dashboard:embed": "Generate embed tokens",
# Enhanced chart permissions
"chart:export": "Export chart data and configs",
"chart:alerts": "Manage chart alerts and notifications",
# Dataset permissions with row-level security
"dataset:rls": "Apply row-level security filters",
"dataset:pii": "Access PII-flagged columns",
}
def get_preset_required_scopes(tool_name: str, context: dict = None) -> List[str]:
"""Map tool calls to Preset-specific permission requirements."""
base_scopes = get_base_required_scopes(tool_name)
# Add tenant-aware scopes
if context and context.get('tenant_id'):
base_scopes.append(f"tenant:{context['tenant_id']}")
# Add workspace-aware scopes
if context and context.get('workspace_id'):
base_scopes.append(f"workspace:{context['workspace_id']}")
return base_scopes
```
### Row-Level Security Integration
Extend data access tools with RLS:
```python
# preset/mcp/rls.py
def apply_preset_rls_filters(query_context: dict, user_context: dict) -> dict:
"""Apply Preset row-level security filters to query context."""
# Get user's RLS rules from Preset metadata
rls_rules = get_user_rls_rules(
user_id=user_context['user_id'],
tenant_id=user_context['tenant_id'],
workspace_id=user_context.get('workspace_id')
)
# Apply RLS filters to query
for rule in rls_rules:
if rule.applies_to_dataset(query_context['datasource']['id']):
query_context = rule.apply_filter(query_context)
return query_context
# Usage in custom tools
@mcp.tool
@preset_tenant_auth_hook(['dataset:read', 'dataset:rls'])
def preset_get_chart_data(request: GetChartDataRequest) -> ChartDataResponse:
"""Get chart data with Preset RLS applied."""
# Apply RLS before executing query
query_context = build_query_context(request)
query_context = apply_preset_rls_filters(
query_context,
{'user_id': g.user.id, 'tenant_id': g.mcp_tenant_id}
)
return execute_chart_data_query(query_context)
```
## OIDC Integration Points
### Preset OIDC Provider
Custom OIDC integration for Preset environments:
```python
# preset/mcp/oidc.py
from superset.mcp_service.auth.providers.bearer import BearerAuthProvider
import requests
from typing import Dict, Any
class PresetOIDCAuthProvider(BearerAuthProvider):
"""OIDC-specific auth provider for Preset.io."""
def __init__(self,
oidc_discovery_url: str,
client_id: str,
client_secret: str = None,
**kwargs):
# Discover OIDC endpoints
self.discovery_doc = self._fetch_discovery_document(oidc_discovery_url)
super().__init__(
jwks_uri=self.discovery_doc['jwks_uri'],
issuer=self.discovery_doc['issuer'],
**kwargs
)
self.client_id = client_id
self.client_secret = client_secret
def _fetch_discovery_document(self, discovery_url: str) -> Dict[str, Any]:
"""Fetch OIDC discovery document."""
response = requests.get(discovery_url)
response.raise_for_status()
return response.json()
def validate_token(self, token: str) -> Dict[str, Any]:
"""Validate JWT token with OIDC-specific claims."""
claims = super().validate_token(token)
# Validate OIDC-specific claims
if claims.get('aud') != self.client_id:
raise ValueError("Invalid audience claim")
# Extract Preset-specific claims
claims['preset_tenant_id'] = claims.get('tenant_id')
claims['preset_workspace_id'] = claims.get('workspace_id')
claims['preset_roles'] = claims.get('roles', [])
return claims
def resolve_user(self, claims: Dict[str, Any]) -> Any:
"""Resolve Superset user from OIDC claims."""
from preset.auth.user_resolver import resolve_preset_user
return resolve_preset_user(
subject=claims['sub'],
email=claims.get('email'),
tenant_id=claims.get('preset_tenant_id'),
roles=claims.get('preset_roles', [])
)
```
### Configuration for OIDC
```python
# In preset_config.py
def create_preset_oidc_auth(app):
"""Factory for Preset OIDC authentication."""
from preset.mcp.oidc import PresetOIDCAuthProvider
return PresetOIDCAuthProvider(
oidc_discovery_url=app.config["PRESET_OIDC_DISCOVERY_URL"],
client_id=app.config["PRESET_OIDC_CLIENT_ID"],
client_secret=app.config["PRESET_OIDC_CLIENT_SECRET"],
audience=app.config["PRESET_MCP_AUDIENCE"],
required_scopes=app.config.get("PRESET_MCP_REQUIRED_SCOPES", [])
)
# MCP Configuration
MCP_AUTH_ENABLED = True
MCP_AUTH_FACTORY = create_preset_oidc_auth
# OIDC Configuration
PRESET_OIDC_DISCOVERY_URL = "https://auth.preset.io/.well-known/openid_configuration"
PRESET_OIDC_CLIENT_ID = "preset-mcp-service"
PRESET_OIDC_CLIENT_SECRET = os.environ.get("PRESET_OIDC_CLIENT_SECRET")
PRESET_MCP_AUDIENCE = "preset-superset-mcp"
PRESET_MCP_REQUIRED_SCOPES = [
"openid", "profile", "email",
"superset:read", "superset:write"
]
```
## Preset-Specific Tools
### Tenant Management Tools
```python
# preset/mcp/tools/tenant.py
@mcp.tool
@preset_tenant_auth_hook(['tenant:read'])
def get_tenant_info(request: GetTenantInfoRequest) -> TenantInfoResponse:
"""Get Preset tenant information and quotas."""
tenant_id = g.mcp_tenant_id
tenant = get_tenant_by_id(tenant_id)
return TenantInfoResponse(
tenant_id=tenant.id,
name=tenant.name,
plan=tenant.plan,
quotas=tenant.quotas,
usage=get_tenant_usage(tenant_id),
workspaces=list_tenant_workspaces(tenant_id)
)
@mcp.tool
@preset_tenant_auth_hook(['workspace:read'])
def list_workspace_assets(request: ListWorkspaceAssetsRequest) -> ListWorkspaceAssetsResponse:
"""List all assets in a Preset workspace."""
workspace_id = request.workspace_id
tenant_id = g.mcp_tenant_id
# Validate workspace belongs to tenant
validate_workspace_access(workspace_id, tenant_id)
assets = {
'dashboards': list_workspace_dashboards(workspace_id),
'charts': list_workspace_charts(workspace_id),
'datasets': list_workspace_datasets(workspace_id)
}
return ListWorkspaceAssetsResponse(
workspace_id=workspace_id,
assets=assets,
total_count=sum(len(v) for v in assets.values())
)
```
### Embed Token Generation
```python
# preset/mcp/tools/embed.py
@mcp.tool
@preset_tenant_auth_hook(['dashboard:embed'])
def generate_embed_token(request: GenerateEmbedTokenRequest) -> EmbedTokenResponse:
"""Generate secure embed token for dashboard/chart."""
# Validate resource access
resource = validate_embed_resource_access(
resource_type=request.resource_type,
resource_id=request.resource_id,
tenant_id=g.mcp_tenant_id
)
# Generate signed embed token
embed_token = create_embed_token(
resource=resource,
user_id=g.user.id,
tenant_id=g.mcp_tenant_id,
permissions=request.permissions,
expiry=request.expiry_hours
)
return EmbedTokenResponse(
embed_token=embed_token,
embed_url=f"{get_preset_base_url()}/embed/{embed_token}",
expires_at=embed_token.expires_at
)
```
## Audit and Compliance Extensions
### Enhanced Audit Logging
```python
# preset/mcp/audit.py
from superset.mcp_service.auth import get_audit_context
def create_preset_audit_context(user_context: dict, tool_name: str,
request_data: dict) -> dict:
"""Create Preset-specific audit context."""
base_context = get_audit_context(user_context, tool_name, request_data)
# Add Preset-specific fields
preset_context = {
**base_context,
'tenant_id': user_context.get('tenant_id'),
'workspace_id': user_context.get('workspace_id'),
'preset_user_role': user_context.get('preset_role'),
'data_classification': classify_request_data(request_data),
'compliance_flags': get_compliance_flags(tool_name, request_data)
}
return preset_context
def log_preset_mcp_access(audit_context: dict):
"""Log MCP access to Preset audit systems."""
# Log to Superset's audit system
log_superset_audit_event(audit_context)
# Log to Preset's compliance system
log_preset_compliance_event(audit_context)
# Log to external SIEM if configured
if app.config.get('PRESET_SIEM_ENABLED'):
log_to_siem(audit_context)
```
### Data Classification
```python
# preset/mcp/classification.py
def classify_request_data(request_data: dict) -> dict:
"""Classify data sensitivity in MCP requests."""
classification = {
'contains_pii': False,
'data_level': 'public',
'retention_policy': 'standard'
}
# Check for PII in request
if contains_pii_fields(request_data):
classification['contains_pii'] = True
classification['data_level'] = 'restricted'
classification['retention_policy'] = 'pii_compliant'
# Check for sensitive datasets
if references_sensitive_datasets(request_data):
classification['data_level'] = 'confidential'
return classification
```
## Deployment Considerations
### Multi-Region Deployment
```python
# preset/mcp/deployment.py
def get_region_specific_config():
"""Get region-specific MCP configuration."""
region = os.environ.get('PRESET_REGION', 'us-east-1')
config_map = {
'us-east-1': {
'jwks_uri': 'https://auth-us.preset.io/.well-known/jwks.json',
'base_url': 'https://app.preset.io',
'data_residency': 'US'
},
'eu-west-1': {
'jwks_uri': 'https://auth-eu.preset.io/.well-known/jwks.json',
'base_url': 'https://eu.preset.io',
'data_residency': 'EU'
}
}
return config_map.get(region, config_map['us-east-1'])
# Usage in config
region_config = get_region_specific_config()
PRESET_JWKS_URI = region_config['jwks_uri']
SUPERSET_WEBSERVER_ADDRESS = region_config['base_url']
```
### Health Check Extensions
```python
# preset/mcp/health.py
@mcp.tool
def preset_health_check() -> HealthCheckResponse:
"""Preset-specific health check for MCP service."""
checks = {
'mcp_service': check_mcp_service_health(),
'database': check_database_health(),
'auth_provider': check_auth_provider_health(),
'tenant_isolation': check_tenant_isolation(),
'rls_engine': check_rls_engine_health()
}
overall_status = 'healthy' if all(
check['status'] == 'healthy' for check in checks.values()
) else 'degraded'
return HealthCheckResponse(
status=overall_status,
checks=checks,
region=os.environ.get('PRESET_REGION'),
version=get_preset_mcp_version()
)
```
## Configuration Templates
### Production Configuration
```python
# preset_production_config.py
from preset.mcp.auth import create_preset_oidc_auth
from preset.mcp.audit import create_preset_audit_context
# MCP Service Configuration
MCP_AUTH_ENABLED = True
MCP_AUTH_FACTORY = create_preset_oidc_auth
MCP_AUDIT_CONTEXT_FACTORY = create_preset_audit_context
# Preset OIDC Configuration
PRESET_OIDC_DISCOVERY_URL = "https://auth.preset.io/.well-known/openid_configuration"
PRESET_OIDC_CLIENT_ID = "preset-mcp-production"
PRESET_MCP_AUDIENCE = "preset-superset-mcp"
# Security Configuration
PRESET_MCP_REQUIRED_SCOPES = [
"openid", "profile", "email",
"tenant:read", "workspace:read",
"dashboard:read", "chart:read", "dataset:read"
]
# Audit Configuration
PRESET_AUDIT_ENABLED = True
PRESET_SIEM_ENABLED = True
PRESET_COMPLIANCE_MODE = "SOC2"
# Performance Configuration
PRESET_MCP_CACHE_ENABLED = True
PRESET_MCP_RATE_LIMIT = "1000/hour"
PRESET_MCP_TIMEOUT = 30
```
This integration guide provides the Preset.io team with concrete extension points for implementing enterprise features while maintaining compatibility with the base MCP service architecture.

View File

@@ -2,6 +2,20 @@
title: CVEs fixed by release
sidebar_position: 2
---
#### Version 5.0.0
| CVE | Title | Affected |
|:---------------|:-----------------------------------------------------------------------------------|---------:|
| CVE-2025-55673 | Exposure of Sensitive Information to an Unauthorized Actor | < 5.0.0 |
| CVE-2025-55674 | Improper Neutralization of Special Elements used in an SQL Command | < 5.0.0 |
| CVE-2025-55675 | Improper Access Control leading to Information Disclosure | < 5.0.0 |
#### Version 4.1.3
| CVE | Title | Affected |
|:---------------|:-----------------------------------------------------------------------------------|---------:|
| CVE-2025-55672 | Improper Neutralization of Input During Web Page Generation | < 4.1.3 |
#### Version 4.1.2
| CVE | Title | Affected |

View File

@@ -28,6 +28,9 @@ const globals = require('globals');
const { defineConfig, globalIgnores } = require('eslint/config');
module.exports = defineConfig([
{
files: ['**/*.{js,jsx,ts,tsx}'],
},
globalIgnores(['build/**/*', '.docusaurus/**/*', 'node_modules/**/*']),
js.configs.recommended,
...ts.configs.recommended,
@@ -36,7 +39,7 @@ module.exports = defineConfig([
files: ['eslint.config.js'],
rules: {
'@typescript-eslint/no-require-imports': 'off',
}
},
},
{
languageOptions: {
@@ -68,5 +71,5 @@ module.exports = defineConfig([
version: 'detect',
},
},
}
])
},
]);

View File

@@ -15,7 +15,7 @@
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc",
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx"
"eslint": "eslint ."
},
"dependencies": {
"@ant-design/icons": "^6.0.0",
@@ -26,33 +26,33 @@
"@emotion/styled": "^10.0.27",
"@saucelabs/theme-github-codeblock": "^0.3.0",
"@superset-ui/style": "^0.14.23",
"antd": "^5.26.3",
"antd": "^5.26.7",
"docusaurus-plugin-less": "^2.0.2",
"less": "^4.3.0",
"less": "^4.4.0",
"less-loader": "^12.3.0",
"prism-react-renderer": "^2.4.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-github-btn": "^1.4.0",
"react-svg-pan-zoom": "^3.13.1",
"swagger-ui-react": "^5.26.0"
"swagger-ui-react": "^5.27.1"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.8.1",
"@docusaurus/tsconfig": "^3.8.1",
"@eslint/js": "^9.31.0",
"@eslint/js": "^9.32.0",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^8.37.0",
"@typescript-eslint/parser": "^8.37.0",
"eslint": "^9.31.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.5.1",
"eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.3",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0",
"prettier": "^3.6.2",
"typescript": "~5.8.3",
"typescript-eslint": "^8.37.0",
"webpack": "^5.99.9"
"typescript-eslint": "^8.39.0",
"webpack": "^5.101.0"
},
"browserslist": {
"production": [

View File

@@ -87,16 +87,6 @@ const sidebars = {
},
],
},
{
type: 'category',
label: 'MCP Service',
items: [
{
type: 'autogenerated',
dirName: 'mcp-service',
},
],
},
{
type: 'doc',
label: 'FAQ',

View File

@@ -2150,14 +2150,7 @@
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz"
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
"@eslint-community/eslint-utils@^4.2.0":
version "4.4.1"
resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz"
integrity sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==
dependencies:
eslint-visitor-keys "^3.4.3"
"@eslint-community/eslint-utils@^4.7.0":
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0":
version "4.7.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a"
integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==
@@ -2205,20 +2198,20 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@9.31.0", "@eslint/js@^9.31.0":
version "9.31.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.31.0.tgz#adb1f39953d8c475c4384b67b67541b0d7206ed8"
integrity sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==
"@eslint/js@9.32.0", "@eslint/js@^9.32.0":
version "9.32.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.32.0.tgz#a02916f58bd587ea276876cb051b579a3d75d091"
integrity sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==
"@eslint/object-schema@^2.1.6":
version "2.1.6"
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f"
integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==
"@eslint/plugin-kit@^0.3.1":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz#32926b59bd407d58d817941e48b2a7049359b1fd"
integrity sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==
"@eslint/plugin-kit@^0.3.4":
version "0.3.4"
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz#c6b9f165e94bf4d9fdd493f1c028a94aaf5fc1cc"
integrity sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==
dependencies:
"@eslint/core" "^0.15.1"
levn "^0.4.1"
@@ -2512,10 +2505,10 @@
classnames "^2.3.2"
rc-util "^5.24.4"
"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.2.7":
version "2.2.7"
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.2.7.tgz#a2b97ecbb93280a3c424e51fa415b371b355d76a"
integrity sha512-Qggj4Z0AA2i5dJhzlfFSmg1Qrziu8dsdHOihROL5Kl18seO2Eh/ZaTYt2c8a/CyGaTChnFry7BEYew1+/fhSbA==
"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.3.0.tgz#9499ada078daca9dd99d01f0f0743ee1ab9e398b"
integrity sha512-iwaxZyzOuK0D7lS+0AQEtW52zUWxoGqTGkke3dRyb8pYiShmRpCjB/8TzPI4R6YySCH7Vm9BZj/31VPiiQTLBg==
dependencies:
"@babel/runtime" "^7.23.2"
"@rc-component/portal" "^1.1.0"
@@ -3422,10 +3415,10 @@
dependencies:
"@types/estree" "*"
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@^1.0.8":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0":
version "5.0.6"
@@ -3724,79 +3717,79 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.37.0", "@typescript-eslint/eslint-plugin@^8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz#332392883f936137cd6252c8eb236d298e514e70"
integrity sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==
"@typescript-eslint/eslint-plugin@8.39.0", "@typescript-eslint/eslint-plugin@^8.37.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz#c9afec1866ee1a6ea3d768b5f8e92201efbbba06"
integrity sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==
dependencies:
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "8.37.0"
"@typescript-eslint/type-utils" "8.37.0"
"@typescript-eslint/utils" "8.37.0"
"@typescript-eslint/visitor-keys" "8.37.0"
"@typescript-eslint/scope-manager" "8.39.0"
"@typescript-eslint/type-utils" "8.39.0"
"@typescript-eslint/utils" "8.39.0"
"@typescript-eslint/visitor-keys" "8.39.0"
graphemer "^1.4.0"
ignore "^7.0.0"
natural-compare "^1.4.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/parser@8.37.0", "@typescript-eslint/parser@^8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.37.0.tgz#b87f6b61e25ad5cc5bbf8baf809b8da889c89804"
integrity sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==
"@typescript-eslint/parser@8.39.0", "@typescript-eslint/parser@^8.37.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.39.0.tgz#c4b895d7a47f4cd5ee6ee77ea30e61d58b802008"
integrity sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==
dependencies:
"@typescript-eslint/scope-manager" "8.37.0"
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/typescript-estree" "8.37.0"
"@typescript-eslint/visitor-keys" "8.37.0"
"@typescript-eslint/scope-manager" "8.39.0"
"@typescript-eslint/types" "8.39.0"
"@typescript-eslint/typescript-estree" "8.39.0"
"@typescript-eslint/visitor-keys" "8.39.0"
debug "^4.3.4"
"@typescript-eslint/project-service@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.37.0.tgz#0594352e32a4ac9258591b88af77b5653800cdfe"
integrity sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==
"@typescript-eslint/project-service@8.39.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.39.0.tgz#71cb29c3f8139f99a905b8705127bffc2ae84759"
integrity sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.37.0"
"@typescript-eslint/types" "^8.37.0"
"@typescript-eslint/tsconfig-utils" "^8.39.0"
"@typescript-eslint/types" "^8.39.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz#a31a3c80ca2ef4ed58de13742debb692e7d4c0a4"
integrity sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==
"@typescript-eslint/scope-manager@8.39.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz#ba4bf6d8257bbc172c298febf16bc22df4856570"
integrity sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==
dependencies:
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/visitor-keys" "8.37.0"
"@typescript-eslint/types" "8.39.0"
"@typescript-eslint/visitor-keys" "8.39.0"
"@typescript-eslint/tsconfig-utils@8.37.0", "@typescript-eslint/tsconfig-utils@^8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz#47a2760d265c6125f8e7864bc5c8537cad2bd053"
integrity sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==
"@typescript-eslint/tsconfig-utils@8.39.0", "@typescript-eslint/tsconfig-utils@^8.39.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz#b2e87fef41a3067c570533b722f6af47be213f13"
integrity sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==
"@typescript-eslint/type-utils@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz#2a682e4c6ff5886712dad57e9787b5e417124507"
integrity sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==
"@typescript-eslint/type-utils@8.39.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz#310ec781ae5e7bb0f5940bfd652573587f22786b"
integrity sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==
dependencies:
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/typescript-estree" "8.37.0"
"@typescript-eslint/utils" "8.37.0"
"@typescript-eslint/types" "8.39.0"
"@typescript-eslint/typescript-estree" "8.39.0"
"@typescript-eslint/utils" "8.39.0"
debug "^4.3.4"
ts-api-utils "^2.1.0"
"@typescript-eslint/types@8.37.0", "@typescript-eslint/types@^8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.37.0.tgz#09517aa9625eb3c68941dde3ac8835740587b6ff"
integrity sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==
"@typescript-eslint/types@8.39.0", "@typescript-eslint/types@^8.39.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.39.0.tgz#80f010b7169d434a91cd0529d70a528dbc9c99c6"
integrity sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==
"@typescript-eslint/typescript-estree@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz#a07e4574d8e6e4355a558f61323730c987f5fcbc"
integrity sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==
"@typescript-eslint/typescript-estree@8.39.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz#b9477a5c47a0feceffe91adf553ad9a3cd4cb3d6"
integrity sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==
dependencies:
"@typescript-eslint/project-service" "8.37.0"
"@typescript-eslint/tsconfig-utils" "8.37.0"
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/visitor-keys" "8.37.0"
"@typescript-eslint/project-service" "8.39.0"
"@typescript-eslint/tsconfig-utils" "8.39.0"
"@typescript-eslint/types" "8.39.0"
"@typescript-eslint/visitor-keys" "8.39.0"
debug "^4.3.4"
fast-glob "^3.3.2"
is-glob "^4.0.3"
@@ -3804,22 +3797,22 @@
semver "^7.6.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/utils@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.37.0.tgz#189ea59b2709f5d898614611f091a776751ee335"
integrity sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==
"@typescript-eslint/utils@8.39.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.39.0.tgz#dfea42f3c7ec85f9f3e994ff0bba8f3b2f09e220"
integrity sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==
dependencies:
"@eslint-community/eslint-utils" "^4.7.0"
"@typescript-eslint/scope-manager" "8.37.0"
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/typescript-estree" "8.37.0"
"@typescript-eslint/scope-manager" "8.39.0"
"@typescript-eslint/types" "8.39.0"
"@typescript-eslint/typescript-estree" "8.39.0"
"@typescript-eslint/visitor-keys@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz#cdb6a6bd3e8d6dd69bd70c1bdda36e2d18737455"
integrity sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==
"@typescript-eslint/visitor-keys@8.39.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz#5d619a6e810cdd3fd1913632719cbccab08bf875"
integrity sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==
dependencies:
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/types" "8.39.0"
eslint-visitor-keys "^4.2.1"
"@ungap/structured-clone@^1.0.0":
@@ -3966,6 +3959,11 @@ accepts@~1.3.4, accepts@~1.3.8:
mime-types "~2.1.34"
negotiator "0.6.3"
acorn-import-phases@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7"
integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==
acorn-jsx@^5.0.0, acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@@ -3978,12 +3976,7 @@ acorn-walk@^8.0.0:
dependencies:
acorn "^8.11.0"
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.8.2:
version "8.14.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb"
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
acorn@^8.15.0:
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.15.0, acorn@^8.8.2:
version "8.15.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
@@ -4107,10 +4100,10 @@ ansi-styles@^6.1.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
antd@^5.26.3:
version "5.26.3"
resolved "https://registry.yarnpkg.com/antd/-/antd-5.26.3.tgz#cbbb7e1b48a972dc7b6ee8b6948f51cc91c263f8"
integrity sha512-M/s9Q39h/+G7AWnS6fbNxmAI9waTH4ti022GVEXBLq2j810V1wJ3UOQps13nEilzDNcyxnFN/EIbqIgS7wSYaA==
antd@^5.26.7:
version "5.26.7"
resolved "https://registry.yarnpkg.com/antd/-/antd-5.26.7.tgz#e2f7e37330b27eec0de7a7789767975373f61602"
integrity sha512-iCyXN6+i2CUVEOSzzJKfbKeg115qoJhGvSkCh5uzAf9hANwHUOJQhsMn+KtN+Lx/2NQ6wfM7nGZ+7NPNO5Pn1w==
dependencies:
"@ant-design/colors" "^7.2.1"
"@ant-design/cssinjs" "^1.23.0"
@@ -4123,7 +4116,7 @@ antd@^5.26.3:
"@rc-component/mutate-observer" "^1.1.0"
"@rc-component/qrcode" "~1.0.0"
"@rc-component/tour" "~1.15.1"
"@rc-component/trigger" "^2.2.7"
"@rc-component/trigger" "^2.3.0"
classnames "^2.5.1"
copy-to-clipboard "^3.3.3"
dayjs "^1.11.11"
@@ -4153,7 +4146,7 @@ antd@^5.26.3:
rc-switch "~4.1.0"
rc-table "~7.51.1"
rc-tabs "~15.6.1"
rc-textarea "~1.10.0"
rc-textarea "~1.10.1"
rc-tooltip "~6.4.0"
rc-tree "~5.13.1"
rc-tree-select "~5.27.0"
@@ -4508,17 +4501,7 @@ braces@^3.0.3, braces@~3.0.2:
dependencies:
fill-range "^7.1.1"
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4:
version "4.24.4"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b"
integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==
dependencies:
caniuse-lite "^1.0.30001688"
electron-to-chromium "^1.5.73"
node-releases "^2.0.19"
update-browserslist-db "^1.1.1"
browserslist@^4.25.0:
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.25.0:
version "4.25.0"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.0.tgz#986aa9c6d87916885da2b50d8eb577ac8d133b2c"
integrity sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==
@@ -4620,7 +4603,7 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702:
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702:
version "1.0.30001714"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001714.tgz#cfd27ff07e6fa20a0f45c7a10d28a0ffeaba2122"
integrity sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==
@@ -5622,20 +5605,13 @@ debug@2.6.9:
dependencies:
ms "2.0.0"
debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.4.0:
debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
dependencies:
ms "^2.1.3"
debug@^4.3.2, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
decode-named-character-reference@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz#5d6ce68792808901210dac42a8e9853511e2b8bf"
@@ -5903,11 +5879,6 @@ electron-to-chromium@^1.5.160:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.170.tgz#9f6697de4339e24da8b234e4492a9ecb91f5989c"
integrity sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==
electron-to-chromium@^1.5.73:
version "1.5.138"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.138.tgz#319e775179bd0889ed96a04d4390d355fb315a44"
integrity sha512-FWlQc52z1dXqm+9cCJ2uyFgJkESd+16j6dBEjsgDNuHjBpuIzL8/lRc0uvh1k8RNI6waGo6tcy2DvwkTBJOLDg==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
@@ -5952,10 +5923,10 @@ encodeurl@~2.0.0:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
enhanced-resolve@^5.17.1:
version "5.18.1"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf"
integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==
enhanced-resolve@^5.17.2:
version "5.18.2"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz#7903c5b32ffd4b2143eeb4b92472bd68effd5464"
integrity sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
@@ -6161,15 +6132,15 @@ escape-string-regexp@^5.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
eslint-config-prettier@^10.1.5:
version "10.1.5"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz#00c18d7225043b6fbce6a665697377998d453782"
integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==
eslint-config-prettier@^10.1.8:
version "10.1.8"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz#15734ce4af8c2778cc32f0b01b37b0b5cd1ecb97"
integrity sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==
eslint-plugin-prettier@^5.5.1:
version "5.5.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz#470820964de9aedb37e9ce62c3266d2d26d08d15"
integrity sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==
eslint-plugin-prettier@^5.5.3:
version "5.5.3"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz#1f88e9220a72ac8be171eec5f9d4e4d529b5f4a0"
integrity sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==
dependencies:
prettier-linter-helpers "^1.0.0"
synckit "^0.11.7"
@@ -6224,10 +6195,10 @@ eslint-visitor-keys@^4.2.1:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
eslint@^9.31.0:
version "9.31.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.31.0.tgz#9a488e6da75bbe05785cd62e43c5ea99356d21ba"
integrity sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==
eslint@^9.32.0:
version "9.32.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.32.0.tgz#4ea28df4a8dbc454e1251e0f3aed4bcf4ce50a47"
integrity sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.12.1"
@@ -6235,8 +6206,8 @@ eslint@^9.31.0:
"@eslint/config-helpers" "^0.3.0"
"@eslint/core" "^0.15.0"
"@eslint/eslintrc" "^3.3.1"
"@eslint/js" "9.31.0"
"@eslint/plugin-kit" "^0.3.1"
"@eslint/js" "9.32.0"
"@eslint/plugin-kit" "^0.3.4"
"@humanfs/node" "^0.16.6"
"@humanwhocodes/module-importer" "^1.0.1"
"@humanwhocodes/retry" "^0.4.2"
@@ -8063,10 +8034,10 @@ less-loader@^12.3.0:
resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-12.3.0.tgz#d4a00361568be86a97da3df4f16954b0d4c15340"
integrity sha512-0M6+uYulvYIWs52y0LqN4+QM9TqWAohYSNTo4htE8Z7Cn3G/qQMEmktfHmyJT23k+20kU9zHH2wrfFXkxNLtVw==
less@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/less/-/less-4.3.0.tgz#ef0cfc260a9ca8079ed8d0e3512bda8a12c82f2a"
integrity sha512-X9RyH9fvemArzfdP8Pi3irr7lor2Ok4rOttDXBhlwDg+wKQsXOXgHWduAJE1EsF7JJx0w0bcO6BC6tCKKYnXKA==
less@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/less/-/less-4.4.0.tgz#deaf881f4880ee80691beae925b8fac699d3a76d"
integrity sha512-kdTwsyRuncDfjEs0DlRILWNvxhDG/Zij4YLO4TMJgDLW+8OzpfkdPnRgrsRuY1o+oaxJGWsps5f/RVBgGmmN0w==
dependencies:
copy-anything "^2.0.1"
parse-node-version "^1.0.1"
@@ -9069,11 +9040,6 @@ ms@2.0.0:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
ms@2.1.3, ms@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
@@ -10714,10 +10680,10 @@ rc-tabs@~15.6.1:
rc-resize-observer "^1.0.0"
rc-util "^5.34.1"
rc-textarea@~1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-1.10.0.tgz#f8f962ef83be0b8e35db97cf03dbfb86ddd9c46c"
integrity sha512-ai9IkanNuyBS4x6sOL8qu/Ld40e6cEs6pgk93R+XLYg0mDSjNBGey6/ZpDs5+gNLD7urQ14po3V6Ck2dJLt9SA==
rc-textarea@~1.10.0, rc-textarea@~1.10.1:
version "1.10.2"
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-1.10.2.tgz#459e3574a95c32939c6793045a1e4db04cb514cc"
integrity sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==
dependencies:
"@babel/runtime" "^7.10.1"
classnames "^2.2.1"
@@ -12100,10 +12066,10 @@ swagger-client@^3.35.5:
ramda "^0.30.1"
ramda-adjunct "^5.1.0"
swagger-ui-react@^5.26.0:
version "5.26.0"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.26.0.tgz#b15a903d556cc0ec2a56a969beb9d5bc9ea52910"
integrity sha512-4e6bP9bdJyh+SqQW0lxulPn/SDno4+oWrKXsuon5Z9kjtV0zeoWEJ1c70Qxp8kN/c3caFwec8OyxDNhvo14pkw==
swagger-ui-react@^5.27.1:
version "5.27.1"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.27.1.tgz#315b59970c33933a5f62ca0f702789741dcedc7c"
integrity sha512-wwDoavIeJI/Pwiavn32FMJ5dfptz0BAOKjSrj7EdU22QdP3gdk9+MZHdzzjxWURmVj0kc0XoQfsFgjln0toJaw==
dependencies:
"@babel/runtime-corejs3" "^7.27.1"
"@scarf/scarf" "=1.4.0"
@@ -12382,15 +12348,15 @@ types-ramda@^0.30.0:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.37.0:
version "8.37.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.37.0.tgz#2235ddfa40cdbdadb1afb05f8bda688a2294b4c2"
integrity sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==
typescript-eslint@^8.39.0:
version "8.39.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.39.0.tgz#b19c1a925cf8566831ae3875d2881ee2349808a5"
integrity sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==
dependencies:
"@typescript-eslint/eslint-plugin" "8.37.0"
"@typescript-eslint/parser" "8.37.0"
"@typescript-eslint/typescript-estree" "8.37.0"
"@typescript-eslint/utils" "8.37.0"
"@typescript-eslint/eslint-plugin" "8.39.0"
"@typescript-eslint/parser" "8.39.0"
"@typescript-eslint/typescript-estree" "8.39.0"
"@typescript-eslint/utils" "8.39.0"
typescript@~5.8.3:
version "5.8.3"
@@ -12525,7 +12491,7 @@ unraw@^3.0.0:
resolved "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz"
integrity sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==
update-browserslist-db@^1.1.1, update-browserslist-db@^1.1.3:
update-browserslist-db@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420"
integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==
@@ -12794,26 +12760,27 @@ webpack-merge@^6.0.1:
flat "^5.0.2"
wildcard "^2.0.1"
webpack-sources@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack-sources@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723"
integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==
webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.9:
version "5.99.9"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.9.tgz#d7de799ec17d0cce3c83b70744b4aedb537d8247"
integrity sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==
webpack@^5.101.0, webpack@^5.88.1, webpack@^5.95.0:
version "5.101.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.101.0.tgz#4b81407ffad9857f81ff03f872e3369b9198cc9d"
integrity sha512-B4t+nJqytPeuZlHuIKTbalhljIFXeNRqrUGAQgTGlfOl2lXXKXw+yZu6bicycP+PUlM44CxBjCFD6aciKFT3LQ==
dependencies:
"@types/eslint-scope" "^3.7.7"
"@types/estree" "^1.0.6"
"@types/estree" "^1.0.8"
"@types/json-schema" "^7.0.15"
"@webassemblyjs/ast" "^1.14.1"
"@webassemblyjs/wasm-edit" "^1.14.1"
"@webassemblyjs/wasm-parser" "^1.14.1"
acorn "^8.14.0"
acorn "^8.15.0"
acorn-import-phases "^1.0.3"
browserslist "^4.24.0"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.17.1"
enhanced-resolve "^5.17.2"
es-module-lexer "^1.2.1"
eslint-scope "5.1.1"
events "^3.2.0"
@@ -12827,7 +12794,7 @@ webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.9:
tapable "^2.1.1"
terser-webpack-plugin "^5.3.11"
watchpack "^2.4.1"
webpack-sources "^3.2.3"
webpack-sources "^3.3.3"
webpackbar@^6.0.1:
version "6.0.1"

View File

@@ -15,7 +15,7 @@
# limitations under the License.
#
apiVersion: v2
appVersion: "4.1.2"
appVersion: "5.0.0"
description: Apache Superset is a modern, enterprise-ready business intelligence web application
name: superset
icon: https://artifacthub.io/image/68c1d717-0e97-491f-b046-754e46f46922@2x
@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.14.3
version: 0.15.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
dependencies:
- name: postgresql
version: 13.4.4

View File

@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.14.3](https://img.shields.io/badge/Version-0.14.3-informational?style=flat-square)
![Version: 0.15.0](https://img.shields.io/badge/Version-0.15.0-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application
@@ -336,3 +336,6 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
| supersetWorker.topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to supersetWorker deployments |
| tolerations | list | `[]` | |
| topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to all deployments |
## Versioning
This chart follows [semantic versioning](https://semver.org/). The chart version is independent of the Superset version. The chart version is incremented when there are changes to the chart itself, such as new features, bug fixes, or changes in configuration options. In addition to semver, the chart version is also incremented in the minor version when there is a breaking change in the Superset appVersion itself. When there are non-breaking changes in the Superset appVersion, the chart version is incremented in the patch version.

View File

@@ -48,3 +48,6 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
{{ template "chart.requirementsSection" . }}
{{ template "chart.valuesSection" . }}
## Versioning
This chart follows [semantic versioning](https://semver.org/). The chart version is independent of the Superset version. The chart version is incremented when there are changes to the chart itself, such as new features, bug fixes, or changes in configuration options. In addition to semver, the chart version is also incremented in the minor version when there is a breaking change in the Superset appVersion itself. When there are non-breaking changes in the Superset appVersion, the chart version is incremented in the patch version.

View File

@@ -79,6 +79,7 @@ dependencies = [
"parsedatetime",
"paramiko>=3.4.0",
"pgsanity",
"Pillow>=11.0.0, <12",
"polyline>=2.0.0, <3.0",
"pyparsing>=3.0.6, <4",
"python-dateutil",
@@ -133,7 +134,6 @@ solr = ["sqlalchemy-solr >= 0.2.0"]
elasticsearch = ["elasticsearch-dbapi>=0.2.9, <0.3.0"]
exasol = ["sqlalchemy-exasol >= 2.4.0, <3.0"]
excel = ["xlrd>=1.2.0, <1.3"]
fastmcp = ["fastmcp>=2.8.1"]
firebird = ["sqlalchemy-firebird>=0.7.0, <0.8"]
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
gevent = ["gevent>=23.9.1"]
@@ -182,7 +182,7 @@ tdengine = [
"taos-ws-py>=0.3.8"
]
teradata = ["teradatasql>=16.20.0.23"]
thumbnails = ["Pillow>=10.0.1, <11"]
thumbnails = [] # deprecated, will be removed in 7.0
vertica = ["sqlalchemy-vertica-python>=0.5.9, < 0.6"]
netezza = ["nzalchemy>=11.0.2"]
starrocks = ["starrocks>=1.0.0"]
@@ -196,6 +196,7 @@ development = [
"grpcio>=1.55.3",
"openapi-spec-validator",
"parameterized",
"pip",
"pre-commit",
"progress>=1.5,<2",
"psutil",
@@ -203,7 +204,6 @@ development = [
"pyinstrument>=4.0.2,<5",
"pylint",
"pytest<8.0.0", # hairy issue with pytest >=8 where current_app proxies are not set in time
"pytest-asyncio", # need this due to not using latest pytest
"pytest-cov",
"pytest-mock",
"python-ldap>=3.4.4",
@@ -401,6 +401,7 @@ authorized_licenses = [
"isc license (iscl)",
"isc license",
"mit",
"mit-cmu",
"mozilla public license 2.0 (mpl 2.0)",
"osi approved",
"osi approved",

View File

@@ -266,6 +266,8 @@ parsedatetime==2.6
# via apache-superset (pyproject.toml)
pgsanity==0.2.9
# via apache-superset (pyproject.toml)
pillow==11.3.0
# via apache_superset (pyproject.toml)
platformdirs==4.3.8
# via requests-cache
ply==3.11

View File

@@ -16,4 +16,4 @@
# specific language governing permissions and limitations
# under the License.
#
-e .[development,bigquery,druid,fastmcp,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]
-e .[development,bigquery,druid,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]

View File

@@ -10,14 +10,6 @@ amqp==5.3.1
# via
# -c requirements/base.txt
# kombu
annotated-types==0.7.0
# via pydantic
anyio==4.9.0
# via
# httpx
# mcp
# sse-starlette
# starlette
apispec==6.6.1
# via
# -c requirements/base.txt
@@ -32,14 +24,11 @@ attrs==25.3.0
# via
# -c requirements/base.txt
# cattrs
# cyclopts
# jsonschema
# outcome
# referencing
# requests-cache
# trio
authlib==1.6.1
# via fastmcp
babel==2.17.0
# via
# -c requirements/base.txt
@@ -88,8 +77,6 @@ celery==5.5.2
certifi==2025.6.15
# via
# -c requirements/base.txt
# httpcore
# httpx
# requests
# selenium
cffi==1.17.1
@@ -114,7 +101,6 @@ click==8.2.1
# click-repl
# flask
# flask-appbuilder
# uvicorn
click-didyoumean==0.3.1
# via
# -c requirements/base.txt
@@ -154,13 +140,10 @@ cryptography==44.0.3
# via
# -c requirements/base.txt
# apache-superset
# authlib
# paramiko
# pyopenssl
cycler==0.12.1
# via matplotlib
cyclopts==3.22.2
# via fastmcp
db-dtypes==1.3.1
# via pandas-gbq
defusedxml==0.7.1
@@ -185,23 +168,14 @@ dnspython==2.7.0
# email-validator
docker==7.0.0
# via apache-superset
docstring-parser==0.17.0
# via cyclopts
docutils==0.21.2
# via rich-rst
email-validator==2.2.0
# via
# -c requirements/base.txt
# flask-appbuilder
# pydantic
et-xmlfile==2.0.0
# via
# -c requirements/base.txt
# openpyxl
exceptiongroup==1.3.0
# via fastmcp
fastmcp==2.10.6
# via apache-superset
filelock==3.12.2
# via virtualenv
flask==2.3.3
@@ -353,8 +327,6 @@ gunicorn==23.0.0
h11==0.16.0
# via
# -c requirements/base.txt
# httpcore
# uvicorn
# wsproto
hashids==1.3.1
# via
@@ -365,14 +337,6 @@ holidays==0.25
# -c requirements/base.txt
# apache-superset
# prophet
httpcore==1.0.9
# via httpx
httpx==0.28.1
# via
# fastmcp
# mcp
httpx-sse==0.4.1
# via mcp
humanize==4.12.3
# via
# -c requirements/base.txt
@@ -382,9 +346,7 @@ identify==2.5.36
idna==3.10
# via
# -c requirements/base.txt
# anyio
# email-validator
# httpx
# requests
# trio
# url-normalize
@@ -416,7 +378,6 @@ jsonschema==4.23.0
# via
# -c requirements/base.txt
# flask-appbuilder
# mcp
# openapi-schema-validator
# openapi-spec-validator
jsonschema-path==0.3.4
@@ -476,8 +437,6 @@ matplotlib==3.9.0
# via prophet
mccabe==0.7.0
# via pylint
mcp==1.12.0
# via fastmcp
mdurl==0.1.2
# via
# -c requirements/base.txt
@@ -516,8 +475,6 @@ odfpy==1.4.1
# via
# -c requirements/base.txt
# pandas
openapi-pydantic==0.5.1
# via fastmcp
openapi-schema-validator==0.6.3
# via
# -c requirements/base.txt
@@ -580,10 +537,12 @@ pgsanity==0.2.9
# via
# -c requirements/base.txt
# apache-superset
pillow==10.3.0
pillow==11.3.0
# via
# apache-superset
# matplotlib
pip==25.1.1
# via apache-superset
platformdirs==4.3.8
# via
# -c requirements/base.txt
@@ -650,16 +609,6 @@ pycparser==2.22
# via
# -c requirements/base.txt
# cffi
pydantic==2.11.7
# via
# fastmcp
# mcp
# openapi-pydantic
# pydantic-settings
pydantic-core==2.33.2
# via pydantic
pydantic-settings==2.10.1
# via mcp
pydata-google-auth==1.9.0
# via pandas-gbq
pydruid==0.6.9
@@ -695,8 +644,6 @@ pyparsing==3.2.3
# -c requirements/base.txt
# apache-superset
# matplotlib
pyperclip==1.9.0
# via fastmcp
pysocks==1.7.1
# via
# -c requirements/base.txt
@@ -704,11 +651,8 @@ pysocks==1.7.1
pytest==7.4.4
# via
# apache-superset
# pytest-asyncio
# pytest-cov
# pytest-mock
pytest-asyncio==0.23.8
# via apache-superset
pytest-cov==6.0.0
# via apache-superset
pytest-mock==3.10.0
@@ -732,16 +676,12 @@ python-dotenv==1.1.0
# via
# -c requirements/base.txt
# apache-superset
# fastmcp
# pydantic-settings
python-geohash==0.8.5
# via
# -c requirements/base.txt
# apache-superset
python-ldap==3.4.4
# via apache-superset
python-multipart==0.0.20
# via mcp
pytz==2025.2
# via
# -c requirements/base.txt
@@ -796,12 +736,7 @@ rfc3339-validator==0.1.4
rich==13.9.4
# via
# -c requirements/base.txt
# cyclopts
# fastmcp
# flask-limiter
# rich-rst
rich-rst==1.3.1
# via cyclopts
rpds-py==0.25.0
# via
# -c requirements/base.txt
@@ -846,7 +781,6 @@ slack-sdk==3.35.0
sniffio==1.3.1
# via
# -c requirements/base.txt
# anyio
# trio
sortedcontainers==2.4.0
# via
@@ -876,14 +810,10 @@ sqlglot==27.3.0
# apache-superset
sqloxide==0.1.51
# via apache-superset
sse-starlette==2.4.1
# via mcp
sshtunnel==0.4.0
# via
# -c requirements/base.txt
# apache-superset
starlette==0.47.2
# via mcp
statsd==4.0.1
# via apache-superset
tabulate==0.9.0
@@ -911,23 +841,13 @@ typing-extensions==4.14.0
# via
# -c requirements/base.txt
# alembic
# anyio
# apache-superset
# cattrs
# exceptiongroup
# limits
# pydantic
# pydantic-core
# pyopenssl
# referencing
# selenium
# shillelagh
# starlette
# typing-inspection
typing-inspection==0.4.1
# via
# pydantic
# pydantic-settings
tzdata==2025.2
# via
# -c requirements/base.txt
@@ -946,8 +866,6 @@ urllib3==2.5.0
# requests
# requests-cache
# selenium
uvicorn==0.35.0
# via mcp
vine==5.1.0
# via
# -c requirements/base.txt

View File

@@ -33,4 +33,4 @@ superset load-test-users
echo "Running tests"
pytest --durations-min=2 --maxfail=1 --cov-report= --cov=superset ./tests/integration_tests "$@"
pytest --durations-min=2 --cov-report= --cov=superset ./tests/integration_tests "$@"

View File

@@ -403,6 +403,7 @@ module.exports = {
'theme-colors/no-literal-colors': 'error',
'icons/no-fa-icons-usage': 'error',
'i18n-strings/no-template-vars': ['error', true],
'i18n-strings/sentence-case-buttons': 'error',
camelcase: [
'error',
{

View File

@@ -19,7 +19,7 @@
import { LOGIN } from 'cypress/utils/urls';
function interceptLogin() {
cy.intercept('POST', '/login/').as('login');
cy.intercept('POST', '**/login/').as('login');
}
describe('Login view', () => {

View File

@@ -94,67 +94,12 @@ describe('Charts list', () => {
});
});
describe('list mode', () => {
before(() => {
visitChartList();
setGridMode('list');
});
it('should load rows in list mode', () => {
cy.getBySel('listview-table').should('be.visible');
cy.getBySel('sort-header').eq(1).contains('Name');
cy.getBySel('sort-header').eq(2).contains('Type');
cy.getBySel('sort-header').eq(3).contains('Dataset');
cy.getBySel('sort-header').eq(4).contains('On dashboards');
cy.getBySel('sort-header').eq(5).contains('Owners');
cy.getBySel('sort-header').eq(6).contains('Last modified');
cy.getBySel('sort-header').eq(7).contains('Actions');
});
it('should bulk select in list mode', () => {
toggleBulkSelect();
cy.get('[aria-label="Select all"]').click();
cy.get('input[type="checkbox"]:checked').should('have.length', 26);
cy.getBySel('bulk-select-copy').contains('25 Selected');
cy.getBySel('bulk-select-action')
.should('have.length', 2)
.then($btns => {
expect($btns).to.contain('Delete');
expect($btns).to.contain('Export');
});
cy.getBySel('bulk-select-deselect-all').click();
cy.get('input[type="checkbox"]:checked').should('have.length', 0);
cy.getBySel('bulk-select-copy').contains('0 Selected');
cy.getBySel('bulk-select-action').should('not.exist');
});
});
describe('card mode', () => {
before(() => {
visitChartList();
setGridMode('card');
});
it('should load rows in card mode', () => {
cy.getBySel('listview-table').should('not.exist');
cy.getBySel('styled-card').should('have.length', 25);
});
it('should bulk select in card mode', () => {
toggleBulkSelect();
cy.getBySel('styled-card').click({ multiple: true });
cy.getBySel('bulk-select-copy').contains('25 Selected');
cy.getBySel('bulk-select-action')
.should('have.length', 2)
.then($btns => {
expect($btns).to.contain('Delete');
expect($btns).to.contain('Export');
});
cy.getBySel('bulk-select-deselect-all').click();
cy.getBySel('bulk-select-copy').contains('0 Selected');
cy.getBySel('bulk-select-action').should('not.exist');
});
it('should preserve other filters when sorting', () => {
cy.getBySel('styled-card').should('have.length', 25);
setFilter('Type', 'Big Number');

View File

@@ -31,6 +31,52 @@ import {
interceptFormDataKey,
} from '../explore/utils';
const interceptDrillInfo = () => {
cy.intercept('GET', '**/api/v1/dataset/*/drill_info/*', {
statusCode: 200,
body: {
result: {
id: 1,
changed_on_humanized: '2 days ago',
created_on_humanized: 'a week ago',
table_name: 'birth_names',
changed_by: {
first_name: 'Admin',
last_name: 'User',
},
created_by: {
first_name: 'Admin',
last_name: 'User',
},
owners: [
{
first_name: 'Admin',
last_name: 'User',
},
],
columns: [
{
column_name: 'gender',
verbose_name: null,
},
{
column_name: 'state',
verbose_name: null,
},
{
column_name: 'name',
verbose_name: null,
},
{
column_name: 'ds',
verbose_name: null,
},
],
},
},
}).as('drillInfo');
};
const closeModal = () => {
cy.get('body').then($body => {
if ($body.find('[data-test="close-drill-by-modal"]').length) {
@@ -62,14 +108,20 @@ const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
cy.get(
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
{ timeout: 15000 },
)
.should('be.visible')
.find('[role="menuitem"]')
.then($el => {
cy.wrap($el)
.contains(new RegExp(`^${targetDrillByColumn}$`))
.trigger('keydown', { keyCode: 13, which: 13, force: true });
});
.contains(new RegExp(`^${targetDrillByColumn}$`))
.click();
cy.get(
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
).trigger('mouseout', { clientX: 0, clientY: 0, force: true });
cy.get(
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
).should('not.exist');
if (isLegacy) {
return cy.wait('@legacyData');
@@ -230,17 +282,19 @@ describe('Drill by modal', () => {
closeModal();
});
before(() => {
interceptDrillInfo();
cy.visit(SUPPORTED_CHARTS_DASHBOARD);
});
describe('Modal actions + Table', () => {
before(() => {
closeModal();
interceptDrillInfo();
openTopLevelTab('Tier 1');
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
});
it('opens the modal from the context menu', () => {
it.only('opens the modal from the context menu', () => {
openTableContextMenu('boy');
drillBy('state').then(intercepted => {
verifyExpectedFormData(intercepted, {
@@ -384,6 +438,7 @@ describe('Drill by modal', () => {
describe('Tier 1 charts', () => {
before(() => {
closeModal();
interceptDrillInfo();
openTopLevelTab('Tier 1');
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
});
@@ -547,6 +602,7 @@ describe('Drill by modal', () => {
describe('Tier 2 charts', () => {
before(() => {
closeModal();
interceptDrillInfo();
openTopLevelTab('Tier 2');
SUPPORTED_TIER2_CHARTS.forEach(waitForChartLoad);
});

View File

@@ -155,7 +155,7 @@ describe('Horizontal FilterBar', () => {
]);
setFilterBarOrientation('horizontal');
cy.get('.filter-item-wrapper').should('have.length', 3);
cy.get('.filter-item-wrapper').should('have.length', 4);
openMoreFilters();
cy.getBySel('form-item-value').should('have.length', 12);
cy.getBySel('filter-control-name').contains('test_3').should('be.visible');

View File

@@ -160,6 +160,74 @@ describe('Native filters', () => {
);
});
it('Dependent filter selects first item based on parent filter selection', () => {
prepareDashboardFilters([
{ name: 'region', column: 'region', datasetId: 2 },
{ name: 'country_name', column: 'country_name', datasetId: 2 },
]);
enterNativeFilterEditModal();
selectFilter(0);
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
() => {
cy.contains('Select first filter value by default')
.should('be.visible')
.click();
},
);
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
() => {
cy.contains('Can select multiple values ')
.should('be.visible')
.click();
},
);
selectFilter(1);
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
() => {
cy.contains('Values are dependent on other filters')
.should('be.visible')
.click();
},
);
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
() => {
cy.contains('Can select multiple values ')
.should('be.visible')
.click();
},
);
addParentFilterWithValue(0, testItems.topTenChart.filterColumnRegion);
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
() => {
cy.contains('Select first filter value by default')
.should('be.visible')
.click();
},
);
// cannot use saveNativeFilterSettings because there is a bug which
// sometimes does not allow charts to load when enabling the 'Select first filter value by default'
// to be saved when using dependent filters so,
// you reload the window.
cy.get(nativeFilters.modal.footer)
.contains('Save')
.should('be.visible')
.click({ force: true });
cy.get(nativeFilters.modal.container).should('not.exist');
cy.reload();
applyNativeFilterValueWithIndex(0, 'North America');
// Check that dependent filter auto-selects the first item
cy.get(nativeFilters.filterFromDashboardView.filterContent)
.eq(1)
.should('contain.text', 'Bermuda');
});
it('User can create filter depend on 2 other filters', () => {
prepareDashboardFilters([
{ name: 'region', column: 'region', datasetId: 2 },
@@ -275,7 +343,7 @@ describe('Native filters', () => {
it('User can delete a native filter', () => {
enterNativeFilterEditModal(false);
cy.get(nativeFilters.filtersList.removeIcon).first().click();
cy.contains('Restore Filter').should('not.exist', { timeout: 10000 });
cy.contains('Restore filter').should('not.exist', { timeout: 10000 });
});
it('User can cancel creating a new filter', () => {

View File

@@ -68,11 +68,13 @@ function verifyDashboardSearch() {
function verifyDashboardLink() {
interceptDashboardGet();
openDashboardsAddedTo();
cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover');
cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover', {
force: true,
});
cy.get('.ant-dropdown-menu-submenu-popup a')
.first()
.invoke('removeAttr', 'target')
.click();
.click({ force: true });
cy.wait('@get');
}

View File

@@ -10227,14 +10227,11 @@
"peer": true
},
"node_modules/tmp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
"dependencies": {
"rimraf": "^3.0.0"
},
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz",
"integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==",
"engines": {
"node": ">=8.17.0"
"node": ">=14.14"
}
},
"node_modules/to-regex-range": {
@@ -18598,12 +18595,9 @@
"peer": true
},
"tmp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
"requires": {
"rimraf": "^3.0.0"
}
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz",
"integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ=="
},
"to-regex-range": {
"version": "5.0.1",

View File

@@ -41,7 +41,7 @@ module.exports = {
context.report({
node,
message:
"Don't use variables in translation string templates. Flask-babel is a static translation service, so it cant handle strings that include variables",
"Don't use variables in translation string templates. Flask-babel is a static translation service, so it can't handle strings that include variables",
});
}
}
@@ -52,5 +52,67 @@ module.exports = {
};
},
},
'sentence-case-buttons': {
create(context) {
function isTitleCase(str) {
// Match "Delete Dataset", "Create Chart", etc. (2+ title-cased words)
return /^[A-Z][a-z]+(\s+[A-Z][a-z]*)+$/.test(str);
}
function isButtonContext(node) {
const { parent } = node;
if (!parent) return false;
// Check for button-specific props
if (parent.type === 'Property') {
const key = parent.key.name;
return [
'primaryButtonName',
'secondaryButtonName',
'confirmButtonText',
'cancelButtonText',
].includes(key);
}
// Check for Button components
if (parent.type === 'JSXExpressionContainer') {
const jsx = parent.parent;
if (jsx?.type === 'JSXElement') {
const elementName = jsx.openingElement.name.name;
return elementName === 'Button';
}
}
return false;
}
function handler(node) {
if (node.arguments.length) {
const firstArg = node.arguments[0];
if (
firstArg.type === 'Literal' &&
typeof firstArg.value === 'string'
) {
const text = firstArg.value;
if (isButtonContext(node) && isTitleCase(text)) {
const sentenceCase = text
.toLowerCase()
.replace(/^\w/, c => c.toUpperCase());
context.report({
node: firstArg,
message: `Button text should use sentence case: "${text}" should be "${sentenceCase}"`,
});
}
}
}
}
return {
"CallExpression[callee.name='t']": handler,
"CallExpression[callee.name='tn']": handler,
};
},
},
},
};

File diff suppressed because it is too large Load Diff

View File

@@ -88,7 +88,7 @@
"@reduxjs/toolkit": "^1.9.3",
"@rjsf/core": "^5.21.1",
"@rjsf/utils": "^5.24.3",
"@rjsf/validator-ajv8": "^5.24.9",
"@rjsf/validator-ajv8": "^5.24.12",
"@scarf/scarf": "^1.4.0",
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
"@superset-ui/core": "file:./packages/superset-ui-core",
@@ -121,15 +121,13 @@
"@visx/scale": "^3.5.0",
"@visx/tooltip": "^3.0.0",
"@visx/xychart": "^3.5.1",
"ag-grid-community": "^34.0.2",
"ag-grid-react": "34.0.2",
"antd": "^5.24.6",
"chrono-node": "^2.7.8",
"classnames": "^2.2.5",
"d3-color": "^3.1.0",
"d3-scale": "^2.1.2",
"dayjs": "^1.11.13",
"dom-to-image-more": "^3.2.0",
"dom-to-image-more": "^3.6.0",
"dom-to-pdf": "^0.3.2",
"echarts": "^5.6.0",
"emotion-rgba": "0.0.12",
@@ -144,7 +142,7 @@
"geostyler-qgis-parser": "2.0.1",
"geostyler-style": "7.5.0",
"geostyler-wfs-parser": "^2.0.3",
"googleapis": "^130.0.0",
"googleapis": "^154.1.0",
"immer": "^10.1.1",
"interweave": "^13.1.0",
"jquery": "^3.7.1",
@@ -176,7 +174,7 @@
"react-hot-loader": "^4.13.1",
"react-intersection-observer": "^9.16.0",
"react-json-tree": "^0.20.0",
"react-lines-ellipsis": "^0.15.4",
"react-lines-ellipsis": "^0.16.1",
"react-loadable": "^5.5.0",
"react-redux": "^7.2.9",
"react-resize-detector": "^7.1.2",
@@ -208,7 +206,7 @@
"devDependencies": {
"@applitools/eyes-storybook": "^3.55.6",
"@babel/cli": "^7.27.2",
"@babel/compat-data": "^7.26.8",
"@babel/compat-data": "^7.28.0",
"@babel/core": "^7.26.0",
"@babel/eslint-parser": "^7.25.9",
"@babel/node": "^7.22.6",
@@ -216,11 +214,11 @@
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
"@babel/plugin-transform-runtime": "^7.27.1",
"@babel/preset-env": "^7.27.2",
"@babel/preset-react": "^7.26.3",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.26.0",
"@babel/register": "^7.23.7",
"@babel/runtime": "^7.26.0",
"@babel/runtime-corejs3": "^7.26.0",
"@babel/runtime": "^7.28.2",
"@babel/runtime-corejs3": "^7.28.2",
"@babel/types": "^7.26.9",
"@cypress/react": "^8.0.2",
"@emotion/babel-plugin": "^11.13.5",
@@ -243,7 +241,6 @@
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^12.8.3",
"@types/classnames": "^2.2.10",
"@types/dom-to-image": "^2.6.7",
"@types/jest": "^29.5.14",
"@types/js-levenshtein": "^1.1.3",
@@ -285,7 +282,7 @@
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^7.2.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-typescript": "^3.7.0",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-cypress": "^3.6.0",
"eslint-plugin-file-progress": "^1.5.0",
"eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
@@ -331,7 +328,7 @@
"ts-jest": "^29.4.0",
"ts-loader": "^9.5.1",
"tscw-config": "^1.1.2",
"tsx": "^4.19.2",
"tsx": "^4.20.3",
"typescript": "5.4.5",
"vm-browserify": "^1.1.2",
"webpack": "^5.99.9",

View File

@@ -36,7 +36,7 @@
"devDependencies": {
"cross-env": "^7.0.3",
"fs-extra": "^11.3.0",
"jest": "^30.0.2",
"jest": "^30.0.5",
"yeoman-test": "^10.1.1"
},
"engines": {

View File

@@ -36,18 +36,19 @@ export const renameOperator: PostProcessingFactory<PostProcessingRename> = (
const columns = ensureIsArray(
queryObject.series_columns || queryObject.columns,
);
const timeOffsets = ensureIsArray(formData.time_compare);
const { truncate_metric } = formData;
const xAxisLabel = getXAxisLabel(formData);
const isTimeComparisonValue = isTimeComparison(formData, queryObject);
// remove or rename top level of column name(metric name) in the MultiIndex when
// 1) at least 1 metric
// 2) dimension exist
// 2) dimension exist or multiple time shift metrics exist
// 3) xAxis exist
// 4) truncate_metric in form_data and truncate_metric is true
if (
metrics.length > 0 &&
columns.length > 0 &&
(columns.length > 0 || timeOffsets.length > 1) &&
xAxisLabel &&
truncate_metric !== undefined &&
!!truncate_metric
@@ -84,7 +85,8 @@ export const renameOperator: PostProcessingFactory<PostProcessingRename> = (
ComparisonType.Percentage,
ComparisonType.Ratio,
].includes(formData.comparison_type) &&
metrics.length === 1
metrics.length === 1 &&
renamePairs.length === 0
) {
renamePairs.push([getMetricLabel(metrics[0]), null]);
}

View File

@@ -177,6 +177,7 @@ const granularity: SharedControlConfig<'SelectControl'> = {
'can type and use simple natural language as in `10 seconds`, ' +
'`1 day` or `56 weeks`',
),
sortComparator: () => 0, // Disable frontend sorting to preserve backend order
};
const time_grain_sqla: SharedControlConfig<'SelectControl'> = {
@@ -204,6 +205,7 @@ const time_grain_sqla: SharedControlConfig<'SelectControl'> = {
choices: (datasource as Dataset)?.time_grain_sqla || [],
}),
visibility: displayTimeRelatedControls,
sortComparator: () => 0, // Disable frontend sorting to preserve backend order
};
const time_range: SharedControlConfig<'DateFilterControl'> = {

View File

@@ -29,3 +29,4 @@ export * from './getStandardizedControls';
export * from './getTemporalColumns';
export * from './displayTimeRelatedControls';
export * from './colorControls';
export * from './metricColumnFilter';

View File

@@ -0,0 +1,135 @@
/**
* 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 { QueryFormMetric, SqlaFormData } from '@superset-ui/core';
import {
shouldSkipMetricColumn,
isRegularMetric,
isPercentMetric,
} from './metricColumnFilter';
const createMetric = (label: string): QueryFormMetric =>
({
label,
expressionType: 'SIMPLE',
column: { column_name: label },
aggregate: 'SUM',
}) as QueryFormMetric;
describe('metricColumnFilter', () => {
const createFormData = (
metrics: string[],
percentMetrics: string[],
): SqlaFormData =>
({
datasource: 'test_datasource',
viz_type: 'table',
metrics: metrics.map(createMetric),
percent_metrics: percentMetrics.map(createMetric),
}) as SqlaFormData;
describe('shouldSkipMetricColumn', () => {
it('should skip unprefixed percent metric columns if prefixed version exists', () => {
const colnames = ['metric1', '%metric1'];
const formData = createFormData([], ['metric1']);
const result = shouldSkipMetricColumn({
colname: 'metric1',
colnames,
formData,
});
expect(result).toBe(true);
});
it('should not skip if column is also a regular metric', () => {
const colnames = ['metric1', '%metric1'];
const formData = createFormData(['metric1'], ['metric1']);
const result = shouldSkipMetricColumn({
colname: 'metric1',
colnames,
formData,
});
expect(result).toBe(false);
});
it('should not skip if column starts with %', () => {
const colnames = ['%metric1'];
const formData = createFormData(['metric1'], []);
const result = shouldSkipMetricColumn({
colname: '%metric1',
colnames,
formData,
});
expect(result).toBe(false);
});
it('should not skip if no prefixed version exists', () => {
const colnames = ['metric1'];
const formData = createFormData([], ['metric1']);
const result = shouldSkipMetricColumn({
colname: 'metric1',
colnames,
formData,
});
expect(result).toBe(false);
});
});
describe('isRegularMetric', () => {
it('should return true for regular metrics', () => {
const formData = createFormData(['metric1', 'metric2'], []);
expect(isRegularMetric('metric1', formData)).toBe(true);
expect(isRegularMetric('metric2', formData)).toBe(true);
});
it('should return false for non-metrics', () => {
const formData = createFormData(['metric1'], []);
expect(isRegularMetric('non_metric', formData)).toBe(false);
});
it('should return false for percentage metrics', () => {
const formData = createFormData([], ['percent_metric1']);
expect(isRegularMetric('percent_metric1', formData)).toBe(false);
});
});
describe('isPercentMetric', () => {
it('should return true for percentage metrics', () => {
const formData = createFormData([], ['percent_metric1']);
expect(isPercentMetric('%percent_metric1', formData)).toBe(true);
});
it('should return false for non-percentage metrics', () => {
const formData = createFormData(['regular_metric'], []);
expect(isPercentMetric('regular_metric', formData)).toBe(false);
});
it('should return false for regular metrics', () => {
const formData = createFormData(['metric1'], []);
expect(isPercentMetric('metric1', formData)).toBe(false);
});
});
});

View File

@@ -0,0 +1,95 @@
/**
* 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 {
QueryFormMetric,
getMetricLabel,
SqlaFormData,
} from '@superset-ui/core';
export interface MetricColumnFilterParams {
colname: string;
colnames: string[];
formData: SqlaFormData;
}
/**
* Determines if a column should be skipped based on metric filtering logic.
*
* This function implements the logic to skip unprefixed percent metric columns
* if a prefixed version exists, but doesn't skip if it's also a regular metric.
*
* @param params - The parameters for metric column filtering
* @returns true if the column should be skipped, false otherwise
*/
export function shouldSkipMetricColumn({
colname,
colnames,
formData,
}: MetricColumnFilterParams): boolean {
if (!colname) {
return false;
}
// Check if this column name exists as a percent metric in form data
const isPercentMetric = formData.percent_metrics?.some(
(metric: QueryFormMetric) => getMetricLabel(metric) === colname,
);
// Check if this column name exists as a regular metric in form data
const isRegularMetric = formData.metrics?.some(
(metric: QueryFormMetric) => getMetricLabel(metric) === colname,
);
// Check if there's a prefixed version of this column in the column list
const hasPrefixedVersion = colnames.includes(`%${colname}`);
// Skip if: has prefixed version AND is percent metric AND is NOT regular metric
return hasPrefixedVersion && isPercentMetric && !isRegularMetric;
}
/**
* Determines if a column is a regular metric.
*
* @param colname - The column name to check
* @param formData - The form data containing metrics
* @returns true if the column is a regular metric, false otherwise
*/
export function isRegularMetric(
colname: string,
formData: SqlaFormData,
): boolean {
return !!formData.metrics?.some(metric => getMetricLabel(metric) === colname);
}
/**
* Determines if a column is a percentage metric.
*
* @param colname: string,
* @param formData - The form data containing percent_metrics
* @returns true if the column is a percentage metric, false otherwise
*/
export function isPercentMetric(
colname: string,
formData: SqlaFormData,
): boolean {
return !!formData.percent_metrics?.some(
(metric: QueryFormMetric) => `%${getMetricLabel(metric)}` === colname,
);
}

View File

@@ -65,6 +65,20 @@ test('should skip renameOperator if series does not exist', () => {
).toEqual(undefined);
});
test('should skip renameOperator if series does not exist and a single time shift exists', () => {
expect(
renameOperator(
{ ...formData, ...{ time_compare: ['1 year ago'] } },
{
...queryObject,
...{
columns: [],
},
},
),
).toEqual(undefined);
});
test('should skip renameOperator if does not exist x_axis and is_timeseries', () => {
expect(
renameOperator(
@@ -93,6 +107,26 @@ test('should add renameOperator', () => {
});
});
test('should add renameOperator if a metric exists and multiple time shift', () => {
expect(
renameOperator(
{
...formData,
...{ time_compare: ['1 year ago', '2 years ago'] },
},
{
...queryObject,
...{
columns: [],
},
},
),
).toEqual({
operation: 'rename',
options: { columns: { 'count(*)': null }, inplace: true, level: 0 },
});
});
test('should add renameOperator if exists derived metrics', () => {
[
ComparisonType.Difference,
@@ -176,7 +210,6 @@ test('should add renameOperator if exist "actual value" time comparison', () =>
operation: 'rename',
options: {
columns: {
'count(*)': null,
'count(*)__1 year ago': '1 year ago',
'count(*)__1 year later': '1 year later',
},

View File

@@ -25,11 +25,13 @@
],
"dependencies": {
"@ant-design/icons": "^5.2.6",
"@babel/runtime": "^7.25.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",
"brace": "^0.11.1",
"classnames": "^2.2.5",
"csstype": "^3.1.3",
@@ -46,10 +48,10 @@
"lodash": "^4.17.21",
"math-expression-evaluator": "^2.0.6",
"pretty-ms": "^9.2.0",
"re-resizable": "^6.10.1",
"re-resizable": "^6.11.2",
"react-ace": "^10.1.0",
"react-js-cron": "^5.2.0",
"react-draggable": "^4.4.6",
"react-draggable": "^4.5.0",
"react-resize-detector": "^7.1.2",
"react-syntax-highlighter": "^15.4.5",
"react-ultimate-pagination": "^1.3.2",
@@ -78,7 +80,7 @@
"@types/lodash": "^4.17.20",
"@types/math-expression-evaluator": "^1.3.3",
"@types/node": "^22.10.3",
"@types/prop-types": "^15.7.2",
"@types/prop-types": "^15.7.15",
"@types/rison": "0.1.0",
"@types/seedrandom": "^3.0.8",
"fetch-mock": "^11.1.4",

View File

@@ -24,6 +24,7 @@ export const Badge = styled((props: BadgeProps) => <AntdBadge {...props} />)`
${({ theme, color, count }) => `
& > sup,
& > sup.ant-badge-count {
box-shadow: none;
${
count !== undefined ? `background: ${color || theme.colorPrimary};` : ''
}

View File

@@ -132,11 +132,12 @@ export function Button(props: ButtonProps) {
'& > span > :first-of-type': {
marginRight: firstChildMargin,
},
':not(:hover)': effectiveButtonStyle === 'secondary' && {
// NOTE: This is the best we can do contrast wise for the secondary button using antd tokens
// and abusing the semantics. Should be revisited when possible. https://github.com/apache/superset/pull/34253#issuecomment-3104834692
color: `${theme.colorPrimaryTextHover} !important`,
},
':not(:hover)': effectiveButtonStyle === 'secondary' &&
!disabled && {
// NOTE: This is the best we can do contrast wise for the secondary button using antd tokens
// and abusing the semantics. Should be revisited when possible. https://github.com/apache/superset/pull/34253#issuecomment-3104834692
color: `${theme.colorPrimaryTextHover} !important`,
},
}}
icon={icon}
{...restProps}

View File

@@ -0,0 +1,22 @@
/**
* 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 type { DrawerProps } from './types';
export { Drawer } from 'antd';
export type { DrawerProps };

View File

@@ -0,0 +1,22 @@
/**
* 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 type { DrawerProps } from 'antd/es/drawer';
export { DrawerProps };

View File

@@ -0,0 +1,395 @@
/**
* 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 {
cloneElement,
forwardRef,
RefObject,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useState,
useRef,
useCallback,
} from 'react';
import { Global } from '@emotion/react';
import { css, t, useTheme, usePrevious } from '@superset-ui/core';
import { useResizeDetector } from 'react-resize-detector';
import { Badge, Icons, Button, Tooltip, Popover } from '..';
import { DropdownContainerProps, DropdownItem, DropdownRef } from './types';
const MAX_HEIGHT = 500;
export const DropdownContainer = forwardRef(
(
{
items,
onOverflowingStateChange,
dropdownContent,
dropdownRef,
dropdownStyle = {},
dropdownTriggerCount,
dropdownTriggerIcon,
dropdownTriggerText = t('More'),
dropdownTriggerTooltip = null,
forceRender,
style,
}: DropdownContainerProps,
outerRef: RefObject<DropdownRef>,
) => {
const theme = useTheme();
const { ref, width = 0 } = useResizeDetector<HTMLDivElement>();
const previousWidth = usePrevious(width) || 0;
const { current } = ref;
const [itemsWidth, setItemsWidth] = useState<number[]>([]);
const [popoverVisible, setPopoverVisible] = useState(false);
// We use React.useState to be able to mock the state in Jest
const [overflowingIndex, setOverflowingIndex] = useState<number>(-1);
let targetRef = useRef<HTMLDivElement>(null);
if (dropdownRef) {
targetRef = dropdownRef;
}
const [showOverflow, setShowOverflow] = useState(false);
// callback to update item widths so that the useLayoutEffect runs whenever
// width of any of the child changes
const recalculateItemWidths = useCallback(() => {
const mainItemsContainerNode = current?.children.item(0);
if (mainItemsContainerNode) {
const visibleChildrenElements = Array.from(
mainItemsContainerNode.children,
);
setItemsWidth(prevGlobalWidths => {
if (prevGlobalWidths.length !== items.length) {
return prevGlobalWidths;
}
const newGlobalWidths = [...prevGlobalWidths];
let changed = false;
visibleChildrenElements.forEach((child, indexInVisible) => {
const originalItemIndex = indexInVisible;
if (originalItemIndex < newGlobalWidths.length) {
const newWidth = child.getBoundingClientRect().width;
if (newGlobalWidths[originalItemIndex] !== newWidth) {
newGlobalWidths[originalItemIndex] = newWidth;
changed = true;
}
}
});
return changed ? newGlobalWidths : prevGlobalWidths;
});
}
}, [current?.children, items.length]);
const reduceItems = (items: DropdownItem[]): [DropdownItem[], string[]] =>
items.reduce(
([items, ids], item) => {
items.push({
id: item.id,
element: cloneElement(item.element, { key: item.id }),
});
ids.push(item.id);
return [items, ids];
},
[[], []] as [DropdownItem[], string[]],
);
const [notOverflowedItems, notOverflowedIds] = useMemo(
() =>
reduceItems(
items.slice(
0,
overflowingIndex !== -1 ? overflowingIndex : items.length,
),
),
[items, overflowingIndex],
);
const [overflowedItems, overflowedIds] = useMemo(
() =>
overflowingIndex !== -1
? reduceItems(items.slice(overflowingIndex))
: [[], []],
[items, overflowingIndex],
);
useEffect(() => {
const container = current?.children.item(0);
if (!container) return;
const childrenArray = Array.from(container.children);
const resizeObserver = new ResizeObserver(() => {
recalculateItemWidths();
});
childrenArray.map(child => resizeObserver.observe(child));
// eslint-disable-next-line consistent-return
return () => {
childrenArray.map(child => resizeObserver.unobserve(child));
resizeObserver.disconnect();
};
}, [items.length, current, recalculateItemWidths]);
useLayoutEffect(() => {
if (popoverVisible) {
return;
}
const container = current?.children.item(0);
if (container) {
const { children } = container;
const childrenArray = Array.from(children);
// If items length change, add all items to the container
// and recalculate the widths
if (itemsWidth.length !== items.length) {
if (childrenArray.length === items.length) {
setItemsWidth(
childrenArray.map(child => child.getBoundingClientRect().width),
);
} else {
setOverflowingIndex(-1);
return;
}
}
// Calculates the index of the first overflowed element
// +1 is to give at least one pixel of difference and avoid flakiness
const index = childrenArray.findIndex(
child =>
child.getBoundingClientRect().right >
container.getBoundingClientRect().right + 1,
);
// If elements fit (-1) and there's overflowed items
// then preserve the overflow index. We can't use overflowIndex
// directly because the items may have been modified
let newOverflowingIndex =
index === -1 && overflowedItems.length > 0
? items.length - overflowedItems.length
: index;
if (width > previousWidth) {
// Calculates remaining space in the container
const button = current?.children.item(1);
const buttonRight = button?.getBoundingClientRect().right || 0;
const containerRight = current?.getBoundingClientRect().right || 0;
const remainingSpace = containerRight - buttonRight;
// Checks if some elements in the dropdown fits in the remaining space
let sum = 0;
for (let i = childrenArray.length; i < items.length; i += 1) {
sum += itemsWidth[i];
if (sum <= remainingSpace) {
newOverflowingIndex = i + 1;
} else {
break;
}
}
}
setOverflowingIndex(newOverflowingIndex);
}
}, [
current,
items.length,
itemsWidth,
overflowedItems.length,
previousWidth,
width,
popoverVisible,
]);
useEffect(() => {
if (onOverflowingStateChange) {
onOverflowingStateChange({
notOverflowed: notOverflowedIds,
overflowed: overflowedIds,
});
}
}, [notOverflowedIds, onOverflowingStateChange, overflowedIds]);
const overflowingCount =
overflowingIndex !== -1 ? items.length - overflowingIndex : 0;
const popoverContent = useMemo(
() =>
dropdownContent || overflowingCount ? (
<div
css={css`
display: flex;
flex-direction: column;
gap: ${theme.sizeUnit * 4}px;
`}
data-test="dropdown-content"
style={dropdownStyle}
ref={targetRef}
>
{dropdownContent
? dropdownContent(overflowedItems)
: overflowedItems.map(item => item.element)}
</div>
) : null,
[
dropdownContent,
overflowingCount,
theme.sizeUnit,
dropdownStyle,
overflowedItems,
],
);
useLayoutEffect(() => {
if (popoverVisible) {
// Measures scroll height after rendering the elements
setTimeout(() => {
if (targetRef.current) {
// We only set overflow when there's enough space to display
// Select's popovers because they are restrained by the overflow property.
setShowOverflow(targetRef.current.scrollHeight > MAX_HEIGHT);
}
}, 100);
}
}, [popoverVisible]);
useImperativeHandle(
outerRef,
() => ({
...(ref.current as HTMLDivElement),
open: () => setPopoverVisible(true),
}),
[ref],
);
// Closes the popover when scrolling on the document
useEffect(() => {
document.onscroll = popoverVisible
? () => setPopoverVisible(false)
: null;
return () => {
document.onscroll = null;
};
}, [popoverVisible]);
return (
<div
ref={ref}
css={css`
display: flex;
align-items: center;
`}
>
<div
css={css`
display: flex;
align-items: center;
gap: ${theme.sizeUnit * 4}px;
margin-right: ${theme.sizeUnit * 4}px;
min-width: 0px;
`}
data-test="container"
style={style}
>
{notOverflowedItems.map(item => item.element)}
</div>
{popoverContent && (
<>
<Global
styles={css`
.ant-popover-inner {
// Some OS versions only show the scroll when hovering.
// These settings will make the scroll always visible.
::-webkit-scrollbar {
-webkit-appearance: none;
width: 14px;
}
::-webkit-scrollbar-thumb {
border-radius: 9px;
background-color: ${theme.colors.grayscale.light1};
border: 3px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-track {
background-color: ${theme.colors.grayscale.light4};
border-left: 1px solid ${theme.colors.grayscale.light2};
}
}
`}
/>
<Popover
styles={{
body: {
maxHeight: `${MAX_HEIGHT}px`,
overflow: showOverflow ? 'auto' : 'visible',
},
}}
content={popoverContent}
trigger="click"
open={popoverVisible}
onOpenChange={visible => setPopoverVisible(visible)}
placement="bottom"
forceRender={forceRender}
>
<Tooltip title={dropdownTriggerTooltip}>
<Button
buttonStyle="secondary"
data-test="dropdown-container-btn"
icon={dropdownTriggerIcon}
css={css`
padding-left: ${theme.paddingXS}px;
padding-right: ${theme.paddingXXS}px;
gap: ${theme.sizeXXS}px;
`}
>
{dropdownTriggerText}
<Badge
count={dropdownTriggerCount ?? overflowingCount}
color={
(dropdownTriggerCount ?? overflowingCount) > 0
? theme.colorPrimary
: theme.colors.grayscale.light1
}
showZero
css={css`
margin-left: ${theme.sizeUnit * 2}px;
`}
/>
<Icons.DownOutlined
iconSize="m"
iconColor={theme.colors.grayscale.light1}
css={css`
.anticon {
display: flex;
}
`}
/>
</Button>
</Tooltip>
</Popover>
</>
)}
</div>
);
},
);

View File

@@ -16,448 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
CSSProperties,
cloneElement,
forwardRef,
ReactElement,
RefObject,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useState,
useRef,
ReactNode,
useCallback,
} from 'react';
import { Global } from '@emotion/react';
import { css, t, useTheme, usePrevious } from '@superset-ui/core';
import { useResizeDetector } from 'react-resize-detector';
import { Badge, Icons, Button, Tooltip, Popover } from '..';
/**
* Container item.
*/
export interface DropdownItem {
/**
* String that uniquely identifies the item.
*/
id: string;
/**
* The element to be rendered.
*/
element: ReactElement;
}
/**
* Horizontal container that displays overflowed items in a dropdown.
* It shows an indicator of how many items are currently overflowing.
*/
export interface DropdownContainerProps {
/**
* Array of items. The id property is used to uniquely identify
* the elements when rendering or dealing with event handlers.
*/
items: DropdownItem[];
/**
* Event handler called every time an element moves between
* main container and dropdown.
*/
onOverflowingStateChange?: (overflowingState: {
notOverflowed: string[];
overflowed: string[];
}) => void;
/**
* Option to customize the content of the dropdown.
*/
dropdownContent?: (overflowedItems: DropdownItem[]) => ReactElement;
/**
* Dropdown ref.
*/
dropdownRef?: RefObject<HTMLDivElement>;
/**
* Dropdown additional style properties.
*/
dropdownStyle?: CSSProperties;
/**
* Displayed count in the dropdown trigger.
*/
dropdownTriggerCount?: number;
/**
* Icon of the dropdown trigger.
*/
dropdownTriggerIcon?: ReactElement;
/**
* Text of the dropdown trigger.
*/
dropdownTriggerText?: string;
/**
* Text of the dropdown trigger tooltip
*/
dropdownTriggerTooltip?: ReactNode | null;
/**
* Main container additional style properties.
*/
style?: CSSProperties;
/**
* Force render popover content before it's first opened
*/
forceRender?: boolean;
}
export type DropdownRef = HTMLDivElement & { open: () => void };
const MAX_HEIGHT = 500;
export const DropdownContainer = forwardRef(
(
{
items,
onOverflowingStateChange,
dropdownContent,
dropdownRef,
dropdownStyle = {},
dropdownTriggerCount,
dropdownTriggerIcon,
dropdownTriggerText = t('More'),
dropdownTriggerTooltip = null,
forceRender,
style,
}: DropdownContainerProps,
outerRef: RefObject<DropdownRef>,
) => {
const theme = useTheme();
const { ref, width = 0 } = useResizeDetector<HTMLDivElement>();
const previousWidth = usePrevious(width) || 0;
const { current } = ref;
const [itemsWidth, setItemsWidth] = useState<number[]>([]);
const [popoverVisible, setPopoverVisible] = useState(false);
// We use React.useState to be able to mock the state in Jest
const [overflowingIndex, setOverflowingIndex] = useState<number>(-1);
let targetRef = useRef<HTMLDivElement>(null);
if (dropdownRef) {
targetRef = dropdownRef;
}
const [showOverflow, setShowOverflow] = useState(false);
// callback to update item widths so that the useLayoutEffect runs whenever
// width of any of the child changes
const recalculateItemWidths = useCallback(() => {
const mainItemsContainerNode = current?.children.item(0);
if (mainItemsContainerNode) {
const visibleChildrenElements = Array.from(
mainItemsContainerNode.children,
);
setItemsWidth(prevGlobalWidths => {
if (prevGlobalWidths.length !== items.length) {
return prevGlobalWidths;
}
const newGlobalWidths = [...prevGlobalWidths];
let changed = false;
visibleChildrenElements.forEach((child, indexInVisible) => {
const originalItemIndex = indexInVisible;
if (originalItemIndex < newGlobalWidths.length) {
const newWidth = child.getBoundingClientRect().width;
if (newGlobalWidths[originalItemIndex] !== newWidth) {
newGlobalWidths[originalItemIndex] = newWidth;
changed = true;
}
}
});
return changed ? newGlobalWidths : prevGlobalWidths;
});
}
}, [current?.children, items.length]);
const reduceItems = (items: DropdownItem[]): [DropdownItem[], string[]] =>
items.reduce(
([items, ids], item) => {
items.push({
id: item.id,
element: cloneElement(item.element, { key: item.id }),
});
ids.push(item.id);
return [items, ids];
},
[[], []] as [DropdownItem[], string[]],
);
const [notOverflowedItems, notOverflowedIds] = useMemo(
() =>
reduceItems(
items.slice(
0,
overflowingIndex !== -1 ? overflowingIndex : items.length,
),
),
[items, overflowingIndex],
);
const [overflowedItems, overflowedIds] = useMemo(
() =>
overflowingIndex !== -1
? reduceItems(items.slice(overflowingIndex))
: [[], []],
[items, overflowingIndex],
);
useEffect(() => {
const container = current?.children.item(0);
if (!container) return;
const childrenArray = Array.from(container.children);
const resizeObserver = new ResizeObserver(() => {
recalculateItemWidths();
});
childrenArray.map(child => resizeObserver.observe(child));
// eslint-disable-next-line consistent-return
return () => {
childrenArray.map(child => resizeObserver.unobserve(child));
resizeObserver.disconnect();
};
}, [items.length, current, recalculateItemWidths]);
useLayoutEffect(() => {
if (popoverVisible) {
return;
}
const container = current?.children.item(0);
if (container) {
const { children } = container;
const childrenArray = Array.from(children);
// If items length change, add all items to the container
// and recalculate the widths
if (itemsWidth.length !== items.length) {
if (childrenArray.length === items.length) {
setItemsWidth(
childrenArray.map(child => child.getBoundingClientRect().width),
);
} else {
setOverflowingIndex(-1);
return;
}
}
// Calculates the index of the first overflowed element
// +1 is to give at least one pixel of difference and avoid flakiness
const index = childrenArray.findIndex(
child =>
child.getBoundingClientRect().right >
container.getBoundingClientRect().right + 1,
);
// If elements fit (-1) and there's overflowed items
// then preserve the overflow index. We can't use overflowIndex
// directly because the items may have been modified
let newOverflowingIndex =
index === -1 && overflowedItems.length > 0
? items.length - overflowedItems.length
: index;
if (width > previousWidth) {
// Calculates remaining space in the container
const button = current?.children.item(1);
const buttonRight = button?.getBoundingClientRect().right || 0;
const containerRight = current?.getBoundingClientRect().right || 0;
const remainingSpace = containerRight - buttonRight;
// Checks if some elements in the dropdown fits in the remaining space
let sum = 0;
for (let i = childrenArray.length; i < items.length; i += 1) {
sum += itemsWidth[i];
if (sum <= remainingSpace) {
newOverflowingIndex = i + 1;
} else {
break;
}
}
}
setOverflowingIndex(newOverflowingIndex);
}
}, [
current,
items.length,
itemsWidth,
overflowedItems.length,
previousWidth,
width,
popoverVisible,
]);
useEffect(() => {
if (onOverflowingStateChange) {
onOverflowingStateChange({
notOverflowed: notOverflowedIds,
overflowed: overflowedIds,
});
}
}, [notOverflowedIds, onOverflowingStateChange, overflowedIds]);
const overflowingCount =
overflowingIndex !== -1 ? items.length - overflowingIndex : 0;
const popoverContent = useMemo(
() =>
dropdownContent || overflowingCount ? (
<div
css={css`
display: flex;
flex-direction: column;
gap: ${theme.sizeUnit * 4}px;
`}
data-test="dropdown-content"
style={dropdownStyle}
ref={targetRef}
>
{dropdownContent
? dropdownContent(overflowedItems)
: overflowedItems.map(item => item.element)}
</div>
) : null,
[
dropdownContent,
overflowingCount,
theme.sizeUnit,
dropdownStyle,
overflowedItems,
],
);
useLayoutEffect(() => {
if (popoverVisible) {
// Measures scroll height after rendering the elements
setTimeout(() => {
if (targetRef.current) {
// We only set overflow when there's enough space to display
// Select's popovers because they are restrained by the overflow property.
setShowOverflow(targetRef.current.scrollHeight > MAX_HEIGHT);
}
}, 100);
}
}, [popoverVisible]);
useImperativeHandle(
outerRef,
() => ({
...(ref.current as HTMLDivElement),
open: () => setPopoverVisible(true),
}),
[ref],
);
// Closes the popover when scrolling on the document
useEffect(() => {
document.onscroll = popoverVisible
? () => setPopoverVisible(false)
: null;
return () => {
document.onscroll = null;
};
}, [popoverVisible]);
return (
<div
ref={ref}
css={css`
display: flex;
align-items: center;
`}
>
<div
css={css`
display: flex;
align-items: center;
gap: ${theme.sizeUnit * 4}px;
margin-right: ${theme.sizeUnit * 4}px;
min-width: 0px;
`}
data-test="container"
style={style}
>
{notOverflowedItems.map(item => item.element)}
</div>
{popoverContent && (
<>
<Global
styles={css`
.ant-popover-inner {
// Some OS versions only show the scroll when hovering.
// These settings will make the scroll always visible.
::-webkit-scrollbar {
-webkit-appearance: none;
width: 14px;
}
::-webkit-scrollbar-thumb {
border-radius: 9px;
background-color: ${theme.colors.grayscale.light1};
border: 3px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-track {
background-color: ${theme.colors.grayscale.light4};
border-left: 1px solid ${theme.colors.grayscale.light2};
}
}
`}
/>
<Popover
styles={{
body: {
maxHeight: `${MAX_HEIGHT}px`,
overflow: showOverflow ? 'auto' : 'visible',
},
}}
content={popoverContent}
trigger="click"
open={popoverVisible}
onOpenChange={visible => setPopoverVisible(visible)}
placement="bottom"
forceRender={forceRender}
>
<Tooltip title={dropdownTriggerTooltip}>
<Button
buttonStyle="secondary"
data-test="dropdown-container-btn"
>
{dropdownTriggerIcon}
{dropdownTriggerText}
<Badge
count={dropdownTriggerCount ?? overflowingCount}
color={
(dropdownTriggerCount ?? overflowingCount) > 0
? theme.colorPrimary
: theme.colors.grayscale.light1
}
showZero
css={css`
margin-left: ${theme.sizeUnit * 2}px;
`}
/>
<Icons.DownOutlined
iconSize="m"
iconColor={theme.colors.grayscale.light1}
css={css`
.anticon {
display: flex;
}
`}
/>
</Button>
</Tooltip>
</Popover>
</>
)}
</div>
);
},
);
export { DropdownContainer } from './DropdownContainer';
export type * from './types';

View File

@@ -17,6 +17,7 @@
* under the License.
*/
import type { CSSProperties, ReactElement, RefObject, ReactNode } from 'react';
import { IconType } from '../Icons';
/**
* Container item.
@@ -69,7 +70,7 @@ export interface DropdownContainerProps {
/**
* Icon of the dropdown trigger.
*/
dropdownTriggerIcon?: ReactElement;
dropdownTriggerIcon?: IconType;
/**
* Text of the dropdown trigger.
*/

View File

@@ -45,7 +45,16 @@ export const DatasetTypeLabel: React.FC<DatasetTypeLabelProps> = ({
const labelType = datasetType === 'physical' ? 'primary' : 'default';
return (
<Label icon={icon} type={labelType}>
<Label
icon={icon}
type={labelType}
style={{
color:
datasetType === 'physical'
? theme.colorPrimaryText
: theme.colorPrimary,
}}
>
{label}
</Label>
);

View File

@@ -1047,6 +1047,119 @@ test('typing and deleting the last character for a new option displays correctly
jest.useRealTimers();
});
describe('grouped options search', () => {
const GROUPED_OPTIONS = [
{
label: 'Male',
options: OPTIONS.filter(option => option.gender === 'Male'),
},
{
label: 'Female',
options: OPTIONS.filter(option => option.gender === 'Female'),
},
];
it('searches within grouped options and shows matching groups', async () => {
render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
await open();
await type('John');
expect(await findSelectOption('John')).toBeInTheDocument();
expect(await findSelectOption('Johnny')).toBeInTheDocument();
expect(screen.queryByText('Female')).not.toBeInTheDocument();
expect(screen.queryByText('Olivia')).not.toBeInTheDocument();
expect(screen.getByText('Male')).toBeInTheDocument();
expect(screen.queryByText('Female')).not.toBeInTheDocument();
});
it('shows multiple groups when search matches both', async () => {
render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
await open();
await type('er');
expect(screen.getByText('Male')).toBeInTheDocument();
expect(screen.getByText('Female')).toBeInTheDocument();
expect(await findSelectOption('Oliver')).toBeInTheDocument();
expect(await findSelectOption('Cher')).toBeInTheDocument();
expect(await findSelectOption('Her')).toBeInTheDocument();
});
it('handles case-insensitive search in grouped options', async () => {
render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
await open();
await type('EMMA');
expect(await findSelectOption('Emma')).toBeInTheDocument();
expect(screen.getByText('Female')).toBeInTheDocument();
expect(screen.queryByText('Male')).not.toBeInTheDocument();
});
it('shows no options when search matches nothing in any group', async () => {
render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
await open();
await type('xyz123');
expect(screen.queryByText('Male')).not.toBeInTheDocument();
expect(screen.queryByText('Female')).not.toBeInTheDocument();
expect(
screen.getByText(NO_DATA, { selector: '.ant-empty-description' }),
).toBeInTheDocument();
});
it('works in multiple selection mode with grouped options', async () => {
render(
<Select {...defaultProps} options={GROUPED_OPTIONS} mode="multiple" />,
);
await open();
await type('John');
await userEvent.click(await findSelectOption('John'));
// Clear search and search for female name
await clearTypedText();
await type('Emma');
await userEvent.click(await findSelectOption('Emma'));
// Both should be selected
const values = await findAllSelectValues();
expect(values).toHaveLength(2);
expect(values[0]).toHaveTextContent('John');
expect(values[1]).toHaveTextContent('Emma');
});
it('preserves group structure when not searching', async () => {
render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
await open();
expect(screen.getByText('Male')).toBeInTheDocument();
expect(screen.getByText('Female')).toBeInTheDocument();
expect(await findSelectOption('John')).toBeInTheDocument();
expect(await findSelectOption('Emma')).toBeInTheDocument();
});
it('handles empty groups gracefully', async () => {
const optionsWithEmptyGroup = [
...GROUPED_OPTIONS,
{
label: 'Empty Group',
options: [],
},
];
render(<Select {...defaultProps} options={optionsWithEmptyGroup} />);
await open();
await type('John');
expect(await findSelectOption('John')).toBeInTheDocument();
expect(screen.queryByText('Empty Group')).not.toBeInTheDocument();
});
});
/*
TODO: Add tests that require scroll interaction. Needs further investigation.
- Fetches more data when scrolling and more data is available

View File

@@ -373,9 +373,27 @@ const Select = forwardRef(
setSelectOptions(updatedOptions);
}
const filteredOptions = updatedOptions.filter(
(option: AntdLabeledValue) => handleFilterOption(search, option),
);
const filteredOptions = updatedOptions
.map((option: any) => {
/*
If it's a group, filter its nested options and only return it
if it has matching options
*/
if ('options' in option && Array.isArray(option.options)) {
const filteredGroupOptions = option.options.filter(
(subOption: AntdLabeledValue) =>
handleFilterOption(search, subOption),
);
return filteredGroupOptions.length > 0
? { ...option, options: filteredGroupOptions }
: null;
}
return handleFilterOption(search, option as AntdLabeledValue)
? option
: null;
})
.filter((option): option is AntdLabeledValue => option !== null);
setVisibleOptions(filteredOptions);
setInputValue(searchValue);

View File

@@ -32,8 +32,9 @@ export const StyledHeader = styled.span<{ headerPosition: string }>`
`;
export const StyledContainer = styled.div<{ headerPosition: string }>`
${({ headerPosition }) => `
${({ headerPosition, theme }) => `
display: flex;
gap: ${theme.sizeUnit}px;
flex-direction: ${headerPosition === 'top' ? 'column' : 'row'};
align-items: ${headerPosition === 'left' ? 'center' : undefined};
width: 100%;

View File

@@ -132,7 +132,7 @@ const VirtualTable = <RecordType extends object>(
if (gridRef.current) {
return gridRef.current?.state?.scrollLeft;
}
return null;
return 0;
},
set: (scrollLeft: number) => {
if (gridRef.current) {

View File

@@ -52,7 +52,9 @@ interface TableCollectionProps<T extends object> {
const StyledTable = styled(Table)`
${({ theme }) => `
th.ant-column-cell {
min-width: fit-content;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.actions {
opacity: 0;
@@ -83,7 +85,6 @@ const StyledTable = styled(Table)`
font-feature-settings: 'tnum' 1;
text-overflow: ellipsis;
overflow: hidden;
max-width: 320px;
line-height: 1;
vertical-align: middle;
padding-left: ${theme.sizeUnit * 4}px;
@@ -149,7 +150,7 @@ function TableCollection<T extends object>({
size={size}
data-test="listview-table"
pagination={false}
tableLayout="auto"
tableLayout="fixed"
rowKey="rowId"
rowSelection={rowSelection}
locale={{ emptyText: null }}

View File

@@ -94,7 +94,7 @@ export function mapColumns<T extends object>(
dataIndex: column.id?.includes('.') ? column.id.split('.') : column.id,
hidden: column.hidden,
key: column.id,
minWidth: column.size ? COLUMN_SIZE_MAP[column.size] : COLUMN_SIZE_MAP.md,
width: column.size ? COLUMN_SIZE_MAP[column.size] : COLUMN_SIZE_MAP.md,
ellipsis: !columnsForWrapText?.includes(column.id),
defaultSortOrder: (isSorted
? isSortedDesc

View File

@@ -0,0 +1,220 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { render, screen } from '@superset-ui/core/spec';
import { AgGridReact } from 'ag-grid-react';
import { createRef } from 'react';
import { ThemeProvider, supersetTheme } from '../../theme';
import { ThemedAgGridReact } from './index';
import * as themeUtils from '../../theme/utils/themeUtils';
// Mock useThemeMode hook
jest.mock('../../theme/utils/themeUtils', () => ({
...jest.requireActual('../../theme/utils/themeUtils'),
useThemeMode: jest.fn(() => false), // Default to light mode
}));
// Mock ag-grid-react to avoid complex setup
jest.mock('ag-grid-react', () => ({
AgGridReact: jest.fn(({ theme, ...props }) => (
<div
data-test="ag-grid-react"
data-theme={JSON.stringify(theme)}
{...props}
>
AgGrid Mock
</div>
)),
}));
// Mock ag-grid-community
jest.mock('ag-grid-community', () => ({
themeQuartz: {
withPart: jest.fn().mockReturnThis(),
withParams: jest.fn(params => ({ ...params, _type: 'theme' })),
},
colorSchemeDark: { _type: 'dark' },
colorSchemeLight: { _type: 'light' },
AllCommunityModule: {},
ClientSideRowModelModule: {},
ModuleRegistry: { registerModules: jest.fn() },
}));
const mockRowData = [
{ id: 1, name: 'Test 1' },
{ id: 2, name: 'Test 2' },
];
const mockColumnDefs = [
{ field: 'id', headerName: 'ID' },
{ field: 'name', headerName: 'Name' },
];
beforeEach(() => {
jest.clearAllMocks();
// Reset to light mode by default
(themeUtils.useThemeMode as jest.Mock).mockReturnValue(false);
});
test('renders the AgGridReact component', () => {
render(
<ThemedAgGridReact rowData={mockRowData} columnDefs={mockColumnDefs} />,
);
expect(screen.getByTestId('ag-grid-react')).toBeInTheDocument();
});
test('applies light theme when background is light', () => {
const lightTheme = {
...supersetTheme,
colorBgBase: '#ffffff',
colorText: '#000000',
};
render(
<ThemeProvider theme={lightTheme}>
<ThemedAgGridReact rowData={mockRowData} columnDefs={mockColumnDefs} />
</ThemeProvider>,
);
const agGrid = screen.getByTestId('ag-grid-react');
const theme = JSON.parse(agGrid.getAttribute('data-theme') || '{}');
expect(theme.browserColorScheme).toBe('light');
expect(theme.foregroundColor).toBe('#000000');
});
test('applies dark theme when background is dark', () => {
// Mock dark mode
(themeUtils.useThemeMode as jest.Mock).mockReturnValue(true);
const darkTheme = {
...supersetTheme,
colorBgBase: '#1a1a1a',
colorText: '#ffffff',
};
render(
<ThemeProvider theme={darkTheme}>
<ThemedAgGridReact rowData={mockRowData} columnDefs={mockColumnDefs} />
</ThemeProvider>,
);
const agGrid = screen.getByTestId('ag-grid-react');
const theme = JSON.parse(agGrid.getAttribute('data-theme') || '{}');
expect(theme.browserColorScheme).toBe('dark');
expect(theme.foregroundColor).toBe('#ffffff');
});
test('forwards ref to AgGridReact', () => {
const ref = createRef<AgGridReact>();
render(
<ThemedAgGridReact
ref={ref}
rowData={mockRowData}
columnDefs={mockColumnDefs}
/>,
);
// Check that AgGridReact was called with the ref
expect(AgGridReact).toHaveBeenCalledWith(
expect.objectContaining({
rowData: mockRowData,
columnDefs: mockColumnDefs,
}),
expect.any(Object), // ref is passed as second argument
);
});
test('passes all props through to AgGridReact', () => {
const onGridReady = jest.fn();
const onCellClicked = jest.fn();
render(
<ThemedAgGridReact
rowData={mockRowData}
columnDefs={mockColumnDefs}
onGridReady={onGridReady}
onCellClicked={onCellClicked}
pagination
paginationPageSize={10}
/>,
);
expect(AgGridReact).toHaveBeenCalledWith(
expect.objectContaining({
rowData: mockRowData,
columnDefs: mockColumnDefs,
onGridReady,
onCellClicked,
pagination: true,
paginationPageSize: 10,
}),
expect.any(Object),
);
});
test('applies custom theme colors from Superset theme', () => {
const customTheme = {
...supersetTheme,
colorFillTertiary: '#e5e5e5',
colorSplit: '#d9d9d9',
};
render(
<ThemeProvider theme={customTheme}>
<ThemedAgGridReact rowData={mockRowData} columnDefs={mockColumnDefs} />
</ThemeProvider>,
);
const agGrid = screen.getByTestId('ag-grid-react');
const theme = JSON.parse(agGrid.getAttribute('data-theme') || '{}');
// Just verify a couple key theme properties are applied
expect(theme.headerBackgroundColor).toBe('#e5e5e5');
expect(theme.borderColor).toBe('#d9d9d9');
});
test('wraps component with proper container div', () => {
const { container } = render(
<ThemedAgGridReact rowData={mockRowData} columnDefs={mockColumnDefs} />,
);
const wrapper = container.querySelector('[data-themed-ag-grid="true"]');
expect(wrapper).toBeInTheDocument();
// Styles are now applied via css prop, not inline styles
expect(wrapper).toHaveAttribute('data-themed-ag-grid', 'true');
});
test('handles missing theme gracefully', () => {
const incompleteTheme = {
...supersetTheme,
colorBgBase: undefined,
};
render(
<ThemeProvider theme={incompleteTheme}>
<ThemedAgGridReact rowData={mockRowData} columnDefs={mockColumnDefs} />
</ThemeProvider>,
);
// Should still render without crashing
expect(screen.getByTestId('ag-grid-react')).toBeInTheDocument();
});

View File

@@ -0,0 +1,190 @@
/**
* 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 { useMemo, forwardRef } from 'react';
import { css } from '@emotion/react';
import { AgGridReact, type AgGridReactProps } from 'ag-grid-react';
import {
themeQuartz,
colorSchemeDark,
colorSchemeLight,
} from 'ag-grid-community';
import { useTheme } from '../../theme';
import { useThemeMode } from '../../theme/utils/themeUtils';
// Note: With ag-grid v34's new theming API, CSS files are injected automatically
// Do NOT import 'ag-grid-community/styles/ag-grid.css' or theme CSS files
export interface ThemedAgGridReactProps extends AgGridReactProps {
/**
* Optional theme parameter overrides to customize specific ag-grid theme values.
* These will be merged with the default Superset theme values.
*
* @example
* ```tsx
* <ThemedAgGridReact
* rowData={data}
* columnDefs={columns}
* themeOverrides={{
* headerBackgroundColor: '#custom-color',
* fontSize: 14,
* }}
* />
* ```
*/
themeOverrides?: Record<string, any>;
}
/**
* ThemedAgGridReact - A wrapper around AgGridReact that applies Superset theming
*
* This component:
* - Preserves the full AgGridReactProps interface for drop-in replacement
* - Applies Superset theme variables via ag-grid's JavaScript theming API
* - Supports automatic dark/light mode switching
* - Allows custom theme parameter overrides
*
* @example
* ```tsx
* <ThemedAgGridReact
* rowData={data}
* columnDefs={columns}
* themeOverrides={{ fontSize: 14 }}
* // ... any other AgGridReactProps
* />
* ```
*/
export const ThemedAgGridReact = forwardRef<
AgGridReact,
ThemedAgGridReactProps
>(function ThemedAgGridReact({ themeOverrides, ...props }, ref) {
const theme = useTheme();
const isDarkMode = useThemeMode();
// Get the appropriate ag-grid theme based on dark/light mode
const agGridTheme = useMemo(() => {
// Use quaternary fill for odd rows
const oddRowBg = theme?.colorFillQuaternary;
const baseTheme = isDarkMode
? themeQuartz.withPart(colorSchemeDark)
: themeQuartz.withPart(colorSchemeLight);
// Use withParams to set colors directly via ag-grid's API
const params = {
// Core colors
backgroundColor: 'transparent',
foregroundColor: theme.colorText,
browserColorScheme: isDarkMode ? 'dark' : 'light',
// Header styling
headerBackgroundColor: theme.colorFillTertiary,
headerTextColor: theme.colorTextHeading,
// Cell and row styling
oddRowBackgroundColor: oddRowBg,
rowHoverColor: theme.colorFillSecondary,
selectedRowBackgroundColor: theme.colorPrimaryBg,
cellTextColor: theme.colorText,
// Borders
borderColor: theme.colorSplit,
columnBorderColor: theme.colorSplit,
// Interactive elements
accentColor: theme.colorPrimary,
rangeSelectionBorderColor: theme.colorPrimary,
rangeSelectionBackgroundColor: theme.colorPrimaryBg,
// Input fields (for filters)
inputBackgroundColor: theme.colorBgContainer,
inputBorderColor: theme.colorSplit,
inputTextColor: theme.colorText,
inputPlaceholderTextColor: theme.colorTextPlaceholder,
// Typography
fontFamily: theme.fontFamily,
fontSize: theme.fontSizeSM,
// Spacing
spacing: theme.sizeUnit,
};
// Only apply params if we have a valid theme
if (!theme || !theme.colorBgBase) {
return baseTheme;
}
// Merge theme overrides if provided
const finalParams = themeOverrides
? { ...params, ...themeOverrides }
: params;
return baseTheme.withParams(finalParams);
}, [theme, isDarkMode, themeOverrides]);
return (
<div
css={css`
width: 100%;
height: 100%;
.ag-cell {
-webkit-font-smoothing: antialiased;
}
`}
data-themed-ag-grid="true"
>
<AgGridReact ref={ref} theme={agGridTheme} {...props} />
</div>
);
});
// Re-export commonly used types for convenience
export type { CustomCellRendererProps } from 'ag-grid-react';
// Re-export commonly used ag-grid-community types
export type {
ColDef,
Column,
GridOptions,
GridState,
GridReadyEvent,
CellClickedEvent,
CellClassParams,
IMenuActionParams,
IHeaderParams,
SortModelItem,
ValueFormatterParams,
ValueGetterParams,
} from 'ag-grid-community';
// Re-export modules and themes commonly used with ThemedAgGridReact
export {
AllCommunityModule,
ClientSideRowModelModule,
ModuleRegistry,
themeQuartz,
colorSchemeDark,
colorSchemeLight,
} from 'ag-grid-community';
// Re-export AgGridReact for ref types
export { AgGridReact } from 'ag-grid-react';
// Export the setup function for AG-Grid modules
export { setupAGGridModules } from './setupAGGridModules';

View File

@@ -38,6 +38,10 @@ import {
CustomFilterModule,
} from 'ag-grid-community';
/**
* Registers the AG-Grid modules required for Superset's table functionality.
* This should be called once during application initialization.
*/
export const setupAGGridModules = () => {
ModuleRegistry.registerModules([
ColumnAutoSizeModule,

View File

@@ -76,6 +76,7 @@ export { CronPicker, type CronError } from './CronPicker';
export * from './DatePicker';
export { DeleteModal, type DeleteModalProps } from './DeleteModal';
export { Divider, type DividerProps } from './Divider';
export { Drawer, type DrawerProps } from './Drawer';
export {
Dropdown,
MenuDotsDropdown,
@@ -165,7 +166,11 @@ export * from './Table';
export * from './TableView';
export * from './Tag';
export * from './TelemetryPixel';
export * from './ThemeSubMenu';
export * from './UnsavedChangesModal';
export * from './constants';
export * from './Result';
export {
ThemedAgGridReact,
type ThemedAgGridReactProps,
setupAGGridModules,
} from './ThemedAgGridReact';

View File

@@ -115,11 +115,15 @@ export default class SupersetClientClass {
return this.getCSRFToken();
}
async postForm(url: string, payload: Record<string, any>, target = '_blank') {
if (url) {
async postForm(
endpoint: string,
payload: Record<string, any>,
target = '_blank',
) {
if (endpoint) {
await this.ensureAuth();
const hiddenForm = document.createElement('form');
hiddenForm.action = url;
hiddenForm.action = this.getUrl({ endpoint });
hiddenForm.method = 'POST';
hiddenForm.target = target;
const payloadWithToken: Record<string, any> = {

View File

@@ -204,6 +204,8 @@ export interface SqlaFormData extends BaseFormData {
export type QueryFormData = SqlaFormData;
export type LatestQueryFormData = Partial<QueryFormData>;
//---------------------------------------------------
// Type guards
//---------------------------------------------------

View File

@@ -431,14 +431,9 @@ export interface ThemeContextType {
}
/**
* Configuration object for complete theme setup including default, dark themes and settings
* Configuration object for complete theme setup including default and dark themes
*/
export interface SupersetThemeConfig {
theme_default: AnyThemeConfig;
theme_dark?: AnyThemeConfig;
theme_settings?: {
enforced?: boolean;
allowSwitching?: boolean;
allowOSPreference?: boolean;
};
}

View File

@@ -17,6 +17,7 @@
* under the License.
*/
import tinycolor from 'tinycolor2';
import { useTheme as useEmotionTheme } from '@emotion/react';
import type { SupersetTheme, FontSizeKey, ColorVariants } from '../types';
const fontSizeMap: Record<FontSizeKey, keyof SupersetTheme> = {
@@ -111,3 +112,12 @@ export function getColorVariants(
export function isThemeDark(theme: SupersetTheme): boolean {
return tinycolor(theme.colorBgContainer).isDark();
}
/**
* Hook to determine if the current theme is dark mode
* @returns true if theme is dark, false if light
*/
export function useThemeMode(): boolean {
const theme = useEmotionTheme() as SupersetTheme;
return isThemeDark(theme);
}

View File

@@ -18,11 +18,9 @@
*/
import rison from 'rison';
import { isEmpty } from 'lodash';
import {
SupersetClient,
getClientErrorObject,
ensureIsArray,
} from '@superset-ui/core';
import { SupersetClient } from '../connection';
import { getClientErrorObject } from '../query';
import { ensureIsArray } from '../utils';
export const SEPARATOR = ' : ';

View File

@@ -23,8 +23,13 @@ import {
ComponentType,
} from 'react';
import type { Editor } from 'brace';
import { BaseFormData } from '../query';
import { JsonResponse } from '../connection';
import type { QueryData } from '../chart/types/QueryResponse';
import type {
BaseFormData,
LatestQueryFormData,
QueryFormData,
} from '../query';
import type { JsonResponse } from '../connection';
/**
* A function which returns text (or marked-up text)
@@ -219,6 +224,19 @@ export interface DateFilterControlProps {
isOverflowingFilterBar?: boolean;
}
export interface ExploreChartHeaderProps {
chartId: number;
queriesResponse: QueryData[] | null;
sliceFormData: QueryFormData | null;
queryFormData: QueryFormData;
lastRendered: number;
latestQueryFormData: LatestQueryFormData;
chartUpdateEndTime: number | null;
chartUpdateStartTime: number;
queryController: AbortController | null;
triggerQuery: boolean;
}
export type Extensions = Partial<{
'alertsreports.header.icon': ComponentType;
'load.drillby.options': LoadDrillByOptions;
@@ -251,4 +269,5 @@ export type Extensions = Partial<{
ComponentType<SQLTablePreviewExtensionProps>,
][];
'filter.dateFilterControl': ComponentType<DateFilterControlProps>;
'explore.chart.header': ComponentType<ExploreChartHeaderProps>;
}>;

View File

@@ -49,6 +49,21 @@ describe('isProbablyHTML', () => {
const trickyText = 'a <= 10 and b > 10';
expect(isProbablyHTML(trickyText)).toBe(false);
});
it('should return false for strings with angle brackets that are not HTML', () => {
// Test case from issue #25561
expect(isProbablyHTML('<abcdef:12345>')).toBe(false);
// Other similar cases
expect(isProbablyHTML('<foo:bar>')).toBe(false);
expect(isProbablyHTML('<123>')).toBe(false);
expect(isProbablyHTML('<test@example.com>')).toBe(false);
expect(isProbablyHTML('<custom-element>')).toBe(false);
// Mathematical expressions
expect(isProbablyHTML('if x < 5 and y > 10')).toBe(false);
expect(isProbablyHTML('price < $100')).toBe(false);
});
});
describe('sanitizeHtmlIfNeeded', () => {

View File

@@ -68,9 +68,87 @@ export function isProbablyHTML(text: string) {
return true;
}
// Check if the string contains common HTML patterns
if (!hasHtmlTagPattern(text)) {
return false;
}
const parser = new DOMParser();
const doc = parser.parseFromString(cleanedStr, 'text/html');
return Array.from(doc.body.childNodes).some(({ nodeType }) => nodeType === 1);
// Check if parsing created actual HTML elements (not just text nodes)
const elements = Array.from(doc.body.childNodes).filter(
node => node.nodeType === 1,
) as Element[];
// If no elements were created, it's not HTML
if (elements.length === 0) {
return false;
}
// Check if the elements are known HTML tags (not custom/unknown tags)
// This prevents strings like "<abcdef:12345>" from being treated as HTML
return elements.some(element => {
const tagName = element.tagName.toLowerCase();
// List of common HTML tags we want to recognize
const knownHtmlTags = [
'div',
'span',
'p',
'a',
'b',
'i',
'u',
'em',
'strong',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'table',
'tr',
'td',
'th',
'tbody',
'thead',
'tfoot',
'ul',
'ol',
'li',
'img',
'br',
'hr',
'pre',
'code',
'blockquote',
'section',
'article',
'nav',
'header',
'footer',
'form',
'input',
'button',
'select',
'option',
'textarea',
'label',
'fieldset',
'legend',
'video',
'audio',
'canvas',
'iframe',
'script',
'style',
'link',
'meta',
'title',
];
return knownHtmlTags.includes(tagName);
});
}
export function sanitizeHtmlIfNeeded(htmlString: string) {

View File

@@ -658,26 +658,36 @@ describe('SupersetClientClass', () => {
jest.restoreAllMocks();
});
it('makes postForm request', async () => {
await client.postForm(mockPostFormUrl, {});
it.each(['', '/prefix'])(
"makes postForm request when appRoot is '%s'",
async appRoot => {
if (appRoot !== '') {
client = new SupersetClientClass({ protocol, host, appRoot });
authSpy = jest.spyOn(SupersetClientClass.prototype, 'ensureAuth');
await client.init();
}
await client.postForm(mockPostFormEndpoint, {});
const hiddenForm = createElement.mock.results[0].value;
const csrfTokenInput = createElement.mock.results[1].value;
const hiddenForm = createElement.mock.results[0].value;
const csrfTokenInput = createElement.mock.results[1].value;
expect(createElement.mock.calls).toHaveLength(2);
expect(createElement.mock.calls).toHaveLength(2);
expect(hiddenForm.action).toBe(mockPostFormUrl);
expect(hiddenForm.method).toBe('POST');
expect(hiddenForm.target).toBe('_blank');
expect(hiddenForm.action).toBe(
`${protocol}//${host}${appRoot}${mockPostFormEndpoint}`,
);
expect(hiddenForm.method).toBe('POST');
expect(hiddenForm.target).toBe('_blank');
expect(csrfTokenInput.type).toBe('hidden');
expect(csrfTokenInput.name).toBe('csrf_token');
expect(csrfTokenInput.value).toBe(1234);
expect(csrfTokenInput.type).toBe('hidden');
expect(csrfTokenInput.name).toBe('csrf_token');
expect(csrfTokenInput.value).toBe(1234);
expect(appendChild.mock.calls).toHaveLength(1);
expect(removeChild.mock.calls).toHaveLength(1);
expect(authSpy).toHaveBeenCalledTimes(1);
});
expect(appendChild.mock.calls).toHaveLength(1);
expect(removeChild.mock.calls).toHaveLength(1);
expect(authSpy).toHaveBeenCalledTimes(1);
},
);
it('makes postForm request with guest token', async () => {
client = new SupersetClientClass({ protocol, host, guestToken });

View File

@@ -43,7 +43,7 @@
"@storybook/types": "8.4.7",
"@types/react-loadable": "^5.5.11",
"core-js": "3.40.0",
"gh-pages": "^6.2.0",
"gh-pages": "^6.3.0",
"jquery": "^3.7.1",
"memoize-one": "^5.2.1",
"react": "^17.0.2",
@@ -54,7 +54,7 @@
"devDependencies": {
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.27.2",
"@babel/preset-react": "^7.26.3",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.23.3",
"@storybook/react-webpack5": "8.2.9",
"babel-loader": "^10.0.0",

View File

@@ -31,7 +31,7 @@
"dependencies": {
"d3": "^3.5.17",
"prop-types": "^15.8.1",
"react": "^19.1.0"
"react": "^19.1.1"
},
"peerDependencies": {
"@superset-ui/chart-controls": "*",

View File

@@ -24,11 +24,11 @@
"lib"
],
"dependencies": {
"@deck.gl/aggregation-layers": "^9.1.13",
"@deck.gl/core": "^9.1.13",
"@deck.gl/aggregation-layers": "^9.1.14",
"@deck.gl/core": "^9.1.14",
"@deck.gl/geo-layers": "^9.1.13",
"@deck.gl/layers": "^9.1.13",
"@deck.gl/react": "^9.1.13",
"@deck.gl/react": "^9.1.14",
"@luma.gl/constants": "^9.1.9",
"@luma.gl/core": "^9.1.9",
"@luma.gl/engine": "^9.1.9",

View File

@@ -89,6 +89,7 @@ export type CategoricalDeckGLContainerProps = {
width: number;
viewport: Viewport;
getLayer: GetLayerType<unknown>;
getHighlightLayer?: GetLayerType<unknown>;
payload: JsonObject;
onAddFilter?: HandlerFunction;
setControlValue: (control: string, value: JsonValue) => void;
@@ -213,6 +214,7 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
const getLayers = useCallback(() => {
const {
getLayer,
getHighlightLayer,
payload,
formData: fd,
onAddFilter,
@@ -244,19 +246,27 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
data: { ...payload.data, features },
};
return [
getLayer({
formData: fd,
payload: filteredPayload,
onAddFilter,
setTooltip,
datasource: props.datasource,
onContextMenu,
filterState,
setDataMask,
emitCrossFilters,
}) as Layer,
];
const layerProps = {
formData: fd,
payload: filteredPayload,
onAddFilter,
setTooltip,
datasource: props.datasource,
onContextMenu,
filterState,
setDataMask,
emitCrossFilters,
};
const layer = getLayer(layerProps) as Layer;
if (emitCrossFilters && filterState?.value && getHighlightLayer) {
const highlightLayer = getHighlightLayer(layerProps) as Layer;
return [layer, highlightLayer];
}
return [layer];
}, [addColor, categories, props, setTooltip]);
const toggleCategory = useCallback(

View File

@@ -23,8 +23,11 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { isEqual } from 'lodash';
import {
AdhocFilter,
ContextMenuFilters,
DataMask,
Datasource,
ensureIsArray,
FilterState,
HandlerFunction,
isDefined,
JsonObject,
@@ -65,7 +68,15 @@ export type DeckMultiProps = {
height: number;
width: number;
datasource: Datasource;
setDataMask?: (dataMask: DataMask) => void;
onContextMenu?: (
clientX: number,
clientY: number,
filters?: ContextMenuFilters,
) => void;
onSelect: () => void;
filterState?: FilterState;
emitCrossFilters?: boolean;
};
const DeckMulti = (props: DeckMultiProps) => {
@@ -175,16 +186,14 @@ const DeckMulti = (props: DeckMultiProps) => {
const createLayerFromData = useCallback(
(subslice: JsonObject, json: JsonObject): Layer =>
// @ts-ignore TODO(hainenber): define proper type for `form_data.viz_type` and call signature for functions in layerGenerators.
layerGenerators[subslice.form_data.viz_type](
subslice.form_data,
json,
props.onAddFilter,
layerGenerators[subslice.form_data.viz_type]({
formData: subslice.form_data,
payload: json,
setTooltip,
props.datasource,
[],
props.onSelect,
),
[props.onAddFilter, props.onSelect, props.datasource, setTooltip],
datasource: props.datasource,
onSelect: props.onSelect,
}),
[props.onSelect, props.datasource, setTooltip],
);
const loadSingleLayer = useCallback(

View File

@@ -87,6 +87,7 @@ interface GetPointsType {
export function createDeckGLComponent(
getLayer: GetLayerType<unknown>,
getPoints: GetPointsType,
getHighlightLayer?: GetLayerType<unknown>,
) {
// Higher order component
return memo((props: DeckGLComponentProps) => {
@@ -118,7 +119,7 @@ export function createDeckGLComponent(
}
}, []);
const computeLayer = useCallback(
const computeLayers = useCallback(
(props: DeckGLComponentProps) => {
const {
formData,
@@ -130,7 +131,7 @@ export function createDeckGLComponent(
emitCrossFilters,
} = props;
return getLayer({
const layerProps = {
formData,
payload,
onAddFilter,
@@ -139,7 +140,17 @@ export function createDeckGLComponent(
onContextMenu,
filterState,
emitCrossFilters,
}) as Layer;
};
const layer = getLayer(layerProps) as Layer;
if (emitCrossFilters && filterState?.value && getHighlightLayer) {
const highlightLayer = getHighlightLayer(layerProps) as Layer;
return [layer, highlightLayer];
}
return [layer];
},
[setTooltip],
);
@@ -152,7 +163,7 @@ export function createDeckGLComponent(
setCategories(categories);
}, [props]);
const [layer, setLayer] = useState(computeLayer(props));
const [layers, setLayers] = useState(computeLayers(props));
useEffect(() => {
// Only recompute the layer if anything BUT the viewport has changed
@@ -167,9 +178,9 @@ export function createDeckGLComponent(
viewport: null,
};
if (!isEqual(prevFdNoVP, currFdNoVP) || prevPayload !== props.payload) {
setLayer(computeLayer(props));
setLayers(computeLayers(props));
}
}, [computeLayer, prevFormData, prevFilterState, prevPayload, props]);
}, [computeLayers, prevFormData, prevFilterState, prevPayload, props]);
const { formData, payload, setControlValue, height, width } = props;
@@ -179,7 +190,7 @@ export function createDeckGLComponent(
ref={containerRef}
mapboxApiAccessToken={payload.data.mapboxApiKey}
viewport={viewport}
layers={[layer]}
layers={layers}
mapStyle={formData.mapbox_style}
setControlValue={setControlValue}
width={width}
@@ -200,6 +211,7 @@ export function createDeckGLComponent(
export function createCategoricalDeckGLComponent(
getLayer: GetLayerType<Layer>,
getPoints: GetPointsType,
getHighlightLayer?: GetLayerType<Layer>,
) {
return function Component(props: DeckGLComponentProps) {
const {
@@ -224,6 +236,7 @@ export function createCategoricalDeckGLComponent(
setControlValue={setControlValue}
viewport={viewport}
getLayer={getLayer}
getHighlightLayer={getHighlightLayer}
payload={payload}
getPoints={getPoints}
width={width}

View File

@@ -23,6 +23,7 @@ import { commonLayerProps } from '../common';
import { GetLayerType, createCategoricalDeckGLComponent } from '../../factory';
import TooltipRow from '../../TooltipRow';
import { Point } from '../../types';
import { HIGHLIGHT_COLOR_ARRAY, TRANSPARENT_COLOR_ARRAY } from '../../utils';
export function getPoints(data: JsonObject[]) {
const points: Point[] = [];
@@ -73,7 +74,7 @@ export const getLayer: GetLayerType<ArcLayer> = function ({
return new ArcLayer({
data,
getSourceColor: (d: any) => {
getSourceColor: (d: JsonObject) => {
if (colorSchemeType === COLOR_SCHEME_TYPES.fixed_color) {
return [sc.r, sc.g, sc.b, 255 * sc.a];
}
@@ -98,7 +99,50 @@ export const getLayer: GetLayerType<ArcLayer> = function ({
filterState,
emitCrossFilters,
}),
opacity: filterState?.value ? 0.1 : 1,
});
};
export default createCategoricalDeckGLComponent(getLayer, getPoints);
export const getHighlightLayer: GetLayerType<ArcLayer> = function ({
formData,
payload,
filterState,
}) {
const fd = formData;
const data = payload.data.features;
const getColor = (d: {
sourcePosition: [number, number];
targetPosition: [number, number];
}) => {
const sourcePosition = filterState?.value[0];
const targetPosition = filterState?.value[1];
if (
sourcePosition &&
targetPosition &&
d.sourcePosition[0] === sourcePosition[0] &&
d.sourcePosition[1] === sourcePosition[1] &&
d.targetPosition[0] === targetPosition[0] &&
d.targetPosition[1] === targetPosition[1]
) {
return HIGHLIGHT_COLOR_ARRAY;
}
return TRANSPARENT_COLOR_ARRAY;
};
return new ArcLayer({
data,
getSourceColor: getColor,
getTargetColor: getColor,
id: `path-hihglight-layer-${fd.slice_id}` as const,
getWidth: fd.stroke_width ? fd.stroke_width : 3,
});
};
export default createCategoricalDeckGLComponent(
getLayer,
getPoints,
getHighlightLayer,
);

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