Compare commits

..

80 Commits

Author SHA1 Message Date
Maxime Beauchemin
e9965abdb9 feat(EmojiTextArea): add Slack-like emoji autocomplete component
Introduces a new EmojiTextArea component with Slack-like emoji autocomplete
behavior:

- Triggers on `:` prefix with 2+ character minimum (configurable)
- Smart trigger detection: colon must be preceded by whitespace, start of
  text, or another emoji (prevents false positives like URLs)
- Prevents accidental Enter key selection when typing quickly
- Includes 400+ curated emojis with shortcodes and keyword search
- Fully typed with TypeScript, includes tests and Storybook stories

Usage:
```tsx
<EmojiTextArea
  placeholder="Type 😄 to add emojis..."
  onChange={(text) => console.log(text)}
  minCharsBeforePopup={2}
/>
```

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 21:34:02 +00:00
dependabot[bot]
45a42396ab chore(deps-dev): update jest requirement from ^30.0.5 to ^30.2.0 in /superset-frontend/packages/generator-superset (#35389)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 12:33:49 -08:00
Mohammad Al-Qasem
4479614754 feat(table): Gradient Toggle (#36280)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-04 12:31:44 -08:00
Evan Rusackas
e1a8886d32 chore(deps): Remove unused direct dependency geostyler-qgis-parser (#36413)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-04 12:31:21 -08:00
dependabot[bot]
23b61b080e chore(deps): bump actions/checkout from 5 to 6 (#36219)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 12:02:43 -08:00
dependabot[bot]
2f14c6cd69 chore(deps): bump jws from 3.2.2 to 3.2.3 in /superset-websocket/utils/client-ws-app (#36426)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 12:01:24 -08:00
dependabot[bot]
964c16f1a4 chore(deps): bump jws from 4.0.0 to 4.0.1 in /superset-frontend (#36427)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 12:01:03 -08:00
dependabot[bot]
b6f1b4db2f chore(deps): bump jws from 3.2.2 to 3.2.3 in /superset-websocket (#36428)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 12:00:44 -08:00
Beto Dealmeida
482c674a0f chore: improve types (#36367) 2025-12-04 13:51:35 -05:00
Beto Dealmeida
16e6452b8c feat: Explorable protocol (#36245) 2025-12-04 13:18:34 -05:00
Elizabeth Thompson
c36ac53445 fix(reports): simplify logging to focus on timing metrics (#36227)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vitor Avila <vitorfragadeavila@gmail.com>
2025-12-04 10:00:17 -08:00
Edison Liem
eabb5bdf7d feat(dashboard): implement boolean conditional formatting (#36338)
Co-authored-by: Morris <morrisho215215@gmail.com>
2025-12-04 09:53:49 -08:00
dependabot[bot]
e5da6d3183 chore(deps-dev): bump prettier from 3.7.3 to 3.7.4 in /docs (#36394)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 09:52:57 -08:00
Sam Firke
3345eb32c5 fix(heatmap): y-axis sorts in order (#36302)
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-04 09:52:10 -08:00
Gabriel Torres Ruiz
1d8d30e5bb fix(echarts): pass vizType to enable theme overrides in all chart types (#36389) 2025-12-04 09:51:40 -08:00
Antonio Rivero
4a249a0745 fix(dashboards): Use same decorators as FAB (#36423) 2025-12-04 16:32:10 +01:00
Declan Zhao
d121cfdbda feat(prune_logs): add optional max_rows_per_run param (#36313) 2025-12-04 10:15:10 -03:00
dependabot[bot]
3eec441abe chore(deps-dev): bump prettier from 3.6.2 to 3.7.4 in /superset-websocket (#36391)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-03 15:27:53 -08:00
dependabot[bot]
22c061c06c chore(deps-dev): bump typescript-eslint from 8.48.0 to 8.48.1 in /superset-websocket (#36395)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-03 15:26:34 -08:00
dependabot[bot]
7f85d92b85 chore(deps-dev): bump typescript-eslint from 8.48.0 to 8.48.1 in /docs (#36399)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-03 15:12:57 -08:00
dependabot[bot]
dc98a3b397 chore(deps): bump caniuse-lite from 1.0.30001757 to 1.0.30001759 in /docs (#36397)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-03 15:12:38 -08:00
Beto Dealmeida
c458f99dd4 chore: cleanup ssh tunnel (#34388) 2025-12-03 14:26:35 -05:00
dependabot[bot]
70aec7fa76 chore(deps): bump express from 4.21.2 to 4.22.0 in /superset-frontend (#36361)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-03 10:30:43 -08:00
Amin Ghadersohi
5c91ab91fb feat(mcp): add tool tags for Tool Search optimization (#36405) 2025-12-03 18:07:22 +01:00
Felipe López
62d86aba14 fix(SQLLab): most recent queries at the top in Query History without refreshing the page (#36359) 2025-12-03 16:48:34 +01:00
Mehmet Salih Yavuz
b40467c7e2 fix(SaveModal): reset chart state when saving and going to a dashboard (#36402) 2025-12-03 17:30:08 +02:00
dependabot[bot]
f955f0d133 chore(deps): bump express from 5.1.0 to 5.2.1 in /superset-websocket/utils/client-ws-app (#36373)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 15:21:58 -08:00
dependabot[bot]
a8111de3ef chore(deps-dev): bump ts-jest from 29.4.5 to 29.4.6 in /superset-websocket (#36372)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 15:02:04 -08:00
dependabot[bot]
9800fb7702 chore(deps): bump hot-shots from 11.2.0 to 11.3.0 in /superset-websocket (#36352)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 15:00:52 -08:00
dependabot[bot]
17027ff5ca chore(deps): bump express from 5.1.0 to 5.2.0 in /superset-websocket/utils/client-ws-app (#36366)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 15:00:25 -08:00
dependabot[bot]
9c963b50e6 chore(deps): bump express from 4.21.2 to 4.22.0 in /docs (#36362)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 14:55:09 -08:00
Joe Li
bf5de3cb50 fix(DatasourceControl): eliminate test flakiness and async race conditions (#35993)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 13:57:06 -08:00
Amin Ghadersohi
2c6bed27aa refactor(mcp): use dynamic APP_NAME instead of hardcoded Superset branding (#36371) 2025-12-02 11:33:10 -08:00
Beto Dealmeida
d05ab91d11 fix: is_column_reference check (#36382) 2025-12-02 14:16:48 -05:00
Viktor Kolev
7748b60ff5 chore(docs): Fix typo in FEATURE_FLAGS.md (#36364) 2025-12-02 10:59:57 -08:00
Beto Dealmeida
e4cb84bc02 feat: DB2 dialect for sqlglot (#36365) 2025-12-02 12:19:52 -05:00
Michael S. Molina
005e4e3ea8 chore: Implement additional SQL Lab core APIs (#36331) 2025-12-02 09:04:59 -05:00
Michael S. Molina
5e3ff0787b docs(extensions): Add community extensions registry page (#36312) 2025-12-02 09:03:04 -05:00
Enzo Martellucci
51798edb23 feat(alert-report-modal): Use extensions registry for DateFilterControl in AlertReportModal (#36376) 2025-12-02 13:03:15 +01:00
Kaito Watanabe
6e0960c3f5 feat: show search filtered total (#36083) 2025-12-01 22:29:23 +01:00
Michael S. Molina
db995ad5bf chore: Adds non-interactive mode to superset-extensions init command (#36308) 2025-12-01 15:45:49 -05:00
Michael S. Molina
b12f5f8394 fix: CI failures caused by a ruff version mismatch (#36358) 2025-12-01 15:35:22 -05:00
dependabot[bot]
92986c2ecc chore(deps): bump node-forge from 1.3.1 to 1.3.2 in /superset-frontend (#36299)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 11:07:25 -08:00
Phin Jensen
d4206de8e0 docs: fix formatting of BaseCache import statement in docs (#36278) 2025-12-01 11:04:32 -08:00
dependabot[bot]
4935938bb1 chore(deps-dev): bump prettier from 3.6.2 to 3.7.3 in /docs (#36353)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 11:02:46 -08:00
dependabot[bot]
2b6b4e363b chore(deps): bump cookie from 1.1.0 to 1.1.1 in /superset-websocket (#36303)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 10:34:00 -08:00
dependabot[bot]
de69377b04 chore(deps): bump node-forge from 1.3.1 to 1.3.2 in /docs (#36300)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 10:31:59 -08:00
Michael S. Molina
fd7ce4976a docs: Improve the Quick Start to make it more AI-friendly (#36307) 2025-12-01 12:10:08 -05:00
Beto Dealmeida
775d1ba061 fix: normalize totals cache keys for async hits (#36274) 2025-12-01 11:11:10 -05:00
Vitor Avila
9fc7a83320 fix: Do no aggregate results for CSV downloads from AG Grid raw records table (#36247) 2025-12-01 12:52:41 -03:00
Mehmet Salih Yavuz
a754258fad fix(timeshift): Add a more reliable strategy for correct temporal col (#36309)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 17:24:05 +03:00
Gabriel Torres Ruiz
a745fd49fa fix(deckgl): use DatasourceType enum in polygon transformProps test + some TS issues (#36336) 2025-11-30 18:56:07 +01:00
om pharate
01f032017f feat(controlPanel): add integer validation for rows per page setting (#36289) 2025-11-28 12:29:17 -08:00
OrhanBC
d5c5dbb3bf refactor(word-cloud): convert rotation and color controls to React components (POC) (#36275)
Co-authored-by: BrandanBurgess <brandanbb13@gmail.com>
2025-11-28 12:28:31 -08:00
PolinaFam
c9a7a85159 feat(chart): add axes settings for trendline (#36002) 2025-11-28 12:22:57 -08:00
Damian Pendrak
de7f41a888 fix(deckgl): polygon elevation fixed value (#35266) 2025-11-28 12:22:38 -08:00
Mehmet Salih Yavuz
a0e63faf62 fix: add a fallback to chart state callback (#36327) 2025-11-28 20:44:41 +03:00
Alexandru Soare
341ae994c5 feat(embed): get charts payload (#36237)
Co-authored-by: Vitor Avila <vitorfragadeavila@gmail.com>
Co-authored-by: Mehmet Salih Yavuz <salih.yavuz@proton.me>
2025-11-28 17:26:30 +03:00
dependabot[bot]
81e561bdc9 chore(deps): bump swagger-ui-react from 5.30.2 to 5.30.3 in /docs (#36284)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 15:16:27 -08:00
dependabot[bot]
170d1b92eb chore(deps): bump cookie from 1.0.2 to 1.1.0 in /superset-websocket (#36283)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 15:16:08 -08:00
dependabot[bot]
2db0107d12 chore(deps): bump caniuse-lite from 1.0.30001756 to 1.0.30001757 in /docs (#36256)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 14:59:21 -08:00
dependabot[bot]
df0211fe29 chore(deps-dev): bump typescript-eslint from 8.47.0 to 8.48.0 in /docs (#36254)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 14:58:47 -08:00
dependabot[bot]
1bf1890084 chore(deps-dev): bump typescript-eslint from 8.46.2 to 8.48.0 in /superset-websocket (#36252)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 14:58:00 -08:00
Amin Ghadersohi
2af5a5adb0 fix(mcp): access wrapped function in dataset tool tests (#36295) 2025-11-26 22:49:42 +01:00
Michael S. Molina
fe21485065 docs: Reorganizes the extensions documentation (#36298)
Co-authored-by: Evan Rusackas <evan@preset.io>
2025-11-26 16:48:36 -05:00
Amin Ghadersohi
18ab5382b1 feat(mcp): implement selective column serialization for list tools (#36035) 2025-11-25 17:31:51 -08:00
Amin Ghadersohi
f98939103b fix(mcp-service): improve MCP tool parameter clarity and validation (#36137)
Co-authored-by: Rafael Benitez <rafael.benitez@contractors.food52.com>
2025-11-25 16:34:43 -08:00
Amin Ghadersohi
ab36bd3965 build: Add pytest-asyncio to enable async MCP service tests (#36251) 2025-11-25 15:47:48 -08:00
Yousuf Ansari
fb2a8ac9a2 docs: clarify duplicate report deliveries for alerts & reports (#36264)
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-25 14:44:51 -08:00
Amin Ghadersohi
06a8f4df02 feat(datasets): add datetime format detection to dataset columns (#36150)
Co-authored-by: Claude Code <claude@anthropic.com>
2025-11-25 14:25:24 -08:00
Amin Ghadersohi
cf88551a56 fix(mcp): Allow MCP tools to accept string or object request formats (#36271) 2025-11-25 14:46:44 -05:00
Beto Dealmeida
aca18fff99 fix: double computation of contribution_totals (#36226) 2025-11-25 14:35:55 -05:00
Ville Brofeldt
a4860075d2 feat: add mcp abstractions to core (#36151) 2025-11-25 08:23:59 -10:00
Daniel Vaz Gaspar
bae716fa83 fix(log): remove unwanted info from logs REST API (#36269) 2025-11-25 18:10:29 +00:00
Beto Dealmeida
0c87034b17 fix: cache key generation (#36225) 2025-11-25 12:50:00 -05:00
Amin Ghadersohi
8d5d71199a feat(mcp): Add flexible input parsing to handle double-serialized requests (#36249) 2025-11-25 18:21:04 +01:00
Daniel Vaz Gaspar
cd36845d56 fix: remove unwanted info from tags REST API (#36266) 2025-11-25 16:22:31 +00:00
Antonio Rivero
c966dd4f9e feat(dashboards): Add config to filter implicit tags in list API (#36246) 2025-11-25 11:57:53 +01:00
Enzo Martellucci
062e4a2922 fix: Columns bleeding into other cells (#36134)
Co-authored-by: Diego Pucci <diegopucci.me@gmail.com>
Co-authored-by: Geidō <60598000+geido@users.noreply.github.com>
2025-11-25 11:09:13 +02:00
amaannawab923
186693b840 feat(ag-grid): add SQLGlot-based SQL escaping for where and having filter clauses (#36136) 2025-11-25 09:35:40 +02:00
309 changed files with 13636 additions and 7957 deletions

1
.github/CODEOWNERS vendored
View File

@@ -33,6 +33,7 @@
# Notify PMC members of changes to extension-related files
/docs/developer_portal/extensions/ @michael-s-molina @villebro @rusackas
/superset-core/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje
/superset-extensions-cli/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje
/superset/core/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje

View File

@@ -32,7 +32,7 @@ jobs:
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: true
ref: master

View File

@@ -31,7 +31,7 @@ jobs:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
if: steps.check_queued.outputs.count >= 20
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Cancel duplicate workflow runs
if: steps.check_queued.outputs.count >= 20

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View File

@@ -25,7 +25,7 @@ jobs:
pull-requests: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Check and notify
uses: actions/github-script@v8
with:

View File

@@ -71,7 +71,7 @@ jobs:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 1

View File

@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Check for file changes
id: check

View File

@@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout Repository"
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: "Dependency Review"
uses: actions/dependency-review-action@v4
continue-on-error: true
@@ -53,7 +53,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: "Checkout Repository"
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Python
uses: ./.github/actions/setup-backend/

View File

@@ -42,7 +42,7 @@ jobs:
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
@@ -117,7 +117,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Check for file changes

View File

@@ -28,7 +28,7 @@ jobs:
run:
working-directory: superset-embedded-sdk
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-node@v5
with:
node-version-file: './superset-embedded-sdk/.nvmrc'

View File

@@ -18,7 +18,7 @@ jobs:
run:
working-directory: superset-embedded-sdk
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-node@v5
with:
node-version-file: './superset-embedded-sdk/.nvmrc'

View File

@@ -160,7 +160,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ needs.ephemeral-env-label.outputs.sha }} : ${{steps.get-sha.outputs.sha}} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ needs.ephemeral-env-label.outputs.sha }}
persist-credentials: false
@@ -220,7 +220,7 @@ jobs:
pull-requests: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false

View File

@@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v5

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View File

@@ -16,7 +16,7 @@ jobs:
pull-requests: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View File

@@ -21,7 +21,7 @@ jobs:
python-version: ["current", "previous", "next"]
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
@@ -71,7 +71,9 @@ jobs:
GIT_DIFF_EXIT_CODE=$?
if [ "${PRE_COMMIT_EXIT_CODE}" -ne 0 ] || [ "${GIT_DIFF_EXIT_CODE}" -ne 0 ]; then
if [ "${PRE_COMMIT_EXIT_CODE}" -ne 0 ]; then
echo "❌ Pre-commit check failed (exit code: ${EXIT_CODE})."
echo "❌ Pre-commit check failed (exit code: ${PRE_COMMIT_EXIT_CODE})."
echo "🔍 Modified files:"
git diff --name-only
else
echo "❌ Git working directory is dirty."
echo "📌 This likely means that pre-commit made changes that were not committed."

View File

@@ -27,7 +27,7 @@ jobs:
pull-requests: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View File

@@ -26,7 +26,7 @@ jobs:
name: Bump version and publish package(s)
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
# pulls all commits (needed for lerna / semantic release to correctly version)
fetch-depth: 0

View File

@@ -147,7 +147,7 @@ jobs:
- name: Checkout PR code (only if build needed)
if: steps.auth.outputs.authorized == 'true' && steps.check.outputs.build_needed == 'true'
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ steps.check.outputs.target_sha }}
persist-credentials: false

View File

@@ -37,7 +37,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View File

@@ -51,7 +51,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View File

@@ -30,7 +30,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View File

@@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View File

@@ -18,7 +18,7 @@ jobs:
name: Link Checking
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
# Do not bump this linkinator-action version without opening
# an ASF Infra ticket to allow the new version first!
- uses: JustinBeckwith/linkinator-action@3d5ba091319fa7b0ac14703761eebb7d100e6f6d # v1.11.0
@@ -58,7 +58,7 @@ jobs:
working-directory: docs
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View File

@@ -69,21 +69,21 @@ jobs:
# Conditional checkout based on context
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
submodules: recursive
- name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
@@ -186,21 +186,21 @@ jobs:
# Conditional checkout based on context (same as Cypress workflow)
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
submodules: recursive
- name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge

View File

@@ -24,7 +24,7 @@ jobs:
working-directory: superset-extensions-cli
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View File

@@ -23,7 +23,7 @@ jobs:
should-run: ${{ steps.check.outputs.frontend }}
steps:
- name: Checkout Code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View File

@@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref_name }}
persist-credentials: true

View File

@@ -60,21 +60,21 @@ jobs:
# Conditional checkout based on context (same as Cypress workflow)
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
submodules: recursive
- name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge

View File

@@ -41,7 +41,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
@@ -99,7 +99,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
@@ -152,7 +152,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View File

@@ -48,7 +48,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
@@ -108,7 +108,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View File

@@ -24,7 +24,7 @@ jobs:
PYTHONPATH: ${{ github.workspace }}
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
@@ -49,7 +49,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install dependencies

View File

@@ -38,7 +38,7 @@ jobs:
});
- name: "Checkout ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false

View File

@@ -47,7 +47,7 @@ jobs:
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -107,7 +107,7 @@ jobs:
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0

View File

@@ -27,7 +27,7 @@ jobs:
name: Generate Reports
steps:
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v5

1
.gitignore vendored
View File

@@ -138,3 +138,4 @@ PROJECT.md
.claude_rc*
.env.local
oxc-custom-build/
*.code-workspace

View File

@@ -106,12 +106,19 @@ repos:
files: helm
verbose: false
args: ["--log-level", "error"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.7
# Using local hooks ensures ruff version matches requirements/development.txt
- repo: local
hooks:
- id: ruff-format
name: ruff-format
entry: ruff format
language: system
types: [python]
- id: ruff
args: [--fix]
name: ruff
entry: ruff check --fix --show-fixes
language: system
types: [python]
- repo: local
hooks:
- id: pylint

View File

@@ -68,7 +68,7 @@ These features flags are **safe for production**. They have been tested and will
### Flags retained for runtime configuration
Currently some of our feature flags act as dynamic configurations that can changed
Currently some of our feature flags act as dynamic configurations that can change
on the fly. This acts in contradiction with the typical ephemeral feature flag use case,
where the flag is used to mature a feature, and eventually deprecated once the feature is
solid. Eventually we'll likely refactor these under a more formal "dynamic configurations" managed

View File

@@ -67,7 +67,6 @@ x-superset-volumes: &superset-volumes
- ./superset-frontend:/app/superset-frontend
- superset_home_light:/app/superset_home
- ./tests:/app/tests
- ./extensions:/app/extensions
x-common-build: &common-build
context: .
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`

View File

@@ -105,15 +105,7 @@ class CeleryConfig:
CELERY_CONFIG = CeleryConfig
# Extensions configuration
# For local development, point to the extensions directory
# Note: If running in Docker, this path needs to be accessible from inside the container
EXTENSIONS_PATH = os.getenv("EXTENSIONS_PATH", "/app/extensions")
FEATURE_FLAGS = {
"ALERT_REPORTS": True,
"ENABLE_EXTENSIONS": True,
}
FEATURE_FLAGS = {"ALERT_REPORTS": True}
ALERT_REPORTS_NOTIFICATION_DRY_RUN = True
WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501
# The base URL for the email report hyperlinks.

View File

@@ -19,6 +19,7 @@
# Import all settings from the main config first
from flask_caching.backends.filesystemcache import FileSystemCache
from superset_config import * # noqa: F403
# Override caching to use simple in-memory cache instead of Redis

View File

@@ -248,6 +248,6 @@ This architecture provides several key benefits:
Now that you understand the architecture, explore:
- **[Extension Project Structure](./extension-project-structure)** - How to organize your extension code
- **[Frontend Contribution Types](./frontend-contribution-types)** - What kinds of extensions you can build
- **[Quick Start](./quick-start)** - Build your first extension
- **[Contribution Types](./contribution-types)** - What kinds of extensions you can build
- **[Development](./development)** - Project structure, APIs, and development workflow

View File

@@ -0,0 +1,130 @@
---
title: Contribution Types
sidebar_position: 4
---
<!--
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.
-->
# Contribution Types
To facilitate the development of extensions, we define a set of well-defined contribution types that extensions can implement. These contribution types serve as the building blocks for extensions, allowing them to interact with the host application and provide new functionality.
## Frontend
Frontend contribution types allow extensions to extend Superset's user interface with new views, commands, and menu items.
### Views
Extensions can add new views or panels to the host application, such as custom SQL Lab panels, dashboards, or other UI components. Each view is registered with a unique ID and can be activated or deactivated as needed. Contribution areas are uniquely identified (e.g., `sqllab.panels` for SQL Lab panels), enabling seamless integration into specific parts of the application.
``` json
"frontend": {
"contributions": {
"views": {
"sqllab.panels": [
{
"id": "my_extension.main",
"name": "My Panel Name"
}
]
}
}
}
```
### Commands
Extensions can define custom commands that can be executed within the host application, such as context-aware actions or menu options. Each command can specify properties like a unique command identifier, an icon, a title, and a description. These commands can be invoked by users through menus, keyboard shortcuts, or other UI elements, enabling extensions to add rich, interactive functionality to Superset.
``` json
"frontend": {
"contributions": {
"commands": [
{
"command": "my_extension.copy_query",
"icon": "CopyOutlined",
"title": "Copy Query",
"description": "Copy the current query to clipboard"
}
]
}
}
```
### Menus
Extensions can contribute new menu items or context menus to the host application, providing users with additional actions and options. Each menu item can specify properties such as the target view, the command to execute, its placement (primary, secondary, or context), and conditions for when it should be displayed. Menu contribution areas are uniquely identified (e.g., `sqllab.editor` for the SQL Lab editor), allowing extensions to seamlessly integrate their functionality into specific menus and workflows within Superset.
``` json
"frontend": {
"contributions": {
"menus": {
"sqllab.editor": {
"primary": [
{
"view": "builtin.editor",
"command": "my_extension.copy_query"
}
],
"secondary": [
{
"view": "builtin.editor",
"command": "my_extension.prettify"
}
],
"context": [
{
"view": "builtin.editor",
"command": "my_extension.clear"
}
]
}
}
}
}
```
## Backend
Backend contribution types allow extensions to extend Superset's server-side capabilities with new API endpoints, MCP tools, and MCP prompts.
### REST API Endpoints
Extensions can register custom REST API endpoints under the `/api/v1/extensions/` namespace. This dedicated namespace prevents conflicts with built-in endpoints and provides a clear separation between core and extension functionality.
``` json
"backend": {
"entryPoints": ["my_extension.entrypoint"],
"files": ["backend/src/my_extension/**/*.py"]
}
```
The entry point module registers the API with Superset:
``` python
from superset_core.api.rest_api import add_extension_api
from .api import MyExtensionAPI
add_extension_api(MyExtensionAPI)
```
### MCP Tools and Prompts
Extensions can contribute Model Context Protocol (MCP) tools and prompts that AI agents can discover and use. See [MCP Integration](./mcp) for detailed documentation.

View File

@@ -1,6 +1,6 @@
---
title: Deploying an Extension
sidebar_position: 8
title: Deployment
sidebar_position: 6
---
<!--
@@ -22,7 +22,7 @@ specific language governing permissions and limitations
under the License.
-->
# Deploying an Extension
# Deployment
Once an extension has been developed, the deployment process involves packaging and uploading it to the host application.
@@ -33,13 +33,17 @@ Packaging is handled by the `superset-extensions bundle` command, which:
3. Generates a `manifest.json` with build-time metadata, including the contents of `extension.json` and references to built assets.
4. Packages everything into a `.supx` file (a zip archive with a specific structure required by Superset).
Uploading is accomplished through Superset's REST API at `/api/v1/extensions/import/`. The endpoint accepts the `.supx` file as form data and processes it by:
To deploy an extension, place the `.supx` file in the extensions directory configured via `EXTENSIONS_PATH` in your `superset_config.py`:
1. Extracting and validating the extension metadata and manifest.
2. Storing extension assets in the metadata database for dynamic loading.
3. Registering the extension in the metadata database, including its name, version, author, and capabilities.
4. Automatically activating the extension, making it immediately available for use and management via the Superset UI or API.
``` python
EXTENSIONS_PATH = "/path/to/extensions"
```
This API-driven approach enables automated deployment workflows and simplifies extension management for administrators. Extensions can be uploaded through the Swagger UI, programmatically via scripts, or through the management interface:
During application startup, Superset automatically discovers and loads all `.supx` files from this directory:
https://github.com/user-attachments/assets/98b16cdd-8ec5-4812-9d5e-9915badd8f0d
1. Scans the configured directory for `.supx` files.
2. Validates each file is a properly formatted zip archive.
3. Extracts and validates the extension manifest and metadata.
4. Loads the extension, making it available for use.
This file-based approach simplifies deployment in containerized environments and enables version control of extensions alongside infrastructure configuration.

View File

@@ -1,48 +0,0 @@
---
title: Development Mode
sidebar_position: 10
---
<!--
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.
-->
# Development Mode
Development mode accelerates extension development by letting developers see changes in Superset quickly, without the need for repeated packaging and uploading. To enable development mode, set the `LOCAL_EXTENSIONS` configuration in your `superset_config.py`:
``` python
LOCAL_EXTENSIONS = [
"/path/to/your/extension1",
"/path/to/your/extension2",
]
```
This instructs Superset to load and serve extensions directly from disk, so you can iterate quickly. Running `superset-extensions dev` watches for file changes and rebuilds assets automatically, while the Webpack development server (started separately with `npm run dev-server`) serves updated files as soon as they're modified. This enables immediate feedback for React components, styles, and other frontend code. Changes to backend files are also detected automatically and immediately synced, ensuring that both frontend and backend updates are reflected in your development environment.
Example output when running in development mode:
```
superset-extensions dev
⚙️ Building frontend assets…
✅ Frontend rebuilt
✅ Backend files synced
✅ Manifest updated
👀 Watching for changes in: /dataset_references/frontend, /dataset_references/backend
```

View File

@@ -0,0 +1,239 @@
---
title: Development
sidebar_position: 5
---
<!--
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.
-->
# Development
This guide covers everything you need to know about developing extensions for Superset, from project structure to development workflow.
## Project Structure
The [apache-superset-extensions-cli](https://github.com/apache/superset/tree/master/superset-extensions-cli) package provides a command-line interface (CLI) that streamlines the extension development workflow. It offers the following commands:
```
superset-extensions init: Generates the initial folder structure and scaffolds a new extension project.
superset-extensions build: Builds extension assets.
superset-extensions bundle: Packages the extension into a .supx file.
superset-extensions dev: Automatically rebuilds the extension as files change.
```
When creating a new extension with `superset-extensions init <extension-name>`, the CLI generates a standardized folder structure:
```
dataset_references/
├── extension.json
├── frontend/
│ ├── src/
│ ├── webpack.config.js
│ ├── tsconfig.json
│ └── package.json
├── backend/
│ ├── src/
│ └── dataset_references/
│ ├── tests/
│ ├── pyproject.toml
│ └── requirements.txt
├── dist/
│ ├── manifest.json
│ ├── frontend
│ └── dist/
│ ├── remoteEntry.d7a9225d042e4ccb6354.js
│ └── 900.038b20cdff6d49cfa8d9.js
│ └── backend
│ └── dataset_references/
│ ├── __init__.py
│ ├── api.py
│ └── entrypoint.py
├── dataset_references-1.0.0.supx
└── README.md
```
The `extension.json` file serves as the declared metadata for the extension, containing the extension's name, version, author, description, and a list of capabilities. This file is essential for the host application to understand how to load and manage the extension.
The `frontend` directory contains the source code for the frontend components of the extension, including React components, styles, and assets. The `webpack.config.js` file is used to configure Webpack for building the frontend code, while the `tsconfig.json` file defines the TypeScript configuration for the project. The `package.json` file specifies the dependencies and scripts for building and testing the frontend code.
The `backend` directory contains the source code for the backend components of the extension, including Python modules, tests, and configuration files. The `pyproject.toml` file is used to define the Python package and its dependencies, while the `requirements.txt` file lists the required Python packages for the extension. The `src` folder contains the functional backend source files, `tests` directory contains unit tests for the backend code, ensuring that the extension behaves as expected and meets the defined requirements.
The `dist` directory is built when running the `build` or `dev` command, and contains the files that will be included in the bundle. The `manifest.json` file contains critical metadata about the extension, including the majority of the contents of the `extension.json` file, but also other build-time information, like the name of the built Webpack Module Federation remote entry file. The files in the `dist` directory will be zipped into the final `.supx` file. Although this file is technically a zip archive, the `.supx` extension makes it clear that it is a Superset extension package and follows a specific file layout. This packaged file can be distributed and installed in Superset instances.
The `README.md` file provides documentation and instructions for using the extension, including how to install, configure, and use its functionality.
## Extension Metadata
The `extension.json` file contains all metadata necessary for the host application to understand and manage the extension:
```json
{
"name": "dataset_references",
"version": "1.0.0",
"frontend": {
"contributions": {
"views": {
"sqllab.panels": [
{
"id": "dataset_references.main",
"name": "Dataset references"
}
]
}
},
"moduleFederation": {
"exposes": ["./index"]
}
},
"backend": {
"entryPoints": ["dataset_references.entrypoint"],
"files": ["backend/src/dataset_references/**/*.py"]
}
}
```
The `contributions` section declares how the extension extends Superset's functionality through views, commands, menus, and other contribution types. The `backend` section specifies entry points and files to include in the bundle.
## Interacting with the Host
Extensions interact with Superset through well-defined, versioned APIs provided by the `@apache-superset/core` (frontend) and `apache-superset-core` (backend) packages. These APIs are designed to be stable, discoverable, and consistent for both built-in and external extensions.
**Note**: The `superset_core.api` module provides abstract classes that are replaced with concrete implementations via dependency injection when Superset initializes. This allows extensions to use the same interfaces as the host application.
### Frontend APIs
The frontend extension APIs (via `@apache-superset/core`) are organized into logical namespaces such as `authentication`, `commands`, `extensions`, `sqlLab`, and others. Each namespace groups related functionality, making it easy for extension authors to discover and use the APIs relevant to their needs. For example, the `sqlLab` namespace provides events and methods specific to SQL Lab, allowing extensions to react to user actions and interact with the SQL Lab environment:
```typescript
export const getCurrentTab: () => Tab | undefined;
export const getDatabases: () => Database[];
export const getTabs: () => Tab[];
export const onDidChangeEditorContent: Event<string>;
export const onDidClosePanel: Event<Panel>;
export const onDidChangeActivePanel: Event<Panel>;
export const onDidChangeTabTitle: Event<string>;
export const onDidQueryRun: Event<Editor>;
export const onDidQueryStop: Event<Editor>;
```
The following code demonstrates more examples of the existing frontend APIs:
```typescript
import { core, commands, sqlLab, authentication, Button } from '@apache-superset/core';
import MyPanel from './MyPanel';
export function activate(context) {
// Register a new panel (view) in SQL Lab and use shared UI components in your extension's React code
const panelDisposable = core.registerView('my_extension.panel', <MyPanel><Button/></MyPanel>);
// Register a custom command
const commandDisposable = commands.registerCommand('my_extension.copy_query', {
title: 'Copy Query',
execute: () => {
// Command logic here
},
});
// Listen for query run events in SQL Lab
const eventDisposable = sqlLab.onDidQueryRun(editor => {
// Handle query execution event
});
// Access a CSRF token for secure API requests
authentication.getCSRFToken().then(token => {
// Use token as needed
});
// Add all disposables for automatic cleanup on deactivation
context.subscriptions.push(panelDisposable, commandDisposable, eventDisposable);
}
```
### Backend APIs
Backend APIs (via `apache-superset-core`) follow a similar pattern, providing access to Superset's models, sessions, and query capabilities. Extensions can register REST API endpoints, access the metadata database, and interact with Superset's core functionality.
Extension endpoints are registered under a dedicated `/extensions` namespace to avoid conflicting with built-in endpoints and also because they don't share the same version constraints. By grouping all extension endpoints under `/extensions`, Superset establishes a clear boundary between core and extension functionality, making it easier to manage, document, and secure both types of APIs.
```python
from superset_core.api.models import Database, get_session
from superset_core.api.daos import DatabaseDAO
from superset_core.api.rest_api import add_extension_api
from .api import DatasetReferencesAPI
# Register a new extension REST API
add_extension_api(DatasetReferencesAPI)
# Fetch Superset entities via the DAO to apply base filters that filter out entities
# that the user doesn't have access to
databases = DatabaseDAO.find_all()
# ..or apply simple filters on top of base filters
databases = DatabaseDAO.filter_by(uuid=database.uuid)
if not databases:
raise Exception("Database not found")
return databases[0]
# Perform complex queries using SQLAlchemy Query, also filtering out
# inaccessible entities
session = get_session()
databases_query = session.query(Database).filter(
Database.database_name.ilike("%abc%")
)
return DatabaseDAO.query(databases_query)
```
In the future, we plan to expand the backend APIs to support configuring security models, database engines, SQL Alchemy dialects, etc.
## Development Mode
Development mode accelerates extension development by letting developers see changes in Superset quickly, without the need for repeated packaging and uploading. To enable development mode, set the `LOCAL_EXTENSIONS` configuration in your `superset_config.py`:
```python
LOCAL_EXTENSIONS = [
"/path/to/your/extension1",
"/path/to/your/extension2",
]
```
This instructs Superset to load and serve extensions directly from disk, so you can iterate quickly. Running `superset-extensions dev` watches for file changes and rebuilds assets automatically, while the Webpack development server (started separately with `npm run dev-server`) serves updated files as soon as they're modified. This enables immediate feedback for React components, styles, and other frontend code. Changes to backend files are also detected automatically and immediately synced, ensuring that both frontend and backend updates are reflected in your development environment.
Example output when running in development mode:
```
superset-extensions dev
⚙️ Building frontend assets…
✅ Frontend rebuilt
✅ Backend files synced
✅ Manifest updated
👀 Watching for changes in: /dataset_references/frontend, /dataset_references/backend
```

View File

@@ -1,55 +0,0 @@
---
title: Extension Metadata
sidebar_position: 4
---
<!--
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.
-->
# Extension Metadata
The `extension.json` file contains all metadata necessary for the host application to understand and manage the extension:
``` json
{
"name": "dataset_references",
"version": "1.0.0",
"frontend": {
"contributions": {
"views": {
"sqllab.panels": [
{
"id": "dataset_references.main",
"name": "Dataset references"
}
]
}
},
"moduleFederation": {
"exposes": ["./index"]
}
},
"backend": {
"entryPoints": ["dataset_references.entrypoint"],
"files": ["backend/src/dataset_references/**/*.py"]
},
}
```
The `contributions` section declares how the extension extends Superset's functionality through views, commands, menus, and other contribution types. The `backend` section specifies entry points and files to include in the bundle.

View File

@@ -0,0 +1,209 @@
---
title: SQL Lab
sidebar_position: 1
---
<!--
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.
-->
# SQL Lab Extension Points
SQL Lab provides 5 extension points where extensions can contribute custom UI components. Each area serves a specific purpose and can be customized to add new functionality.
## Layout Overview
```
┌──────────┬─────────────────────────────────────────┬─────────────┐
│ │ │ │
│ │ │ │
│ │ Editor │ │
│ │ │ │
│ Left │ │ Right │
│ Sidebar ├─────────────────────────────────────────┤ Sidebar │
│ │ │ │
│ │ Panels │ │
│ │ │ │
│ │ │ │
│ │ │ │
├──────────┴─────────────────────────────────────────┴─────────────┤
│ Status Bar │
└──────────────────────────────────────────────────────────────────┘
```
| Extension Point | ID | Description |
| ----------------- | --------------------- | ---------------------------------------------------------- |
| **Left Sidebar** | `sqllab.leftSidebar` | Navigation and browsing (database explorer, saved queries) |
| **Editor** | `sqllab.editor` | SQL query editor workspace |
| **Right Sidebar** | `sqllab.rightSidebar` | Contextual tools (AI assistants, query analysis) |
| **Panels** | `sqllab.panels` | Results and related views (visualizations, data profiling) |
| **Status Bar** | `sqllab.statusBar` | Connection status and query metrics |
## Area Customizations
Each extension point area supports three types of action customizations:
```
┌───────────────────────────────────────────────────────────────┐
│ Area Title [Button] [Button] [•••] │
├───────────────────────────────────────────────────────────────┤
│ │
│ │
│ Area Content │
│ │
│ (right-click for context menu) │
│ │
│ │
└───────────────────────────────────────────────────────────────┘
```
| Action Type | Location | Use Case |
| --------------------- | ----------------- | ----------------------------------------------------- |
| **Primary Actions** | Top-right buttons | Frequently used actions (e.g., run, refresh, add new) |
| **Secondary Actions** | 3-dot menu (•••) | Less common actions (e.g., export, settings) |
| **Context Actions** | Right-click menu | Context-sensitive actions on content |
## Examples
### Adding a Panel
This example adds a "Data Profiler" panel to SQL Lab:
```json
{
"name": "data_profiler",
"version": "1.0.0",
"frontend": {
"contributions": {
"views": {
"sqllab.panels": [
{
"id": "data_profiler.main",
"name": "Data Profiler"
}
]
}
}
}
}
```
```typescript
import { core } from '@apache-superset/core';
import DataProfilerPanel from './DataProfilerPanel';
export function activate(context) {
// Register the panel view with the ID declared in extension.json
const disposable = core.registerView('data_profiler.main', <DataProfilerPanel />);
context.subscriptions.push(disposable);
}
```
### Adding Actions to the Editor
This example adds primary, secondary, and context actions to the editor:
```json
{
"name": "query_tools",
"version": "1.0.0",
"frontend": {
"contributions": {
"commands": [
{
"command": "query_tools.format",
"title": "Format Query",
"icon": "FormatPainterOutlined"
},
{
"command": "query_tools.explain",
"title": "Explain Query"
},
{
"command": "query_tools.copy_as_cte",
"title": "Copy as CTE"
}
],
"menus": {
"sqllab.editor": {
"primary": [
{
"view": "builtin.editor",
"command": "query_tools.format"
}
],
"secondary": [
{
"view": "builtin.editor",
"command": "query_tools.explain"
}
],
"context": [
{
"view": "builtin.editor",
"command": "query_tools.copy_as_cte"
}
]
}
}
}
}
}
```
```typescript
import { commands, sqlLab } from '@apache-superset/core';
export function activate(context) {
// Register the commands declared in extension.json
const formatCommand = commands.registerCommand('query_tools.format', {
execute: () => {
const tab = sqlLab.getCurrentTab();
if (tab?.editor) {
// Format the SQL query
}
},
});
const explainCommand = commands.registerCommand('query_tools.explain', {
execute: () => {
const tab = sqlLab.getCurrentTab();
if (tab?.editor) {
// Show query explanation
}
},
});
const copyAsCteCommand = commands.registerCommand('query_tools.copy_as_cte', {
execute: () => {
const tab = sqlLab.getCurrentTab();
if (tab?.editor) {
// Copy selected text as CTE
}
},
});
context.subscriptions.push(formatCommand, explainCommand, copyAsCteCommand);
}
```
## Next Steps
- **[Contribution Types](../contribution-types)** - Learn about other contribution types (commands, menus)
- **[Development](../development)** - Set up your development environment
- **[Quick Start](../quick-start)** - Build a complete extension

View File

@@ -1,78 +0,0 @@
---
title: Extension Project Structure
sidebar_position: 3
---
<!--
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.
-->
# Extension Project Structure
The `apache-superset-extensions-cli` package provides a command-line interface (CLI) that streamlines the extension development workflow. It offers the following commands:
```
superset-extensions init: Generates the initial folder structure and scaffolds a new extension project.
superset-extensions build: Builds extension assets.
superset-extensions bundle: Packages the extension into a .supx file.
superset-extensions dev: Automatically rebuilds the extension as files change.
```
When creating a new extension with `superset-extensions init <extension-name>`, the CLI generates a standardized folder structure:
```
dataset_references/
├── extension.json
├── frontend/
│ ├── src/
│ ├── webpack.config.js
│ ├── tsconfig.json
│ └── package.json
├── backend/
│ ├── src/
│ └── dataset_references/
│ ├── tests/
│ ├── pyproject.toml
│ └── requirements.txt
├── dist/
│ ├── manifest.json
│ ├── frontend
│ └── dist/
│ ├── remoteEntry.d7a9225d042e4ccb6354.js
│ └── 900.038b20cdff6d49cfa8d9.js
│ └── backend
│ └── dataset_references/
│ ├── __init__.py
│ ├── api.py
│ └── entrypoint.py
├── dataset_references-1.0.0.supx
└── README.md
```
The `extension.json` file serves as the declared metadata for the extension, containing the extension's name, version, author, description, and a list of capabilities. This file is essential for the host application to understand how to load and manage the extension.
The `frontend` directory contains the source code for the frontend components of the extension, including React components, styles, and assets. The `webpack.config.js` file is used to configure Webpack for building the frontend code, while the `tsconfig.json` file defines the TypeScript configuration for the project. The `package.json` file specifies the dependencies and scripts for building and testing the frontend code.
The `backend` directory contains the source code for the backend components of the extension, including Python modules, tests, and configuration files. The `pyproject.toml` file is used to define the Python package and its dependencies, while the r`equirements.txt` file lists the required Python packages for the extension. The `src` folder contains the functional backend source files, `tests` directory contains unit tests for the backend code, ensuring that the extension behaves as expected and meets the defined requirements.
The `dist` directory is built when running the `build` or `dev` command, and contains the files that will be included in the bundle. The `manifest.json` file contains critical metadata about the extension, including the majority of the contents of the `extension.json` file, but also other build-time information, like the name of the built Webpack Module Federation remote entry file. The files in the `dist` directory will be zipped into the final `.supx` file. Although this file is technically a zip archive, the `.supx` extension makes it clear that it is a Superset extension package and follows a specific file layout. This packaged file can be distributed and installed in Superset instances.
The `README.md` file provides documentation and instructions for using the extension, including how to install, configure, and use its functionality.

View File

@@ -1,90 +0,0 @@
---
title: Frontend Contribution Types
sidebar_position: 5
---
<!--
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.
-->
# Frontend Contribution Types
To facilitate the development of extensions, we will define a set of well-defined contribution types that extensions can implement. These contribution types will serve as the building blocks for extensions, allowing them to interact with the host application and provide new functionality. The initial set of contribution types will include:
## Views
Extensions can add new views or panels to the host application, such as custom SQL Lab panels, dashboards, or other UI components. Each view is registered with a unique ID and can be activated or deactivated as needed. Contribution areas are uniquely identified (e.g., `sqllab.panels` for SQL Lab panels), enabling seamless integration into specific parts of the application.
``` json
"views": {
"sqllab.panels": [
{
"id": "dataset_references.main",
"name": "Table references"
}
]
},
```
## Commands
Extensions can define custom commands that can be executed within the host application, such as context-aware actions or menu options. Each command can specify properties like a unique command identifier, an icon, a title, and a description. These commands can be invoked by users through menus, keyboard shortcuts, or other UI elements, enabling extensions to add rich, interactive functionality to Superset.
``` json
"commands": [
{
"command": "extension1.copy_query",
"icon": "CopyOutlined",
"title": "Copy Query",
"description": "Copy the current query to clipboard"
},
]
```
## Menus
Extensions can contribute new menu items or context menus to the host application, providing users with additional actions and options. Each menu item can specify properties such as the target view, the command to execute, its placement (primary, secondary, or context), and conditions for when it should be displayed. Menu contribution areas are uniquely identified (e.g., `sqllab.editor` for the SQL Lab editor), allowing extensions to seamlessly integrate their functionality into specific menus and workflows within Superset.
``` json
"menus": {
"sqllab.editor": {
"primary": [
{
"view": "builtin.editor",
"command": "extension1.copy_query"
}
],
"secondary": [
{
"view": "builtin.editor",
"command": "extension1.prettify"
}
],
"context": [
{
"view": "builtin.editor",
"command": "extension1.clear"
},
{
"view": "builtin.editor",
"command": "extension1.refresh"
}
]
},
}
```

View File

@@ -1,129 +0,0 @@
---
title: Interacting with the Host
sidebar_position: 6
---
<!--
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.
-->
# Interacting with the Host
Extensions interact with Superset through well-defined, versioned APIs provided by the `@apache-superset/core` (frontend) and `apache-superset-core` (backend) packages. These APIs are designed to be stable, discoverable, and consistent for both built-in and external extensions.
**Note**: The `superset_core.api` module provides abstract classes that are replaced with concrete implementations via dependency injection when Superset initializes. This allows extensions to use the same interfaces as the host application.
**Frontend APIs** (via `@apache-superset/core)`:
The frontend extension APIs in Superset are organized into logical namespaces such as `authentication`, `commands`, `extensions`, `sqlLab`, and others. Each namespace groups related functionality, making it easy for extension authors to discover and use the APIs relevant to their needs. For example, the `sqlLab` namespace provides events and methods specific to SQL Lab, allowing extensions to react to user actions and interact with the SQL Lab environment:
``` typescript
export const getCurrentTab: () => Tab | undefined;
export const getDatabases: () => Database[];
export const getTabs: () => Tab[];
export const onDidChangeEditorContent: Event<string>;
export const onDidClosePanel: Event<Panel>;
export const onDidChangeActivePanel: Event<Panel>;
export const onDidChangeTabTitle: Event<string>;
export const onDidQueryRun: Event<Editor>;
export const onDidQueryStop: Event<Editor>;
```
The following code demonstrates more examples of the existing frontend APIs:
``` typescript
import { core, commands, sqlLab, authentication, Button } from '@apache-superset/core';
import MyPanel from './MyPanel';
export function activate(context) {
// Register a new panel (view) in SQL Lab and use shared UI components in your extension's React code
const panelDisposable = core.registerView('my_extension.panel', <MyPanel><Button/></MyPanel>);
// Register a custom command
const commandDisposable = commands.registerCommand('my_extension.copy_query', {
title: 'Copy Query',
execute: () => {
// Command logic here
},
});
// Listen for query run events in SQL Lab
const eventDisposable = sqlLab.onDidQueryRun(editor => {
// Handle query execution event
});
// Access a CSRF token for secure API requests
authentication.getCSRFToken().then(token => {
// Use token as needed
});
// Add all disposables for automatic cleanup on deactivation
context.subscriptions.push(panelDisposable, commandDisposable, eventDisposable);
}
```
**Backend APIs** (via `apache-superset-core`):
Backend APIs follow a similar pattern, providing access to Superset's models, sessions, and query capabilities. Extensions can register REST API endpoints, access the metadata database, and interact with Superset's core functionality.
Extension endpoints are registered under a dedicated `/extensions` namespace to avoid conflicting with built-in endpoints and also because they don't share the same version constraints. By grouping all extension endpoints under `/extensions`, Superset establishes a clear boundary between core and extension functionality, making it easier to manage, document, and secure both types of APIs.
``` python
from superset_core.api.models import Database, get_session
from superset_core.api.daos import DatabaseDAO
from superset_core.api.rest_api import add_extension_api
from .api import DatasetReferencesAPI
# Register a new extension REST API
add_extension_api(DatasetReferencesAPI)
# Fetch Superset entities via the DAO to apply base filters that filter out entities
# that the user doesn't have access to
databases = DatabaseDAO.find_all()
# ..or apply simple filters on top of base filters
databases = DatabaseDAO.filter_by(uuid=database.uuid)
if not databases:
raise Exception("Database not found")
return databases[0]
# Perform complex queries using SQLAlchemy Query, also filtering out
# inaccessible entities
session = get_session()
databases_query = session.query(Database).filter(
Database.database_name.ilike("%abc%")
)
return DatabaseDAO.query(databases_query)
# Bypass security model for highly custom use cases
session = get_session()
all_databases_containing_abc = session.query(Database).filter(
Database.database_name.ilike("%abc%")
).all()
```
In the future, we plan to expand the backend APIs to support configuring security models, database engines, SQL Alchemy dialects, etc.

View File

@@ -0,0 +1,459 @@
---
title: MCP Integration
hide_title: true
sidebar_position: 7
version: 1
---
<!--
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.
-->
# MCP Integration
Model Context Protocol (MCP) integration allows extensions to register custom AI agent capabilities that integrate seamlessly with Superset's MCP service. Extensions can provide both **tools** (executable functions) and **prompts** (interactive guidance) that AI agents can discover and use.
## What is MCP?
MCP enables extensions to extend Superset's AI capabilities in two ways:
### MCP Tools
Tools are Python functions that AI agents can call to perform specific tasks. They provide executable functionality that extends Superset's capabilities.
**Examples of MCP tools:**
- Data processing and transformation functions
- Custom analytics calculations
- Integration with external APIs
- Specialized report generation
- Business-specific operations
### MCP Prompts
Prompts provide interactive guidance and context to AI agents. They help agents understand how to better assist users with specific workflows or domain knowledge.
**Examples of MCP prompts:**
- Step-by-step workflow guidance
- Domain-specific context and knowledge
- Interactive troubleshooting assistance
- Template generation helpers
- Best practices recommendations
## Getting Started
## MCP Tools
### Basic Tool Registration
The simplest way to create an MCP tool is using the `@tool` decorator:
```python
from superset_core.mcp import tool
@tool
def hello_world() -> dict:
"""A simple greeting tool."""
return {"message": "Hello from my extension!"}
```
This creates a tool that AI agents can call by name. The tool name defaults to the function name.
### Decorator Parameters
The `@tool` decorator accepts several optional parameters:
**Parameter details:**
- **`name`**: Tool identifier (AI agents use this to call your tool)
- **`description`**: Explains what the tool does (helps AI agents decide when to use it)
- **`tags`**: Categories for organization and discovery
- **`protect`**: Whether the tool requires user authentication (defaults to `True`)
### Naming Your Tools
For extensions, include your extension ID in tool names to avoid conflicts:
## Complete Example
Here's a more comprehensive example showing best practices:
```python
# backend/mcp_tools.py
import random
from datetime import datetime, timezone
from pydantic import BaseModel, Field
from superset_core.mcp import tool
class RandomNumberRequest(BaseModel):
"""Request schema for random number generation."""
min_value: int = Field(
description="Minimum value (inclusive) for random number generation",
ge=-2147483648,
le=2147483647
)
max_value: int = Field(
description="Maximum value (inclusive) for random number generation",
ge=-2147483648,
le=2147483647
)
@tool(
name="example_extension.random_number",
tags=["extension", "utility", "random", "generator"]
)
def random_number_generator(request: RandomNumberRequest) -> dict:
"""
Generate a random integer between specified bounds.
This tool validates input ranges and provides detailed error messages
for invalid requests.
"""
# Validate business logic (Pydantic handles type/range validation)
if request.min_value > request.max_value:
return {
"status": "error",
"error": f"min_value ({request.min_value}) cannot be greater than max_value ({request.max_value})",
"timestamp": datetime.now(timezone.utc).isoformat()
}
# Generate random number
result = random.randint(request.min_value, request.max_value)
return {
"status": "success",
"random_number": result,
"min_value": request.min_value,
"max_value": request.max_value,
"range_size": request.max_value - request.min_value + 1,
"timestamp": datetime.now(timezone.utc).isoformat()
}
```
## Best Practices
### Response Format
Use consistent response structures:
```python
# Success response
{
"status": "success",
"result": "your_data_here",
"timestamp": "2024-01-01T00:00:00Z"
}
# Error response
{
"status": "error",
"error": "Clear error message",
"timestamp": "2024-01-01T00:00:00Z"
}
```
### Documentation
Write clear descriptions and docstrings:
```python
@tool(
name="my_extension.process_data",
description="Process customer data and generate insights. Requires valid customer ID and date range.",
tags=["analytics", "customer", "reporting"]
)
def process_data(customer_id: int, start_date: str, end_date: str) -> dict:
"""
Process customer data for the specified date range.
This tool analyzes customer behavior patterns and generates
actionable insights for business decision-making.
Args:
customer_id: Unique customer identifier
start_date: Analysis start date (YYYY-MM-DD format)
end_date: Analysis end date (YYYY-MM-DD format)
Returns:
Dictionary containing analysis results and recommendations
"""
# Implementation here
pass
```
### Tool Naming
- **Extension tools**: Use prefixed names like `my_extension.tool_name`
- **Descriptive names**: `calculate_tax_amount` vs `calculate`
- **Consistent naming**: Follow patterns within your extension
## How AI Agents Use Your Tools
Once registered, AI agents can discover and use your tools automatically:
```
User: "Generate a random number between 1 and 100"
Agent: I'll use the random number generator tool.
→ Calls: example_extension.random_number(min_value=1, max_value=100)
← Returns: {"status": "success", "random_number": 42, ...}
Agent: I generated the number 42 for you.
```
The AI agent sees your tool's:
- **Name**: How to call it
- **Description**: What it does and when to use it
- **Parameters**: What inputs it expects (from Pydantic schema)
- **Tags**: Categories for discovery
## Troubleshooting
### Tool Not Available to AI Agents
1. **Check extension registration**: Verify your tool module is listed in extension entrypoints
2. **Verify decorator**: Ensure `@tool` is correctly applied
3. **Extension loading**: Confirm your extension is installed and enabled
### Input Validation Errors
1. **Pydantic models**: Ensure field types match expected inputs
2. **Field constraints**: Check min/max values and string lengths are reasonable
3. **Required fields**: Verify which parameters are required vs optional
### Runtime Issues
1. **Error handling**: Add try/catch blocks with clear error messages
2. **Response format**: Use consistent status/error/timestamp structure
3. **Testing**: Test your tools with various input scenarios
### Development Tips
1. **Start simple**: Begin with basic tools, add complexity gradually
2. **Test locally**: Use MCP clients (like Claude Desktop) to test your tools
3. **Clear descriptions**: Write tool descriptions as if explaining to a new user
4. **Meaningful tags**: Use tags that help categorize and discover tools
5. **Error messages**: Provide specific, actionable error messages
## MCP Prompts
### Basic Prompt Registration
Create interactive prompts using the `@prompt` decorator:
```python
from superset_core.mcp import prompt
from fastmcp import Context
@prompt("my_extension.workflow_guide")
async def workflow_guide(ctx: Context) -> str:
"""Interactive guide for data analysis workflows."""
return """
# Data Analysis Workflow Guide
Here's a step-by-step approach to effective data analysis in Superset:
## 1. Data Discovery
- Start by exploring your datasets using the dataset browser
- Check data quality and identify key metrics
- Look for patterns and relationships in your data
## 2. Chart Creation
- Choose appropriate visualizations for your data types
- Apply filters to focus on relevant subsets
- Configure proper aggregations and groupings
## 3. Dashboard Assembly
- Combine related charts into coherent dashboards
- Use filters and parameters for interactivity
- Add markdown components for context and explanations
Would you like guidance on any specific step?
"""
```
### Advanced Prompt Examples
#### Domain-Specific Context
```python
@prompt(
"sales_extension.sales_analysis_guide",
title="Sales Analysis Guide",
description="Specialized guidance for sales data analysis workflows"
)
async def sales_analysis_guide(ctx: Context) -> str:
"""Provides sales-specific analysis guidance and best practices."""
return """
# Sales Data Analysis Best Practices
## Key Metrics to Track
- **Revenue Growth**: Month-over-month and year-over-year trends
- **Conversion Rates**: Lead-to-opportunity-to-close ratios
- **Customer Lifetime Value**: Total value per customer segment
- **Sales Cycle Length**: Time from lead to close by product/region
## Recommended Chart Types
- **Time Series**: Revenue trends, seasonal patterns
- **Funnel Charts**: Conversion analysis across sales stages
- **Geographic Maps**: Regional performance comparison
- **Cohort Analysis**: Customer retention and growth patterns
## Common Pitfalls to Avoid
- Don't mix different time granularities without proper context
- Always normalize for business days when comparing periods
- Consider external factors (holidays, market events) in analysis
- Segment by relevant dimensions (product, region, channel)
## Next Steps
1. Identify your primary sales KPIs
2. Create baseline trend charts for each metric
3. Build comparative views across segments
4. Set up automated alerts for significant changes
"""
```
#### Interactive Troubleshooting
```python
@prompt("support_extension.troubleshoot_charts")
async def troubleshoot_charts(ctx: Context) -> str:
"""Interactive troubleshooting assistant for chart issues."""
return """
# Chart Troubleshooting Assistant
Let's diagnose your chart issue step by step:
## Common Issues and Solutions
### 🚫 No Data Showing
**Possible causes:**
- Filters are too restrictive
- Date range doesn't match your data
- Database connection issues
- Missing permissions
**Check:** Try removing all filters and expanding the date range first.
### 📊 Unexpected Aggregation Results
**Possible causes:**
- Incorrect grouping dimensions
- Wrong aggregation function (SUM vs COUNT vs AVG)
- Data quality issues (duplicates, nulls)
- Time zone mismatches
**Check:** Verify your GROUP BY columns and aggregation logic.
### 🐌 Slow Performance
**Possible causes:**
- Large dataset without proper indexing
- Complex joins or calculations
- Missing query optimizations
- Resource constraints
**Check:** Simplify the query and add appropriate filters first.
## Debug Steps
1. **Start Simple**: Create a basic count query first
2. **Add Gradually**: Introduce complexity step by step
3. **Check SQL**: Review the generated SQL for issues
4. **Test Data**: Verify with a small sample first
What specific issue are you experiencing?
"""
```
### Prompt Best Practices
#### Content Structure
- **Use clear headings** and sections for easy navigation
- **Provide actionable steps** rather than just theory
- **Include examples** relevant to the user's domain
- **Offer next steps** to continue the workflow
#### Interactive Design
- **Ask questions** to engage the user
- **Provide options** for different scenarios
- **Reference specific Superset features** by name
- **Link to related tools** when appropriate
#### Context Awareness
```python
@prompt("analytics_extension.context_aware_guide")
async def context_aware_guide(ctx: Context) -> str:
"""Provides guidance based on current user context."""
# Access user information if available
user_info = getattr(ctx, 'user', None)
guidance = """# Personalized Analytics Guide\n\n"""
if user_info:
guidance += f"Welcome back! Here's guidance tailored for your role:\n\n"
guidance += """
## Getting Started
Based on your previous activity, here are recommended next steps:
1. **Review Recent Dashboards**: Check your most-used dashboards for updates
2. **Explore New Data**: Look for recently added datasets in your domain
3. **Share Insights**: Consider sharing successful analyses with your team
## Advanced Techniques
- Set up automated alerts for key metrics
- Create parameterized dashboards for different audiences
- Use SQL Lab for complex custom analyses
"""
return guidance
```
## Combining Tools and Prompts
Extensions can provide both tools and prompts that work together:
```python
# Tool for data processing
@tool("analytics_extension.calculate_metrics")
def calculate_metrics(data: dict) -> dict:
"""Calculate advanced analytics metrics."""
# Implementation here
pass
# Prompt that guides users to the tool
@prompt("analytics_extension.metrics_guide")
async def metrics_guide(ctx: Context) -> str:
"""Guide users through advanced metrics calculation."""
return """
# Advanced Metrics Calculation
Use the `calculate_metrics` tool to compute specialized analytics:
## Available Metrics
- Customer Lifetime Value (CLV)
- Cohort Retention Rates
- Statistical Significance Tests
- Predictive Trend Analysis
## Usage
Call the tool with your dataset to get detailed calculations
and recommendations for visualization approaches.
Would you like to calculate metrics for your current dataset?
"""
```
## Next Steps
- **[Development](./development)** - Project structure, APIs, and dev workflow
- **[Security](./security)** - Security best practices for extensions

View File

@@ -24,53 +24,30 @@ under the License.
# Overview
Apache Superset's extension system allows developers to enhance and customize Superset's functionality through a modular, plugin-based architecture. Extensions can add new visualization types, custom UI components, data processing capabilities, and integration points.
Apache Superset's extension system enables organizations to build custom features without modifying the core codebase. Inspired by the [VS Code extension model](https://code.visualstudio.com/api), this architecture addresses a long-standing challenge: teams previously had to fork Superset or make invasive modifications to add capabilities like query optimizers, custom panels, or specialized integrations—resulting in maintenance overhead and codebase fragmentation.
The extension system introduces a modular, plugin-based architecture where both built-in features and external extensions use the same well-defined APIs. This "lean core" approach ensures that any capability available to Superset's internal features is equally accessible to community-developed extensions, fostering a vibrant ecosystem while reducing the maintenance burden on core contributors.
## What are Superset Extensions?
Superset extensions are self-contained packages that extend the core platform's capabilities. They follow a standardized architecture that ensures compatibility, security, and maintainability while providing powerful customization options.
## Extension Architecture
- **[Architecture](./architecture)** - Architectural principles and high-level system overview
- **[Extension Project Structure](./extension-project-structure)** - Standard project layout and organization
- **[Extension Metadata](./extension-metadata)** - Configuration and manifest structure
## Development Guide
- **[Frontend Contribution Types](./frontend-contribution-types)** - Types of UI contributions available
- **[Interacting with Host](./interacting-with-host)** - Communication patterns with Superset core
- **[Development Mode](./development-mode)** - Tools and workflows for extension development
For information about runtime loading and dependency management, see the [Dynamic Module Loading](./architecture#dynamic-module-loading) section in the Architecture page.
## Deployment & Management
- **[Deploying Extension](./deploying-extension)** - Packaging and distribution strategies
- **[Security Implications](./security-implications)** - Security considerations and best practices
## Hands-on Examples
- **[Quick Start](./quick-start)** - Complete Hello World extension walkthrough
Superset extensions are self-contained `.supx` packages that extend the platform's capabilities through standardized contribution points. Each extension can include both frontend (React/TypeScript) and backend (Python) components, bundled together and loaded dynamically at runtime using Webpack Module Federation.
## Extension Capabilities
Extensions can provide:
- **Custom Visualizations**: New chart types and data visualization components
- **UI Enhancements**: Custom dashboards, panels, and interactive elements
- **Data Connectors**: Integration with external data sources and APIs
- **Workflow Automation**: Custom actions and batch processing capabilities
- **Authentication Providers**: SSO and custom authentication mechanisms
- **Theme Customization**: Custom styling and branding options
- **Custom UI Components**: New panels, views, and interactive elements
- **Commands and Menus**: Custom actions accessible via menus and keyboard shortcuts
- **REST API Endpoints**: Backend services under the `/api/v1/extensions/` namespace
- **MCP Tools and Prompts**: AI agent capabilities for enhanced user assistance
## Getting Started
## Next Steps
1. **Learn the Architecture**: Start with [Architecture](./architecture) to understand the design philosophy
2. **Set up Development**: Follow the [Development Mode](./development-mode) guide to configure your environment
3. **Build Your First Extension**: Complete the [Quick Start](./quick-start) tutorial
4. **Deploy and Share**: Use the [Deploying Extension](./deploying-extension) guide to package your extension
## Extension Ecosystem
The extension system is designed to foster a vibrant ecosystem of community-contributed functionality. By following the established patterns and guidelines, developers can create extensions that seamlessly integrate with Superset while maintaining the platform's reliability and performance standards.
- **[Quick Start](./quick-start)** - Build your first extension with a complete walkthrough
- **[Architecture](./architecture)** - Design principles and system overview
- **[Contribution Types](./contribution-types)** - Available extension points
- **[Development](./development)** - Project structure, APIs, and development workflow
- **[Deployment](./deployment)** - Packaging and deploying extensions
- **[MCP Integration](./mcp)** - Adding AI agent capabilities using extensions
- **[Security](./security)** - Security considerations and best practices
- **[Community Extensions](./registry)** - Browse extensions shared by the community

View File

@@ -191,7 +191,125 @@ This registers your API with Superset when the extension loads.
## Step 5: Create Frontend Component
The CLI generated boilerplate files. The webpack config and package.json are already properly configured with Module Federation.
The CLI generates the frontend configuration files. Below are the key configurations that enable Module Federation integration with Superset.
**`frontend/package.json`**
The `@apache-superset/core` package must be listed in both `peerDependencies` (to declare runtime compatibility) and `devDependencies` (to provide TypeScript types during build):
```json
{
"name": "hello_world",
"version": "0.1.0",
"private": true,
"license": "Apache-2.0",
"scripts": {
"start": "webpack serve --mode development",
"build": "webpack --stats-error-details --mode production"
},
"peerDependencies": {
"@apache-superset/core": "^x.x.x",
"react": "^x.x.x",
"react-dom": "^x.x.x"
},
"devDependencies": {
"@apache-superset/core": "^x.x.x",
"@types/react": "^x.x.x",
"ts-loader": "^x.x.x",
"typescript": "^x.x.x",
"webpack": "^5.x.x",
"webpack-cli": "^x.x.x",
"webpack-dev-server": "^x.x.x"
}
}
```
**`frontend/webpack.config.js`**
The webpack configuration requires specific settings for Module Federation. Key settings include `externalsType: "window"` and `externals` to map `@apache-superset/core` to `window.superset` at runtime, `import: false` for shared modules to use the host's React instead of bundling a separate copy, and `remoteEntry.[contenthash].js` for cache busting:
```javascript
const path = require("path");
const { ModuleFederationPlugin } = require("webpack").container;
const packageConfig = require("./package.json");
module.exports = (env, argv) => {
const isProd = argv.mode === "production";
return {
entry: isProd ? {} : "./src/index.tsx",
mode: isProd ? "production" : "development",
devServer: {
port: 3001,
headers: {
"Access-Control-Allow-Origin": "*",
},
},
output: {
filename: isProd ? undefined : "[name].[contenthash].js",
chunkFilename: "[name].[contenthash].js",
clean: true,
path: path.resolve(__dirname, "dist"),
publicPath: `/api/v1/extensions/${packageConfig.name}/`,
},
resolve: {
extensions: [".ts", ".tsx", ".js", ".jsx"],
},
// Map @apache-superset/core imports to window.superset at runtime
externalsType: "window",
externals: {
"@apache-superset/core": "superset",
},
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
plugins: [
new ModuleFederationPlugin({
name: packageConfig.name,
filename: "remoteEntry.[contenthash].js",
exposes: {
"./index": "./src/index.tsx",
},
shared: {
react: {
singleton: true,
requiredVersion: packageConfig.peerDependencies.react,
import: false, // Use host's React, don't bundle
},
"react-dom": {
singleton: true,
requiredVersion: packageConfig.peerDependencies["react-dom"],
import: false,
},
},
}),
],
};
};
```
**`frontend/tsconfig.json`**
```json
{
"compilerOptions": {
"baseUrl": ".",
"moduleResolution": "node",
"jsx": "react",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"]
}
```
**Create `frontend/src/HelloWorldPanel.tsx`**
@@ -388,10 +506,9 @@ Here's what happens when your extension loads:
Now that you have a working extension, explore:
- **[Development Mode](./development-mode)** - Faster iteration with local development and watch mode
- **[Extension Project Structure](./extension-project-structure)** - Best practices for organizing larger extensions
- **[Frontend Contribution Types](./frontend-contribution-types)** - Other UI contribution points beyond panels
- **[Interacting with Host](./interacting-with-host)** - Advanced APIs for interacting with Superset
- **[Security Implications](./security-implications)** - Security best practices for extensions
- **[Development](./development)** - Project structure, APIs, and development workflow
- **[Contribution Types](./contribution-types)** - Other contribution points beyond panels
- **[Deployment](./deployment)** - Packaging and deploying your extension
- **[Security](./security)** - Security best practices for extensions
For a complete real-world example, examine the query insights extension in the Superset codebase.

View File

@@ -0,0 +1,47 @@
---
title: Community Extensions
sidebar_position: 9
---
<!--
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.
-->
# Community Extensions
This page serves as a registry of community-created Superset extensions. These extensions are developed and maintained by community members and are not officially supported or vetted by the Apache Superset project. **Before installing any community extension, administrators are responsible for evaluating the extension's source code for security vulnerabilities, performance impact, UI/UX quality, and compatibility with their Superset deployment.**
## Extensions
| Name | Description | Author | Preview |
| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Extensions API Explorer](https://github.com/michael-s-molina/superset-extensions/tree/main/api_explorer) | A SQL Lab panel that demonstrates the Extensions API by providing an interactive explorer for testing commands like getTabs, getCurrentTab, and getDatabases. Useful for extension developers to understand and experiment with the available APIs. | Michael S. Molina | <a href="/img/extensions/api_explorer.png" target="_blank"><img src="/img/extensions/api_explorer.png" alt="Extensions API Explorer" width="120" /></a> |
## How to Add Your Extension
To add your extension to this registry, submit a pull request to the [Apache Superset repository](https://github.com/apache/superset) with the following changes:
1. Add a row to the **Extensions** table above using this format:
```markdown
| [Your Extension](https://github.com/your-username/your-repo) | A brief description of your extension. | Your Name | <a href="/img/extensions/your-screenshot.png" target="_blank"><img src="/img/extensions/your-screenshot.png" alt="Your Extension" width="120" /></a> |
```
2. Add a screenshot to `docs/static/img/extensions/` (recommended size: 800x450px, PNG or JPG format)
3. Submit your PR with a title like "docs: Add [Extension Name] to community extensions registry"

View File

@@ -1,6 +1,6 @@
---
title: Security Implications and Responsibilities
sidebar_position: 12
title: Security
sidebar_position: 8
---
<!--
@@ -22,12 +22,14 @@ specific language governing permissions and limitations
under the License.
-->
# Security Implications and Responsibilities
# Security
By default, extensions are disabled and must be explicitly enabled by setting the `ENABLE_EXTENSIONS` feature flag. Built-in extensions are included as part of the Superset codebase and are held to the same security standards and review processes as the rest of the application.
For external extensions, administrators are responsible for evaluating and verifying the security of any extensions they choose to install, just as they would when installing third-party NPM or PyPI packages. At this stage, all extensions run in the same context as the host application, without additional sandboxing. This means that external extensions can impact the security and performance of a Superset environment in the same way as any other installed dependency.
We plan to introduce an optional sandboxed execution model for extensions in the future (as part of an additional SIP). Until then, administrators should exercise caution and follow best practices when selecting and deploying third-party extensions. A directory of known Superset extensions may be maintained in a means similar to [this page](https://github.com/apache/superset/wiki/Superset-Third%E2%80%90Party-Plugins-Directory) on the wiki. We also discussed the possibility of introducing a shared registry for vetted extensions but decided to leave it out of the initial scope of the project. We might introduce a registry at a later stage depending on the evolution of extensions created by the community.
We plan to introduce an optional sandboxed execution model for extensions in the future (as part of an additional SIP). Until then, administrators should exercise caution and follow best practices when selecting and deploying third-party extensions. A directory of community extensions is available in the [Community Extensions](./registry) page. Note that these extensions are not vetted by the Apache Superset project—administrators must evaluate each extension before installation.
Any performance or security vulnerabilities introduced by external extensions should be reported directly to the extension author, not as Superset vulnerabilities. Any security concerns regarding built-in extensions (included in Superset's monorepo) should be reported to the Superset Security mailing list for triage and resolution by maintainers.
**Any performance or security vulnerabilities introduced by external extensions should be reported directly to the extension author, not as Superset vulnerabilities.**
Any security concerns regarding built-in extensions (included in Superset's monorepo) should be reported to the Superset Security mailing list for triage and resolution by maintainers.

View File

@@ -36,13 +36,20 @@ module.exports = {
'extensions/overview',
'extensions/quick-start',
'extensions/architecture',
'extensions/extension-project-structure',
'extensions/extension-metadata',
'extensions/frontend-contribution-types',
'extensions/interacting-with-host',
'extensions/deploying-extension',
'extensions/development-mode',
'extensions/security-implications',
'extensions/contribution-types',
{
type: 'category',
label: 'Extension Points',
collapsed: true,
items: [
'extensions/extension-points/sqllab',
],
},
'extensions/development',
'extensions/deployment',
'extensions/mcp',
'extensions/security',
'extensions/registry',
],
},
{

View File

@@ -294,6 +294,14 @@ Check this by attempting to `curl` the URL of a report that you see in the error
In a deployment with authentication measures enabled like HTTPS and Single Sign-On, it may make sense to have the worker navigate directly to the Superset application running in the same location, avoiding the need to sign in. For instance, you could use `WEBDRIVER_BASEURL="http://superset_app:8088"` for a docker compose deployment, and set `"force_https": False,` in your `TALISMAN_CONFIG`.
### Duplicate report deliveries
In some deployment configurations a scheduled report can be delivered more than once around its planned time. This typically happens when more than one process is responsible for running the alerts & reports schedule (for example, multiple schedulers or Celery beat instances). To avoid duplicate emails or notifications:
- Ensure that only a **single scheduler/beat process** is configured to trigger alerts and reports for a given environment.
- If you run **multiple Celery workers**, verify that there is still only one component responsible for scheduling the report tasks (workers should execute tasks, not schedule them independently).
- Review your deployment/orchestration setup (for example systemd, Docker, or Kubernetes) to make sure the alerts & reports scheduler is **not started from multiple places by accident**.
## Scheduling Queries as Reports
You can optionally allow your users to schedule queries directly in SQL Lab. This is done by adding

View File

@@ -52,11 +52,11 @@ To start a job which schedules periodic background jobs, run the following comma
celery --app=superset.tasks.celery_app:app beat
```
To setup a result backend, you need to pass an instance of a derivative of from
from flask_caching.backends.base import BaseCache to the RESULTS_BACKEND configuration key in your superset_config.py. You can
use Memcached, Redis, S3 (https://pypi.python.org/pypi/s3werkzeugcache), memory or the file system
(in a single server-type setup or for testing), or to write your own caching interface. Your
`superset_config.py` may look something like:
To setup a result backend, you need to pass an instance of a derivative of `BaseCache` (`from
flask_caching.backends.base import BaseCache`) to the RESULTS_BACKEND configuration key in your
superset_config.py. You can use Memcached, Redis, S3 (https://pypi.python.org/pypi/s3werkzeugcache),
memory or the file system (in a single server-type setup or for testing), or to write your own
caching interface. Your `superset_config.py` may look something like:
```python
# On S3

View File

@@ -50,7 +50,7 @@
"@storybook/theming": "^8.6.11",
"@superset-ui/core": "^0.20.4",
"antd": "^5.29.1",
"caniuse-lite": "^1.0.30001756",
"caniuse-lite": "^1.0.30001759",
"docusaurus-plugin-less": "^2.0.2",
"json-bigint": "^1.0.0",
"less": "^4.4.2",
@@ -63,7 +63,7 @@
"remark-import-partial": "^0.0.2",
"reselect": "^5.1.1",
"storybook": "^8.6.11",
"swagger-ui-react": "^5.30.2",
"swagger-ui-react": "^5.30.3",
"tinycolor2": "^1.4.2",
"ts-loader": "^9.5.4"
},
@@ -79,9 +79,9 @@
"eslint-plugin-prettier": "^5.5.3",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.5.0",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"typescript": "~5.9.3",
"typescript-eslint": "^8.47.0",
"typescript-eslint": "^8.48.1",
"webpack": "^5.103.0"
},
"browserslist": {

View File

@@ -80,13 +80,20 @@ const sidebars = {
'extensions/overview',
'extensions/quick-start',
'extensions/architecture',
'extensions/extension-project-structure',
'extensions/extension-metadata',
'extensions/frontend-contribution-types',
'extensions/interacting-with-host',
'extensions/deploying-extension',
'extensions/development-mode',
'extensions/security-implications',
'extensions/contribution-types',
{
type: 'category',
label: 'Extension Points',
collapsed: true,
items: [
'extensions/extension-points/sqllab',
],
},
'extensions/development',
'extensions/deployment',
'extensions/mcp',
'extensions/security',
'extensions/registry',
],
},
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

View File

@@ -4192,102 +4192,101 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.47.0", "@typescript-eslint/eslint-plugin@^8.37.0":
version "8.47.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz#c53edeec13a79483f4ca79c298d5231b02e9dc17"
integrity sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==
"@typescript-eslint/eslint-plugin@8.48.1", "@typescript-eslint/eslint-plugin@^8.37.0":
version "8.48.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz#c772d1dbdd97cfddf85f5a161a97783233643631"
integrity sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==
dependencies:
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "8.47.0"
"@typescript-eslint/type-utils" "8.47.0"
"@typescript-eslint/utils" "8.47.0"
"@typescript-eslint/visitor-keys" "8.47.0"
"@typescript-eslint/scope-manager" "8.48.1"
"@typescript-eslint/type-utils" "8.48.1"
"@typescript-eslint/utils" "8.48.1"
"@typescript-eslint/visitor-keys" "8.48.1"
graphemer "^1.4.0"
ignore "^7.0.0"
natural-compare "^1.4.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/parser@8.47.0", "@typescript-eslint/parser@^8.46.4":
version "8.47.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.47.0.tgz#51b14ab2be2057ec0f57073b9ff3a9c078b0a964"
integrity sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==
"@typescript-eslint/parser@8.48.1", "@typescript-eslint/parser@^8.46.4":
version "8.48.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.48.1.tgz#4e3c66d9ec20683ec142417fafeadab61c479c3f"
integrity sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==
dependencies:
"@typescript-eslint/scope-manager" "8.47.0"
"@typescript-eslint/types" "8.47.0"
"@typescript-eslint/typescript-estree" "8.47.0"
"@typescript-eslint/visitor-keys" "8.47.0"
"@typescript-eslint/scope-manager" "8.48.1"
"@typescript-eslint/types" "8.48.1"
"@typescript-eslint/typescript-estree" "8.48.1"
"@typescript-eslint/visitor-keys" "8.48.1"
debug "^4.3.4"
"@typescript-eslint/project-service@8.47.0":
version "8.47.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.47.0.tgz#b8afc65e0527568018af911b702dcfbfdca16471"
integrity sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==
"@typescript-eslint/project-service@8.48.1":
version "8.48.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.48.1.tgz#cfe1741613b9112d85ae766de9e09b27a7d3f2f1"
integrity sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.47.0"
"@typescript-eslint/types" "^8.47.0"
"@typescript-eslint/tsconfig-utils" "^8.48.1"
"@typescript-eslint/types" "^8.48.1"
debug "^4.3.4"
"@typescript-eslint/scope-manager@8.47.0":
version "8.47.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz#d1c36a973a5499fed3a99e2e6a66aec5c9b1e542"
integrity sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==
"@typescript-eslint/scope-manager@8.48.1":
version "8.48.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz#8bc70643e7cca57864b1ff95dd350fc27756bec0"
integrity sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==
dependencies:
"@typescript-eslint/types" "8.47.0"
"@typescript-eslint/visitor-keys" "8.47.0"
"@typescript-eslint/types" "8.48.1"
"@typescript-eslint/visitor-keys" "8.48.1"
"@typescript-eslint/tsconfig-utils@8.47.0", "@typescript-eslint/tsconfig-utils@^8.47.0":
version "8.47.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz#4f178b62813538759e0989dd081c5474fad39b84"
integrity sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==
"@typescript-eslint/tsconfig-utils@8.48.1", "@typescript-eslint/tsconfig-utils@^8.48.1":
version "8.48.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz#68139ce2d258f984e2b33a95389158f1212af646"
integrity sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==
"@typescript-eslint/type-utils@8.47.0":
version "8.47.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz#b9b0141d99bd5bece3811d7eee68a002597ffa55"
integrity sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==
"@typescript-eslint/type-utils@8.48.1":
version "8.48.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz#955bd3ddd648450f0a627925ff12ade63fb7516d"
integrity sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==
dependencies:
"@typescript-eslint/types" "8.47.0"
"@typescript-eslint/typescript-estree" "8.47.0"
"@typescript-eslint/utils" "8.47.0"
"@typescript-eslint/types" "8.48.1"
"@typescript-eslint/typescript-estree" "8.48.1"
"@typescript-eslint/utils" "8.48.1"
debug "^4.3.4"
ts-api-utils "^2.1.0"
"@typescript-eslint/types@8.47.0", "@typescript-eslint/types@^8.47.0":
version "8.47.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.47.0.tgz#c7fc9b6642d03505f447a8392934b9d1850de5af"
integrity sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==
"@typescript-eslint/types@8.48.1", "@typescript-eslint/types@^8.48.1":
version "8.48.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.48.1.tgz#a9ff808f5f798f28767d5c0b015a88fa7ce46bd7"
integrity sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==
"@typescript-eslint/typescript-estree@8.47.0":
version "8.47.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz#86416dad58db76c4b3bd6a899b1381f9c388489a"
integrity sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==
"@typescript-eslint/typescript-estree@8.48.1":
version "8.48.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz#0d0e31fc47c5796c6463ab50cde19e1718d465b1"
integrity sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==
dependencies:
"@typescript-eslint/project-service" "8.47.0"
"@typescript-eslint/tsconfig-utils" "8.47.0"
"@typescript-eslint/types" "8.47.0"
"@typescript-eslint/visitor-keys" "8.47.0"
"@typescript-eslint/project-service" "8.48.1"
"@typescript-eslint/tsconfig-utils" "8.48.1"
"@typescript-eslint/types" "8.48.1"
"@typescript-eslint/visitor-keys" "8.48.1"
debug "^4.3.4"
fast-glob "^3.3.2"
is-glob "^4.0.3"
minimatch "^9.0.4"
semver "^7.6.0"
tinyglobby "^0.2.15"
ts-api-utils "^2.1.0"
"@typescript-eslint/utils@8.47.0":
version "8.47.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.47.0.tgz#d6c30690431dbfdab98fc027202af12e77c91419"
integrity sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==
"@typescript-eslint/utils@8.48.1":
version "8.48.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.48.1.tgz#6cf7b99e0943b33a983ef687b9a86b65578b5c32"
integrity sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==
dependencies:
"@eslint-community/eslint-utils" "^4.7.0"
"@typescript-eslint/scope-manager" "8.47.0"
"@typescript-eslint/types" "8.47.0"
"@typescript-eslint/typescript-estree" "8.47.0"
"@typescript-eslint/scope-manager" "8.48.1"
"@typescript-eslint/types" "8.48.1"
"@typescript-eslint/typescript-estree" "8.48.1"
"@typescript-eslint/visitor-keys@8.47.0":
version "8.47.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz#35f36ed60a170dfc9d4d738e78387e217f24c29f"
integrity sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==
"@typescript-eslint/visitor-keys@8.48.1":
version "8.48.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz#247d4fe6dcc044f45b7f1c15110bf95e5d73b334"
integrity sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==
dependencies:
"@typescript-eslint/types" "8.47.0"
"@typescript-eslint/types" "8.48.1"
eslint-visitor-keys "^4.2.1"
"@ungap/structured-clone@^1.0.0":
@@ -4957,23 +4956,23 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
body-parser@1.20.3:
version "1.20.3"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6"
integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==
body-parser@~1.20.3:
version "1.20.4"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.4.tgz#f8e20f4d06ca8a50a71ed329c15dccad1cdc547f"
integrity sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==
dependencies:
bytes "3.1.2"
bytes "~3.1.2"
content-type "~1.0.5"
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
http-errors "2.0.0"
iconv-lite "0.4.24"
on-finished "2.4.1"
qs "6.13.0"
raw-body "2.5.2"
destroy "~1.2.0"
http-errors "~2.0.1"
iconv-lite "~0.4.24"
on-finished "~2.4.1"
qs "~6.14.0"
raw-body "~2.5.3"
type-is "~1.6.18"
unpipe "1.0.0"
unpipe "~1.0.0"
bonjour-service@^1.2.1:
version "1.3.0"
@@ -5079,7 +5078,7 @@ bytes@3.0.0:
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==
bytes@3.1.2:
bytes@3.1.2, bytes@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
@@ -5161,10 +5160,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001746, caniuse-lite@^1.0.30001756:
version "1.0.30001756"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz#fe80104631102f88e58cad8aa203a2c3e5ec9ebd"
integrity sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001746, caniuse-lite@^1.0.30001759:
version "1.0.30001759"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz#d569e7b010372c6b0ca3946e30dada0a2e9d5006"
integrity sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==
ccount@^2.0.0:
version "2.0.1"
@@ -5471,7 +5470,7 @@ content-disposition@0.5.2:
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==
content-disposition@0.5.4:
content-disposition@~0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
@@ -5493,15 +5492,15 @@ convert-source-map@^2.0.0:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
cookie-signature@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.7.tgz#ab5dd7ab757c54e60f37ef6550f481c426d10454"
integrity sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==
cookie@0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9"
integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==
cookie@~0.7.1:
version "0.7.2"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
copy-anything@^2.0.1:
version "2.0.6"
@@ -6290,7 +6289,7 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
depd@2.0.0:
depd@2.0.0, depd@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
@@ -6305,7 +6304,7 @@ dequal@^2.0.0, dequal@^2.0.3:
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
destroy@1.2.0:
destroy@1.2.0, destroy@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
@@ -7026,38 +7025,38 @@ execa@5.1.1:
strip-final-newline "^2.0.0"
express@^4.21.2:
version "4.21.2"
resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32"
integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==
version "4.22.0"
resolved "https://registry.yarnpkg.com/express/-/express-4.22.0.tgz#a9d7abdce6d774ed1b4479019387763d1798bd03"
integrity sha512-c2iPh3xp5vvCLgaHK03+mWLFPhox7j1LwyxcZwFVApEv5i0X+IjPpbT50SJJwwLpdBVfp45AkK/v+AFgv/XlfQ==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
body-parser "1.20.3"
content-disposition "0.5.4"
body-parser "~1.20.3"
content-disposition "~0.5.4"
content-type "~1.0.4"
cookie "0.7.1"
cookie-signature "1.0.6"
cookie "~0.7.1"
cookie-signature "~1.0.6"
debug "2.6.9"
depd "2.0.0"
encodeurl "~2.0.0"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "1.3.1"
fresh "0.5.2"
http-errors "2.0.0"
finalhandler "~1.3.1"
fresh "~0.5.2"
http-errors "~2.0.0"
merge-descriptors "1.0.3"
methods "~1.1.2"
on-finished "2.4.1"
on-finished "~2.4.1"
parseurl "~1.3.3"
path-to-regexp "0.1.12"
path-to-regexp "~0.1.12"
proxy-addr "~2.0.7"
qs "6.13.0"
qs "~6.14.0"
range-parser "~1.2.1"
safe-buffer "5.2.1"
send "0.19.0"
serve-static "1.16.2"
send "~0.19.0"
serve-static "~1.16.2"
setprototypeof "1.2.0"
statuses "2.0.1"
statuses "~2.0.1"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
@@ -7089,7 +7088,7 @@ fast-diff@^1.1.2:
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.2:
fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.0:
version "3.3.3"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
@@ -7148,6 +7147,11 @@ faye-websocket@^0.11.3:
dependencies:
websocket-driver ">=0.5.1"
fdir@^6.5.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350"
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
feed@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e"
@@ -7189,17 +7193,17 @@ fill-range@^7.1.1:
dependencies:
to-regex-range "^5.0.1"
finalhandler@1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019"
integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==
finalhandler@~1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.2.tgz#1ebc2228fc7673aac4a472c310cc05b77d852b88"
integrity sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==
dependencies:
debug "2.6.9"
encodeurl "~2.0.0"
escape-html "~1.0.3"
on-finished "2.4.1"
on-finished "~2.4.1"
parseurl "~1.3.3"
statuses "2.0.1"
statuses "~2.0.2"
unpipe "~1.0.0"
find-cache-dir@^4.0.0:
@@ -7292,7 +7296,7 @@ fraction.js@^4.3.7:
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
fresh@0.5.2:
fresh@0.5.2, fresh@~0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
@@ -7855,6 +7859,17 @@ http-errors@~1.6.2:
setprototypeof "1.1.0"
statuses ">= 1.4.0 < 2"
http-errors@~2.0.0, http-errors@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b"
integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==
dependencies:
depd "~2.0.0"
inherits "~2.0.4"
setprototypeof "~1.2.0"
statuses "~2.0.2"
toidentifier "~1.0.1"
http-parser-js@>=0.5.1:
version "0.5.10"
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.10.tgz#b3277bd6d7ed5588e20ea73bf724fcbe44609075"
@@ -7898,13 +7913,6 @@ hyperdyperid@^1.2.0:
resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b"
integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
iconv-lite@0.6, iconv-lite@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
@@ -7912,6 +7920,13 @@ iconv-lite@0.6, iconv-lite@^0.6.3:
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
iconv-lite@~0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
icss-utils@^5.0.0, icss-utils@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
@@ -7980,7 +7995,7 @@ inherits@2.0.3:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -8498,10 +8513,10 @@ js-file-download@^0.4.12:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@=4.1.0, js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
js-yaml@=4.1.1, js-yaml@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
dependencies:
argparse "^2.0.1"
@@ -10018,9 +10033,9 @@ node-fetch-commonjs@^3.3.2:
web-streams-polyfill "^3.0.3"
node-forge@^1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==
version "1.3.2"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.2.tgz#d0d2659a26eef778bf84d73e7f55c08144ee7750"
integrity sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==
node-gyp-build@^4.8.0, node-gyp-build@^4.8.2, node-gyp-build@^4.8.4:
version "4.8.4"
@@ -10136,7 +10151,7 @@ obuf@^1.0.0, obuf@^1.1.2:
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
on-finished@2.4.1, on-finished@^2.4.1:
on-finished@2.4.1, on-finished@^2.4.1, on-finished@~2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
@@ -10409,11 +10424,6 @@ path-parse@^1.0.7:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-to-regexp@0.1.12:
version "0.1.12"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
path-to-regexp@3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b"
@@ -10426,6 +10436,11 @@ path-to-regexp@^1.7.0:
dependencies:
isarray "0.0.1"
path-to-regexp@~0.1.12:
version "0.1.12"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -10446,6 +10461,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
picomatch@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
pify@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
@@ -11063,10 +11083,10 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"
prettier@^3.6.2:
version "3.6.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393"
integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==
prettier@^3.7.4:
version "3.7.4"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f"
integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==
pretty-error@^4.0.0:
version "4.0.0"
@@ -11173,12 +11193,12 @@ pupa@^3.1.0:
dependencies:
escape-goat "^4.0.0"
qs@6.13.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
qs@~6.14.0:
version "6.14.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930"
integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==
dependencies:
side-channel "^1.0.6"
side-channel "^1.1.0"
quansync@^0.2.11:
version "0.2.11"
@@ -11235,15 +11255,15 @@ range-parser@^1.2.1, range-parser@~1.2.1:
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
raw-body@~2.5.3:
version "2.5.3"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.3.tgz#11c6650ee770a7de1b494f197927de0c923822e2"
integrity sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==
dependencies:
bytes "3.1.2"
http-errors "2.0.0"
iconv-lite "0.4.24"
unpipe "1.0.0"
bytes "~3.1.2"
http-errors "~2.0.1"
iconv-lite "~0.4.24"
unpipe "~1.0.0"
rc-cascader@~3.34.0:
version "3.34.0"
@@ -12442,6 +12462,25 @@ send@0.19.0:
range-parser "~1.2.1"
statuses "2.0.1"
send@~0.19.0:
version "0.19.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.19.1.tgz#1c2563b2ee4fe510b806b21ec46f355005a369f9"
integrity sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==
dependencies:
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
encodeurl "~2.0.0"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "2.0.0"
mime "1.6.0"
ms "2.1.3"
on-finished "2.4.1"
range-parser "~1.2.1"
statuses "2.0.1"
serialize-error@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-8.1.0.tgz#3a069970c712f78634942ddd50fbbc0eaebe2f67"
@@ -12482,7 +12521,7 @@ serve-index@^1.9.1:
mime-types "~2.1.17"
parseurl "~1.3.2"
serve-static@1.16.2:
serve-static@~1.16.2:
version "1.16.2"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296"
integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==
@@ -12528,7 +12567,7 @@ setprototypeof@1.1.0:
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
setprototypeof@1.2.0:
setprototypeof@1.2.0, setprototypeof@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
@@ -12605,7 +12644,7 @@ side-channel-weakmap@^1.0.2:
object-inspect "^1.13.3"
side-channel-map "^1.0.1"
side-channel@^1.0.6, side-channel@^1.1.0:
side-channel@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9"
integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==
@@ -12760,6 +12799,11 @@ statuses@2.0.1:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
statuses@~2.0.1, statuses@~2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382"
integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==
std-env@^3.7.0:
version "3.9.0"
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1"
@@ -13027,10 +13071,10 @@ swagger-client@^3.36.0:
ramda "^0.30.1"
ramda-adjunct "^5.1.0"
swagger-ui-react@^5.30.2:
version "5.30.2"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.30.2.tgz#d02fe73e3f895f67d1ab8bc02aadccfad55b1a2b"
integrity sha512-0tS9GOcswKuQrIpCyvDoCDs6xS8B6MRC+iE7P99WfVXDhAIU+U7iFHuS4e7zucSh9qXvcL7KsXs623c+4oBe6w==
swagger-ui-react@^5.30.3:
version "5.30.3"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.30.3.tgz#5c8fd646a82d00e038f1bc0b689b10192e2611ed"
integrity sha512-QIy32nPql6yiV2NVwbww1P7f6HEOAuYrnk8VEJkzPC/p6Xc5Xnz9hhmSHzXTuM70fDbVw/qPzCJ0mZMMultpiw==
dependencies:
"@babel/runtime-corejs3" "^7.27.1"
"@scarf/scarf" "=1.4.0"
@@ -13043,7 +13087,7 @@ swagger-ui-react@^5.30.2:
ieee754 "^1.2.1"
immutable "^3.x.x"
js-file-download "^0.4.12"
js-yaml "=4.1.0"
js-yaml "=4.1.1"
lodash "^4.17.21"
prop-types "^15.8.1"
randexp "^0.5.3"
@@ -13148,6 +13192,14 @@ tinyexec@^1.0.1:
resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.0.1.tgz#70c31ab7abbb4aea0a24f55d120e5990bfa1e0b1"
integrity sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==
tinyglobby@^0.2.15:
version "0.2.15"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
dependencies:
fdir "^6.5.0"
picomatch "^4.0.3"
tinypool@^1.0.2:
version "1.1.1"
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.1.1.tgz#059f2d042bd37567fbc017d3d426bdd2a2612591"
@@ -13174,7 +13226,7 @@ toggle-selection@^1.0.6:
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
toidentifier@1.0.1:
toidentifier@1.0.1, toidentifier@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
@@ -13358,15 +13410,15 @@ types-ramda@^0.30.1:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.47.0:
version "8.47.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.47.0.tgz#bb8fcf4f2c69ffcd5d088f7f30cd52936ff05cbc"
integrity sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==
typescript-eslint@^8.48.1:
version "8.48.1"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.48.1.tgz#436028540f5859755687b8b1b28e19ed9194aaad"
integrity sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==
dependencies:
"@typescript-eslint/eslint-plugin" "8.47.0"
"@typescript-eslint/parser" "8.47.0"
"@typescript-eslint/typescript-estree" "8.47.0"
"@typescript-eslint/utils" "8.47.0"
"@typescript-eslint/eslint-plugin" "8.48.1"
"@typescript-eslint/parser" "8.48.1"
"@typescript-eslint/typescript-estree" "8.48.1"
"@typescript-eslint/utils" "8.48.1"
typescript@~5.9.3:
version "5.9.3"
@@ -13569,7 +13621,7 @@ universalify@^2.0.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
unpipe@1.0.0, unpipe@~1.0.0:
unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==

View File

@@ -1,5 +0,0 @@
# Requirements for the Snowflake Semantic Layer extension
# Install with: pip install -r extensions/requirements-snowflake.txt
snowflake-connector-python>=3.0.0
snowflake-sqlalchemy>=1.5.0

View File

@@ -212,6 +212,7 @@ 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",
"pytest-cov",
"pytest-mock",
"python-ldap>=3.4.4",
@@ -351,6 +352,7 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
"superset/translations/utils.py" = ["TID251"]
"superset/extensions/__init__.py" = ["TID251"]
"superset/utils/json.py" = ["TID251"]
"docker/*" = ["I"] # Docker config files have non-standard imports that vary by environment
[tool.ruff.lint.isort]
case-sensitive = false

View File

@@ -19,3 +19,4 @@ testpaths =
tests
python_files = *_test.py test_*.py *_tests.py *viz/utils.py
addopts = -p no:warnings
asyncio_mode = auto

View File

@@ -298,7 +298,9 @@ pyasn1-modules==0.4.2
pycparser==2.22
# via cffi
pydantic==2.11.7
# via apache-superset (pyproject.toml)
# via
# apache-superset (pyproject.toml)
# apache-superset-core
pydantic-core==2.33.2
# via pydantic
pygments==2.19.1

View File

@@ -718,6 +718,7 @@ pydantic==2.11.7
# via
# -c requirements/base-constraint.txt
# apache-superset
# apache-superset-core
# fastmcp
# mcp
# openapi-pydantic
@@ -774,8 +775,11 @@ pytest==7.4.4
# via
# apache-superset
# apache-superset-extensions-cli
# pytest-asyncio
# pytest-cov
# pytest-mock
pytest-asyncio==0.23.8
# via apache-superset
pytest-cov==6.0.0
# via
# apache-superset
@@ -882,7 +886,7 @@ rsa==4.9.1
# via
# -c requirements/base-constraint.txt
# google-auth
ruff==0.8.0
ruff==0.9.7
# via apache-superset
secretstorage==3.4.1
# via keyring

View File

@@ -43,6 +43,7 @@ classifiers = [
]
dependencies = [
"flask-appbuilder>=5.0.2,<6",
"pydantic>=2.8.0",
"sqlalchemy>=1.4.0,<2.0",
"sqlalchemy-utils>=0.38.0",
"sqlglot>=27.15.2, <28",

View File

@@ -0,0 +1,161 @@
# 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.
"""
MCP (Model Context Protocol) tool registration for Superset MCP server.
This module provides a decorator interface to register MCP tools with the
host application.
Usage:
from superset_core.mcp import tool
@tool(name="my_tool", description="Custom business logic", tags=["extension"])
def my_extension_tool(param: str) -> dict:
return {"message": f"Hello {param}!"}
# Or use function name and docstring:
@tool
def another_tool(value: int) -> str:
'''Tool description from docstring'''
return str(value * 2)
"""
from typing import Any, Callable, TypeVar
# Type variable for decorated functions
F = TypeVar("F", bound=Callable[..., Any])
def tool(
func_or_name: str | Callable[..., Any] | None = None,
*,
name: str | None = None,
description: str | None = None,
tags: list[str] | None = None,
protect: bool = True,
) -> Any: # Use Any to avoid mypy issues with dependency injection
"""
Decorator to register an MCP tool with optional authentication.
This decorator combines FastMCP tool registration with optional authentication.
Can be used as:
@tool
def my_tool(): ...
Or:
@tool(name="custom_name", protect=False)
def my_tool(): ...
Args:
func_or_name: When used as @tool, this will be the function.
When used as @tool("name"), this will be the name.
name: Tool name (defaults to function name, prefixed with extension ID)
description: Tool description (defaults to function docstring)
tags: List of tags for categorizing the tool (defaults to empty list)
protect: Whether to require Superset authentication (defaults to True)
Returns:
Decorator function that registers and wraps the tool, or the wrapped function
Raises:
NotImplementedError: If called before host implementation is initialized
Example:
@tool(name="my_tool", description="Does something useful", tags=["utility"])
def my_custom_tool(param: str) -> dict:
return {"result": param}
@tool # Uses function name and docstring with auth
def simple_tool(value: int) -> str:
'''Doubles the input value'''
return str(value * 2)
@tool(protect=False) # No authentication required
def public_tool() -> str:
'''Public tool accessible without auth'''
return "Hello world"
"""
raise NotImplementedError(
"MCP tool decorator not initialized. "
"This decorator should be replaced during Superset startup."
)
def prompt(
func_or_name: str | Callable[..., Any] | None = None,
*,
name: str | None = None,
title: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
protect: bool = True,
) -> Any: # Use Any to avoid mypy issues with dependency injection
"""
Decorator to register an MCP prompt with optional authentication.
This decorator combines FastMCP prompt registration with optional authentication.
Can be used as:
@prompt
async def my_prompt_handler(): ...
Or:
@prompt("my_prompt")
async def my_prompt_handler(): ...
Or:
@prompt("my_prompt", protected=False, title="Custom Title")
async def my_prompt_handler(): ...
Args:
func_or_name: When used as @prompt, this will be the function.
When used as @prompt("name"), this will be the name.
name: Prompt name (defaults to function name if not provided)
title: Prompt title (defaults to function name)
description: Prompt description (defaults to function docstring)
tags: Set of tags for categorizing the prompt
protect: Whether to require Superset authentication (defaults to True)
Returns:
Decorator function that registers and wraps the prompt, or the wrapped function
Raises:
NotImplementedError: If called before host implementation is initialized
Example:
@prompt
async def my_prompt_handler(ctx: Context) -> str:
'''Interactive prompt for doing something.'''
return "Prompt instructions here..."
@prompt("custom_prompt", protect=False, title="Custom Title")
async def public_prompt_handler(ctx: Context) -> str:
'''Public prompt accessible without auth'''
return "Public prompt accessible without auth"
"""
raise NotImplementedError(
"MCP prompt decorator not initialized. "
"This decorator should be replaced during Superset startup."
)
__all__ = [
"tool",
"prompt",
]

View File

@@ -1,12 +1,12 @@
{
"name": "@superset-ui/embedded-sdk",
"version": "0.2.0",
"version": "0.3.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@superset-ui/embedded-sdk",
"version": "0.2.0",
"version": "0.3.0",
"license": "Apache-2.0",
"dependencies": {
"@superset-ui/switchboard": "^0.20.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@superset-ui/embedded-sdk",
"version": "0.2.0",
"version": "0.3.0",
"description": "SDK for embedding resources from Superset into your own application",
"access": "public",
"keywords": [

View File

@@ -93,6 +93,7 @@ export type EmbeddedDashboard = {
) => void;
getDataMask: () => Promise<Record<string, any>>;
getChartStates: () => Promise<Record<string, any>>;
getChartDataPayloads: (params?: { chartId?: number }) => Promise<Record<string, any>>;
setThemeConfig: (themeConfig: Record<string, any>) => void;
setThemeMode: (mode: ThemeMode) => void;
};
@@ -249,6 +250,8 @@ export async function embedDashboard({
const getActiveTabs = () => ourPort.get<string[]>('getActiveTabs');
const getDataMask = () => ourPort.get<Record<string, any>>('getDataMask');
const getChartStates = () => ourPort.get<Record<string, any>>('getChartStates');
const getChartDataPayloads = (params?: { chartId?: number }) =>
ourPort.get<Record<string, any>>('getChartDataPayloads', params);
const observeDataMask = (
callbackFn: ObserveDataMaskCallbackFn,
) => {
@@ -288,6 +291,7 @@ export async function embedDashboard({
observeDataMask,
getDataMask,
getChartStates,
getChartDataPayloads,
setThemeConfig,
setThemeMode,
};

View File

@@ -404,19 +404,57 @@ def dev(ctx: click.Context) -> None:
@app.command()
def init() -> None:
id_ = click.prompt("Extension ID (unique identifier, alphanumeric only)", type=str)
@click.option(
"--id",
"id_opt",
default=None,
help="Extension ID (alphanumeric and underscores only)",
)
@click.option("--name", "name_opt", default=None, help="Extension display name")
@click.option(
"--version", "version_opt", default=None, help="Initial version (default: 0.1.0)"
)
@click.option(
"--license", "license_opt", default=None, help="License (default: Apache-2.0)"
)
@click.option(
"--frontend/--no-frontend", "frontend_opt", default=None, help="Include frontend"
)
@click.option(
"--backend/--no-backend", "backend_opt", default=None, help="Include backend"
)
def init(
id_opt: str | None,
name_opt: str | None,
version_opt: str | None,
license_opt: str | None,
frontend_opt: bool | None,
backend_opt: bool | None,
) -> None:
id_ = id_opt or click.prompt(
"Extension ID (unique identifier, alphanumeric only)", type=str
)
if not re.match(r"^[a-zA-Z0-9_]+$", id_):
click.secho(
"❌ ID must be alphanumeric (letters, digits, underscore).", fg="red"
)
sys.exit(1)
name = click.prompt("Extension name (human-readable display name)", type=str)
version = click.prompt("Initial version", default="0.1.0")
license = click.prompt("License", default="Apache-2.0")
include_frontend = click.confirm("Include frontend?", default=True)
include_backend = click.confirm("Include backend?", default=True)
name = name_opt or click.prompt(
"Extension name (human-readable display name)", type=str
)
version = version_opt or click.prompt("Initial version", default="0.1.0")
license_ = license_opt or click.prompt("License", default="Apache-2.0")
include_frontend = (
frontend_opt
if frontend_opt is not None
else click.confirm("Include frontend?", default=True)
)
include_backend = (
backend_opt
if backend_opt is not None
else click.confirm("Include backend?", default=True)
)
target_dir = Path.cwd() / id_
if target_dir.exists():
@@ -431,7 +469,7 @@ def init() -> None:
"name": name,
"include_frontend": include_frontend,
"include_backend": include_backend,
"license": license,
"license": license_,
"version": version,
}

View File

@@ -360,3 +360,139 @@ def test_full_init_workflow_integration(cli_runner, isolated_filesystem):
pyproject_content = (extension_path / "backend" / "pyproject.toml").read_text()
assert "awesome_charts" in pyproject_content
# Non-interactive mode tests
@pytest.mark.cli
def test_init_non_interactive_with_all_options(cli_runner, isolated_filesystem):
"""Test that init works in non-interactive mode with all CLI options."""
result = cli_runner.invoke(
app,
[
"init",
"--id",
"my_ext",
"--name",
"My Extension",
"--version",
"1.0.0",
"--license",
"MIT",
"--frontend",
"--backend",
],
)
assert result.exit_code == 0, f"Command failed with output: {result.output}"
assert "🎉 Extension My Extension (ID: my_ext) initialized" in result.output
extension_path = isolated_filesystem / "my_ext"
assert_directory_exists(extension_path)
assert_directory_exists(extension_path / "frontend")
assert_directory_exists(extension_path / "backend")
extension_json = load_json_file(extension_path / "extension.json")
assert extension_json["id"] == "my_ext"
assert extension_json["name"] == "My Extension"
assert extension_json["version"] == "1.0.0"
assert extension_json["license"] == "MIT"
@pytest.mark.cli
def test_init_frontend_only_with_cli_options(cli_runner, isolated_filesystem):
"""Test init with frontend only using CLI options."""
result = cli_runner.invoke(
app,
[
"init",
"--id",
"frontend_ext",
"--name",
"Frontend Extension",
"--version",
"1.0.0",
"--license",
"MIT",
"--frontend",
"--no-backend",
],
)
assert result.exit_code == 0, f"Command failed with output: {result.output}"
extension_path = isolated_filesystem / "frontend_ext"
assert_directory_exists(extension_path / "frontend")
assert not (extension_path / "backend").exists()
@pytest.mark.cli
def test_init_backend_only_with_cli_options(cli_runner, isolated_filesystem):
"""Test init with backend only using CLI options."""
result = cli_runner.invoke(
app,
[
"init",
"--id",
"backend_ext",
"--name",
"Backend Extension",
"--version",
"1.0.0",
"--license",
"MIT",
"--no-frontend",
"--backend",
],
)
assert result.exit_code == 0, f"Command failed with output: {result.output}"
extension_path = isolated_filesystem / "backend_ext"
assert not (extension_path / "frontend").exists()
assert_directory_exists(extension_path / "backend")
@pytest.mark.cli
def test_init_prompts_for_missing_options(cli_runner, isolated_filesystem):
"""Test that init prompts for options not provided via CLI and uses defaults."""
# Provide id and name via CLI, but version/license will be prompted (accept defaults)
result = cli_runner.invoke(
app,
[
"init",
"--id",
"default_ext",
"--name",
"Default Extension",
"--frontend",
"--backend",
],
input="\n\n", # Accept defaults for version and license prompts
)
assert result.exit_code == 0, f"Command failed with output: {result.output}"
extension_path = isolated_filesystem / "default_ext"
extension_json = load_json_file(extension_path / "extension.json")
assert extension_json["version"] == "0.1.0"
assert extension_json["license"] == "Apache-2.0"
@pytest.mark.cli
def test_init_non_interactive_validates_id(cli_runner, isolated_filesystem):
"""Test that non-interactive mode validates extension ID."""
result = cli_runner.invoke(
app,
[
"init",
"--id",
"invalid-id",
"--name",
"Invalid Extension",
"--frontend",
"--backend",
],
)
assert result.exit_code == 1
assert "must be alphanumeric" in result.output

File diff suppressed because it is too large Load Diff

View File

@@ -156,7 +156,6 @@
"geostyler": "^14.1.3",
"geostyler-data": "^1.1.0",
"geostyler-openlayers-parser": "^4.3.0",
"geostyler-qgis-parser": "2.0.1",
"geostyler-style": "7.5.0",
"geostyler-wfs-parser": "^2.0.3",
"googleapis": "^154.1.0",

View File

@@ -36,7 +36,7 @@
"devDependencies": {
"cross-env": "^10.1.0",
"fs-extra": "^11.3.2",
"jest": "^30.0.5",
"jest": "^30.2.0",
"yeoman-test": "^10.1.1"
},
"engines": {

View File

@@ -462,6 +462,10 @@ export enum Comparator {
EndsWith = 'ends with',
Containing = 'containing',
NotContaining = 'not containing',
IsTrue = 'is true',
IsFalse = 'is false',
IsNull = 'is null',
IsNotNull = 'is not null',
}
export const MultipleValueComparators = [
@@ -480,13 +484,16 @@ export type ConditionalFormattingConfig = {
colorScheme?: string;
toAllRow?: boolean;
toTextColor?: boolean;
useGradient?: boolean;
};
export type ColorFormatters = {
column: string;
toAllRow?: boolean;
toTextColor?: boolean;
getColorFromValue: (value: number | string) => string | undefined;
getColorFromValue: (
value: number | string | boolean | null,
) => string | undefined;
}[];
export default {};

View File

@@ -32,7 +32,7 @@ const MIN_OPACITY_BOUNDED = 0.05;
const MIN_OPACITY_UNBOUNDED = 0;
const MAX_OPACITY = 1;
export const getOpacity = (
value: number | string,
value: number | string | boolean | null,
cutoffPoint: number | string,
extremeValue: number | string,
minOpacity = MIN_OPACITY_BOUNDED,
@@ -69,16 +69,17 @@ export const getColorFunction = (
targetValueLeft,
targetValueRight,
colorScheme,
useGradient,
}: ConditionalFormattingConfig,
columnValues: number[] | string[],
columnValues: number[] | string[] | (boolean | null)[],
alpha?: boolean,
) => {
let minOpacity = MIN_OPACITY_BOUNDED;
const maxOpacity = MAX_OPACITY;
let comparatorFunction: (
value: number | string,
allValues: number[] | string[],
value: number | string | boolean | null,
allValues: number[] | string[] | (boolean | null)[],
) => false | { cutoffValue: number | string; extremeValue: number | string };
if (operator === undefined || colorScheme === undefined) {
return () => undefined;
@@ -221,16 +222,48 @@ export const getColorFunction = (
!value?.toLowerCase().includes((targetValue as string).toLowerCase())
? { cutoffValue: targetValue!, extremeValue: targetValue! }
: false;
break;
case Comparator.IsTrue:
comparatorFunction = (value: boolean | null) =>
isBoolean(value) && value
? { cutoffValue: targetValue!, extremeValue: targetValue! }
: false;
break;
case Comparator.IsFalse:
comparatorFunction = (value: boolean | null) =>
isBoolean(value) && !value
? { cutoffValue: targetValue!, extremeValue: targetValue! }
: false;
break;
case Comparator.IsNull:
comparatorFunction = (value: boolean | null) =>
value === null
? { cutoffValue: targetValue!, extremeValue: targetValue! }
: false;
break;
case Comparator.IsNotNull:
comparatorFunction = (value: boolean | null) =>
isBoolean(value) && value !== null
? { cutoffValue: targetValue!, extremeValue: targetValue! }
: false;
break;
default:
comparatorFunction = () => false;
break;
}
return (value: number | string) => {
return (value: number | string | boolean | null) => {
const compareResult = comparatorFunction(value, columnValues);
if (compareResult === false) return undefined;
const { cutoffValue, extremeValue } = compareResult;
// If useGradient is explicitly false, return solid color
if (useGradient === false) {
return colorScheme;
}
// Otherwise apply gradient (default behavior for backward compatibility)
if (alpha === undefined || alpha) {
return addAlpha(
colorScheme,
@@ -289,3 +322,7 @@ export const getColorFormatters = memoizeOne(
function isString(value: unknown) {
return typeof value === 'string';
}
function isBoolean(value: unknown) {
return typeof value === 'boolean';
}

View File

@@ -35,6 +35,9 @@ const countValues = mockData.map(row => row.count);
const strData = [{ name: 'Brian' }, { name: 'Carlos' }, { name: 'Diana' }];
const strValues = strData.map(row => row.name);
const boolData = [{ isMember: true }, { isMember: false }, { isMember: null }];
const boolValues = boolData.map(row => row.isMember);
test('round', () => {
expect(round(1)).toEqual(1);
expect(round(1, 2)).toEqual(1);
@@ -443,6 +446,66 @@ test('getColorFunction None', () => {
expect(colorFunction('Brian')).toEqual('#FF0000FF');
});
test('getColorFunction IsTrue', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.IsTrue,
targetValue: '',
colorScheme: '#FF0000',
column: 'isMember',
},
boolValues,
);
expect(colorFunction(true)).toEqual('#FF0000FF');
expect(colorFunction(false)).toBeUndefined();
expect(colorFunction(null)).toBeUndefined();
});
test('getColorFunction IsFalse', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.IsFalse,
targetValue: '',
colorScheme: '#FF0000',
column: 'isMember',
},
boolValues,
);
expect(colorFunction(true)).toBeUndefined();
expect(colorFunction(false)).toEqual('#FF0000FF');
expect(colorFunction(null)).toBeUndefined();
});
test('getColorFunction IsNull', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.IsNull,
targetValue: '',
colorScheme: '#FF0000',
column: 'isMember',
},
boolValues,
);
expect(colorFunction(true)).toBeUndefined();
expect(colorFunction(false)).toBeUndefined();
expect(colorFunction(null)).toEqual('#FF0000FF');
});
test('getColorFunction IsNotNull', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.IsNotNull,
targetValue: '',
colorScheme: '#FF0000',
column: 'isMember',
},
boolValues,
);
expect(colorFunction(true)).toEqual('#FF0000FF');
expect(colorFunction(false)).toEqual('#FF0000FF');
expect(colorFunction(null)).toBeUndefined();
});
test('correct column config', () => {
const columnConfig = [
{
@@ -532,3 +595,145 @@ test('correct column string config', () => {
expect(colorFormatters[3].column).toEqual('name');
expect(colorFormatters[3].getColorFromValue('Carlos')).toEqual('#FF0000FF');
});
test('getColorFunction with useGradient false returns solid color', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.GreaterOrEqual,
targetValue: 50,
colorScheme: '#FF0000',
column: 'count',
useGradient: false,
},
countValues,
);
// When useGradient is false, should return solid color without opacity
expect(colorFunction(50)).toEqual('#FF0000');
expect(colorFunction(100)).toEqual('#FF0000');
expect(colorFunction(0)).toBeUndefined();
});
test('getColorFunction with useGradient true returns gradient color', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.GreaterOrEqual,
targetValue: 50,
colorScheme: '#FF0000',
column: 'count',
useGradient: true,
},
countValues,
);
// When useGradient is true, should return gradient color with opacity
expect(colorFunction(50)).toEqual('#FF00000D');
expect(colorFunction(100)).toEqual('#FF0000FF');
expect(colorFunction(0)).toBeUndefined();
});
test('getColorFunction with useGradient undefined defaults to gradient (backward compatibility)', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.GreaterOrEqual,
targetValue: 50,
colorScheme: '#FF0000',
column: 'count',
// useGradient is undefined
},
countValues,
);
// When useGradient is undefined, should default to gradient for backward compatibility
expect(colorFunction(50)).toEqual('#FF00000D');
expect(colorFunction(100)).toEqual('#FF0000FF');
expect(colorFunction(0)).toBeUndefined();
});
test('getColorFunction with useGradient false and None operator returns solid color', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: '#FF0000',
column: 'count',
useGradient: false,
},
countValues,
);
// When useGradient is false, all matching values should return solid color
expect(colorFunction(20)).toBeUndefined();
expect(colorFunction(50)).toEqual('#FF0000');
expect(colorFunction(75)).toEqual('#FF0000');
expect(colorFunction(100)).toEqual('#FF0000');
expect(colorFunction(120)).toBeUndefined();
});
test('getColorFormatters with useGradient flag', () => {
const columnConfig = [
{
operator: Comparator.GreaterThan,
targetValue: 50,
colorScheme: '#FF0000',
column: 'count',
useGradient: false,
},
{
operator: Comparator.GreaterThan,
targetValue: 50,
colorScheme: '#00FF00',
column: 'count',
useGradient: true,
},
];
const colorFormatters = getColorFormatters(columnConfig, mockData);
expect(colorFormatters.length).toEqual(2);
// First formatter with useGradient: false should return solid color
expect(colorFormatters[0].column).toEqual('count');
expect(colorFormatters[0].getColorFromValue(100)).toEqual('#FF0000');
// Second formatter with useGradient: true should return gradient color
expect(colorFormatters[1].column).toEqual('count');
expect(colorFormatters[1].getColorFromValue(100)).toEqual('#00FF00FF');
});
test('correct column boolean config', () => {
const columnConfigBoolean = [
{
operator: Comparator.IsTrue,
targetValue: '',
colorScheme: '#FF0000',
column: 'isMember',
},
{
operator: Comparator.IsFalse,
targetValue: '',
colorScheme: '#FF0000',
column: 'isMember',
},
{
operator: Comparator.IsNull,
targetValue: '',
colorScheme: '#FF0000',
column: 'isMember',
},
{
operator: Comparator.IsNotNull,
targetValue: '',
colorScheme: '#FF0000',
column: 'isMember',
},
];
const colorFormatters = getColorFormatters(columnConfigBoolean, boolData);
expect(colorFormatters.length).toEqual(4);
expect(colorFormatters[0].column).toEqual('isMember');
expect(colorFormatters[0].getColorFromValue(true)).toEqual('#FF0000FF');
expect(colorFormatters[1].column).toEqual('isMember');
expect(colorFormatters[1].getColorFromValue(false)).toEqual('#FF0000FF');
expect(colorFormatters[2].column).toEqual('isMember');
expect(colorFormatters[2].getColorFromValue(null)).toEqual('#FF0000FF');
expect(colorFormatters[3].column).toEqual('isMember');
expect(colorFormatters[3].getColorFromValue(true)).toEqual('#FF0000FF');
expect(colorFormatters[3].getColorFromValue(false)).toEqual('#FF0000FF');
});

View File

@@ -0,0 +1,331 @@
/**
* 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 { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { EmojiTextArea, type EmojiItem } from '.';
const meta: Meta<typeof EmojiTextArea> = {
title: 'Components/EmojiTextArea',
component: EmojiTextArea,
parameters: {
docs: {
description: {
component: `
A TextArea component with Slack-like emoji autocomplete.
## Features
- **Colon prefix trigger**: Type \`:sm\` to see smile emoji suggestions
- **Minimum 2 characters**: Popup only shows after typing 2+ characters (configurable)
- **Smart trigger detection**: Colon must be preceded by whitespace, start of line, or another emoji
- **Prevents accidental selection**: Quick Enter keypress creates newline instead of selecting
## Usage
\`\`\`tsx
import { EmojiTextArea } from '@superset-ui/core/components';
<EmojiTextArea
placeholder="Type :smile: to add emojis..."
onChange={(text) => console.log(text)}
onEmojiSelect={(emoji) => console.log('Selected:', emoji)}
/>
\`\`\`
## Trigger Behavior (Slack-like)
The emoji picker triggers in these scenarios:
- \`:sm\` - at the start of text
- \`hello :sm\` - after a space
- \`😀:sm\` - after another emoji
It does NOT trigger in:
- \`hello:sm\` - no space before colon
- \`http://example.com\` - colon preceded by letter
Try it out below!
`,
},
},
},
argTypes: {
minCharsBeforePopup: {
control: { type: 'number', min: 1, max: 5 },
description: 'Minimum characters after colon before showing popup',
defaultValue: 2,
},
maxSuggestions: {
control: { type: 'number', min: 1, max: 20 },
description: 'Maximum number of emoji suggestions to show',
defaultValue: 10,
},
placeholder: {
control: 'text',
description: 'Placeholder text',
},
rows: {
control: { type: 'number', min: 1, max: 20 },
description: 'Number of visible rows',
},
},
};
export default meta;
type Story = StoryObj<typeof EmojiTextArea>;
export const Default: Story = {
args: {
placeholder: 'Type :smile: or :thumbsup: to add emojis...',
rows: 4,
style: { width: '100%', maxWidth: 500 },
},
};
export const WithMinChars: Story = {
args: {
...Default.args,
minCharsBeforePopup: 3,
placeholder: 'Requires 3 characters after colon (e.g., :smi)',
},
};
export const WithMaxSuggestions: Story = {
args: {
...Default.args,
maxSuggestions: 5,
placeholder: 'Shows max 5 suggestions',
},
};
export const Controlled: Story = {
render: function ControlledStory() {
const [value, setValue] = useState('');
const [selectedEmojis, setSelectedEmojis] = useState<EmojiItem[]>([]);
return (
<div style={{ maxWidth: 500 }}>
<EmojiTextArea
value={value}
onChange={setValue}
onEmojiSelect={emoji => setSelectedEmojis(prev => [...prev, emoji])}
placeholder="Type :smile: or :heart: to add emojis..."
rows={4}
style={{ width: '100%' }}
/>
<div style={{ marginTop: 16 }}>
<strong>Current value:</strong>
<pre
style={{
background: 'var(--ant-color-bg-container)',
padding: 8,
borderRadius: 4,
border: '1px solid var(--ant-color-border)',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{value || '(empty)'}
</pre>
</div>
{selectedEmojis.length > 0 && (
<div style={{ marginTop: 16 }}>
<strong>Selected emojis:</strong>
<div style={{ fontSize: 24, marginTop: 8 }}>
{selectedEmojis.map((e, i) => (
<span key={i} title={`:${e.shortcode}:`}>
{e.emoji}
</span>
))}
</div>
</div>
)}
</div>
);
},
};
export const SlackBehaviorDemo: Story = {
render: function SlackBehaviorDemoStory() {
const examples = [
{ input: ':sm', works: true, desc: 'Start of text' },
{ input: 'hello :sm', works: true, desc: 'After space' },
{
input: '😀:sm',
works: true,
desc: 'After emoji',
needsEmoji: true,
},
{ input: 'hello:sm', works: false, desc: 'No space before colon' },
{ input: ':s', works: false, desc: 'Only 1 character' },
];
return (
<div style={{ maxWidth: 600 }}>
<h3>Slack-like Trigger Behavior</h3>
<p style={{ color: 'var(--ant-color-text-secondary)' }}>
The emoji picker mimics Slack&apos;s behavior. Try these examples:
</p>
<table
style={{
width: '100%',
borderCollapse: 'collapse',
marginBottom: 24,
}}
>
<thead>
<tr>
<th
style={{
textAlign: 'left',
padding: 8,
borderBottom: '1px solid var(--ant-color-border)',
}}
>
Input
</th>
<th
style={{
textAlign: 'left',
padding: 8,
borderBottom: '1px solid var(--ant-color-border)',
}}
>
Shows Popup?
</th>
<th
style={{
textAlign: 'left',
padding: 8,
borderBottom: '1px solid var(--ant-color-border)',
}}
>
Reason
</th>
</tr>
</thead>
<tbody>
{examples.map((ex, i) => (
<tr key={i}>
<td
style={{
padding: 8,
borderBottom: '1px solid var(--ant-color-border)',
fontFamily: 'monospace',
}}
>
{ex.input}
</td>
<td
style={{
padding: 8,
borderBottom: '1px solid var(--ant-color-border)',
}}
>
{ex.works ? '✅ Yes' : '❌ No'}
</td>
<td
style={{
padding: 8,
borderBottom: '1px solid var(--ant-color-border)',
}}
>
{ex.desc}
</td>
</tr>
))}
</tbody>
</table>
<EmojiTextArea
placeholder="Try the examples above..."
rows={4}
style={{ width: '100%' }}
/>
</div>
);
},
};
export const InForm: Story = {
render: function InFormStory() {
const [description, setDescription] = useState('');
const [title, setTitle] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// eslint-disable-next-line no-alert
alert(`Title: ${title}\nDescription: ${description}`);
};
return (
<form onSubmit={handleSubmit} style={{ maxWidth: 500 }}>
<div style={{ marginBottom: 16 }}>
<label htmlFor="title" style={{ display: 'block', marginBottom: 4 }}>
Title
</label>
<input
id="title"
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="Enter a title"
style={{
width: '100%',
padding: 8,
borderRadius: 4,
border: '1px solid var(--ant-color-border)',
}}
/>
</div>
<div style={{ marginBottom: 16 }}>
<label
htmlFor="description"
style={{ display: 'block', marginBottom: 4 }}
>
Description (with emoji support)
</label>
<EmojiTextArea
id="description"
value={description}
onChange={setDescription}
placeholder="Add a description... use :smile: for emojis!"
rows={4}
style={{ width: '100%' }}
/>
</div>
<button
type="submit"
style={{
padding: '8px 16px',
background: 'var(--ant-color-primary)',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
}}
>
Submit
</button>
</form>
);
},
};

View File

@@ -0,0 +1,170 @@
/**
* 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, userEvent } from '@superset-ui/core/spec';
import { EmojiTextArea } from '.';
import { filterEmojis, EMOJI_DATA } from './emojiData';
test('renders EmojiTextArea with placeholder', () => {
render(<EmojiTextArea placeholder="Type something..." />);
expect(screen.getByPlaceholderText('Type something...')).toBeInTheDocument();
});
test('renders EmojiTextArea as textarea element', () => {
render(<EmojiTextArea placeholder="Type here" />);
const textarea = screen.getByPlaceholderText('Type here');
expect(textarea.tagName.toLowerCase()).toBe('textarea');
});
test('allows typing in the textarea', async () => {
render(<EmojiTextArea placeholder="Type here" />);
const textarea = screen.getByPlaceholderText('Type here');
await userEvent.type(textarea, 'Hello world');
expect(textarea).toHaveValue('Hello world');
});
test('calls onChange when typing', async () => {
const onChange = jest.fn();
render(<EmojiTextArea placeholder="Type here" onChange={onChange} />);
const textarea = screen.getByPlaceholderText('Type here');
await userEvent.type(textarea, 'Hi');
expect(onChange).toHaveBeenCalled();
});
test('passes through rows prop', () => {
render(<EmojiTextArea placeholder="Type here" rows={5} />);
const textarea = screen.getByPlaceholderText('Type here');
expect(textarea).toHaveAttribute('rows', '5');
});
test('forwards ref to underlying component', () => {
const ref = { current: null };
render(<EmojiTextArea ref={ref} placeholder="Type here" />);
expect(ref.current).not.toBeNull();
});
test('renders controlled component with value prop', () => {
render(<EmojiTextArea value="Hello" onChange={() => {}} />);
expect(screen.getByDisplayValue('Hello')).toBeInTheDocument();
});
// ============================================
// Unit tests for filterEmojis utility function
// ============================================
test('filterEmojis returns matching emojis by shortcode', () => {
const results = filterEmojis('smile');
expect(results.length).toBeGreaterThan(0);
expect(results[0].shortcode).toBe('smile');
});
test('filterEmojis returns matching emojis by partial shortcode', () => {
const results = filterEmojis('sm');
expect(results.length).toBeGreaterThan(0);
// Should include smile, smirk, etc.
expect(results.some(e => e.shortcode.includes('sm'))).toBe(true);
});
test('filterEmojis returns matching emojis by keyword', () => {
const results = filterEmojis('happy');
expect(results.length).toBeGreaterThan(0);
// Should include emojis with 'happy' keyword
expect(results.some(e => e.keywords?.includes('happy'))).toBe(true);
});
test('filterEmojis is case insensitive', () => {
const results1 = filterEmojis('SMILE');
const results2 = filterEmojis('smile');
expect(results1.length).toBe(results2.length);
expect(results1[0].shortcode).toBe(results2[0].shortcode);
});
test('filterEmojis respects limit parameter', () => {
const results = filterEmojis('a', 5);
expect(results.length).toBeLessThanOrEqual(5);
});
test('filterEmojis returns empty array for empty search', () => {
const results = filterEmojis('');
expect(results).toEqual([]);
});
test('filterEmojis returns empty array for no matches', () => {
const results = filterEmojis('zzzznotanemoji');
expect(results).toEqual([]);
});
// ============================================
// Unit tests for EMOJI_DATA
// ============================================
test('EMOJI_DATA contains expected smileys', () => {
const smile = EMOJI_DATA.find(e => e.shortcode === 'smile');
expect(smile).toBeDefined();
expect(smile?.emoji).toBe('😄');
const joy = EMOJI_DATA.find(e => e.shortcode === 'joy');
expect(joy).toBeDefined();
expect(joy?.emoji).toBe('😂');
});
test('EMOJI_DATA contains expected gestures', () => {
const thumbsup = EMOJI_DATA.find(e => e.shortcode === 'thumbsup');
expect(thumbsup).toBeDefined();
expect(thumbsup?.emoji).toBe('👍');
const clap = EMOJI_DATA.find(e => e.shortcode === 'clap');
expect(clap).toBeDefined();
expect(clap?.emoji).toBe('👏');
});
test('EMOJI_DATA contains expected symbols', () => {
const heart = EMOJI_DATA.find(e => e.shortcode === 'heart');
expect(heart).toBeDefined();
expect(heart?.emoji).toBe('❤️');
const fire = EMOJI_DATA.find(e => e.shortcode === 'fire');
expect(fire).toBeDefined();
expect(fire?.emoji).toBe('🔥');
const checkmark = EMOJI_DATA.find(e => e.shortcode === 'white_check_mark');
expect(checkmark).toBeDefined();
expect(checkmark?.emoji).toBe('✅');
});
test('EMOJI_DATA items have required properties', () => {
EMOJI_DATA.forEach(item => {
expect(item).toHaveProperty('shortcode');
expect(item).toHaveProperty('emoji');
expect(typeof item.shortcode).toBe('string');
expect(typeof item.emoji).toBe('string');
expect(item.shortcode.length).toBeGreaterThan(0);
expect(item.emoji.length).toBeGreaterThan(0);
});
});
test('EMOJI_DATA shortcodes are unique', () => {
const shortcodes = EMOJI_DATA.map(e => e.shortcode);
const uniqueShortcodes = new Set(shortcodes);
expect(uniqueShortcodes.size).toBe(shortcodes.length);
});
test('EMOJI_DATA has a reasonable number of emojis', () => {
// Ensure we have a substantial emoji set
expect(EMOJI_DATA.length).toBeGreaterThan(100);
});

View File

@@ -0,0 +1,569 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export interface EmojiItem {
shortcode: string;
emoji: string;
keywords?: string[];
}
/**
* Common emoji data with shortcodes.
* This is a curated subset of emojis commonly used in Slack-like applications.
* Can be extended or replaced with a more comprehensive emoji library.
*/
export const EMOJI_DATA: EmojiItem[] = [
// Smileys & Emotion
{ shortcode: 'smile', emoji: '😄', keywords: ['happy', 'joy', 'glad'] },
{ shortcode: 'smiley', emoji: '😃', keywords: ['happy', 'joy'] },
{ shortcode: 'grinning', emoji: '😀', keywords: ['happy', 'smile'] },
{ shortcode: 'blush', emoji: '😊', keywords: ['happy', 'shy', 'smile'] },
{ shortcode: 'wink', emoji: '😉', keywords: ['flirt'] },
{
shortcode: 'heart_eyes',
emoji: '😍',
keywords: ['love', 'crush', 'adore'],
},
{ shortcode: 'kissing_heart', emoji: '😘', keywords: ['love', 'kiss'] },
{ shortcode: 'laughing', emoji: '😆', keywords: ['happy', 'haha', 'lol'] },
{ shortcode: 'sweat_smile', emoji: '😅', keywords: ['nervous', 'phew'] },
{ shortcode: 'joy', emoji: '😂', keywords: ['tears', 'laugh', 'lol', 'lmao'] },
{
shortcode: 'rofl',
emoji: '🤣',
keywords: ['rolling', 'laugh', 'lol', 'lmao'],
},
{ shortcode: 'relaxed', emoji: '☺️', keywords: ['calm', 'peace'] },
{ shortcode: 'yum', emoji: '😋', keywords: ['tasty', 'delicious'] },
{ shortcode: 'relieved', emoji: '😌', keywords: ['calm', 'peaceful'] },
{ shortcode: 'sunglasses', emoji: '😎', keywords: ['cool', 'awesome'] },
{ shortcode: 'smirk', emoji: '😏', keywords: ['sly', 'confident'] },
{ shortcode: 'neutral_face', emoji: '😐', keywords: ['meh', 'blank'] },
{ shortcode: 'expressionless', emoji: '😑', keywords: ['blank', 'meh'] },
{ shortcode: 'unamused', emoji: '😒', keywords: ['bored', 'meh'] },
{ shortcode: 'sweat', emoji: '😓', keywords: ['nervous', 'worried'] },
{ shortcode: 'pensive', emoji: '😔', keywords: ['sad', 'thoughtful'] },
{ shortcode: 'confused', emoji: '😕', keywords: ['puzzled', 'unsure'] },
{ shortcode: 'upside_down', emoji: '🙃', keywords: ['silly', 'sarcasm'] },
{ shortcode: 'thinking', emoji: '🤔', keywords: ['ponder', 'hmm'] },
{ shortcode: 'zipper_mouth', emoji: '🤐', keywords: ['secret', 'quiet'] },
{ shortcode: 'raised_eyebrow', emoji: '🤨', keywords: ['skeptical', 'doubt'] },
{ shortcode: 'rolling_eyes', emoji: '🙄', keywords: ['annoyed', 'whatever'] },
{ shortcode: 'grimacing', emoji: '😬', keywords: ['awkward', 'nervous'] },
{ shortcode: 'lying_face', emoji: '🤥', keywords: ['liar', 'pinocchio'] },
{ shortcode: 'shushing', emoji: '🤫', keywords: ['quiet', 'secret'] },
{ shortcode: 'hand_over_mouth', emoji: '🤭', keywords: ['oops', 'giggle'] },
{ shortcode: 'face_vomiting', emoji: '🤮', keywords: ['sick', 'gross'] },
{ shortcode: 'exploding_head', emoji: '🤯', keywords: ['mind', 'blown'] },
{ shortcode: 'cowboy', emoji: '🤠', keywords: ['western', 'yeehaw'] },
{ shortcode: 'partying', emoji: '🥳', keywords: ['party', 'celebration'] },
{ shortcode: 'star_struck', emoji: '🤩', keywords: ['excited', 'amazed'] },
{ shortcode: 'sleeping', emoji: '😴', keywords: ['zzz', 'tired'] },
{ shortcode: 'drooling', emoji: '🤤', keywords: ['hungry', 'want'] },
{ shortcode: 'sleepy', emoji: '😪', keywords: ['tired', 'zzz'] },
{ shortcode: 'mask', emoji: '😷', keywords: ['sick', 'covid'] },
{ shortcode: 'nerd', emoji: '🤓', keywords: ['geek', 'smart'] },
{ shortcode: 'monocle', emoji: '🧐', keywords: ['curious', 'inspect'] },
{ shortcode: 'worried', emoji: '😟', keywords: ['concerned', 'anxious'] },
{ shortcode: 'frowning', emoji: '🙁', keywords: ['sad', 'unhappy'] },
{ shortcode: 'open_mouth', emoji: '😮', keywords: ['surprised', 'wow'] },
{ shortcode: 'hushed', emoji: '😯', keywords: ['surprised', 'quiet'] },
{ shortcode: 'astonished', emoji: '😲', keywords: ['shocked', 'wow'] },
{ shortcode: 'flushed', emoji: '😳', keywords: ['embarrassed', 'shy'] },
{ shortcode: 'pleading', emoji: '🥺', keywords: ['puppy', 'please'] },
{ shortcode: 'cry', emoji: '😢', keywords: ['sad', 'tear'] },
{ shortcode: 'sob', emoji: '😭', keywords: ['crying', 'sad', 'tears'] },
{ shortcode: 'scream', emoji: '😱', keywords: ['scared', 'horror'] },
{ shortcode: 'confounded', emoji: '😖', keywords: ['frustrated'] },
{ shortcode: 'persevere', emoji: '😣', keywords: ['struggling'] },
{ shortcode: 'disappointed', emoji: '😞', keywords: ['sad', 'let down'] },
{ shortcode: 'fearful', emoji: '😨', keywords: ['scared', 'afraid'] },
{ shortcode: 'cold_sweat', emoji: '😰', keywords: ['nervous', 'anxious'] },
{ shortcode: 'weary', emoji: '😩', keywords: ['tired', 'exhausted'] },
{ shortcode: 'tired_face', emoji: '😫', keywords: ['exhausted'] },
{ shortcode: 'angry', emoji: '😠', keywords: ['mad', 'grumpy'] },
{ shortcode: 'rage', emoji: '😡', keywords: ['angry', 'furious'] },
{ shortcode: 'triumph', emoji: '😤', keywords: ['proud', 'huffing'] },
{ shortcode: 'skull', emoji: '💀', keywords: ['dead', 'death'] },
{ shortcode: 'poop', emoji: '💩', keywords: ['crap', 'shit'] },
{ shortcode: 'clown', emoji: '🤡', keywords: ['funny', 'circus'] },
{ shortcode: 'imp', emoji: '👿', keywords: ['devil', 'evil'] },
{ shortcode: 'ghost', emoji: '👻', keywords: ['boo', 'spooky'] },
{ shortcode: 'alien', emoji: '👽', keywords: ['ufo', 'space'] },
{ shortcode: 'robot', emoji: '🤖', keywords: ['bot', 'machine'] },
{ shortcode: 'cat', emoji: '😺', keywords: ['kitty', 'meow'] },
{ shortcode: 'heart_eyes_cat', emoji: '😻', keywords: ['love', 'cat'] },
{ shortcode: 'joy_cat', emoji: '😹', keywords: ['laugh', 'cat'] },
{ shortcode: 'crying_cat', emoji: '😿', keywords: ['sad', 'cat'] },
{ shortcode: 'pouting_cat', emoji: '😾', keywords: ['angry', 'cat'] },
{ shortcode: 'see_no_evil', emoji: '🙈', keywords: ['monkey', 'shy'] },
{ shortcode: 'hear_no_evil', emoji: '🙉', keywords: ['monkey'] },
{ shortcode: 'speak_no_evil', emoji: '🙊', keywords: ['monkey', 'secret'] },
// Gestures & Body
{ shortcode: 'wave', emoji: '👋', keywords: ['hello', 'bye', 'hi'] },
{ shortcode: 'raised_hand', emoji: '✋', keywords: ['stop', 'high five'] },
{ shortcode: 'ok_hand', emoji: '👌', keywords: ['perfect', 'nice'] },
{ shortcode: 'pinching_hand', emoji: '🤏', keywords: ['small', 'tiny'] },
{ shortcode: 'v', emoji: '✌️', keywords: ['peace', 'victory'] },
{ shortcode: 'crossed_fingers', emoji: '🤞', keywords: ['luck', 'hope'] },
{ shortcode: 'love_you', emoji: '🤟', keywords: ['ily', 'sign'] },
{ shortcode: 'metal', emoji: '🤘', keywords: ['rock', 'horns'] },
{ shortcode: 'call_me', emoji: '🤙', keywords: ['phone', 'shaka'] },
{ shortcode: 'point_left', emoji: '👈', keywords: ['direction'] },
{ shortcode: 'point_right', emoji: '👉', keywords: ['direction'] },
{ shortcode: 'point_up', emoji: '👆', keywords: ['direction'] },
{ shortcode: 'point_down', emoji: '👇', keywords: ['direction'] },
{ shortcode: 'middle_finger', emoji: '🖕', keywords: ['flip', 'rude'] },
{ shortcode: 'thumbsup', emoji: '👍', keywords: ['yes', 'good', '+1'] },
{ shortcode: 'thumbsdown', emoji: '👎', keywords: ['no', 'bad', '-1'] },
{ shortcode: 'fist', emoji: '✊', keywords: ['power', 'punch'] },
{ shortcode: 'punch', emoji: '👊', keywords: ['fist', 'bump'] },
{ shortcode: 'clap', emoji: '👏', keywords: ['applause', 'bravo'] },
{ shortcode: 'raised_hands', emoji: '🙌', keywords: ['celebration', 'yay'] },
{ shortcode: 'open_hands', emoji: '👐', keywords: ['hug', 'open'] },
{ shortcode: 'palms_up', emoji: '🤲', keywords: ['prayer', 'request'] },
{ shortcode: 'handshake', emoji: '🤝', keywords: ['deal', 'agreement'] },
{ shortcode: 'pray', emoji: '🙏', keywords: ['please', 'thanks', 'namaste'] },
{ shortcode: 'writing', emoji: '✍️', keywords: ['write', 'pen'] },
{ shortcode: 'nail_care', emoji: '💅', keywords: ['nails', 'fabulous'] },
{ shortcode: 'selfie', emoji: '🤳', keywords: ['photo', 'camera'] },
{ shortcode: 'muscle', emoji: '💪', keywords: ['strong', 'flex', 'bicep'] },
{ shortcode: 'leg', emoji: '🦵', keywords: ['kick'] },
{ shortcode: 'foot', emoji: '🦶', keywords: ['kick', 'step'] },
{ shortcode: 'ear', emoji: '👂', keywords: ['listen', 'hear'] },
{ shortcode: 'nose', emoji: '👃', keywords: ['smell', 'sniff'] },
{ shortcode: 'brain', emoji: '🧠', keywords: ['think', 'smart'] },
{ shortcode: 'eyes', emoji: '👀', keywords: ['look', 'see', 'watch'] },
{ shortcode: 'eye', emoji: '👁️', keywords: ['look', 'see'] },
{ shortcode: 'tongue', emoji: '👅', keywords: ['taste', 'lick'] },
{ shortcode: 'lips', emoji: '👄', keywords: ['mouth', 'kiss'] },
{ shortcode: 'baby', emoji: '👶', keywords: ['child', 'infant'] },
{ shortcode: 'person', emoji: '🧑', keywords: ['human', 'adult'] },
{ shortcode: 'man', emoji: '👨', keywords: ['male', 'guy'] },
{ shortcode: 'woman', emoji: '👩', keywords: ['female', 'lady'] },
{ shortcode: 'older_person', emoji: '🧓', keywords: ['senior', 'elderly'] },
// Hearts & Love
{ shortcode: 'heart', emoji: '❤️', keywords: ['love', 'red'] },
{ shortcode: 'orange_heart', emoji: '🧡', keywords: ['love'] },
{ shortcode: 'yellow_heart', emoji: '💛', keywords: ['love'] },
{ shortcode: 'green_heart', emoji: '💚', keywords: ['love'] },
{ shortcode: 'blue_heart', emoji: '💙', keywords: ['love'] },
{ shortcode: 'purple_heart', emoji: '💜', keywords: ['love'] },
{ shortcode: 'black_heart', emoji: '🖤', keywords: ['love', 'dark'] },
{ shortcode: 'white_heart', emoji: '🤍', keywords: ['love', 'pure'] },
{ shortcode: 'brown_heart', emoji: '🤎', keywords: ['love'] },
{ shortcode: 'broken_heart', emoji: '💔', keywords: ['sad', 'heartbreak'] },
{ shortcode: 'heartbeat', emoji: '💓', keywords: ['love', 'pulse'] },
{ shortcode: 'heartpulse', emoji: '💗', keywords: ['love', 'growing'] },
{ shortcode: 'two_hearts', emoji: '💕', keywords: ['love', 'romance'] },
{ shortcode: 'revolving_hearts', emoji: '💞', keywords: ['love'] },
{ shortcode: 'cupid', emoji: '💘', keywords: ['love', 'arrow'] },
{ shortcode: 'sparkling_heart', emoji: '💖', keywords: ['love', 'sparkle'] },
{ shortcode: 'gift_heart', emoji: '💝', keywords: ['love', 'valentine'] },
{ shortcode: 'heart_decoration', emoji: '💟', keywords: ['love'] },
{ shortcode: 'kiss', emoji: '💋', keywords: ['love', 'lips'] },
{ shortcode: 'love_letter', emoji: '💌', keywords: ['email', 'message'] },
// Symbols & Objects
{ shortcode: 'fire', emoji: '🔥', keywords: ['hot', 'lit', 'flame'] },
{ shortcode: 'star', emoji: '⭐', keywords: ['favorite', 'rating'] },
{ shortcode: 'sparkles', emoji: '✨', keywords: ['shiny', 'new', 'magic'] },
{ shortcode: 'zap', emoji: '⚡', keywords: ['lightning', 'power'] },
{ shortcode: 'boom', emoji: '💥', keywords: ['explosion', 'collision'] },
{ shortcode: 'dizzy', emoji: '💫', keywords: ['star', 'dazed'] },
{ shortcode: 'speech_balloon', emoji: '💬', keywords: ['talk', 'chat'] },
{ shortcode: 'thought_balloon', emoji: '💭', keywords: ['think', 'idea'] },
{ shortcode: 'zzz', emoji: '💤', keywords: ['sleep', 'tired'] },
{ shortcode: 'wave_emoji', emoji: '🌊', keywords: ['ocean', 'water'] },
{ shortcode: 'droplet', emoji: '💧', keywords: ['water', 'sweat'] },
{ shortcode: 'sweat_drops', emoji: '💦', keywords: ['water', 'splash'] },
{ shortcode: 'dash', emoji: '💨', keywords: ['wind', 'running'] },
{ shortcode: 'hole', emoji: '🕳️', keywords: ['empty', 'void'] },
{ shortcode: 'bomb', emoji: '💣', keywords: ['explosive', 'danger'] },
{ shortcode: 'money', emoji: '💰', keywords: ['bag', 'cash', 'dollar'] },
{ shortcode: 'dollar', emoji: '💵', keywords: ['money', 'cash'] },
{ shortcode: 'gem', emoji: '💎', keywords: ['diamond', 'jewel'] },
{ shortcode: 'bulb', emoji: '💡', keywords: ['idea', 'light'] },
{ shortcode: 'bell', emoji: '🔔', keywords: ['notification', 'alert'] },
{ shortcode: 'loudspeaker', emoji: '📢', keywords: ['announce'] },
{ shortcode: 'mega', emoji: '📣', keywords: ['megaphone', 'announce'] },
{ shortcode: 'lock', emoji: '🔒', keywords: ['secure', 'closed'] },
{ shortcode: 'unlock', emoji: '🔓', keywords: ['open', 'access'] },
{ shortcode: 'key', emoji: '🔑', keywords: ['password', 'access'] },
{ shortcode: 'magnifying_glass', emoji: '🔍', keywords: ['search', 'find'] },
{ shortcode: 'link', emoji: '🔗', keywords: ['chain', 'url'] },
{ shortcode: 'paperclip', emoji: '📎', keywords: ['attach'] },
{ shortcode: 'scissors', emoji: '✂️', keywords: ['cut', 'snip'] },
{ shortcode: 'hammer', emoji: '🔨', keywords: ['tool', 'build'] },
{ shortcode: 'wrench', emoji: '🔧', keywords: ['tool', 'fix'] },
{ shortcode: 'gear', emoji: '⚙️', keywords: ['settings', 'cog'] },
{ shortcode: 'shield', emoji: '🛡️', keywords: ['protect', 'security'] },
{ shortcode: 'trophy', emoji: '🏆', keywords: ['win', 'first', 'award'] },
{ shortcode: 'medal', emoji: '🏅', keywords: ['award', 'sports'] },
{ shortcode: 'first_place', emoji: '🥇', keywords: ['gold', 'winner'] },
{ shortcode: 'second_place', emoji: '🥈', keywords: ['silver'] },
{ shortcode: 'third_place', emoji: '🥉', keywords: ['bronze'] },
{ shortcode: 'soccer', emoji: '⚽', keywords: ['football', 'sports'] },
{ shortcode: 'basketball', emoji: '🏀', keywords: ['sports', 'ball'] },
{ shortcode: 'football', emoji: '🏈', keywords: ['sports', 'american'] },
{ shortcode: 'baseball', emoji: '⚾', keywords: ['sports', 'ball'] },
{ shortcode: 'tennis', emoji: '🎾', keywords: ['sports', 'ball'] },
{ shortcode: 'dart', emoji: '🎯', keywords: ['target', 'bullseye'] },
{ shortcode: 'video_game', emoji: '🎮', keywords: ['gaming', 'controller'] },
{ shortcode: 'slot_machine', emoji: '🎰', keywords: ['gambling', 'casino'] },
{ shortcode: 'game_die', emoji: '🎲', keywords: ['dice', 'random'] },
{ shortcode: 'jigsaw', emoji: '🧩', keywords: ['puzzle', 'piece'] },
{ shortcode: 'art', emoji: '🎨', keywords: ['palette', 'paint'] },
{ shortcode: 'performing_arts', emoji: '🎭', keywords: ['theater', 'drama'] },
{ shortcode: 'microphone', emoji: '🎤', keywords: ['sing', 'karaoke'] },
{ shortcode: 'headphones', emoji: '🎧', keywords: ['music', 'audio'] },
{ shortcode: 'musical_note', emoji: '🎵', keywords: ['music', 'song'] },
{ shortcode: 'notes', emoji: '🎶', keywords: ['music', 'melody'] },
{ shortcode: 'guitar', emoji: '🎸', keywords: ['music', 'rock'] },
{ shortcode: 'piano', emoji: '🎹', keywords: ['music', 'keys'] },
{ shortcode: 'drum', emoji: '🥁', keywords: ['music', 'beat'] },
{ shortcode: 'trumpet', emoji: '🎺', keywords: ['music', 'brass'] },
{ shortcode: 'violin', emoji: '🎻', keywords: ['music', 'string'] },
{ shortcode: 'movie_camera', emoji: '🎥', keywords: ['film', 'video'] },
{ shortcode: 'camera', emoji: '📷', keywords: ['photo', 'picture'] },
{ shortcode: 'tv', emoji: '📺', keywords: ['television', 'watch'] },
{ shortcode: 'computer', emoji: '💻', keywords: ['laptop', 'pc'] },
{ shortcode: 'keyboard', emoji: '⌨️', keywords: ['type', 'computer'] },
{ shortcode: 'phone', emoji: '📱', keywords: ['mobile', 'cell'] },
{ shortcode: 'email', emoji: '📧', keywords: ['mail', 'message'] },
{ shortcode: 'inbox', emoji: '📥', keywords: ['mail', 'receive'] },
{ shortcode: 'outbox', emoji: '📤', keywords: ['mail', 'send'] },
{ shortcode: 'package', emoji: '📦', keywords: ['box', 'delivery'] },
{ shortcode: 'memo', emoji: '📝', keywords: ['note', 'write'] },
{ shortcode: 'page', emoji: '📄', keywords: ['document', 'file'] },
{ shortcode: 'bookmark', emoji: '🔖', keywords: ['save', 'tag'] },
{ shortcode: 'book', emoji: '📖', keywords: ['read', 'open'] },
{ shortcode: 'books', emoji: '📚', keywords: ['library', 'study'] },
{ shortcode: 'newspaper', emoji: '📰', keywords: ['news', 'article'] },
{ shortcode: 'calendar', emoji: '📅', keywords: ['date', 'schedule'] },
{ shortcode: 'chart', emoji: '📈', keywords: ['graph', 'increase'] },
{ shortcode: 'chart_down', emoji: '📉', keywords: ['graph', 'decrease'] },
{ shortcode: 'bar_chart', emoji: '📊', keywords: ['graph', 'stats'] },
{ shortcode: 'clipboard', emoji: '📋', keywords: ['list', 'todo'] },
{ shortcode: 'pushpin', emoji: '📌', keywords: ['pin', 'location'] },
{ shortcode: 'round_pushpin', emoji: '📍', keywords: ['pin', 'location'] },
{ shortcode: 'triangular_ruler', emoji: '📐', keywords: ['math', 'measure'] },
{ shortcode: 'straight_ruler', emoji: '📏', keywords: ['math', 'measure'] },
{ shortcode: 'pencil', emoji: '✏️', keywords: ['write', 'draw'] },
{ shortcode: 'pen', emoji: '🖊️', keywords: ['write', 'sign'] },
{ shortcode: 'crayon', emoji: '🖍️', keywords: ['draw', 'color'] },
{ shortcode: 'paintbrush', emoji: '🖌️', keywords: ['art', 'paint'] },
{ shortcode: 'folder', emoji: '📁', keywords: ['file', 'directory'] },
{ shortcode: 'open_folder', emoji: '📂', keywords: ['file', 'directory'] },
// Nature & Animals
{ shortcode: 'dog', emoji: '🐶', keywords: ['puppy', 'pet', 'woof'] },
{ shortcode: 'cat_face', emoji: '🐱', keywords: ['kitty', 'pet', 'meow'] },
{ shortcode: 'mouse', emoji: '🐭', keywords: ['rodent'] },
{ shortcode: 'hamster', emoji: '🐹', keywords: ['pet', 'rodent'] },
{ shortcode: 'rabbit', emoji: '🐰', keywords: ['bunny', 'pet'] },
{ shortcode: 'fox', emoji: '🦊', keywords: ['animal'] },
{ shortcode: 'bear', emoji: '🐻', keywords: ['animal'] },
{ shortcode: 'panda', emoji: '🐼', keywords: ['animal', 'cute'] },
{ shortcode: 'koala', emoji: '🐨', keywords: ['animal', 'australia'] },
{ shortcode: 'tiger', emoji: '🐯', keywords: ['animal', 'cat'] },
{ shortcode: 'lion', emoji: '🦁', keywords: ['animal', 'king'] },
{ shortcode: 'cow', emoji: '🐮', keywords: ['animal', 'farm'] },
{ shortcode: 'pig', emoji: '🐷', keywords: ['animal', 'farm'] },
{ shortcode: 'frog', emoji: '🐸', keywords: ['animal', 'toad'] },
{ shortcode: 'monkey_face', emoji: '🐵', keywords: ['animal', 'ape'] },
{ shortcode: 'chicken', emoji: '🐔', keywords: ['animal', 'farm', 'hen'] },
{ shortcode: 'penguin', emoji: '🐧', keywords: ['animal', 'bird'] },
{ shortcode: 'bird', emoji: '🐦', keywords: ['animal', 'fly'] },
{ shortcode: 'eagle', emoji: '🦅', keywords: ['animal', 'bird'] },
{ shortcode: 'duck', emoji: '🦆', keywords: ['animal', 'bird', 'quack'] },
{ shortcode: 'owl', emoji: '🦉', keywords: ['animal', 'bird', 'night'] },
{ shortcode: 'bat', emoji: '🦇', keywords: ['animal', 'night', 'vampire'] },
{ shortcode: 'wolf', emoji: '🐺', keywords: ['animal'] },
{ shortcode: 'horse', emoji: '🐴', keywords: ['animal'] },
{ shortcode: 'unicorn', emoji: '🦄', keywords: ['animal', 'magic'] },
{ shortcode: 'bee', emoji: '🐝', keywords: ['insect', 'honey'] },
{ shortcode: 'bug', emoji: '🐛', keywords: ['insect', 'caterpillar'] },
{ shortcode: 'butterfly', emoji: '🦋', keywords: ['insect', 'pretty'] },
{ shortcode: 'snail', emoji: '🐌', keywords: ['slow'] },
{ shortcode: 'lady_beetle', emoji: '🐞', keywords: ['insect', 'bug'] },
{ shortcode: 'ant', emoji: '🐜', keywords: ['insect', 'bug'] },
{ shortcode: 'spider', emoji: '🕷️', keywords: ['insect', 'scary'] },
{ shortcode: 'turtle', emoji: '🐢', keywords: ['animal', 'slow'] },
{ shortcode: 'snake', emoji: '🐍', keywords: ['animal', 'reptile'] },
{ shortcode: 'dragon', emoji: '🐲', keywords: ['animal', 'mythical'] },
{ shortcode: 'dinosaur', emoji: '🦕', keywords: ['animal', 'extinct'] },
{ shortcode: 't_rex', emoji: '🦖', keywords: ['animal', 'dinosaur'] },
{ shortcode: 'whale', emoji: '🐳', keywords: ['animal', 'ocean'] },
{ shortcode: 'dolphin', emoji: '🐬', keywords: ['animal', 'ocean'] },
{ shortcode: 'fish', emoji: '🐟', keywords: ['animal', 'ocean'] },
{ shortcode: 'tropical_fish', emoji: '🐠', keywords: ['animal', 'ocean'] },
{ shortcode: 'shark', emoji: '🦈', keywords: ['animal', 'ocean'] },
{ shortcode: 'octopus', emoji: '🐙', keywords: ['animal', 'ocean'] },
{ shortcode: 'crab', emoji: '🦀', keywords: ['animal', 'ocean'] },
{ shortcode: 'lobster', emoji: '🦞', keywords: ['animal', 'ocean'] },
{ shortcode: 'shrimp', emoji: '🦐', keywords: ['animal', 'ocean'] },
// Plants & Nature
{ shortcode: 'bouquet', emoji: '💐', keywords: ['flowers', 'gift'] },
{ shortcode: 'cherry_blossom', emoji: '🌸', keywords: ['flower', 'spring'] },
{ shortcode: 'rose', emoji: '🌹', keywords: ['flower', 'love'] },
{ shortcode: 'tulip', emoji: '🌷', keywords: ['flower', 'spring'] },
{ shortcode: 'sunflower', emoji: '🌻', keywords: ['flower', 'summer'] },
{ shortcode: 'hibiscus', emoji: '🌺', keywords: ['flower', 'tropical'] },
{ shortcode: 'seedling', emoji: '🌱', keywords: ['plant', 'grow'] },
{ shortcode: 'evergreen_tree', emoji: '🌲', keywords: ['tree', 'pine'] },
{ shortcode: 'deciduous_tree', emoji: '🌳', keywords: ['tree'] },
{ shortcode: 'palm_tree', emoji: '🌴', keywords: ['tree', 'tropical'] },
{ shortcode: 'cactus', emoji: '🌵', keywords: ['plant', 'desert'] },
{ shortcode: 'herb', emoji: '🌿', keywords: ['plant', 'leaf'] },
{ shortcode: 'shamrock', emoji: '☘️', keywords: ['clover', 'irish'] },
{ shortcode: 'four_leaf_clover', emoji: '🍀', keywords: ['luck', 'irish'] },
{ shortcode: 'maple_leaf', emoji: '🍁', keywords: ['fall', 'autumn'] },
{ shortcode: 'fallen_leaf', emoji: '🍂', keywords: ['fall', 'autumn'] },
{ shortcode: 'leaves', emoji: '🍃', keywords: ['leaf', 'wind'] },
{ shortcode: 'mushroom', emoji: '🍄', keywords: ['fungus'] },
// Food & Drink
{ shortcode: 'apple', emoji: '🍎', keywords: ['fruit', 'red'] },
{ shortcode: 'green_apple', emoji: '🍏', keywords: ['fruit'] },
{ shortcode: 'pear', emoji: '🍐', keywords: ['fruit'] },
{ shortcode: 'orange', emoji: '🍊', keywords: ['fruit', 'citrus'] },
{ shortcode: 'lemon', emoji: '🍋', keywords: ['fruit', 'citrus'] },
{ shortcode: 'banana', emoji: '🍌', keywords: ['fruit'] },
{ shortcode: 'watermelon', emoji: '🍉', keywords: ['fruit', 'summer'] },
{ shortcode: 'grapes', emoji: '🍇', keywords: ['fruit', 'wine'] },
{ shortcode: 'strawberry', emoji: '🍓', keywords: ['fruit', 'berry'] },
{ shortcode: 'cherries', emoji: '🍒', keywords: ['fruit'] },
{ shortcode: 'peach', emoji: '🍑', keywords: ['fruit'] },
{ shortcode: 'mango', emoji: '🥭', keywords: ['fruit', 'tropical'] },
{ shortcode: 'pineapple', emoji: '🍍', keywords: ['fruit', 'tropical'] },
{ shortcode: 'coconut', emoji: '🥥', keywords: ['fruit', 'tropical'] },
{ shortcode: 'avocado', emoji: '🥑', keywords: ['fruit', 'guacamole'] },
{ shortcode: 'tomato', emoji: '🍅', keywords: ['vegetable', 'red'] },
{ shortcode: 'eggplant', emoji: '🍆', keywords: ['vegetable', 'purple'] },
{ shortcode: 'potato', emoji: '🥔', keywords: ['vegetable', 'spud'] },
{ shortcode: 'carrot', emoji: '🥕', keywords: ['vegetable', 'orange'] },
{ shortcode: 'corn', emoji: '🌽', keywords: ['vegetable', 'maize'] },
{ shortcode: 'hot_pepper', emoji: '🌶️', keywords: ['spicy', 'chili'] },
{ shortcode: 'broccoli', emoji: '🥦', keywords: ['vegetable', 'green'] },
{ shortcode: 'bread', emoji: '🍞', keywords: ['food', 'toast'] },
{ shortcode: 'croissant', emoji: '🥐', keywords: ['food', 'french'] },
{ shortcode: 'pretzel', emoji: '🥨', keywords: ['food', 'snack'] },
{ shortcode: 'bagel', emoji: '🥯', keywords: ['food', 'breakfast'] },
{ shortcode: 'cheese', emoji: '🧀', keywords: ['food', 'dairy'] },
{ shortcode: 'egg', emoji: '🥚', keywords: ['food', 'breakfast'] },
{ shortcode: 'bacon', emoji: '🥓', keywords: ['food', 'breakfast'] },
{ shortcode: 'pancakes', emoji: '🥞', keywords: ['food', 'breakfast'] },
{ shortcode: 'waffle', emoji: '🧇', keywords: ['food', 'breakfast'] },
{ shortcode: 'steak', emoji: '🥩', keywords: ['food', 'meat'] },
{ shortcode: 'poultry_leg', emoji: '🍗', keywords: ['food', 'chicken'] },
{ shortcode: 'hamburger', emoji: '🍔', keywords: ['food', 'burger'] },
{ shortcode: 'fries', emoji: '🍟', keywords: ['food', 'fast'] },
{ shortcode: 'pizza', emoji: '🍕', keywords: ['food', 'italian'] },
{ shortcode: 'hot_dog', emoji: '🌭', keywords: ['food', 'fast'] },
{ shortcode: 'sandwich', emoji: '🥪', keywords: ['food', 'lunch'] },
{ shortcode: 'taco', emoji: '🌮', keywords: ['food', 'mexican'] },
{ shortcode: 'burrito', emoji: '🌯', keywords: ['food', 'mexican'] },
{ shortcode: 'sushi', emoji: '🍣', keywords: ['food', 'japanese'] },
{ shortcode: 'ramen', emoji: '🍜', keywords: ['food', 'noodles'] },
{ shortcode: 'spaghetti', emoji: '🍝', keywords: ['food', 'pasta'] },
{ shortcode: 'curry', emoji: '🍛', keywords: ['food', 'rice'] },
{ shortcode: 'rice', emoji: '🍚', keywords: ['food', 'white'] },
{ shortcode: 'salad', emoji: '🥗', keywords: ['food', 'healthy'] },
{ shortcode: 'popcorn', emoji: '🍿', keywords: ['food', 'movie'] },
{ shortcode: 'cake', emoji: '🎂', keywords: ['food', 'birthday'] },
{ shortcode: 'cupcake', emoji: '🧁', keywords: ['food', 'sweet'] },
{ shortcode: 'pie', emoji: '🥧', keywords: ['food', 'dessert'] },
{ shortcode: 'cookie', emoji: '🍪', keywords: ['food', 'sweet'] },
{ shortcode: 'chocolate', emoji: '🍫', keywords: ['food', 'sweet'] },
{ shortcode: 'candy', emoji: '🍬', keywords: ['food', 'sweet'] },
{ shortcode: 'lollipop', emoji: '🍭', keywords: ['food', 'sweet'] },
{ shortcode: 'donut', emoji: '🍩', keywords: ['food', 'sweet'] },
{ shortcode: 'ice_cream', emoji: '🍨', keywords: ['food', 'dessert'] },
{ shortcode: 'icecream', emoji: '🍦', keywords: ['food', 'dessert', 'cone'] },
{ shortcode: 'coffee', emoji: '☕', keywords: ['drink', 'caffeine'] },
{ shortcode: 'tea', emoji: '🍵', keywords: ['drink', 'green'] },
{ shortcode: 'beer', emoji: '🍺', keywords: ['drink', 'alcohol'] },
{ shortcode: 'beers', emoji: '🍻', keywords: ['drink', 'cheers'] },
{ shortcode: 'wine_glass', emoji: '🍷', keywords: ['drink', 'alcohol'] },
{ shortcode: 'cocktail', emoji: '🍸', keywords: ['drink', 'alcohol'] },
{ shortcode: 'tropical_drink', emoji: '🍹', keywords: ['drink', 'vacation'] },
{ shortcode: 'champagne', emoji: '🍾', keywords: ['drink', 'celebrate'] },
{ shortcode: 'milk', emoji: '🥛', keywords: ['drink', 'dairy'] },
{ shortcode: 'baby_bottle', emoji: '🍼', keywords: ['drink', 'infant'] },
{ shortcode: 'juice', emoji: '🧃', keywords: ['drink', 'box'] },
{ shortcode: 'cup_with_straw', emoji: '🥤', keywords: ['drink', 'soda'] },
// Weather & Nature
{ shortcode: 'sun', emoji: '☀️', keywords: ['weather', 'sunny', 'bright'] },
{ shortcode: 'moon', emoji: '🌙', keywords: ['night', 'sleep'] },
{ shortcode: 'full_moon', emoji: '🌕', keywords: ['night', 'lunar'] },
{ shortcode: 'new_moon', emoji: '🌑', keywords: ['night', 'dark'] },
{ shortcode: 'star2', emoji: '🌟', keywords: ['glow', 'sparkle'] },
{ shortcode: 'milky_way', emoji: '🌌', keywords: ['galaxy', 'space'] },
{ shortcode: 'cloud', emoji: '☁️', keywords: ['weather', 'sky'] },
{ shortcode: 'sun_behind_cloud', emoji: '⛅', keywords: ['weather'] },
{ shortcode: 'cloud_with_rain', emoji: '🌧️', keywords: ['weather', 'rainy'] },
{ shortcode: 'thunder', emoji: '⛈️', keywords: ['weather', 'storm'] },
{ shortcode: 'snowflake', emoji: '❄️', keywords: ['weather', 'cold'] },
{ shortcode: 'snowman', emoji: '☃️', keywords: ['winter', 'snow'] },
{ shortcode: 'wind_blowing', emoji: '🌬️', keywords: ['weather', 'air'] },
{ shortcode: 'tornado', emoji: '🌪️', keywords: ['weather', 'storm'] },
{ shortcode: 'fog', emoji: '🌫️', keywords: ['weather', 'mist'] },
{ shortcode: 'umbrella', emoji: '☂️', keywords: ['rain', 'weather'] },
{ shortcode: 'rainbow', emoji: '🌈', keywords: ['weather', 'pride'] },
{ shortcode: 'earth', emoji: '🌍', keywords: ['world', 'planet'] },
{ shortcode: 'earth_americas', emoji: '🌎', keywords: ['world', 'planet'] },
{ shortcode: 'earth_asia', emoji: '🌏', keywords: ['world', 'planet'] },
{ shortcode: 'rocket', emoji: '🚀', keywords: ['space', 'launch'] },
{ shortcode: 'satellite', emoji: '🛰️', keywords: ['space', 'orbit'] },
{ shortcode: 'ufo', emoji: '🛸', keywords: ['alien', 'space'] },
// Checkmarks & Common Symbols
{ shortcode: 'white_check_mark', emoji: '✅', keywords: ['done', 'yes', 'ok'] },
{ shortcode: 'check', emoji: '✔️', keywords: ['done', 'yes'] },
{ shortcode: 'x', emoji: '❌', keywords: ['no', 'wrong', 'cancel'] },
{ shortcode: 'cross_mark', emoji: '❎', keywords: ['no', 'wrong'] },
{ shortcode: 'plus', emoji: '', keywords: ['add', 'math'] },
{ shortcode: 'minus', emoji: '', keywords: ['subtract', 'math'] },
{ shortcode: 'divide', emoji: '➗', keywords: ['math', 'division'] },
{ shortcode: 'multiply', emoji: '✖️', keywords: ['math', 'times'] },
{ shortcode: 'infinity', emoji: '♾️', keywords: ['forever', 'endless'] },
{ shortcode: 'question', emoji: '❓', keywords: ['ask', 'what'] },
{ shortcode: 'grey_question', emoji: '❔', keywords: ['ask', 'what'] },
{ shortcode: 'exclamation', emoji: '❗', keywords: ['alert', 'important'] },
{ shortcode: 'grey_exclamation', emoji: '❕', keywords: ['alert'] },
{ shortcode: 'warning', emoji: '⚠️', keywords: ['alert', 'caution'] },
{ shortcode: 'no_entry', emoji: '⛔', keywords: ['stop', 'forbidden'] },
{ shortcode: 'prohibited', emoji: '🚫', keywords: ['stop', 'banned'] },
{ shortcode: 'recycle', emoji: '♻️', keywords: ['environment', 'green'] },
{ shortcode: 'arrow_up', emoji: '⬆️', keywords: ['direction', 'north'] },
{ shortcode: 'arrow_down', emoji: '⬇️', keywords: ['direction', 'south'] },
{ shortcode: 'arrow_left', emoji: '⬅️', keywords: ['direction', 'west'] },
{ shortcode: 'arrow_right', emoji: '➡️', keywords: ['direction', 'east'] },
{
shortcode: 'arrow_upper_right',
emoji: '↗️',
keywords: ['direction', 'northeast'],
},
{
shortcode: 'arrow_lower_right',
emoji: '↘️',
keywords: ['direction', 'southeast'],
},
{
shortcode: 'arrow_lower_left',
emoji: '↙️',
keywords: ['direction', 'southwest'],
},
{
shortcode: 'arrow_upper_left',
emoji: '↖️',
keywords: ['direction', 'northwest'],
},
{
shortcode: 'left_right_arrow',
emoji: '↔️',
keywords: ['direction', 'horizontal'],
},
{
shortcode: 'up_down_arrow',
emoji: '↕️',
keywords: ['direction', 'vertical'],
},
{ shortcode: 'arrows_clockwise', emoji: '🔃', keywords: ['refresh', 'sync'] },
{
shortcode: 'arrows_counterclockwise',
emoji: '🔄',
keywords: ['refresh', 'sync'],
},
{ shortcode: 'back', emoji: '🔙', keywords: ['return', 'previous'] },
{ shortcode: 'end', emoji: '🔚', keywords: ['finish', 'last'] },
{ shortcode: 'on', emoji: '🔛', keywords: ['active'] },
{ shortcode: 'soon', emoji: '🔜', keywords: ['coming', 'future'] },
{ shortcode: 'top', emoji: '🔝', keywords: ['best', 'first'] },
{ shortcode: 'new', emoji: '🆕', keywords: ['fresh', 'latest'] },
{ shortcode: 'free', emoji: '🆓', keywords: ['gratis', 'cost'] },
{ shortcode: 'up', emoji: '🆙', keywords: ['increase', 'level'] },
{ shortcode: 'cool', emoji: '🆒', keywords: ['nice', 'awesome'] },
{ shortcode: 'ok', emoji: '🆗', keywords: ['yes', 'approve'] },
{ shortcode: 'sos', emoji: '🆘', keywords: ['help', 'emergency'] },
{ shortcode: 'stop_sign', emoji: '🛑', keywords: ['halt', 'cease'] },
{ shortcode: 'a', emoji: '🅰️', keywords: ['letter', 'blood'] },
{ shortcode: 'b', emoji: '🅱️', keywords: ['letter', 'blood'] },
{ shortcode: 'o', emoji: '🅾️', keywords: ['letter', 'blood'] },
{ shortcode: 'information', emoji: '', keywords: ['info', 'help'] },
{ shortcode: 'copyright', emoji: '©️', keywords: ['legal', 'ip'] },
{ shortcode: 'registered', emoji: '®️', keywords: ['legal', 'brand'] },
{ shortcode: 'tm', emoji: '™️', keywords: ['legal', 'trademark'] },
{ shortcode: 'one', emoji: '1⃣', keywords: ['number', 'first'] },
{ shortcode: 'two', emoji: '2⃣', keywords: ['number', 'second'] },
{ shortcode: 'three', emoji: '3⃣', keywords: ['number', 'third'] },
{ shortcode: 'four', emoji: '4⃣', keywords: ['number'] },
{ shortcode: 'five', emoji: '5⃣', keywords: ['number'] },
{ shortcode: 'six', emoji: '6⃣', keywords: ['number'] },
{ shortcode: 'seven', emoji: '7⃣', keywords: ['number'] },
{ shortcode: 'eight', emoji: '8⃣', keywords: ['number'] },
{ shortcode: 'nine', emoji: '9⃣', keywords: ['number'] },
{ shortcode: 'zero', emoji: '0⃣', keywords: ['number'] },
{ shortcode: 'keycap_ten', emoji: '🔟', keywords: ['number', 'ten'] },
{ shortcode: 'hash', emoji: '#️⃣', keywords: ['number', 'pound', 'hashtag'] },
{ shortcode: 'asterisk', emoji: '*️⃣', keywords: ['star', 'symbol'] },
{ shortcode: 'eject', emoji: '⏏️', keywords: ['media', 'remove'] },
{ shortcode: 'play', emoji: '▶️', keywords: ['media', 'start'] },
{ shortcode: 'pause', emoji: '⏸️', keywords: ['media', 'wait'] },
{ shortcode: 'stop', emoji: '⏹️', keywords: ['media', 'end'] },
{ shortcode: 'record', emoji: '⏺️', keywords: ['media', 'red'] },
{ shortcode: 'fast_forward', emoji: '⏩', keywords: ['media', 'skip'] },
{ shortcode: 'rewind', emoji: '⏪', keywords: ['media', 'back'] },
{ shortcode: 'next_track', emoji: '⏭️', keywords: ['media', 'skip'] },
{ shortcode: 'previous_track', emoji: '⏮️', keywords: ['media', 'back'] },
{ shortcode: 'cinema', emoji: '🎦', keywords: ['movie', 'film'] },
{ shortcode: 'low_brightness', emoji: '🔅', keywords: ['dim', 'light'] },
{ shortcode: 'high_brightness', emoji: '🔆', keywords: ['bright', 'light'] },
{ shortcode: 'signal_strength', emoji: '📶', keywords: ['wifi', 'bars'] },
{ shortcode: 'vibration', emoji: '📳', keywords: ['phone', 'mode'] },
{ shortcode: 'mobile_off', emoji: '📴', keywords: ['phone', 'silent'] },
{ shortcode: 'female', emoji: '♀️', keywords: ['woman', 'gender'] },
{ shortcode: 'male', emoji: '♂️', keywords: ['man', 'gender'] },
{ shortcode: 'medical', emoji: '⚕️', keywords: ['health', 'doctor'] },
{ shortcode: 'atom', emoji: '⚛️', keywords: ['science', 'physics'] },
];
/**
* Filter emojis by search text (checks shortcode and keywords)
*/
export function filterEmojis(
searchText: string,
limit: number = 10,
): EmojiItem[] {
if (!searchText) return [];
const lowerSearch = searchText.toLowerCase();
return EMOJI_DATA.filter(
item =>
item.shortcode.toLowerCase().includes(lowerSearch) ||
item.keywords?.some(keyword =>
keyword.toLowerCase().includes(lowerSearch),
),
).slice(0, limit);
}

View File

@@ -0,0 +1,247 @@
/**
* 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 { forwardRef, useCallback, useMemo, useState, useRef } from 'react';
import { Mentions } from 'antd';
import type { MentionsRef, MentionsProps } from 'antd/es/mentions';
import { filterEmojis, type EmojiItem } from './emojiData';
const MIN_CHARS_BEFORE_POPUP = 2;
// Regex to match emoji characters (simplified, covers most common emojis)
const EMOJI_REGEX =
/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]/u;
export interface EmojiTextAreaProps
extends Omit<MentionsProps, 'prefix' | 'options' | 'onSelect'> {
/**
* Minimum characters after colon before showing popup.
* @default 2 (Slack-like behavior)
*/
minCharsBeforePopup?: number;
/**
* Maximum number of emoji suggestions to show.
* @default 10
*/
maxSuggestions?: number;
/**
* Called when an emoji is selected from the popup.
*/
onEmojiSelect?: (emoji: EmojiItem) => void;
}
/**
* A TextArea component with Slack-like emoji autocomplete.
*
* Features:
* - Triggers on `:` prefix (like Slack)
* - Only shows popup after 2+ characters are typed (configurable)
* - Colon must be preceded by a space, start of line, or another emoji
* - Prevents accidental Enter key selection when typing quickly
*
* @example
* ```tsx
* <EmojiTextArea
* placeholder="Type :sm to see emoji suggestions..."
* onChange={(text) => console.log(text)}
* />
* ```
*/
export const EmojiTextArea = forwardRef<MentionsRef, EmojiTextAreaProps>(
(
{
minCharsBeforePopup = MIN_CHARS_BEFORE_POPUP,
maxSuggestions = 10,
onEmojiSelect,
onChange,
onKeyDown,
...restProps
},
ref,
) => {
const [options, setOptions] = useState<
Array<{ value: string; label: React.ReactNode }>
>([]);
const [isPopupVisible, setIsPopupVisible] = useState(false);
const lastSearchRef = useRef<string>('');
const lastKeyPressTimeRef = useRef<number>(0);
/**
* Validates whether the colon trigger should activate the popup.
* Implements Slack-like behavior:
* - Colon must be preceded by whitespace, start of text, or emoji
* - At least minCharsBeforePopup characters must be typed after colon
*/
const validateSearch = useCallback(
(text: string, props: MentionsProps): boolean => {
// Get the full value to check what precedes the colon
const fullValue = (props.value as string) || '';
// Find where this search text starts in the full value
// The search text is what comes after the `:` prefix
const colonIndex = fullValue.lastIndexOf(`:${text}`);
if (colonIndex === -1) {
setIsPopupVisible(false);
return false;
}
// Check what precedes the colon
if (colonIndex > 0) {
const charBefore = fullValue[colonIndex - 1];
// Must be preceded by whitespace, newline, or emoji
const isWhitespace = /\s/.test(charBefore);
const isEmoji = EMOJI_REGEX.test(charBefore);
if (!isWhitespace && !isEmoji) {
setIsPopupVisible(false);
return false;
}
}
// Check minimum character requirement
if (text.length < minCharsBeforePopup) {
setIsPopupVisible(false);
return false;
}
setIsPopupVisible(true);
return true;
},
[minCharsBeforePopup],
);
/**
* Handles search and filters emoji suggestions.
*/
const handleSearch = useCallback(
(searchText: string) => {
lastSearchRef.current = searchText;
if (searchText.length < minCharsBeforePopup) {
setOptions([]);
return;
}
const filteredEmojis = filterEmojis(searchText, maxSuggestions);
const newOptions = filteredEmojis.map(item => ({
value: item.emoji,
label: (
<span>
<span style={{ marginRight: 8 }}>{item.emoji}</span>
<span style={{ color: 'var(--ant-color-text-secondary)' }}>
:{item.shortcode}:
</span>
</span>
),
// Store the full item for onSelect callback
data: item,
}));
setOptions(newOptions);
},
[minCharsBeforePopup, maxSuggestions],
);
/**
* Handles emoji selection from the popup.
*/
const handleSelect = useCallback(
(option: { value: string; data?: EmojiItem }) => {
if (option.data && onEmojiSelect) {
onEmojiSelect(option.data);
}
setIsPopupVisible(false);
},
[onEmojiSelect],
);
/**
* Handles key down events to prevent accidental selection on Enter.
* If the user presses Enter very quickly after typing (< 100ms),
* we treat it as a newline intent rather than selection.
*/
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const now = Date.now();
const timeSinceLastKey = now - lastKeyPressTimeRef.current;
// If Enter is pressed and popup is visible
if (e.key === 'Enter' && isPopupVisible) {
// If typed very quickly (< 100ms since last keypress) and
// there's meaningful search text, allow the Enter to create newline
// This prevents accidental selection when typing something like:
// "let me show you an example:[Enter]"
if (timeSinceLastKey < 100 && lastSearchRef.current.length === 0) {
// Let the default behavior (newline) happen
setIsPopupVisible(false);
return;
}
}
lastKeyPressTimeRef.current = now;
// Call original onKeyDown if provided
onKeyDown?.(e);
},
[isPopupVisible, onKeyDown],
);
const handleChange = useCallback(
(text: string) => {
lastKeyPressTimeRef.current = Date.now();
onChange?.(text);
},
[onChange],
);
// Memoize the Mentions component props
const mentionsProps = useMemo(
() => ({
prefix: ':',
split: '',
options,
validateSearch,
onSearch: handleSearch,
onSelect: handleSelect,
onKeyDown: handleKeyDown,
onChange: handleChange,
notFoundContent: null, // Don't show "Not Found" message
...restProps,
}),
[
options,
validateSearch,
handleSearch,
handleSelect,
handleKeyDown,
handleChange,
restProps,
],
);
return <Mentions ref={ref} {...mentionsProps} />;
},
);
EmojiTextArea.displayName = 'EmojiTextArea';
export type { EmojiItem };
export { filterEmojis, EMOJI_DATA } from './emojiData';

View File

@@ -102,6 +102,13 @@ export {
type DynamicEditableTitleProps,
} from './DynamicEditableTitle';
export { EditableTitle, type EditableTitleProps } from './EditableTitle';
export {
EmojiTextArea,
type EmojiTextAreaProps,
type EmojiItem,
filterEmojis,
EMOJI_DATA,
} from './EmojiTextArea';
export { EmptyState, type EmptyStateProps } from './EmptyState';
export { Empty, type EmptyProps } from './EmptyState/Empty';
export { FaveStar, type FaveStarProps } from './FaveStar';

View File

@@ -19,27 +19,16 @@
import { DatasourceType } from './types/Datasource';
const DATASOURCE_TYPE_MAP: Record<string, DatasourceType> = {
table: DatasourceType.Table,
query: DatasourceType.Query,
dataset: DatasourceType.Dataset,
sl_table: DatasourceType.SlTable,
saved_query: DatasourceType.SavedQuery,
semantic_view: DatasourceType.SemanticView,
};
export default class DatasourceKey {
readonly id: number | string;
readonly id: number;
readonly type: DatasourceType;
constructor(key: string) {
const [idStr, typeStr] = key.split('__');
// Only parse as integer if the entire string is numeric
// (parseInt would incorrectly parse "85d3139f..." as 85)
const isNumeric = /^\d+$/.test(idStr);
this.id = isNumeric ? parseInt(idStr, 10) : idStr;
this.type = DATASOURCE_TYPE_MAP[typeStr] ?? DatasourceType.Table;
this.id = parseInt(idStr, 10);
this.type = DatasourceType.Table; // default to SqlaTable model
this.type = typeStr === 'query' ? DatasourceType.Query : this.type;
}
public toString() {

View File

@@ -26,7 +26,6 @@ export enum DatasourceType {
Dataset = 'dataset',
SlTable = 'sl_table',
SavedQuery = 'saved_query',
SemanticView = 'semantic_view',
}
export interface Currency {
@@ -38,7 +37,7 @@ export interface Currency {
* Datasource metadata.
*/
export interface Datasource {
id: number | string;
id: number;
name: string;
type: DatasourceType;
columns: Column[];

View File

@@ -74,6 +74,9 @@ export type QueryObjectExtras = Partial<{
instant_time_comparison_range?: string;
time_compare?: string;
/** If true, WHERE/HAVING clauses need transpilation to target dialect */
transpile_to_dialect?: boolean;
}>;
export type ResidualQueryObjectData = {
@@ -156,7 +159,7 @@ export interface QueryObject
export interface QueryContext {
datasource: {
id: number | string;
id: number;
type: DatasourceType;
};
/** Force refresh of all queries */

View File

@@ -0,0 +1,456 @@
/**
* 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 buildQuery, { DeckPolygonFormData } from './buildQuery';
describe('Polygon buildQuery', () => {
const baseFormData: DeckPolygonFormData = {
datasource: '1__table',
viz_type: 'deck_polygon',
line_column: 'polygon_geom',
};
test('should require line_column', () => {
const formDataWithoutLineColumn = {
...baseFormData,
line_column: undefined,
};
expect(() => buildQuery(formDataWithoutLineColumn)).toThrow(
'Polygon column is required for Polygon charts',
);
});
test('should build basic query with minimal data', () => {
const queryContext = buildQuery(baseFormData);
const [query] = queryContext.queries;
expect(query.columns).toEqual(['polygon_geom']);
expect(query.metrics).toEqual([]);
expect(query.is_timeseries).toBe(false);
expect(query.filters).toEqual([
{
col: 'polygon_geom',
op: 'IS NOT NULL',
},
]);
});
test('should include metric in query when provided', () => {
const formDataWithMetric = {
...baseFormData,
metric: 'population',
};
const queryContext = buildQuery(formDataWithMetric);
const [query] = queryContext.queries;
expect(query.metrics).toEqual(['population']);
expect(query.filters).toContainEqual({
col: 'population',
op: 'IS NOT NULL',
});
});
describe('point_radius_fixed legacy structure', () => {
test('should not add metrics to query when value is simple string', () => {
const formDataWithFixValue = {
...baseFormData,
point_radius_fixed: {
value: '1000',
},
};
const queryContext = buildQuery(formDataWithFixValue);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([]);
});
test('should not add metrics to query when point_radius_fixed.value is undefined', () => {
const formDataWithEmptyValue = {
...baseFormData,
point_radius_fixed: {
value: undefined,
},
};
const queryContext = buildQuery(formDataWithEmptyValue);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([]);
});
test('should not add metrics to query when point_radius_fixed is undefined', () => {
const formDataWithoutFixedRadius = {
...baseFormData,
point_radius_fixed: undefined,
};
const queryContext = buildQuery(formDataWithoutFixedRadius);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([]);
});
});
describe('point_radius_fixed "fix" type', () => {
test('should not add metrics to query when point_radius_fixed type is "fix"', () => {
const formDataWithFixType = {
...baseFormData,
point_radius_fixed: {
type: 'fix',
value: '1000',
},
} as any;
const queryContext = buildQuery(formDataWithFixType);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([]);
});
test('should not add metrics to query when point_radius_fixed type is "fix" with zero value', () => {
const formDataWithZeroFix = {
...baseFormData,
point_radius_fixed: {
type: 'fix',
value: '0',
},
} as any;
const queryContext = buildQuery(formDataWithZeroFix);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([]);
});
test('should not add metrics to query when point_radius_fixed type is "fix" with decimal value', () => {
const formDataWithDecimalFix = {
...baseFormData,
point_radius_fixed: {
type: 'fix',
value: '500.5',
},
} as any;
const queryContext = buildQuery(formDataWithDecimalFix);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([]);
});
});
describe('point_radius_fixed "metric" type', () => {
test('should add metric object to query when point_radius_fixed type is "metric"', () => {
const metricObject = {
expressionType: 'SQL',
sqlExpression: 'SUM(population)/SUM(area)',
column: null,
aggregate: null,
datasourceWarning: false,
hasCustomLabel: false,
label: 'SUM(population)/SUM(area)',
optionName: 'metric_c5rvwrzoo86_293h6yrv2ic',
};
const formDataWithMetricType = {
...baseFormData,
point_radius_fixed: {
type: 'metric',
value: metricObject,
},
} as any;
const queryContext = buildQuery(formDataWithMetricType);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([metricObject]);
});
test('should add simple column metric to query when point_radius_fixed type is "metric"', () => {
const simpleMetricObject = {
expressionType: 'simple',
column: {
column_name: 'avg_elevation',
type: 'NUMERIC',
},
aggregate: 'avg',
label: 'AVG(avg_elevation)',
};
const formDataWithSimpleMetric = {
...baseFormData,
point_radius_fixed: {
type: 'metric',
value: simpleMetricObject,
},
} as any;
const queryContext = buildQuery(formDataWithSimpleMetric);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([simpleMetricObject]);
});
test('should include both regular metric and point_radius_fixed metric in query when both are specified', () => {
const metricObject = {
expressionType: 'simple',
column: { column_name: 'elevation' },
aggregate: 'sum',
label: 'SUM(elevation)',
};
const formDataWithBothMetrics = {
...baseFormData,
metric: 'population',
point_radius_fixed: {
type: 'metric',
value: metricObject,
},
} as any;
const queryContext = buildQuery(formDataWithBothMetrics);
const [query] = queryContext.queries;
expect(query.metrics).toEqual(['population', metricObject]);
});
});
describe('Edge cases and error handling', () => {
test('should not add metrics to query when point_radius_fixed is null', () => {
const formDataWithNull = {
...baseFormData,
point_radius_fixed: null,
} as any;
const queryContext = buildQuery(formDataWithNull);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([]);
});
test('should handle null metric values gracefully', () => {
const formDataWithNullMetric = {
...baseFormData,
point_radius_fixed: {
type: 'metric',
value: null,
},
} as any;
const queryContext = buildQuery(formDataWithNullMetric);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([]);
});
test('should handle undefined metric values gracefully', () => {
const formDataWithUndefinedMetric = {
...baseFormData,
point_radius_fixed: {
type: 'metric',
value: undefined,
},
} as any;
const queryContext = buildQuery(formDataWithUndefinedMetric);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([]);
});
test('should not add metrics to query when point_radius_fixed is empty object', () => {
const formDataWithEmptyObject = {
...baseFormData,
point_radius_fixed: {},
};
const queryContext = buildQuery(formDataWithEmptyObject);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([]);
});
test('should not add metrics to query when point_radius_fixed has unsupported type', () => {
const formDataWithUnsupportedType = {
...baseFormData,
point_radius_fixed: {
type: 'unsupported_type',
value: 'some_value',
},
} as any;
const queryContext = buildQuery(formDataWithUnsupportedType);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([]);
});
test('should not add metrics to query when point_radius_fixed has missing type field', () => {
const formDataWithMissingType = {
...baseFormData,
point_radius_fixed: {
value: 'some_value',
},
};
const queryContext = buildQuery(formDataWithMissingType);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([]);
});
});
describe('Integration with other form data fields', () => {
test('should include js_columns in query columns', () => {
const formDataWithJsColumns = {
...baseFormData,
js_columns: ['custom_col1', 'custom_col2'],
};
const queryContext = buildQuery(formDataWithJsColumns);
const [query] = queryContext.queries;
expect(query.columns).toEqual([
'polygon_geom',
'custom_col1',
'custom_col2',
]);
});
test('should include tooltip_contents columns in query', () => {
const formDataWithTooltips = {
...baseFormData,
tooltip_contents: [
{ item_type: 'column', column_name: 'tooltip_col' },
'another_tooltip_col',
],
};
const queryContext = buildQuery(formDataWithTooltips);
const [query] = queryContext.queries;
expect(query.columns).toContain('tooltip_col');
expect(query.columns).toContain('another_tooltip_col');
});
test('should not add null filters when filter_nulls is false', () => {
const formDataWithoutNullFilters = {
...baseFormData,
filter_nulls: false,
metric: 'population',
};
const queryContext = buildQuery(formDataWithoutNullFilters);
const [query] = queryContext.queries;
expect(query.filters).toEqual([]);
});
test('should build comprehensive query when multiple form data fields are specified', () => {
const complexFormData = {
...baseFormData,
metric: 'population',
point_radius_fixed: {
type: 'metric',
value: {
expressionType: 'simple',
column: { column_name: 'elevation' },
aggregate: 'avg',
label: 'AVG(elevation)',
},
},
js_columns: ['custom_prop'],
tooltip_contents: [
{ item_type: 'column', column_name: 'tooltip_info' },
],
filter_nulls: true,
} as any;
const queryContext = buildQuery(complexFormData);
const [query] = queryContext.queries;
expect(query.columns).toContain('polygon_geom');
expect(query.columns).toContain('custom_prop');
expect(query.columns).toContain('tooltip_info');
expect(query.metrics).toContain('population');
expect(query.metrics).toContain(complexFormData.point_radius_fixed.value);
expect(query.filters).toContainEqual({
col: 'polygon_geom',
op: 'IS NOT NULL',
});
expect(query.filters).toContainEqual({
col: 'population',
op: 'IS NOT NULL',
});
});
});
describe('Current implementation behavior', () => {
test('should not add fixed values to metrics for legacy point_radius_fixed structure', () => {
const formDataWithFix = {
...baseFormData,
point_radius_fixed: {
value: '1000',
},
};
const queryContext = buildQuery(formDataWithFix);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([]);
});
test('should add metric objects to query when point_radius_fixed type is "metric"', () => {
const metricObject = {
expressionType: 'SQL',
sqlExpression: 'AVG(elevation)',
label: 'AVG(elevation)',
};
const formDataWithMetricObject = {
...baseFormData,
point_radius_fixed: {
type: 'metric',
value: metricObject,
},
} as any;
const queryContext = buildQuery(formDataWithMetricObject);
const [query] = queryContext.queries;
expect(query.metrics).toContain(metricObject);
});
test('should respect type information when processing point_radius_fixed', () => {
const formDataWithTypeInfo = {
...baseFormData,
point_radius_fixed: {
type: 'fix',
value: '500',
},
} as any;
const queryContext = buildQuery(formDataWithTypeInfo);
const [query] = queryContext.queries;
expect(query.metrics).toEqual([]);
});
});
});

View File

@@ -24,6 +24,7 @@ import {
QueryObjectFilterClause,
QueryObject,
QueryFormColumn,
QueryFormMetric,
} from '@superset-ui/core';
import { addTooltipColumnsToQuery } from '../buildQueryUtils';
@@ -31,9 +32,18 @@ export interface DeckPolygonFormData extends SqlaFormData {
line_column?: string;
line_type?: string;
metric?: string;
point_radius_fixed?: {
value?: string;
};
point_radius_fixed?:
| {
value?: string;
}
| {
type: 'fix';
value: string;
}
| {
type: 'metric';
value: QueryFormMetric;
};
reverse_long_lat?: boolean;
filter_nulls?: boolean;
js_columns?: string[];
@@ -74,8 +84,16 @@ export default function buildQuery(formData: DeckPolygonFormData) {
if (metric) {
metrics.push(metric);
}
if (point_radius_fixed?.value) {
metrics.push(point_radius_fixed.value);
if (point_radius_fixed) {
if ('type' in point_radius_fixed) {
if (
point_radius_fixed.type === 'metric' &&
point_radius_fixed.value != null
) {
metrics.push(point_radius_fixed.value);
}
}
}
const filters = ensureIsArray(baseQueryObject.filters || []);

View File

@@ -0,0 +1,260 @@
/**
* 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 { ChartProps, DatasourceType } from '@superset-ui/core';
import transformProps from './transformProps';
interface PolygonFeature {
polygon?: number[][];
elevation?: number;
extraProps?: Record<string, unknown>;
metrics?: Record<string, number | string>;
}
jest.mock('../spatialUtils', () => ({
...jest.requireActual('../spatialUtils'),
getMapboxApiKey: jest.fn(() => 'mock-mapbox-key'),
}));
describe('Polygon transformProps', () => {
const mockChartProps: Partial<ChartProps> = {
rawFormData: {
line_column: 'geom',
line_type: 'json',
viewport: {},
},
queriesData: [
{
data: [
{
geom: JSON.stringify([
[-122.4, 37.8],
[-122.3, 37.8],
[-122.3, 37.9],
[-122.4, 37.9],
]),
'AVG(elevation)': 150.5,
population: 50000,
},
],
},
],
datasource: {
type: DatasourceType.Table,
id: 1,
name: 'test_datasource',
columns: [],
metrics: [],
},
height: 400,
width: 600,
hooks: {},
filterState: {},
emitCrossFilters: false,
};
test('should use constant elevation value when point_radius_fixed type is "fix"', () => {
const fixProps = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
point_radius_fixed: {
type: 'fix',
value: '1000',
},
},
};
const result = transformProps(fixProps as ChartProps);
const features = result.payload.data.features as PolygonFeature[];
expect(features).toHaveLength(1);
expect(features[0]?.elevation).toBe(1000);
});
test('should use database metric value for elevation when point_radius_fixed type is "metric"', () => {
const metricProps = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
point_radius_fixed: {
type: 'metric',
value: {
expressionType: 'SQL',
sqlExpression: 'AVG(elevation)',
label: 'AVG(elevation)',
},
},
},
};
const result = transformProps(metricProps as ChartProps);
const features = result.payload.data.features as PolygonFeature[];
expect(features).toHaveLength(1);
expect(features[0]?.elevation).toBe(150.5);
});
test('should use constant elevation value when point_radius_fixed has legacy structure', () => {
const legacyProps = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
point_radius_fixed: {
value: '750',
},
},
};
const result = transformProps(legacyProps as ChartProps);
const features = result.payload.data.features as PolygonFeature[];
expect(features).toHaveLength(1);
expect(features[0]?.elevation).toBe(750);
});
test('should not set elevation when point_radius_fixed is not specified', () => {
const noElevationProps = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
},
};
const result = transformProps(noElevationProps as ChartProps);
const features = result.payload.data.features as PolygonFeature[];
expect(features).toHaveLength(1);
expect(features[0]?.elevation).toBeUndefined();
});
test('should use decimal constant elevation value when point_radius_fixed type is "fix"', () => {
const decimalFixProps = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
point_radius_fixed: {
type: 'fix',
value: '500.75',
},
},
};
const result = transformProps(decimalFixProps as ChartProps);
const features = result.payload.data.features as PolygonFeature[];
expect(features).toHaveLength(1);
expect(features[0]?.elevation).toBe(500.75);
});
test('should handle invalid numeric strings gracefully', () => {
const invalidNumericProps = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
point_radius_fixed: {
type: 'fix',
value: 'not-a-number',
},
},
};
const result = transformProps(invalidNumericProps as ChartProps);
const features = result.payload.data.features as PolygonFeature[];
expect(features).toHaveLength(1);
expect(features[0]?.elevation).toBeUndefined();
});
test('should handle empty string elevation values gracefully', () => {
const emptyStringProps = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
point_radius_fixed: {
type: 'fix',
value: '',
},
},
};
const result = transformProps(emptyStringProps as ChartProps);
const features = result.payload.data.features as PolygonFeature[];
expect(features).toHaveLength(1);
expect(features[0]?.elevation).toBeUndefined();
});
test('should handle null metric elevation values gracefully', () => {
const nullMetricProps = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
point_radius_fixed: {
type: 'metric',
value: null,
},
},
};
const result = transformProps(nullMetricProps as ChartProps);
const features = result.payload.data.features as PolygonFeature[];
expect(features).toHaveLength(1);
expect(features[0]?.elevation).toBeUndefined();
});
test('should handle invalid JSON in polygon data gracefully', () => {
const invalidJsonProps = {
...mockChartProps,
queriesData: [
{
data: [
{
geom: 'invalid-json-string',
},
],
},
],
};
const result = transformProps(invalidJsonProps as ChartProps);
const features = result.payload.data.features as PolygonFeature[];
expect(features).toHaveLength(0);
});
test('should handle legacy point_radius_fixed with invalid value gracefully', () => {
const legacyInvalidProps = {
...mockChartProps,
rawFormData: {
...mockChartProps.rawFormData,
point_radius_fixed: {
value: 'invalid-number',
},
},
};
const result = transformProps(legacyInvalidProps as ChartProps);
const features = result.payload.data.features as PolygonFeature[];
expect(features).toHaveLength(1);
expect(features[0]?.elevation).toBeUndefined();
});
});

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ChartProps } from '@superset-ui/core';
import { ChartProps, getMetricLabel } from '@superset-ui/core';
import { addJsColumnsToExtraProps, DataRecord } from '../spatialUtils';
import {
createBaseTransformResult,
@@ -27,6 +27,11 @@ import {
} from '../transformUtils';
import { DeckPolygonFormData } from './buildQuery';
function parseElevationValue(value: string): number | undefined {
const parsed = parseFloat(value);
return Number.isNaN(parsed) ? undefined : parsed;
}
interface PolygonFeature {
polygon?: number[][];
name?: string;
@@ -53,7 +58,28 @@ function processPolygonData(
}
const metricLabel = getMetricLabelFromFormData(metric);
const elevationLabel = getMetricLabelFromFormData(point_radius_fixed);
let elevationLabel: string | undefined;
let fixedElevationValue: number | undefined;
if (point_radius_fixed) {
if ('type' in point_radius_fixed) {
if (
point_radius_fixed.type === 'metric' &&
point_radius_fixed.value != null
) {
elevationLabel = getMetricLabel(point_radius_fixed.value);
} else if (
point_radius_fixed.type === 'fix' &&
point_radius_fixed.value
) {
fixedElevationValue = parseElevationValue(point_radius_fixed.value);
}
} else if (point_radius_fixed.value) {
fixedElevationValue = parseElevationValue(point_radius_fixed.value);
}
}
const excludeKeys = new Set([line_column, ...(js_columns || [])]);
return records
@@ -109,7 +135,9 @@ function processPolygonData(
feature.polygon = polygonCoords;
if (elevationLabel && record[elevationLabel] != null) {
if (fixedElevationValue !== undefined) {
feature.elevation = fixedElevationValue;
} else if (elevationLabel && record[elevationLabel] != null) {
const elevationValue = parseMetricValue(record[elevationLabel]);
if (elevationValue !== undefined) {
feature.elevation = elevationValue;

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