mirror of
https://github.com/apache/superset.git
synced 2026-05-20 23:35:14 +00:00
Compare commits
23 Commits
enxdev/fea
...
feat/glyph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26b7b8cdea | ||
|
|
2908674e1f | ||
|
|
86f2f056d2 | ||
|
|
79eb91da96 | ||
|
|
96be1ccb2d | ||
|
|
f63e369687 | ||
|
|
67231d194e | ||
|
|
816583b602 | ||
|
|
49b9afa591 | ||
|
|
8efda4f704 | ||
|
|
12ebe0f032 | ||
|
|
857ba7f855 | ||
|
|
dd1226f8e5 | ||
|
|
46555d12cb | ||
|
|
5b0d895322 | ||
|
|
1eae7b4ee5 | ||
|
|
6a054f9170 | ||
|
|
865b75c90e | ||
|
|
6fc18b38fa | ||
|
|
3f61764543 | ||
|
|
9dad9cbfc2 | ||
|
|
b2c58a0856 | ||
|
|
2e16b8266a |
1
.claude/scheduled_tasks.lock
Normal file
1
.claude/scheduled_tasks.lock
Normal file
@@ -0,0 +1 @@
|
||||
{"sessionId":"a0289491-ebb9-4d03-aa1d-023b0219c585","pid":48965,"procStart":"Fri May 1 19:01:07 2026","acquiredAt":1778687635094}
|
||||
4
.github/workflows/codeql-analysis.yml
vendored
4
.github/workflows/codeql-analysis.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
|
||||
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -53,6 +53,6 @@ jobs:
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
|
||||
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -111,6 +111,8 @@ services:
|
||||
superset-init-light:
|
||||
condition: service_completed_successfully
|
||||
volumes: *superset-volumes
|
||||
ports:
|
||||
- "${SUPERSET_PORT:-8088}:8088"
|
||||
environment:
|
||||
DATABASE_HOST: db-light
|
||||
DATABASE_DB: superset_light
|
||||
@@ -162,7 +164,7 @@ services:
|
||||
environment:
|
||||
# set this to false if you have perf issues running the npm i; npm run dev in-docker
|
||||
# if you do so, you have to run this manually on the host, which should perform better!
|
||||
BUILD_SUPERSET_FRONTEND_IN_DOCKER: true
|
||||
BUILD_SUPERSET_FRONTEND_IN_DOCKER: false
|
||||
NPM_RUN_PRUNE: false
|
||||
SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}"
|
||||
DISABLE_TS_CHECKER: "${DISABLE_TS_CHECKER:-true}"
|
||||
|
||||
@@ -34,6 +34,14 @@ x-superset-volumes: &superset-volumes
|
||||
- superset_home:/app/superset_home
|
||||
- ./tests:/app/tests
|
||||
- superset_data:/app/data
|
||||
# Python package metadata for the editable `uv pip install -e .` that
|
||||
# docker-bootstrap.sh runs at container start. Without these bind mounts
|
||||
# the editable install reads stale metadata baked into the image at
|
||||
# build time and may conflict with apache-superset-core's current pins.
|
||||
- ./pyproject.toml:/app/pyproject.toml
|
||||
- ./setup.py:/app/setup.py
|
||||
- ./MANIFEST.in:/app/MANIFEST.in
|
||||
- ./README.md:/app/README.md
|
||||
x-common-build: &common-build
|
||||
context: .
|
||||
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`
|
||||
|
||||
@@ -52,11 +52,11 @@ yarn serve # Serve built site locally
|
||||
# For maximum-detail databases.json, drop the `database-diagnostics`
|
||||
# artifact from Python-Integration CI at src/data/databases.json before
|
||||
# cutting. See README.md "Before You Cut".
|
||||
yarn version:add:user_docs <version> # Add new docs version
|
||||
yarn version:add:docs <version> # Add new docs version
|
||||
yarn version:add:admin_docs <version> # Add admin docs version
|
||||
yarn version:add:developer_docs <version> # Add developer docs version
|
||||
yarn version:add:components <version> # Add components version
|
||||
yarn version:remove:user_docs <version> # Remove docs version
|
||||
yarn version:remove:docs <version> # Remove docs version
|
||||
yarn version:remove:admin_docs <version> # Remove admin docs version
|
||||
yarn version:remove:developer_docs <version> # Remove developer docs version
|
||||
yarn version:remove:components <version> # Remove components version
|
||||
@@ -228,20 +228,17 @@ Versions are managed through `versions-config.json`:
|
||||
|
||||
```bash
|
||||
# ✅ CORRECT - Updates both Docusaurus and versions-config.json
|
||||
yarn version:add:user_docs 6.1.0
|
||||
yarn version:add:docs 6.1.0
|
||||
|
||||
# ❌ WRONG - Only updates Docusaurus, breaks version dropdown
|
||||
yarn docusaurus docs:version 6.1.0
|
||||
```
|
||||
|
||||
### Version Files Created
|
||||
When versioning, these files are created (per section, with the
|
||||
section's plugin id as prefix):
|
||||
- `<section>_versioned_docs/version-X.X.X/` - Snapshot of current docs
|
||||
- `<section>_versioned_sidebars/version-X.X.X-sidebars.json` - Sidebar config
|
||||
- `<section>_versions.json` - List of all versions
|
||||
|
||||
Section plugin ids: `user_docs`, `admin_docs`, `developer_docs`, `components`.
|
||||
When versioning, these files are created:
|
||||
- `versioned_docs/version-X.X.X/` - Snapshot of current docs
|
||||
- `versioned_sidebars/version-X.X.X-sidebars.json` - Sidebar config
|
||||
- `versions.json` - List of all versions
|
||||
|
||||
## 🎨 Styling and Theming
|
||||
|
||||
@@ -389,7 +386,7 @@ Docusaurus includes Algolia DocSearch integration configured in `docusaurus.conf
|
||||
|
||||
## 🚫 Common Pitfalls to Avoid
|
||||
|
||||
1. **Never use `yarn docusaurus docs:version`** - Use `yarn version:add:user_docs` instead
|
||||
1. **Never use `yarn docusaurus docs:version`** - Use `yarn version:add:docs` instead
|
||||
2. **Don't edit versioned docs directly** - Edit current docs and create new version
|
||||
3. **Avoid absolute paths in links** - Use relative paths for maintainability
|
||||
4. **Don't forget frontmatter** - Every doc needs title and description
|
||||
@@ -419,7 +416,7 @@ yarn eslint
|
||||
### Version Issues
|
||||
If versions don't appear in dropdown:
|
||||
1. Check `versions-config.json` includes the version
|
||||
2. Verify version files exist in `<section>_versioned_docs/`
|
||||
2. Verify version files exist in `versioned_docs/`
|
||||
3. Restart dev server
|
||||
|
||||
## 📚 Resources
|
||||
|
||||
@@ -53,7 +53,7 @@ Also: confirm `master` CI is green, and that your local checkout matches the SHA
|
||||
|
||||
```bash
|
||||
# Main Documentation
|
||||
yarn version:add:user_docs 1.2.0
|
||||
yarn version:add:docs 1.2.0
|
||||
|
||||
# Admin Docs
|
||||
yarn version:add:admin_docs 1.2.0
|
||||
@@ -98,7 +98,7 @@ If creating versions manually, you'll need to:
|
||||
- **Versioned sidebars**: `[section]_versioned_sidebars/version-X.X.X-sidebars.json`
|
||||
- **Versions list**: `[section]_versions.json`
|
||||
|
||||
All four sections (`user_docs`, `admin_docs`, `developer_docs`, `components`) follow this naming pattern uniformly.
|
||||
Note: For main docs, the prefix is omitted (e.g., `versioned_docs/` instead of `docs_versioned_docs/`)
|
||||
|
||||
3. **Important**: After adding a version, restart the development server to see changes:
|
||||
```bash
|
||||
@@ -111,7 +111,7 @@ If creating versions manually, you'll need to:
|
||||
#### Using Automated Scripts (Recommended)
|
||||
```bash
|
||||
# Main Documentation
|
||||
yarn version:remove:user_docs 1.0.0
|
||||
yarn version:remove:docs 1.0.0
|
||||
|
||||
# Admin Docs
|
||||
yarn version:remove:admin_docs 1.0.0
|
||||
@@ -127,19 +127,19 @@ yarn version:remove:components 1.0.0
|
||||
To manually remove a version:
|
||||
|
||||
1. **Delete the version folder** from the appropriate location:
|
||||
- User Docs: `user_docs_versioned_docs/version-X.X.X/`
|
||||
- Main docs: `versioned_docs/version-X.X.X/` (no prefix for main)
|
||||
- Admin Docs: `admin_docs_versioned_docs/version-X.X.X/`
|
||||
- Developer Docs: `developer_docs_versioned_docs/version-X.X.X/`
|
||||
- Components: `components_versioned_docs/version-X.X.X/`
|
||||
|
||||
2. **Delete the version metadata file**:
|
||||
- User Docs: `user_docs_versioned_sidebars/version-X.X.X-sidebars.json`
|
||||
- Main docs: `versioned_sidebars/version-X.X.X-sidebars.json` (no prefix)
|
||||
- Admin Docs: `admin_docs_versioned_sidebars/version-X.X.X-sidebars.json`
|
||||
- Developer Docs: `developer_docs_versioned_sidebars/version-X.X.X-sidebars.json`
|
||||
- Components: `components_versioned_sidebars/version-X.X.X-sidebars.json`
|
||||
|
||||
3. **Update the versions list file**:
|
||||
- User Docs: `user_docs_versions.json`
|
||||
- Main docs: `versions.json`
|
||||
- Admin Docs: `admin_docs_versions.json`
|
||||
- Developer Docs: `developer_docs_versions.json`
|
||||
- Components: `components_versions.json`
|
||||
@@ -212,8 +212,8 @@ docs: {
|
||||
If you accidentally used `yarn docusaurus docs:version` instead of `yarn version:add`:
|
||||
1. **Problem**: The version files were created but `versions-config.json` wasn't updated
|
||||
2. **Solution**: Either:
|
||||
- Revert the changes: `git restore user_docs_versions.json && rm -rf user_docs_versioned_docs/ user_docs_versioned_sidebars/`
|
||||
- Then use the correct command: `yarn version:add:user_docs <version>`
|
||||
- Revert the changes: `git restore versions.json && rm -rf versioned_docs/ versioned_sidebars/`
|
||||
- Then use the correct command: `yarn version:add:docs <version>`
|
||||
|
||||
For other issues:
|
||||
- **Restart the server**: Changes to version configuration require a server restart
|
||||
|
||||
@@ -502,7 +502,6 @@ All MCP settings go in `superset_config.py`. Defaults are defined in `superset/m
|
||||
| `MCP_DEBUG` | `False` | Enable debug logging |
|
||||
| `MCP_DEV_USERNAME` | -- | Superset username for development mode (no auth) |
|
||||
| `MCP_RBAC_ENABLED` | `True` | Enforce Superset's role-based access control on MCP tool calls. When `True`, each tool checks that the authenticated user has the required FAB permission before executing. Disable only for testing or trusted-network deployments. |
|
||||
| `MCP_DISABLED_TOOLS` | `set()` | Set of tool names to remove from the MCP server at startup. Disabled tools are never advertised to AI clients during tool discovery. Useful when a custom extension tool should replace a built-in Superset tool. See [Disabling built-in tools](#disabling-built-in-tools). |
|
||||
|
||||
### Authentication
|
||||
|
||||
@@ -826,32 +825,6 @@ while True:
|
||||
page += 1
|
||||
```
|
||||
|
||||
## Disabling built-in tools
|
||||
|
||||
If you have deployed a custom tool via a Superset extension that supersedes one of the built-in Superset tools, you can suppress the built-in version so AI clients only discover your replacement. Disabled tools are removed from the server at startup and are never advertised during tool discovery.
|
||||
|
||||
Set `MCP_DISABLED_TOOLS` in your `superset_config.py` to a set of tool names:
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
|
||||
# Disable one tool
|
||||
MCP_DISABLED_TOOLS = {"execute_sql"}
|
||||
|
||||
# Disable multiple tools
|
||||
MCP_DISABLED_TOOLS = {"execute_sql", "health_check"}
|
||||
```
|
||||
|
||||
Tool names match the function name used in the `@tool` decorator (e.g., `execute_sql`, `list_charts`, `health_check`). Extension-prefixed tools can also be disabled using their full prefixed name:
|
||||
|
||||
```python
|
||||
MCP_DISABLED_TOOLS = {"extensions.myorg.myextension.some_tool"}
|
||||
```
|
||||
|
||||
:::note
|
||||
Specifying a tool name that does not exist logs a warning at startup and is otherwise ignored — it will not prevent the server from starting.
|
||||
:::
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
- **Use TLS** for all production MCP endpoints -- place the server behind a reverse proxy with HTTPS
|
||||
|
||||
@@ -36,42 +36,6 @@ const versionsConfig = JSON.parse(fs.readFileSync(versionsConfigPath, 'utf8'));
|
||||
// Build plugins array dynamically based on disabled flags
|
||||
const dynamicPlugins = [];
|
||||
|
||||
// Add user_docs (formerly the preset-classic default docs instance) as an
|
||||
// explicit plugin instance, so its versioned dirs follow the same
|
||||
// `<id>_versioned_docs` / `<id>_versioned_sidebars` / `<id>_versions.json`
|
||||
// naming as the other sections instead of the bare `versioned_*` prefix
|
||||
// Docusaurus uses for the default plugin id.
|
||||
if (!versionsConfig.user_docs.disabled) {
|
||||
dynamicPlugins.push([
|
||||
'@docusaurus/plugin-content-docs',
|
||||
{
|
||||
id: 'user_docs',
|
||||
path: 'docs',
|
||||
routeBasePath: 'user-docs',
|
||||
sidebarPath: require.resolve('./sidebars.js'),
|
||||
editUrl: ({ versionDocsDirPath, docPath }: { versionDocsDirPath: string; docPath: string }) => {
|
||||
if (docPath === 'intro.md') {
|
||||
return 'https://github.com/apache/superset/edit/master/README.md';
|
||||
}
|
||||
return `https://github.com/apache/superset/edit/master/docs/${versionDocsDirPath}/${docPath}`;
|
||||
},
|
||||
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges, remarkTechArticleSchema],
|
||||
admonitions: {
|
||||
keywords: ['note', 'tip', 'info', 'warning', 'danger', 'resources'],
|
||||
extendDefaults: true,
|
||||
},
|
||||
docItemComponent: '@theme/DocItem',
|
||||
includeCurrentVersion: versionsConfig.user_docs.includeCurrentVersion,
|
||||
lastVersion: versionsConfig.user_docs.lastVersion,
|
||||
onlyIncludeVersions: versionsConfig.user_docs.onlyIncludeVersions,
|
||||
versions: versionsConfig.user_docs.versions,
|
||||
disableVersioning: false,
|
||||
showLastUpdateAuthor: true,
|
||||
showLastUpdateTime: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// Add components plugin if not disabled
|
||||
if (!versionsConfig.components.disabled) {
|
||||
dynamicPlugins.push([
|
||||
@@ -739,12 +703,29 @@ const config: Config = {
|
||||
[
|
||||
'@docusaurus/preset-classic',
|
||||
{
|
||||
// The user-docs section is configured as an explicit plugin
|
||||
// instance above (id: 'user_docs') rather than the preset's
|
||||
// default docs slot, so that all four sections use parallel
|
||||
// `<id>_versioned_docs` / `<id>_versioned_sidebars` /
|
||||
// `<id>_versions.json` naming on disk.
|
||||
docs: false,
|
||||
docs: {
|
||||
routeBasePath: 'user-docs',
|
||||
sidebarPath: require.resolve('./sidebars.js'),
|
||||
editUrl: ({ versionDocsDirPath, docPath }) => {
|
||||
if (docPath === 'intro.md') {
|
||||
return 'https://github.com/apache/superset/edit/master/README.md';
|
||||
}
|
||||
return `https://github.com/apache/superset/edit/master/docs/${versionDocsDirPath}/${docPath}`;
|
||||
},
|
||||
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges, remarkTechArticleSchema],
|
||||
admonitions: {
|
||||
keywords: ['note', 'tip', 'info', 'warning', 'danger', 'resources'],
|
||||
extendDefaults: true,
|
||||
},
|
||||
includeCurrentVersion: versionsConfig.docs.includeCurrentVersion,
|
||||
lastVersion: versionsConfig.docs.lastVersion, // Make 'next' the default
|
||||
onlyIncludeVersions: versionsConfig.docs.onlyIncludeVersions,
|
||||
versions: versionsConfig.docs.versions,
|
||||
disableVersioning: false,
|
||||
showLastUpdateAuthor: true,
|
||||
showLastUpdateTime: true,
|
||||
docItemComponent: '@theme/DocItem',
|
||||
},
|
||||
blog: {
|
||||
showReadingTime: true,
|
||||
// Please change this to your repo.
|
||||
|
||||
@@ -33,11 +33,11 @@
|
||||
"lint:docs-links": "node scripts/lint-docs-links.mjs",
|
||||
"version:add": "node scripts/manage-versions.mjs add",
|
||||
"version:remove": "node scripts/manage-versions.mjs remove",
|
||||
"version:add:user_docs": "node scripts/manage-versions.mjs add user_docs",
|
||||
"version:add:docs": "node scripts/manage-versions.mjs add docs",
|
||||
"version:add:admin_docs": "node scripts/manage-versions.mjs add admin_docs",
|
||||
"version:add:developer_docs": "node scripts/manage-versions.mjs add developer_docs",
|
||||
"version:add:components": "node scripts/manage-versions.mjs add components",
|
||||
"version:remove:user_docs": "node scripts/manage-versions.mjs remove user_docs",
|
||||
"version:remove:docs": "node scripts/manage-versions.mjs remove docs",
|
||||
"version:remove:admin_docs": "node scripts/manage-versions.mjs remove admin_docs",
|
||||
"version:remove:developer_docs": "node scripts/manage-versions.mjs remove developer_docs",
|
||||
"version:remove:components": "node scripts/manage-versions.mjs remove components"
|
||||
@@ -71,9 +71,9 @@
|
||||
"@storybook/theming": "^8.6.15",
|
||||
"@superset-ui/core": "^0.20.4",
|
||||
"@swc/core": "^1.15.33",
|
||||
"antd": "^6.4.3",
|
||||
"baseline-browser-mapping": "^2.10.30",
|
||||
"caniuse-lite": "^1.0.30001793",
|
||||
"antd": "^6.3.7",
|
||||
"baseline-browser-mapping": "^2.10.29",
|
||||
"caniuse-lite": "^1.0.30001792",
|
||||
"docusaurus-plugin-openapi-docs": "^5.0.2",
|
||||
"docusaurus-theme-openapi-docs": "^5.0.2",
|
||||
"js-yaml": "^4.1.1",
|
||||
@@ -87,7 +87,7 @@
|
||||
"react-svg-pan-zoom": "^3.13.1",
|
||||
"react-table": "^7.8.0",
|
||||
"remark-import-partial": "^0.0.2",
|
||||
"reselect": "^5.2.0",
|
||||
"reselect": "^5.1.1",
|
||||
"storybook": "^8.6.18",
|
||||
"swagger-ui-react": "^5.32.5",
|
||||
"swc-loader": "^0.2.7",
|
||||
|
||||
@@ -34,7 +34,7 @@ const rawArgs = process.argv.slice(2);
|
||||
const skipGenerate = rawArgs.includes('--skip-generate');
|
||||
const args = rawArgs.filter((a) => a !== '--skip-generate');
|
||||
const command = args[0]; // 'add' or 'remove'
|
||||
const section = args[1]; // 'user_docs', 'admin_docs', 'developer_docs', or 'components'
|
||||
const section = args[1]; // 'docs', 'admin_docs', 'developer_docs', or 'components'
|
||||
const version = args[2]; // version string like '1.2.0'
|
||||
|
||||
function loadConfig() {
|
||||
@@ -54,13 +54,13 @@ function freezeDataImports(section, version) {
|
||||
// historical version's content silently changes whenever the data file
|
||||
// is updated. Copy each escaping data import into a snapshot-local
|
||||
// `_versioned_data/` dir and rewrite the import to point there.
|
||||
// The user_docs section's source content lives in `docs/docs/` (the
|
||||
// historical folder name), while admin_docs / developer_docs /
|
||||
// components match their plugin id 1:1.
|
||||
const sectionDir = section === 'user_docs' ? 'docs' : section;
|
||||
const sectionRoot = path.join(__dirname, '..', sectionDir);
|
||||
const sectionRoot = section === 'docs'
|
||||
? path.join(__dirname, '..', 'docs')
|
||||
: path.join(__dirname, '..', section);
|
||||
const docsRoot = path.join(__dirname, '..');
|
||||
const versionedDocsDir = `${section}_versioned_docs/version-${version}`;
|
||||
const versionedDocsDir = section === 'docs'
|
||||
? `versioned_docs/version-${version}`
|
||||
: `${section}_versioned_docs/version-${version}`;
|
||||
const versionedDocsPath = path.join(__dirname, '..', versionedDocsDir);
|
||||
const frozenDataDir = path.join(versionedDocsPath, '_versioned_data');
|
||||
|
||||
@@ -148,7 +148,9 @@ function fixVersionedImports(section, version) {
|
||||
// Versioned content lands one directory deeper than the source content,
|
||||
// so any `../../src/` or `../../data/` imports in .md/.mdx files need
|
||||
// an extra `../` to keep reaching docs/src and docs/data.
|
||||
const versionedDocsDir = `${section}_versioned_docs/version-${version}`;
|
||||
const versionedDocsDir = section === 'docs'
|
||||
? `versioned_docs/version-${version}`
|
||||
: `${section}_versioned_docs/version-${version}`;
|
||||
const versionedDocsPath = path.join(__dirname, '..', versionedDocsDir);
|
||||
|
||||
if (!fs.existsSync(versionedDocsPath)) {
|
||||
@@ -236,7 +238,9 @@ function addVersion(section, version) {
|
||||
}
|
||||
|
||||
// Run Docusaurus version command
|
||||
const docusaurusCommand = `yarn docusaurus docs:version:${section} ${version}`;
|
||||
const docusaurusCommand = section === 'docs'
|
||||
? `yarn docusaurus docs:version ${version}`
|
||||
: `yarn docusaurus docs:version:${section} ${version}`;
|
||||
|
||||
try {
|
||||
execSync(docusaurusCommand, { stdio: 'inherit' });
|
||||
@@ -258,9 +262,10 @@ function addVersion(section, version) {
|
||||
config[section].onlyIncludeVersions.splice(versionIndex, 0, version);
|
||||
|
||||
// Add version metadata
|
||||
const versionPath = section === 'docs' ? version : version;
|
||||
config[section].versions[version] = {
|
||||
label: version,
|
||||
path: version,
|
||||
path: versionPath,
|
||||
banner: 'none'
|
||||
};
|
||||
|
||||
@@ -300,8 +305,13 @@ function removeVersion(section, version) {
|
||||
console.log(`Removing version ${version} from ${section}...`);
|
||||
|
||||
// Determine file paths based on section
|
||||
const versionedDocsDir = `${section}_versioned_docs/version-${version}`;
|
||||
const versionedSidebarsFile = `${section}_versioned_sidebars/version-${version}-sidebars.json`;
|
||||
const versionedDocsDir = section === 'docs'
|
||||
? `versioned_docs/version-${version}`
|
||||
: `${section}_versioned_docs/version-${version}`;
|
||||
|
||||
const versionedSidebarsFile = section === 'docs'
|
||||
? `versioned_sidebars/version-${version}-sidebars.json`
|
||||
: `${section}_versioned_sidebars/version-${version}-sidebars.json`;
|
||||
|
||||
// Remove versioned files
|
||||
const docsPath = path.join(__dirname, '..', versionedDocsDir);
|
||||
@@ -318,7 +328,9 @@ function removeVersion(section, version) {
|
||||
}
|
||||
|
||||
// Update versions.json file
|
||||
const versionsJsonFile = `${section}_versions.json`;
|
||||
const versionsJsonFile = section === 'docs'
|
||||
? 'versions.json'
|
||||
: `${section}_versions.json`;
|
||||
const versionsJsonPath = path.join(__dirname, '..', versionsJsonFile);
|
||||
|
||||
if (fs.existsSync(versionsJsonPath)) {
|
||||
@@ -365,14 +377,14 @@ Usage:
|
||||
node scripts/manage-versions.mjs remove <section> <version>
|
||||
|
||||
Where:
|
||||
- section: 'user_docs', 'admin_docs', 'developer_docs', or 'components'
|
||||
- section: 'docs', 'developer_docs', 'admin_docs', or 'components'
|
||||
- version: version string (e.g., '1.2.0', '2.0.0')
|
||||
- --skip-generate: skip refreshing auto-generated docs before snapshotting
|
||||
(use when you've already placed a fresh databases.json
|
||||
from CI and want to preserve it)
|
||||
|
||||
Examples:
|
||||
node scripts/manage-versions.mjs add user_docs 2.0.0
|
||||
node scripts/manage-versions.mjs add docs 2.0.0
|
||||
node scripts/manage-versions.mjs add developer_docs 1.3.0
|
||||
node scripts/manage-versions.mjs remove components 1.0.0
|
||||
`);
|
||||
|
||||
@@ -31,15 +31,17 @@ import { DownOutlined } from '@ant-design/icons';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
// Map each versioned plugin id to the URL prefix it actually serves
|
||||
// content from. The plugin ids use underscores while several
|
||||
// routeBasePath values use hyphens (and `user_docs` → `/user-docs`),
|
||||
// so without this map the basePath derivation below would mis-split
|
||||
// the pathname for those sections and the version dropdown would
|
||||
// jump to the section root instead of preserving the current page.
|
||||
// content from. Three of the four routeBasePath values differ from
|
||||
// their pluginId — the default preset-classic docs plugin lives at
|
||||
// `/user-docs`, and admin_docs / developer_docs use hyphens in their
|
||||
// URLs even though the plugin ids use underscores. Without this map
|
||||
// the basePath derivation below would mis-split the pathname for
|
||||
// those sections and the version dropdown would jump to the section
|
||||
// root instead of preserving the current page.
|
||||
//
|
||||
// Keep in sync with the `routeBasePath` values in docusaurus.config.ts.
|
||||
const PLUGIN_ID_TO_BASE_PATH = {
|
||||
user_docs: '/user-docs',
|
||||
default: '/user-docs',
|
||||
components: '/components',
|
||||
admin_docs: '/admin-docs',
|
||||
developer_docs: '/developer-docs',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"user_docs": {
|
||||
"docs": {
|
||||
"disabled": false,
|
||||
"lastVersion": "current",
|
||||
"includeCurrentVersion": true,
|
||||
|
||||
234
docs/yarn.lock
234
docs/yarn.lock
@@ -212,7 +212,7 @@
|
||||
resolved "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz"
|
||||
integrity sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==
|
||||
|
||||
"@ant-design/icons@^6.2.3":
|
||||
"@ant-design/icons@^6.1.1", "@ant-design/icons@^6.2.3":
|
||||
version "6.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.2.3.tgz#66e1c7fdea009b9c3fab6964062bedc76f308ad8"
|
||||
integrity sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg==
|
||||
@@ -1158,10 +1158,10 @@
|
||||
dependencies:
|
||||
core-js-pure "^3.43.0"
|
||||
|
||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.25.6", "@babel/runtime@^7.25.9", "@babel/runtime@^7.28.4", "@babel/runtime@^7.29.2":
|
||||
version "7.29.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.2.tgz#9a6e2d05f4b6692e1801cd4fb176ad823930ed5e"
|
||||
integrity sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==
|
||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.25.6", "@babel/runtime@^7.25.9", "@babel/runtime@^7.28.4":
|
||||
version "7.28.4"
|
||||
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz"
|
||||
integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
|
||||
|
||||
"@babel/template@^7.27.1", "@babel/template@^7.27.2", "@babel/template@^7.28.6":
|
||||
version "7.28.6"
|
||||
@@ -2924,13 +2924,13 @@
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.24.4"
|
||||
|
||||
"@rc-component/cascader@~1.15.0":
|
||||
version "1.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/cascader/-/cascader-1.15.0.tgz#554cba8e01e94a1288547cec96422b2cfc73ff40"
|
||||
integrity sha512-ZzpMtwFCRo3fbXHuDnncARJMZQjdqA2w7aDuPofNQt+aDx39st1hgfIpEwTBLhe2Hqsvs/zOr8RTtgxTkCPySw==
|
||||
"@rc-component/cascader@~1.14.0":
|
||||
version "1.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/cascader/-/cascader-1.14.0.tgz#74e1fca58cb14f8f75f6e4bf1debd90534aaea7c"
|
||||
integrity sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ==
|
||||
dependencies:
|
||||
"@rc-component/select" "~1.6.0"
|
||||
"@rc-component/tree" "~1.3.0"
|
||||
"@rc-component/tree" "~1.2.0"
|
||||
"@rc-component/util" "^1.4.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
@@ -2968,10 +2968,10 @@
|
||||
dependencies:
|
||||
"@rc-component/util" "^1.3.0"
|
||||
|
||||
"@rc-component/dialog@~1.9.0":
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/dialog/-/dialog-1.9.0.tgz#3134f8fa8644d9bc228c862668b90de048c7ea1a"
|
||||
integrity sha512-zbAAogkg4kkKum79sLE6M+vq1jSAW25zdkafrahgcTP9t9S//SD634Znd1A4c8F2Gc12ZKnehGLsVaaOvZzD2A==
|
||||
"@rc-component/dialog@~1.8.4":
|
||||
version "1.8.4"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/dialog/-/dialog-1.8.4.tgz#e1f05f311539852f40a5717bc3874ce0af64c6ff"
|
||||
integrity sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw==
|
||||
dependencies:
|
||||
"@rc-component/motion" "^1.1.3"
|
||||
"@rc-component/portal" "^2.1.0"
|
||||
@@ -3025,30 +3025,30 @@
|
||||
"@rc-component/util" "^1.4.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/input@~1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/input/-/input-1.3.0.tgz#a8c113000bbc39089cf75337bec68120115b9e05"
|
||||
integrity sha512-IUUNOdAuWuEvDEFFgfmwQl818tiDbvXwLgon4HL1q2hJeYkqrRrYwYhJN0zfPHGTDxs3gvyVC/C02D4hWFoIcA==
|
||||
"@rc-component/input@~1.1.0", "@rc-component/input@~1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/@rc-component/input/-/input-1.1.2.tgz"
|
||||
integrity sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg==
|
||||
dependencies:
|
||||
"@rc-component/resize-observer" "^1.1.1"
|
||||
"@rc-component/util" "^1.4.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/mentions@~1.9.0":
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/mentions/-/mentions-1.9.0.tgz#1e133d607835854430e264b681b7b32c4b49daa7"
|
||||
integrity sha512-WUwfFKDSOF5S9UPsNsXcLYtzjTxBGsftTXWRbZuxX6BYrsySISTnujfJNgaaQ6qVzaCDJ35QUkZKvsYxip1C5g==
|
||||
"@rc-component/mentions@~1.6.0":
|
||||
version "1.6.0"
|
||||
resolved "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.6.0.tgz"
|
||||
integrity sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ==
|
||||
dependencies:
|
||||
"@rc-component/input" "~1.3.0"
|
||||
"@rc-component/menu" "~1.3.0"
|
||||
"@rc-component/input" "~1.1.0"
|
||||
"@rc-component/menu" "~1.2.0"
|
||||
"@rc-component/textarea" "~1.1.0"
|
||||
"@rc-component/trigger" "^3.0.0"
|
||||
"@rc-component/util" "^1.3.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/menu@~1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/menu/-/menu-1.3.0.tgz#fc70d81ca76ae6013b0d7955f20a2393adef04b3"
|
||||
integrity sha512-u3NfiwpiEgT177qa5Yxm5QsI8i/93EBGpWj8HYZQDnh2pCZ2xtQCe/+w3pSR2NlwKOZDTCKzEhEyD09mGphssA==
|
||||
"@rc-component/menu@~1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/@rc-component/menu/-/menu-1.2.0.tgz"
|
||||
integrity sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg==
|
||||
dependencies:
|
||||
"@rc-component/motion" "^1.1.4"
|
||||
"@rc-component/overflow" "^1.0.0"
|
||||
@@ -3078,13 +3078,13 @@
|
||||
dependencies:
|
||||
"@rc-component/util" "^1.2.0"
|
||||
|
||||
"@rc-component/notification@~2.0.7":
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/notification/-/notification-2.0.7.tgz#f2450a482f87e4698285833c4a8efcac169acabb"
|
||||
integrity sha512-nqZzpf6BPdaj+3ILx7si79LLmqPKyUmQoXa+/9gg0SkH0v1DbD66oJgRMSBEVnd/zUT3D4gwxWIHUKebYf2ZXQ==
|
||||
"@rc-component/notification@~1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/@rc-component/notification/-/notification-1.2.0.tgz"
|
||||
integrity sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA==
|
||||
dependencies:
|
||||
"@rc-component/motion" "^1.1.4"
|
||||
"@rc-component/util" "^1.11.0"
|
||||
"@rc-component/util" "^1.2.1"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/overflow@^1.0.0":
|
||||
@@ -3105,10 +3105,10 @@
|
||||
"@rc-component/util" "^1.3.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/picker@~1.10.0":
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/picker/-/picker-1.10.0.tgz#6989f0ae67fca8db00e31f81a8217c8bc370cd34"
|
||||
integrity sha512-vVOXP2RVWozwpERGUFAehVH1Jz6o/uRrAb9qSZm1LC+iJs8rvEwFo1bzz2jlOYV+uWwu0dIuG86tnDui14Ea0w==
|
||||
"@rc-component/picker@~1.9.1":
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/picker/-/picker-1.9.1.tgz#7ffcb1e4d4655fe2f3d712773e1d3ab9cd5c2a5c"
|
||||
integrity sha512-9FBYYsvH3HMLICaPDA/1Th5FLaDkFa7qAtangIdlhKb3ZALaR745e9PsOhheJb6asS4QXc12ffiAcjdkZ4C5/g==
|
||||
dependencies:
|
||||
"@rc-component/overflow" "^1.0.0"
|
||||
"@rc-component/resize-observer" "^1.0.0"
|
||||
@@ -3199,10 +3199,10 @@
|
||||
"@rc-component/util" "^1.3.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/table@~1.10.0":
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/table/-/table-1.10.0.tgz#7a98d68176f23f50a762df464f4c9142e7db3942"
|
||||
integrity sha512-SjtpcCf+rL7dDc62GKT3rXTdERjVuJvRiqjpU7g0Jc/ewCifXynHc7Nm3Em1XsD+WhGrgQtxNDScI/0+Lpfr0w==
|
||||
"@rc-component/table@~1.9.1":
|
||||
version "1.9.1"
|
||||
resolved "https://registry.npmjs.org/@rc-component/table/-/table-1.9.1.tgz"
|
||||
integrity sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg==
|
||||
dependencies:
|
||||
"@rc-component/context" "^2.0.1"
|
||||
"@rc-component/resize-observer" "^1.0.0"
|
||||
@@ -3210,18 +3210,28 @@
|
||||
"@rc-component/virtual-list" "^1.0.1"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/tabs@~1.9.0":
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/tabs/-/tabs-1.9.0.tgz#8f3e3755450e5a90d240d1ed3dc140d520b1fbef"
|
||||
integrity sha512-tn1slmbbaTyt8mgwyWJcT8jo/qNiYUs6u1H7OgGQt9faYO06BJIkU5cTmMqORzIrNmSEeeUY6pD5i+JlqSHYhg==
|
||||
"@rc-component/tabs@~1.7.0":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.7.0.tgz"
|
||||
integrity sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w==
|
||||
dependencies:
|
||||
"@rc-component/dropdown" "~1.0.0"
|
||||
"@rc-component/menu" "~1.3.0"
|
||||
"@rc-component/menu" "~1.2.0"
|
||||
"@rc-component/motion" "^1.1.3"
|
||||
"@rc-component/resize-observer" "^1.0.0"
|
||||
"@rc-component/util" "^1.3.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/textarea@~1.1.0", "@rc-component/textarea@~1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/@rc-component/textarea/-/textarea-1.1.2.tgz"
|
||||
integrity sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A==
|
||||
dependencies:
|
||||
"@rc-component/input" "~1.1.0"
|
||||
"@rc-component/resize-observer" "^1.0.0"
|
||||
"@rc-component/util" "^1.3.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/tooltip@~1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.4.0.tgz"
|
||||
@@ -3231,30 +3241,30 @@
|
||||
"@rc-component/util" "^1.3.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/tour@~2.4.0":
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/tour/-/tour-2.4.0.tgz#caf89cf8f2f9fb68f1fb0e0c867610015d01f432"
|
||||
integrity sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg==
|
||||
"@rc-component/tour@~2.3.0":
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/@rc-component/tour/-/tour-2.3.0.tgz"
|
||||
integrity sha512-K04K9r32kUC+auBSQfr+Fss4SpSIS9JGe56oq/ALAX0p+i2ylYOI1MgR83yBY7v96eO6ZFXcM/igCQmubps0Ow==
|
||||
dependencies:
|
||||
"@rc-component/portal" "^2.2.0"
|
||||
"@rc-component/trigger" "^3.0.0"
|
||||
"@rc-component/util" "^1.7.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/tree-select@~1.9.0":
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/tree-select/-/tree-select-1.9.0.tgz#13ea516478b6cb558e04181abb0a01ae6fbdd31f"
|
||||
integrity sha512-GXcFe15a+trUl1/J3OHWQhsVWFpwFpGFK2cqYWZ1sK22Zs3KZTvMwDpzr75PIo1s6QVioVxpE/pRwRopkeDQ6w==
|
||||
"@rc-component/tree-select@~1.8.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/tree-select/-/tree-select-1.8.0.tgz#480e84221befbd1fa93ab2034423e2b064e41981"
|
||||
integrity sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ==
|
||||
dependencies:
|
||||
"@rc-component/select" "~1.6.0"
|
||||
"@rc-component/tree" "~1.3.0"
|
||||
"@rc-component/tree" "~1.2.0"
|
||||
"@rc-component/util" "^1.4.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/tree@~1.3.0", "@rc-component/tree@~1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/tree/-/tree-1.3.1.tgz#6983ca6bd9d5f6d04dd7258d00cb0fe71cdfe661"
|
||||
integrity sha512-zlL0PW0bTFlveTtLcA01VD/yMWKK73EywItFMgIZUY5sb6tMOAw7zV6qGzqldufqrV93ZWQB4H3NBNoTMCueJA==
|
||||
"@rc-component/tree@~1.2.0", "@rc-component/tree@~1.2.4":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/tree/-/tree-1.2.4.tgz#cb4f7d818118b3447763e74d3a82fba6454c7317"
|
||||
integrity sha512-5Gli43+m4R7NhpYYz3Z61I6LOw9yI6CNChxgVtvrO6xB1qML7iE6QMLVMB3+FTjo2yF6uFdAHtqWPECz/zbX5w==
|
||||
dependencies:
|
||||
"@rc-component/motion" "^1.0.0"
|
||||
"@rc-component/util" "^1.8.1"
|
||||
@@ -3280,10 +3290,10 @@
|
||||
"@rc-component/util" "^1.3.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/util@^1.1.0", "@rc-component/util@^1.10.1", "@rc-component/util@^1.11.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.6.2", "@rc-component/util@^1.7.0", "@rc-component/util@^1.8.1", "@rc-component/util@^1.9.0":
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.11.0.tgz#965c8b44a3f57fc96dc14e5072afbe32e422fd4d"
|
||||
integrity sha512-jHG3/BYgUWiP5c7RZHiaUNToyw1L3nlPSKG2RPu+YoiD9b3ajiJwBWhsjO+ZELmCsKFAjNR5DelbKdlF0e2BDA==
|
||||
"@rc-component/util@^1.1.0", "@rc-component/util@^1.10.1", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.6.2", "@rc-component/util@^1.7.0", "@rc-component/util@^1.8.1", "@rc-component/util@^1.9.0":
|
||||
version "1.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.10.1.tgz#213c84c77e8b2001095530d3b0dc47c49c34ffe3"
|
||||
integrity sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng==
|
||||
dependencies:
|
||||
is-mobile "^5.0.0"
|
||||
react-is "^18.2.0"
|
||||
@@ -5342,13 +5352,6 @@ address@^1.0.1:
|
||||
resolved "https://registry.npmjs.org/address/-/address-1.2.2.tgz"
|
||||
integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==
|
||||
|
||||
agent-base@6:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
|
||||
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
|
||||
dependencies:
|
||||
debug "4"
|
||||
|
||||
aggregate-error@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz"
|
||||
@@ -5498,36 +5501,36 @@ ansis@^3.2.0:
|
||||
resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.17.0.tgz#fa8d9c2a93fe7d1177e0c17f9eeb562a58a832d7"
|
||||
integrity sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==
|
||||
|
||||
antd@^6.4.3:
|
||||
version "6.4.3"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-6.4.3.tgz#80a7aab9c13c35daa0e0e7eea80585ba57cb7203"
|
||||
integrity sha512-6H2avkxCGfxcF67r3J2mwm9Ck50el1pks/73vfM1wDsPL/tPtj5vHuauMgJFnrqmq7CH3g8aoZ0VBQbt+jpAsw==
|
||||
antd@^6.3.7:
|
||||
version "6.3.7"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-6.3.7.tgz#620354ec04135356cbc5ce0a666871ddc73e4117"
|
||||
integrity sha512-WTHi4bHVNKpYXLHESzU0Tts7rRNQeL84Bph9dfI3Qw7mHbTulExDcYKNHny5CTXcrBBOpraXbU9miBAwUR5vaw==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^8.0.1"
|
||||
"@ant-design/cssinjs" "^2.1.2"
|
||||
"@ant-design/cssinjs-utils" "^2.1.2"
|
||||
"@ant-design/fast-color" "^3.0.1"
|
||||
"@ant-design/icons" "^6.2.3"
|
||||
"@ant-design/icons" "^6.1.1"
|
||||
"@ant-design/react-slick" "~2.0.0"
|
||||
"@babel/runtime" "^7.29.2"
|
||||
"@rc-component/cascader" "~1.15.0"
|
||||
"@babel/runtime" "^7.28.4"
|
||||
"@rc-component/cascader" "~1.14.0"
|
||||
"@rc-component/checkbox" "~2.0.0"
|
||||
"@rc-component/collapse" "~1.2.0"
|
||||
"@rc-component/color-picker" "~3.1.1"
|
||||
"@rc-component/dialog" "~1.9.0"
|
||||
"@rc-component/dialog" "~1.8.4"
|
||||
"@rc-component/drawer" "~1.4.2"
|
||||
"@rc-component/dropdown" "~1.0.2"
|
||||
"@rc-component/form" "~1.8.1"
|
||||
"@rc-component/image" "~1.9.0"
|
||||
"@rc-component/input" "~1.3.0"
|
||||
"@rc-component/input" "~1.1.2"
|
||||
"@rc-component/input-number" "~1.6.2"
|
||||
"@rc-component/mentions" "~1.9.0"
|
||||
"@rc-component/menu" "~1.3.0"
|
||||
"@rc-component/mentions" "~1.6.0"
|
||||
"@rc-component/menu" "~1.2.0"
|
||||
"@rc-component/motion" "^1.3.2"
|
||||
"@rc-component/mutate-observer" "^2.0.1"
|
||||
"@rc-component/notification" "~2.0.7"
|
||||
"@rc-component/notification" "~1.2.0"
|
||||
"@rc-component/pagination" "~1.2.0"
|
||||
"@rc-component/picker" "~1.10.0"
|
||||
"@rc-component/picker" "~1.9.1"
|
||||
"@rc-component/progress" "~1.0.2"
|
||||
"@rc-component/qrcode" "~1.1.1"
|
||||
"@rc-component/rate" "~1.0.1"
|
||||
@@ -5537,15 +5540,16 @@ antd@^6.4.3:
|
||||
"@rc-component/slider" "~1.0.1"
|
||||
"@rc-component/steps" "~1.2.2"
|
||||
"@rc-component/switch" "~1.0.3"
|
||||
"@rc-component/table" "~1.10.0"
|
||||
"@rc-component/tabs" "~1.9.0"
|
||||
"@rc-component/table" "~1.9.1"
|
||||
"@rc-component/tabs" "~1.7.0"
|
||||
"@rc-component/textarea" "~1.1.2"
|
||||
"@rc-component/tooltip" "~1.4.0"
|
||||
"@rc-component/tour" "~2.4.0"
|
||||
"@rc-component/tree" "~1.3.1"
|
||||
"@rc-component/tree-select" "~1.9.0"
|
||||
"@rc-component/tour" "~2.3.0"
|
||||
"@rc-component/tree" "~1.2.4"
|
||||
"@rc-component/tree-select" "~1.8.0"
|
||||
"@rc-component/trigger" "^3.9.0"
|
||||
"@rc-component/upload" "~1.1.0"
|
||||
"@rc-component/util" "^1.11.0"
|
||||
"@rc-component/util" "^1.10.1"
|
||||
clsx "^2.1.1"
|
||||
dayjs "^1.11.11"
|
||||
scroll-into-view-if-needed "^3.1.0"
|
||||
@@ -5733,13 +5737,12 @@ available-typed-arrays@^1.0.7:
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
axios@^1.15.0:
|
||||
version "1.16.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.16.1.tgz#517e29291d19d6e8cf919ff264f4fe157261ba12"
|
||||
integrity sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==
|
||||
version "1.15.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.15.0.tgz#0fcee91ef03d386514474904b27863b2c683bf4f"
|
||||
integrity sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==
|
||||
dependencies:
|
||||
follow-redirects "^1.16.0"
|
||||
follow-redirects "^1.15.11"
|
||||
form-data "^4.0.5"
|
||||
https-proxy-agent "^5.0.1"
|
||||
proxy-from-env "^2.1.0"
|
||||
|
||||
babel-loader@^9.2.1:
|
||||
@@ -5810,10 +5813,10 @@ base64-js@^1.3.1, base64-js@^1.5.1:
|
||||
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
baseline-browser-mapping@^2.10.30, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
|
||||
version "2.10.30"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.30.tgz#58915c74388b05f3b3504026194ea9fa98f6e6b6"
|
||||
integrity sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg==
|
||||
baseline-browser-mapping@^2.10.29, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
|
||||
version "2.10.29"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz#47bdc13027af28d341f367a4f35a07ce872e27b4"
|
||||
integrity sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==
|
||||
|
||||
batch@0.6.1:
|
||||
version "0.6.1"
|
||||
@@ -6051,10 +6054,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.30001759, caniuse-lite@^1.0.30001793:
|
||||
version "1.0.30001793"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz#238887ddf5fcfc8c36d872394d0a78a517312a72"
|
||||
integrity sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001792:
|
||||
version "1.0.30001792"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz#ca8bb9be244835a335e2018272ce7223691873c5"
|
||||
integrity sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==
|
||||
|
||||
ccount@^2.0.0:
|
||||
version "2.0.1"
|
||||
@@ -7098,7 +7101,12 @@ data-view-byte-offset@^1.0.1:
|
||||
es-errors "^1.3.0"
|
||||
is-data-view "^1.0.1"
|
||||
|
||||
dayjs@^1.11.11, dayjs@^1.11.19:
|
||||
dayjs@^1.11.11:
|
||||
version "1.11.13"
|
||||
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz"
|
||||
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
|
||||
|
||||
dayjs@^1.11.19:
|
||||
version "1.11.20"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.20.tgz#88d919fd639dc991415da5f4cb6f1b6650811938"
|
||||
integrity sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==
|
||||
@@ -8253,7 +8261,7 @@ flatted@^3.2.9:
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726"
|
||||
integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==
|
||||
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.16.0:
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.15.11:
|
||||
version "1.16.0"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc"
|
||||
integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==
|
||||
@@ -8947,14 +8955,6 @@ http2-wrapper@^2.1.10:
|
||||
quick-lru "^5.1.1"
|
||||
resolve-alpn "^1.2.0"
|
||||
|
||||
https-proxy-agent@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
|
||||
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
|
||||
dependencies:
|
||||
agent-base "6"
|
||||
debug "4"
|
||||
|
||||
human-signals@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz"
|
||||
@@ -13291,10 +13291,10 @@ reselect@^4.0.0:
|
||||
resolved "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz"
|
||||
integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==
|
||||
|
||||
reselect@^5.1.0, reselect@^5.1.1, reselect@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.2.0.tgz#f380ef7664332d26ea06c1cba04bdbbdcaa955f1"
|
||||
integrity sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==
|
||||
reselect@^5.1.0, reselect@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz"
|
||||
integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==
|
||||
|
||||
resize-observer-polyfill@1.5.1:
|
||||
version "1.5.1"
|
||||
|
||||
@@ -53,11 +53,11 @@ dependencies = [
|
||||
"flask-compress>=1.13, <2.0",
|
||||
"flask-talisman>=1.0.0, <2.0",
|
||||
"flask-login>=0.6.0, < 1.0",
|
||||
"flask-migrate>=3.1.0, <5.0",
|
||||
"flask-migrate>=3.1.0, <4.0",
|
||||
"flask-session>=0.4.0, <1.0",
|
||||
"flask-wtf>=1.1.0, <2.0",
|
||||
"geopy",
|
||||
"greenlet>=3.0.3, <=3.5.0",
|
||||
"greenlet>=3.0.3, <=3.1.1",
|
||||
"gunicorn>=22.0.0; sys_platform != 'win32'",
|
||||
"hashids>=1.3.1, <2",
|
||||
# holidays>=0.45 required for security fix
|
||||
@@ -121,7 +121,7 @@ bigquery = [
|
||||
"sqlalchemy-bigquery>=1.15.0",
|
||||
"google-cloud-bigquery>=3.10.0",
|
||||
]
|
||||
clickhouse = ["clickhouse-connect>=0.13.0, <2.0"]
|
||||
clickhouse = ["clickhouse-connect>=0.13.0, <1.0"]
|
||||
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
|
||||
crate = ["sqlalchemy-cratedb>=0.40.1, <1"]
|
||||
d1 = [
|
||||
@@ -143,7 +143,7 @@ duckdb = ["duckdb>=1.4.2,<2", "duckdb-engine>=0.17.0"]
|
||||
dynamodb = ["pydynamodb>=0.4.2"]
|
||||
solr = ["sqlalchemy-solr >= 0.2.0"]
|
||||
elasticsearch = ["elasticsearch-dbapi>=0.2.13, <0.3.0"]
|
||||
exasol = ["sqlalchemy-exasol >= 2.4.0, < 8.0"]
|
||||
exasol = ["sqlalchemy-exasol >= 2.4.0, <3.0"]
|
||||
excel = ["xlrd>=1.2.0, <1.3"]
|
||||
fastmcp = [
|
||||
"fastmcp>=3.2.4,<4.0",
|
||||
@@ -156,7 +156,7 @@ firebird = ["sqlalchemy-firebird>=0.7.0, <2.2"]
|
||||
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
|
||||
gevent = ["gevent>=23.9.1"]
|
||||
gsheets = ["shillelagh[gsheetsapi]>=1.4.4, <2"]
|
||||
hana = ["hdbcli==2.28.20", "sqlalchemy_hana==0.4.0"]
|
||||
hana = ["hdbcli==2.4.162", "sqlalchemy_hana==0.4.0"]
|
||||
hive = [
|
||||
"pyhive[hive]>=0.6.5;python_version<'3.11'",
|
||||
"pyhive[hive_pure_sasl]>=0.7.0",
|
||||
|
||||
12
superset-embedded-sdk/package-lock.json
generated
12
superset-embedded-sdk/package-lock.json
generated
@@ -6756,9 +6756,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
@@ -12811,9 +12811,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
|
||||
89
superset-frontend/cypress-base/package-lock.json
generated
89
superset-frontend/cypress-base/package-lock.json
generated
@@ -6523,9 +6523,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
|
||||
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@@ -7076,6 +7076,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "16.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
|
||||
@@ -7545,6 +7554,15 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
@@ -7906,14 +7924,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz",
|
||||
"integrity": "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==",
|
||||
"version": "5.3.16",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz",
|
||||
"integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jest-worker": "^27.4.5",
|
||||
"schema-utils": "^4.3.0",
|
||||
"serialize-javascript": "^6.0.2",
|
||||
"terser": "^5.31.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -7927,39 +7946,12 @@
|
||||
"webpack": "^5.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@minify-html/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/css": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/html": {
|
||||
"optional": true
|
||||
},
|
||||
"clean-css": {
|
||||
"optional": true
|
||||
},
|
||||
"cssnano": {
|
||||
"optional": true
|
||||
},
|
||||
"csso": {
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"html-minifier-terser": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"postcss": {
|
||||
"optional": true
|
||||
},
|
||||
"uglify-js": {
|
||||
"optional": true
|
||||
}
|
||||
@@ -13507,9 +13499,9 @@
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
|
||||
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@@ -13910,6 +13902,15 @@
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
|
||||
},
|
||||
"randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "16.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
|
||||
@@ -14272,6 +14273,15 @@
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
|
||||
},
|
||||
"serialize-javascript": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
@@ -14556,14 +14566,15 @@
|
||||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz",
|
||||
"integrity": "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==",
|
||||
"version": "5.3.16",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz",
|
||||
"integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jest-worker": "^27.4.5",
|
||||
"schema-utils": "^4.3.0",
|
||||
"serialize-javascript": "^6.0.2",
|
||||
"terser": "^5.31.1"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -27,6 +27,11 @@ module.exports = {
|
||||
'\\.svg$': '<rootDir>/spec/__mocks__/svgrMock.tsx',
|
||||
'^src/(.*)$': '<rootDir>/src/$1',
|
||||
'^spec/(.*)$': '<rootDir>/spec/$1',
|
||||
// mapping glyph-core to local package source
|
||||
'^@superset-ui/glyph-core$':
|
||||
'<rootDir>/packages/superset-ui-glyph-core/src',
|
||||
'^@superset-ui/glyph-core/(.*)$':
|
||||
'<rootDir>/packages/superset-ui-glyph-core/src/$1',
|
||||
// mapping plugins of superset-ui to source code
|
||||
'^@superset-ui/([^/]+)/(.*)$':
|
||||
'<rootDir>/node_modules/@superset-ui/$1/src/$2',
|
||||
|
||||
14912
superset-frontend/package-lock.json
generated
14912
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -109,7 +109,9 @@
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@fontsource/fira-code": "^5.2.7",
|
||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@googleapis/sheets": "^13.0.1",
|
||||
"@luma.gl/constants": "~9.2.5",
|
||||
"@luma.gl/core": "~9.2.5",
|
||||
@@ -161,8 +163,8 @@
|
||||
"@visx/scale": "^3.5.0",
|
||||
"@visx/tooltip": "^3.0.0",
|
||||
"@visx/xychart": "^3.5.1",
|
||||
"ag-grid-community": "35.3.0",
|
||||
"ag-grid-react": "35.3.0",
|
||||
"ag-grid-community": "35.2.1",
|
||||
"ag-grid-react": "35.2.1",
|
||||
"antd": "^5.26.0",
|
||||
"chrono-node": "^2.9.1",
|
||||
"classnames": "^2.2.5",
|
||||
@@ -183,7 +185,7 @@
|
||||
"geostyler-style": "11.0.2",
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"google-auth-library": "^10.6.2",
|
||||
"immer": "^11.1.8",
|
||||
"immer": "^11.1.7",
|
||||
"interweave": "^13.1.1",
|
||||
"jquery": "^4.0.0",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
@@ -199,10 +201,11 @@
|
||||
"nanoid": "^5.1.11",
|
||||
"ol": "^10.9.0",
|
||||
"pretty-ms": "^9.3.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "9.3.1",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^18.2.0",
|
||||
"react-arborist": "^3.6.1",
|
||||
"react-arborist": "^3.5.0",
|
||||
"react-checkbox-tree": "^1.8.0",
|
||||
"react-diff-viewer-continued": "^4.2.2",
|
||||
"react-dnd": "^11.1.3",
|
||||
@@ -289,7 +292,7 @@
|
||||
"@types/js-levenshtein": "^1.1.3",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^25.8.0",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
@@ -305,7 +308,7 @@
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.59.3",
|
||||
"babel-jest": "^30.4.1",
|
||||
"babel-jest": "^30.0.2",
|
||||
"babel-loader": "^10.1.1",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
@@ -367,7 +370,7 @@
|
||||
"terser-webpack-plugin": "^5.6.0",
|
||||
"ts-jest": "^29.4.9",
|
||||
"tscw-config": "^1.1.2",
|
||||
"tsx": "^4.22.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "5.4.5",
|
||||
"unzipper": "^0.12.3",
|
||||
"vm-browserify": "^1.1.2",
|
||||
@@ -375,7 +378,7 @@
|
||||
"webpack": "^5.106.2",
|
||||
"webpack-bundle-analyzer": "^5.3.0",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.4",
|
||||
"webpack-dev-server": "^5.2.3",
|
||||
"webpack-manifest-plugin": "^5.0.1",
|
||||
"webpack-sources": "^3.4.1",
|
||||
"webpack-visualizer-plugin2": "^2.0.0"
|
||||
@@ -411,8 +414,7 @@
|
||||
"@luma.gl/engine": "~9.2.5",
|
||||
"@luma.gl/gltf": "~9.2.5",
|
||||
"@luma.gl/shadertools": "~9.2.5",
|
||||
"@luma.gl/webgl": "~9.2.5",
|
||||
"fast-xml-parser": "^5.8.0"
|
||||
"@luma.gl/webgl": "~9.2.5"
|
||||
},
|
||||
"readme": "ERROR: No README data found!",
|
||||
"scarfSettings": {
|
||||
|
||||
@@ -30,13 +30,13 @@
|
||||
"dependencies": {
|
||||
"chalk": "^5.6.2",
|
||||
"lodash-es": "^4.18.1",
|
||||
"yeoman-generator": "^8.1.2",
|
||||
"yeoman-generator": "^8.2.2",
|
||||
"yosay": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^10.1.0",
|
||||
"fs-extra": "^11.3.5",
|
||||
"jest": "^30.4.2",
|
||||
"jest": "^30.3.0",
|
||||
"yeoman-test": "^11.5.2"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -441,6 +441,8 @@ export interface ControlPanelConfig {
|
||||
sectionOverrides?: SectionOverrides;
|
||||
onInit?: (state: ControlStateMapping) => void;
|
||||
formDataOverrides?: (formData: QueryFormData) => QueryFormData;
|
||||
/** @internal Raw glyph argument definitions from defineChart() – used for native control panel rendering */
|
||||
_glyphArgs?: unknown;
|
||||
}
|
||||
|
||||
export type ControlOverrides = {
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
"lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.2.3",
|
||||
"@ant-design/icons": "^6.2.2",
|
||||
"@apache-superset/core": "*",
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@visx/responsive": "^3.12.0",
|
||||
"ace-builds": "^1.44.0",
|
||||
"ag-grid-community": "35.3.0",
|
||||
"ag-grid-react": "35.3.0",
|
||||
"ag-grid-community": "35.2.1",
|
||||
"ag-grid-react": "35.2.1",
|
||||
"brace": "^0.11.1",
|
||||
"classnames": "^2.5.1",
|
||||
"core-js": "^3.49.0",
|
||||
@@ -42,7 +42,7 @@
|
||||
"d3-time": "^3.1.0",
|
||||
"d3-time-format": "^4.1.0",
|
||||
"dayjs": "^1.11.20",
|
||||
"dompurify": "^3.4.2",
|
||||
"dompurify": "^3.4.1",
|
||||
"fetch-retry": "^6.0.0",
|
||||
"handlebars": "^4.7.9",
|
||||
"jed": "^1.1.1",
|
||||
@@ -76,7 +76,7 @@
|
||||
"@types/d3-time-format": "^4.0.3",
|
||||
"@types/jquery": "^4.0.0",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "^25.8.0",
|
||||
"@types/node": "^25.7.0",
|
||||
"@types/prop-types": "^15.7.15",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/react-table": "^7.7.20",
|
||||
|
||||
@@ -33,6 +33,14 @@ export enum Behavior {
|
||||
*/
|
||||
DrillToDetail = 'DRILL_TO_DETAIL',
|
||||
DrillBy = 'DRILL_BY',
|
||||
|
||||
/**
|
||||
* Include `ALLOWS_EMPTY_RESULTS` behavior if the chart handles empty/no data
|
||||
* gracefully (e.g., showing a drop zone for drag-and-drop configuration).
|
||||
* Charts with this behavior will receive empty data instead of seeing
|
||||
* the "No results" message.
|
||||
*/
|
||||
AllowsEmptyResults = 'ALLOWS_EMPTY_RESULTS',
|
||||
}
|
||||
|
||||
export interface ContextMenuFilters {
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
# Glyph Pattern Migration Guide
|
||||
|
||||
This guide documents how to migrate traditional Superset chart plugins to the single-file Glyph pattern.
|
||||
|
||||
## Overview
|
||||
|
||||
The Glyph pattern simplifies chart plugin development by:
|
||||
- **Arguments define BOTH controls AND render props** - No separate files needed
|
||||
- **No `controlPanel.ts`** - Generated from argument definitions
|
||||
- **No `transformProps.ts`** - Arguments are passed directly to render
|
||||
- **No `buildQuery.ts`** - Inferred from Metric/Dimension/Temporal arguments
|
||||
- **Single file** - Everything in one place (~200 lines vs 500+ across multiple files)
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### 1. Analyze the Existing Chart
|
||||
|
||||
Identify from the original chart:
|
||||
- **Metrics/Dimensions**: What data does it query?
|
||||
- **Controls**: What options does the user configure?
|
||||
- **Styling**: What visual customizations exist?
|
||||
- **Rendering**: How is the data displayed?
|
||||
|
||||
### 2. Create the Glyph Chart File
|
||||
|
||||
Create a new file: `src/BigNumber/BigNumberGlyph/index.tsx`
|
||||
|
||||
```typescript
|
||||
import { t } from '@apache-superset/core';
|
||||
import { styled } from '@apache-superset/core/ui';
|
||||
import { Behavior, getNumberFormatter, CurrencyFormatter } from '@superset-ui/core';
|
||||
|
||||
import {
|
||||
defineChart,
|
||||
Metric,
|
||||
Select,
|
||||
Text,
|
||||
Checkbox,
|
||||
NumberFormat,
|
||||
Currency,
|
||||
TimeFormat,
|
||||
ConditionalFormatting,
|
||||
} from '@superset-ui/glyph-core';
|
||||
```
|
||||
|
||||
### 3. Define Arguments (Controls + Props)
|
||||
|
||||
**CRITICAL: Use camelCase for argument names!**
|
||||
|
||||
Superset converts control names to camelCase in `formData`. If you use snake_case (`show_metric_name`), it won't match the camelCase key in formData (`showMetricName`).
|
||||
|
||||
```typescript
|
||||
arguments: {
|
||||
// Data arguments
|
||||
metric: Metric.with({ label: t('Metric') }),
|
||||
|
||||
// Visual arguments - USE CAMELCASE!
|
||||
headerFontSize: Select.with({
|
||||
label: t('Font Size'),
|
||||
options: [
|
||||
{ label: t('Small'), value: 0.2 },
|
||||
{ label: t('Large'), value: 0.4 },
|
||||
],
|
||||
default: 0.4,
|
||||
}),
|
||||
|
||||
showMetricName: Checkbox.with({
|
||||
label: t('Show Metric Name'),
|
||||
default: false,
|
||||
}),
|
||||
|
||||
// Declarative visibility (preferred)
|
||||
metricNameFontSize: {
|
||||
arg: Select.with({ ... }),
|
||||
visibleWhen: { showMetricName: true },
|
||||
},
|
||||
|
||||
// Declarative disabled state
|
||||
subtitleFontSize: {
|
||||
arg: Select.with({ ... }),
|
||||
disabledWhen: { subtitle: '' },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Available Argument Types
|
||||
|
||||
| Type | Control Generated | Value Type | Properties |
|
||||
|------|------------------|------------|------------|
|
||||
| `Metric` | MetricControl | `{ value, name, formattedValue }` | `label` |
|
||||
| `Dimension` | GroupByControl | `string[]` | `label` |
|
||||
| `Temporal` | TemporalControl | `string` | `label` |
|
||||
| `Select` | SelectControl | `string \| number` | `label`, `description`, `options`, `default` |
|
||||
| `Text` | TextControl | `string` | `label`, `description`, `default`, `placeholder` |
|
||||
| `Checkbox` | CheckboxControl | `boolean` | `label`, `description`, `default` |
|
||||
| `Int` | SliderControl | `number` | `label`, `description`, `default`, `min`, `max`, `step` |
|
||||
| `Color` | ColorPickerControl | `string` (hex) | `label`, `description`, `default` |
|
||||
| `NumberFormat` | SelectControl (freeform) | `string` | `label`, `description`, `default` |
|
||||
| `Currency` | CurrencyControl | `{ symbol?, symbolPosition? }` | `label`, `description`, `default` |
|
||||
| `TimeFormat` | SelectControl (freeform) | `string` | `label`, `description`, `default` |
|
||||
| `ConditionalFormatting` | ConditionalFormattingControl | `Rule[]` | `label`, `description` |
|
||||
|
||||
### 5. Declarative Visibility & Disabled States
|
||||
|
||||
Instead of Redux `mapStateToProps`, use declarative conditions:
|
||||
|
||||
```typescript
|
||||
// Simple equality check - visible when showMetricName is true
|
||||
metricNameFontSize: {
|
||||
arg: Select.with({ ... }),
|
||||
visibleWhen: { showMetricName: true },
|
||||
},
|
||||
|
||||
// Function check - visible when subtitle is not empty
|
||||
subtitleFontSize: {
|
||||
arg: Select.with({ ... }),
|
||||
visibleWhen: { subtitle: (val) => !!val },
|
||||
},
|
||||
|
||||
// Multiple conditions (AND) - visible when both conditions are met
|
||||
advancedOption: {
|
||||
arg: Checkbox.with({ ... }),
|
||||
visibleWhen: {
|
||||
showMetricName: true,
|
||||
subtitle: (val) => !!val,
|
||||
},
|
||||
},
|
||||
|
||||
// Disabled state (control visible but not editable)
|
||||
formatOption: {
|
||||
arg: Select.with({ ... }),
|
||||
disabledWhen: { forceTimestampFormatting: true },
|
||||
},
|
||||
```
|
||||
|
||||
### 6. Number, Currency, and Time Formatting
|
||||
|
||||
Use the built-in format argument types:
|
||||
|
||||
```typescript
|
||||
arguments: {
|
||||
numberFormat: NumberFormat.with({
|
||||
label: t('Number Format'),
|
||||
description: t('D3 format string'),
|
||||
default: 'SMART_NUMBER',
|
||||
}),
|
||||
|
||||
currencyFormat: Currency.with({
|
||||
label: t('Currency Format'),
|
||||
}),
|
||||
|
||||
timeFormat: TimeFormat.with({
|
||||
label: t('Date Format'),
|
||||
default: 'smart_date',
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
Then use them directly in the render function:
|
||||
|
||||
```typescript
|
||||
render: ({ numberFormat, currencyFormat, timeFormat, metric }) => {
|
||||
const formatter = currencyFormat?.symbol
|
||||
? new CurrencyFormatter({
|
||||
currency: { symbol: currencyFormat.symbol, symbolPosition: currencyFormat.symbolPosition ?? 'prefix' },
|
||||
d3Format: numberFormat,
|
||||
})
|
||||
: getNumberFormatter(numberFormat);
|
||||
|
||||
return <div>{formatter(metric.value)}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Conditional Formatting (Colors)
|
||||
|
||||
Use `ConditionalFormatting` for color-based rules:
|
||||
|
||||
```typescript
|
||||
import { getColorFormatters } from '@superset-ui/chart-controls';
|
||||
|
||||
arguments: {
|
||||
conditionalFormatting: ConditionalFormatting.with({
|
||||
label: t('Conditional Formatting'),
|
||||
description: t('Apply conditional color formatting to metric'),
|
||||
}),
|
||||
},
|
||||
|
||||
render: ({ conditionalFormatting, metric, data, theme }) => {
|
||||
let numberColor: string | undefined;
|
||||
|
||||
if (conditionalFormatting?.length > 0 && metric.value != null) {
|
||||
const colorFormatters = getColorFormatters(conditionalFormatting, data, theme, false);
|
||||
if (colorFormatters) {
|
||||
for (const formatter of colorFormatters) {
|
||||
const color = formatter.getColorFromValue(metric.value as number);
|
||||
if (color) {
|
||||
numberColor = color;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <BigNumberText color={numberColor}>{metric.formattedValue}</BigNumberText>;
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Styled Components
|
||||
|
||||
Use Superset's theme properties with template literal syntax:
|
||||
|
||||
```typescript
|
||||
const Container = styled.div<{ height: number }>`
|
||||
${({ theme, height }) => `
|
||||
height: ${height}px;
|
||||
padding: ${theme.sizeUnit * 4}px;
|
||||
font-family: ${theme.fontFamily};
|
||||
color: ${theme.colorText};
|
||||
`}
|
||||
`;
|
||||
```
|
||||
|
||||
**Common theme properties:**
|
||||
| Property | Description |
|
||||
|----------|-------------|
|
||||
| `theme.sizeUnit` | Base spacing unit (typically 4px) |
|
||||
| `theme.fontFamily` | Default font family |
|
||||
| `theme.fontWeightNormal` | Normal font weight |
|
||||
| `theme.fontWeightLight` | Light font weight |
|
||||
| `theme.fontSizeSM` | Small font size |
|
||||
| `theme.colorText` | Primary text color |
|
||||
| `theme.colorTextTertiary` | Muted/secondary text color |
|
||||
| `theme.borderRadius` | Standard border radius |
|
||||
|
||||
### 9. Render Function
|
||||
|
||||
The render function receives all arguments directly - no formData lookup needed:
|
||||
|
||||
```typescript
|
||||
render: ({
|
||||
metric,
|
||||
headerFontSize,
|
||||
showMetricName,
|
||||
numberFormat,
|
||||
currencyFormat,
|
||||
conditionalFormatting,
|
||||
height,
|
||||
data,
|
||||
theme,
|
||||
}) => {
|
||||
// All arguments are directly available!
|
||||
const formatter = currencyFormat?.symbol
|
||||
? new CurrencyFormatter({ currency: currencyFormat, d3Format: numberFormat })
|
||||
: getNumberFormatter(numberFormat);
|
||||
|
||||
const formattedValue = metric.value != null
|
||||
? formatter(metric.value as number)
|
||||
: t('No data');
|
||||
|
||||
return (
|
||||
<Container height={height}>
|
||||
{showMetricName && <MetricName>{metric.name}</MetricName>}
|
||||
<BigNumberText>{formattedValue}</BigNumberText>
|
||||
</Container>
|
||||
);
|
||||
},
|
||||
```
|
||||
|
||||
### 10. Register the Plugin
|
||||
|
||||
In `BigNumber/index.ts`:
|
||||
```typescript
|
||||
export { default as BigNumberGlyphChartPlugin } from './BigNumberGlyph';
|
||||
```
|
||||
|
||||
In `plugin-chart-echarts/src/index.ts`:
|
||||
```typescript
|
||||
export { BigNumberGlyphChartPlugin } from './BigNumber';
|
||||
```
|
||||
|
||||
In `MainPreset.js`:
|
||||
```typescript
|
||||
import { BigNumberGlyphChartPlugin } from '@superset-ui/plugin-chart-echarts';
|
||||
|
||||
new BigNumberGlyphChartPlugin().configure({ key: 'big_number_glyph' }),
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### 1. Snake Case vs Camel Case
|
||||
- **WRONG**: `show_metric_name` - won't match formData
|
||||
- **RIGHT**: `showMetricName` - matches Superset's camelCase conversion
|
||||
|
||||
### 2. Theme Undefined
|
||||
- **WRONG**: `theme.gridUnit` - crashes if theme is undefined
|
||||
- **RIGHT**: `theme?.gridUnit ?? 4` - safe with fallback
|
||||
|
||||
### 3. Metric Value Extraction
|
||||
The Glyph core automatically extracts metric values from query results. The `metric` argument provides:
|
||||
- `metric.value` - The raw numeric value
|
||||
- `metric.name` - The metric label/name
|
||||
- `metric.formattedValue` - Basic string representation
|
||||
|
||||
### 4. Visibility vs Legacy Functions
|
||||
- **Prefer**: `visibleWhen: { showMetricName: true }` - declarative, clean
|
||||
- **Legacy**: `visibility: ({ controls }) => controls?.showMetricName?.value === true` - still works
|
||||
|
||||
## File Structure Comparison
|
||||
|
||||
### Traditional (5+ files, ~500 lines)
|
||||
```
|
||||
BigNumberTotal/
|
||||
├── index.ts # Plugin registration
|
||||
├── controlPanel.ts # Control definitions (~100 lines)
|
||||
├── transformProps.ts # Data transformation (~150 lines)
|
||||
├── buildQuery.ts # Query building (~50 lines)
|
||||
├── BigNumberViz.tsx # React component (~150 lines)
|
||||
└── types.ts # TypeScript types (~50 lines)
|
||||
```
|
||||
|
||||
### Glyph Pattern (1 file, ~250 lines)
|
||||
```
|
||||
BigNumberGlyph/
|
||||
└── index.tsx # Everything in one file!
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
See `BigNumber/BigNumberGlyph/index.tsx` for a complete working example with:
|
||||
- Metric display
|
||||
- Number/currency/time formatting
|
||||
- Conditional color formatting
|
||||
- Declarative visibility
|
||||
- Subtitle support
|
||||
- Font size controls
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@superset-ui/glyph-core",
|
||||
"version": "0.20.3",
|
||||
"description": "Glyph Core - A declarative visualization plugin framework for Apache Superset",
|
||||
"sideEffects": false,
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
"files": [
|
||||
"esm",
|
||||
"lib"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/apache/superset.git",
|
||||
"directory": "superset-frontend/packages/superset-ui-glyph-core"
|
||||
},
|
||||
"keywords": [
|
||||
"superset",
|
||||
"glyph",
|
||||
"visualization",
|
||||
"chart"
|
||||
],
|
||||
"author": "Superset",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache/superset/issues"
|
||||
},
|
||||
"homepage": "https://github.com/apache/superset#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,646 @@
|
||||
/**
|
||||
* 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 {
|
||||
ColumnType,
|
||||
SelectOptions,
|
||||
SelectOption,
|
||||
TextOptions,
|
||||
CheckboxOptions,
|
||||
IntOptions,
|
||||
ColorOptions,
|
||||
MetricOptions,
|
||||
DimensionOptions,
|
||||
NumberFormatOptions,
|
||||
CurrencyOptions,
|
||||
CurrencyValue,
|
||||
TimeFormatOptions,
|
||||
ConditionalFormattingOptions,
|
||||
ConditionalFormattingRule,
|
||||
SliderOptions,
|
||||
BoundsOptions,
|
||||
BoundsValue,
|
||||
ColorPickerOptions,
|
||||
RadioButtonOptions,
|
||||
RadioOption,
|
||||
RgbaColor,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Base Argument class - all argument types extend from this.
|
||||
*
|
||||
* Arguments define:
|
||||
* 1. What the chart needs (semantically)
|
||||
* 2. How to render controls in the control panel
|
||||
* 3. Default values and validation
|
||||
*/
|
||||
export class Argument {
|
||||
static label: string | null = null;
|
||||
static description: string | null = null;
|
||||
static columnType: ColumnType = ColumnType.Argument;
|
||||
static controlType: string = 'TextControl';
|
||||
|
||||
value: unknown;
|
||||
|
||||
constructor(value: unknown) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Metric - represents a numeric aggregation (SUM, COUNT, AVG, etc.)
|
||||
*
|
||||
* Maps to Superset's MetricsControl in the query section.
|
||||
*/
|
||||
export class Metric extends Argument {
|
||||
static override label: string | null = 'Metric';
|
||||
static override description: string | null =
|
||||
'A numeric aggregation (SUM, COUNT, AVG, etc.)';
|
||||
static override columnType = ColumnType.Metric;
|
||||
static override controlType = 'MetricsControl';
|
||||
static multi = false;
|
||||
|
||||
static with(options: MetricOptions): typeof Metric {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override multi = options.multi ?? Base.multi;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dimension - represents a categorical column for grouping data
|
||||
*
|
||||
* Maps to Superset's GroupByControl in the query section.
|
||||
*/
|
||||
export class Dimension extends Argument {
|
||||
static override label: string | null = 'Dimension';
|
||||
static override description: string | null =
|
||||
'A categorical column for grouping data';
|
||||
static override columnType = ColumnType.Dimension;
|
||||
static override controlType = 'GroupByControl';
|
||||
static multi = true;
|
||||
|
||||
static with(options: DimensionOptions): typeof Dimension {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override multi = options.multi ?? Base.multi;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporal - represents a time column
|
||||
*
|
||||
* Maps to Superset's temporal controls (x_axis, time_grain_sqla).
|
||||
*/
|
||||
export class Temporal extends Argument {
|
||||
static override label: string | null = 'Time Column';
|
||||
static override description: string | null =
|
||||
'A temporal column for time series data';
|
||||
static override columnType = ColumnType.Temporal;
|
||||
static override controlType = 'TemporalControl';
|
||||
|
||||
static with(options: {
|
||||
label?: string;
|
||||
description?: string;
|
||||
}): typeof Temporal {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select - dropdown selection from predefined options
|
||||
*
|
||||
* Maps to Superset's SelectControl.
|
||||
*/
|
||||
export class Select extends Argument {
|
||||
static override label: string | null = 'Select';
|
||||
static override description: string | null = 'Choose from options';
|
||||
static override controlType = 'SelectControl';
|
||||
static default: string | number = '';
|
||||
static options: SelectOption[] = [];
|
||||
static clearable = false;
|
||||
|
||||
static with(options: SelectOptions): typeof Select {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override options = options.options ?? Base.options;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text - free-form text input
|
||||
*
|
||||
* Maps to Superset's TextControl.
|
||||
*/
|
||||
export class Text extends Argument {
|
||||
static override label: string | null = 'Text';
|
||||
static override description: string | null = 'Text input';
|
||||
static override controlType = 'TextControl';
|
||||
static default: string = '';
|
||||
static placeholder: string = '';
|
||||
|
||||
static with(options: TextOptions): typeof Text {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override placeholder = options.placeholder ?? Base.placeholder;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox - boolean toggle
|
||||
*
|
||||
* Maps to Superset's CheckboxControl.
|
||||
*/
|
||||
export class Checkbox extends Argument {
|
||||
static override label: string | null = 'Checkbox';
|
||||
static override description: string | null = 'Toggle option';
|
||||
static override controlType = 'CheckboxControl';
|
||||
static default: boolean = false;
|
||||
|
||||
static with(options: CheckboxOptions): typeof Checkbox {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Int - numeric input with slider
|
||||
*
|
||||
* Maps to Superset's SliderControl.
|
||||
*/
|
||||
export class Int extends Argument {
|
||||
static override label: string | null = 'Integer';
|
||||
static override description: string | null = 'A numeric value';
|
||||
static override controlType = 'SliderControl';
|
||||
static default: number = 0;
|
||||
static min: number = 0;
|
||||
static max: number = 100;
|
||||
static step: number = 1;
|
||||
|
||||
static with(options: IntOptions): typeof Int {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override min = options.min ?? Base.min;
|
||||
static override max = options.max ?? Base.max;
|
||||
static override step = options.step ?? Base.step;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Color - color picker
|
||||
*
|
||||
* Maps to Superset's ColorPickerControl.
|
||||
*/
|
||||
export class Color extends Argument {
|
||||
static override label: string | null = 'Color';
|
||||
static override description: string | null = 'A color value';
|
||||
static override controlType = 'ColorPickerControl';
|
||||
// eslint-disable-next-line theme-colors/no-literal-colors
|
||||
static default: string = '#000000';
|
||||
|
||||
static with(options: ColorOptions): typeof Color {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NumberFormat - D3 number format string selection
|
||||
*
|
||||
* Maps to Superset's SelectControl with D3 format options.
|
||||
* Allows freeform input for custom formats.
|
||||
*/
|
||||
export class NumberFormat extends Argument {
|
||||
static override label: string | null = 'Number Format';
|
||||
static override description: string | null =
|
||||
'D3 format string for number display (e.g., ".2f", ".1%", ",.0f")';
|
||||
static override controlType = 'NumberFormatControl';
|
||||
static default: string = 'SMART_NUMBER';
|
||||
|
||||
// Standard D3 format options
|
||||
static readonly FORMAT_OPTIONS: SelectOption[] = [
|
||||
{ label: 'Adaptive formatting', value: 'SMART_NUMBER' },
|
||||
{ label: 'Original value', value: '~g' },
|
||||
{ label: '12,345.432', value: ',.3f' },
|
||||
{ label: '12,345.43', value: ',.2f' },
|
||||
{ label: '12,345.4', value: ',.1f' },
|
||||
{ label: '12,345', value: ',.0f' },
|
||||
{ label: '12345.432', value: '.3f' },
|
||||
{ label: '12345.43', value: '.2f' },
|
||||
{ label: '12345.4', value: '.1f' },
|
||||
{ label: '12345', value: '.0f' },
|
||||
{ label: '12K', value: '.0s' },
|
||||
{ label: '12.3K', value: '.1s' },
|
||||
{ label: '12.35K', value: '.2s' },
|
||||
{ label: '12.346K', value: '.3s' },
|
||||
{ label: '1234543.21%', value: '.2%' },
|
||||
{ label: '1234543%', value: '.0%' },
|
||||
{ label: '12.34%', value: '.2r' },
|
||||
{ label: '+12,345.4', value: '+,.1f' },
|
||||
{ label: '$12,345.43', value: '$,.2f' },
|
||||
{ label: 'Duration (1m 6s)', value: 'DURATION' },
|
||||
{ label: 'Duration (1ms 400µs)', value: 'DURATION_SUB' },
|
||||
];
|
||||
|
||||
static with(options: NumberFormatOptions): typeof NumberFormat {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Currency - currency format with symbol and position
|
||||
*
|
||||
* Maps to Superset's CurrencyControl.
|
||||
* Value is { symbol: 'USD', symbolPosition: 'prefix' | 'suffix' }
|
||||
*/
|
||||
export class Currency extends Argument {
|
||||
static override label: string | null = 'Currency Format';
|
||||
static override description: string | null =
|
||||
'Currency symbol and position for formatting';
|
||||
static override controlType = 'CurrencyControl';
|
||||
static default: CurrencyValue = {};
|
||||
|
||||
static with(options: CurrencyOptions): typeof Currency {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TimeFormat - D3 time format string selection
|
||||
*
|
||||
* Maps to Superset's SelectControl with D3 time format options.
|
||||
* Allows freeform input for custom formats.
|
||||
*/
|
||||
export class TimeFormat extends Argument {
|
||||
static override label: string | null = 'Time Format';
|
||||
static override description: string | null =
|
||||
'D3 time format string (e.g., "%Y-%m-%d", "%H:%M:%S")';
|
||||
static override controlType = 'TimeFormatControl';
|
||||
static default: string = 'smart_date';
|
||||
|
||||
// Standard D3 time format options
|
||||
static readonly FORMAT_OPTIONS: SelectOption[] = [
|
||||
{ label: 'Adaptive formatting', value: 'smart_date' },
|
||||
{ label: '%d/%m/%Y | 14/01/2019', value: '%d/%m/%Y' },
|
||||
{ label: '%m/%d/%Y | 01/14/2019', value: '%m/%d/%Y' },
|
||||
{ label: '%d.%m.%Y | 14.01.2019', value: '%d.%m.%Y' },
|
||||
{ label: '%Y-%m-%d | 2019-01-14', value: '%Y-%m-%d' },
|
||||
{
|
||||
label: '%Y-%m-%d %H:%M:%S | 2019-01-14 01:32:10',
|
||||
value: '%Y-%m-%d %H:%M:%S',
|
||||
},
|
||||
{
|
||||
label: '%d-%m-%Y %H:%M:%S | 14-01-2019 01:32:10',
|
||||
value: '%d-%m-%Y %H:%M:%S',
|
||||
},
|
||||
{ label: '%H:%M:%S | 01:32:10', value: '%H:%M:%S' },
|
||||
];
|
||||
|
||||
static with(options: TimeFormatOptions): typeof TimeFormat {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ConditionalFormatting - apply color rules based on metric values
|
||||
*
|
||||
* This is a special argument type that encapsulates the complex
|
||||
* mapStateToProps logic needed for conditional formatting controls.
|
||||
* The control automatically receives numeric column options from the chart response.
|
||||
*/
|
||||
export class ConditionalFormatting extends Argument {
|
||||
static override label: string | null = 'Conditional Formatting';
|
||||
static override description: string | null =
|
||||
'Apply conditional color formatting to metric values';
|
||||
static override controlType = 'ConditionalFormattingControl';
|
||||
static default: ConditionalFormattingRule[] = [];
|
||||
|
||||
static with(
|
||||
options: ConditionalFormattingOptions,
|
||||
): typeof ConditionalFormatting {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Slider - continuous floating point values with min/max/step
|
||||
*
|
||||
* Similar to Int but for float values.
|
||||
* Maps to Superset's SliderControl.
|
||||
*/
|
||||
export class Slider extends Argument {
|
||||
static override label: string | null = 'Slider';
|
||||
static override description: string | null = 'A continuous numeric value';
|
||||
static override controlType = 'SliderControl';
|
||||
static default: number = 0;
|
||||
static min: number = 0;
|
||||
static max: number = 1;
|
||||
static step: number = 0.1;
|
||||
|
||||
static with(options: SliderOptions): typeof Slider {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override min = options.min ?? Base.min;
|
||||
static override max = options.max ?? Base.max;
|
||||
static override step = options.step ?? Base.step;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounds - min/max value pairs
|
||||
*
|
||||
* Used for axis bounds, value ranges, etc.
|
||||
* Maps to Superset's BoundsControl.
|
||||
*/
|
||||
export class Bounds extends Argument {
|
||||
static override label: string | null = 'Bounds';
|
||||
static override description: string | null = 'Min and max value bounds';
|
||||
static override controlType = 'BoundsControl';
|
||||
static default: BoundsValue = [null, null];
|
||||
|
||||
static with(options: BoundsOptions): typeof Bounds {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ColorPicker - RGBA color selection
|
||||
*
|
||||
* Different from Color (which uses hex strings).
|
||||
* Maps to Superset's ColorPickerControl with RGBA format.
|
||||
*/
|
||||
export class ColorPicker extends Argument {
|
||||
static override label: string | null = 'Color';
|
||||
static override description: string | null = 'Select a color';
|
||||
static override controlType = 'ColorPickerControl';
|
||||
static default: RgbaColor = { r: 0, g: 0, b: 0, a: 1 };
|
||||
|
||||
static with(options: ColorPickerOptions): typeof ColorPicker {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RadioButton - mutually exclusive options
|
||||
*
|
||||
* Use for small sets of exclusive choices (2-4 options).
|
||||
* Maps to Superset's RadioButtonControl.
|
||||
*/
|
||||
export class RadioButton extends Argument {
|
||||
static override label: string | null = 'Option';
|
||||
static override description: string | null = 'Select one option';
|
||||
static override controlType = 'RadioButtonControl';
|
||||
static default: string | boolean = '';
|
||||
static options: RadioOption[] = [];
|
||||
|
||||
static with(options: RadioButtonOptions): typeof RadioButton {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override options = options.options;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a ConditionalFormatting type
|
||||
*/
|
||||
export function isConditionalFormattingArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof ConditionalFormatting {
|
||||
return argClass.controlType === 'ConditionalFormattingControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a TimeFormat type
|
||||
*/
|
||||
export function isTimeFormatArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof TimeFormat {
|
||||
return argClass.controlType === 'TimeFormatControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a NumberFormat type
|
||||
*/
|
||||
export function isNumberFormatArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof NumberFormat {
|
||||
return argClass.controlType === 'NumberFormatControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Currency type
|
||||
*/
|
||||
export function isCurrencyArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Currency {
|
||||
return argClass.controlType === 'CurrencyControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Select type
|
||||
*/
|
||||
export function isSelectArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Select {
|
||||
return (
|
||||
'options' in argClass && Array.isArray((argClass as typeof Select).options)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Checkbox type
|
||||
*/
|
||||
export function isCheckboxArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Checkbox {
|
||||
return (
|
||||
'default' in argClass &&
|
||||
typeof (argClass as typeof Checkbox).default === 'boolean'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Text type
|
||||
*/
|
||||
export function isTextArg(argClass: typeof Argument): argClass is typeof Text {
|
||||
return (
|
||||
argClass.controlType === 'TextControl' ||
|
||||
(argClass.prototype instanceof Text &&
|
||||
!isSelectArg(argClass) &&
|
||||
!isCheckboxArg(argClass))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is an Int type
|
||||
*/
|
||||
export function isIntArg(argClass: typeof Argument): argClass is typeof Int {
|
||||
return 'min' in argClass && 'max' in argClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Color type
|
||||
*/
|
||||
export function isColorArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Color {
|
||||
return (
|
||||
argClass.controlType === 'ColorPickerControl' ||
|
||||
argClass.prototype instanceof Color
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Metric type
|
||||
*/
|
||||
export function isMetricArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Metric {
|
||||
return argClass.columnType === ColumnType.Metric;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Dimension type
|
||||
*/
|
||||
export function isDimensionArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Dimension {
|
||||
return argClass.columnType === ColumnType.Dimension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Temporal type
|
||||
*/
|
||||
export function isTemporalArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Temporal {
|
||||
return argClass.columnType === ColumnType.Temporal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Slider type
|
||||
*/
|
||||
export function isSliderArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Slider {
|
||||
return (
|
||||
argClass.controlType === 'SliderControl' &&
|
||||
'step' in argClass &&
|
||||
typeof (argClass as typeof Slider).step === 'number'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Bounds type
|
||||
*/
|
||||
export function isBoundsArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Bounds {
|
||||
return argClass.controlType === 'BoundsControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a ColorPicker type
|
||||
*/
|
||||
export function isColorPickerArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof ColorPicker {
|
||||
return (
|
||||
argClass.controlType === 'ColorPickerControl' &&
|
||||
'default' in argClass &&
|
||||
typeof (argClass as typeof ColorPicker).default === 'object' &&
|
||||
'r' in ((argClass as typeof ColorPicker).default as object)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a RadioButton type
|
||||
*/
|
||||
export function isRadioButtonArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof RadioButton {
|
||||
return argClass.controlType === 'RadioButtonControl';
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Cross-Filter Utilities for Glyph Charts
|
||||
*
|
||||
* This module provides helpers for implementing cross-filtering in Glyph charts.
|
||||
* Cross-filtering allows charts to filter other charts on the dashboard when
|
||||
* users click on data points.
|
||||
*
|
||||
* ## Quick Start
|
||||
*
|
||||
* 1. Add behaviors to metadata:
|
||||
* ```typescript
|
||||
* metadata: {
|
||||
* behaviors: [Behavior.InteractiveChart, Behavior.DrillToDetail, Behavior.DrillBy],
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* 2. Extract cross-filter props in transform:
|
||||
* ```typescript
|
||||
* transform: (chartProps) => {
|
||||
* const crossFilterProps = extractCrossFilterProps(chartProps, groupby, labelMap);
|
||||
* return { transformedProps: { ...otherProps, ...crossFilterProps } };
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* 3. Use event handlers in render:
|
||||
* ```typescript
|
||||
* render: ({ transformedProps }) => {
|
||||
* const eventHandlers = allEventHandlers(transformedProps);
|
||||
* return <Echart eventHandlers={eventHandlers} ... />;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import type {
|
||||
ChartProps,
|
||||
FilterState,
|
||||
QueryFormColumn,
|
||||
SetDataMaskHook,
|
||||
ContextMenuFilters,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
/**
|
||||
* Props needed for cross-filtering in the render component.
|
||||
* These are typically returned from the transform function and passed to Echart.
|
||||
*/
|
||||
export interface CrossFilterRenderProps {
|
||||
/** Groupby columns used for filtering */
|
||||
groupby: QueryFormColumn[];
|
||||
/** Maps series names to their groupby column values */
|
||||
labelMap: Record<string, string[]>;
|
||||
/** Callback to emit cross-filter data mask */
|
||||
setDataMask: SetDataMaskHook;
|
||||
/** Maps series indices to selected value names */
|
||||
selectedValues: Record<number, string>;
|
||||
/** Whether cross-filters are enabled for this chart */
|
||||
emitCrossFilters?: boolean;
|
||||
/** Context menu handler for drill actions */
|
||||
onContextMenu?: (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
filters?: ContextMenuFilters,
|
||||
) => void;
|
||||
/** Column type mapping for formatting */
|
||||
coltypeMapping?: Record<string, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a selectedValues map from filterState.
|
||||
*
|
||||
* The selectedValues map is used by the Echart component to track which
|
||||
* data points are currently selected (for highlighting).
|
||||
*
|
||||
* @param filterState - Current filter state from chartProps
|
||||
* @param seriesNames - Array of series/data point names
|
||||
* @returns Map of index -> name for selected values
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const selectedValues = createSelectedValuesMap(
|
||||
* filterState,
|
||||
* transformedData.map(d => d.name),
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function createSelectedValuesMap(
|
||||
filterState: FilterState | undefined,
|
||||
seriesNames: string[],
|
||||
): Record<number, string> {
|
||||
return (filterState?.selectedValues || []).reduce(
|
||||
(acc: Record<number, string>, selectedValue: string) => {
|
||||
const index = seriesNames.findIndex(name => name === selectedValue);
|
||||
if (index >= 0) {
|
||||
return { ...acc, [index]: selectedValue };
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract cross-filter related props from ChartProps.
|
||||
*
|
||||
* This is a convenience function that extracts all the props needed for
|
||||
* cross-filtering from the standard ChartProps object.
|
||||
*
|
||||
* @param chartProps - The chart props from Superset
|
||||
* @param groupby - The groupby columns (dimensions) from form data
|
||||
* @param labelMap - A map from series names to their groupby values
|
||||
* @param seriesNames - Array of series/data point names for selectedValues mapping
|
||||
* @param coltypeMapping - Optional column type mapping
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In transform function:
|
||||
* const labelMap = data.reduce((acc, datum) => ({
|
||||
* ...acc,
|
||||
* [extractGroupbyLabel({ datum, groupby })]: groupby.map(col => datum[col]),
|
||||
* }), {});
|
||||
*
|
||||
* const crossFilterProps = extractCrossFilterProps(
|
||||
* chartProps,
|
||||
* groupby,
|
||||
* labelMap,
|
||||
* transformedData.map(d => d.name),
|
||||
* coltypeMapping,
|
||||
* );
|
||||
*
|
||||
* return {
|
||||
* transformedProps: {
|
||||
* echartOptions,
|
||||
* formData,
|
||||
* width,
|
||||
* height,
|
||||
* refs,
|
||||
* ...crossFilterProps,
|
||||
* },
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export function extractCrossFilterProps(
|
||||
chartProps: ChartProps,
|
||||
groupby: QueryFormColumn[],
|
||||
labelMap: Record<string, string[]>,
|
||||
seriesNames: string[],
|
||||
coltypeMapping?: Record<string, number>,
|
||||
): CrossFilterRenderProps {
|
||||
const { hooks, filterState, emitCrossFilters, formData } = chartProps;
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks ?? {};
|
||||
|
||||
const selectedValues = createSelectedValuesMap(filterState, seriesNames);
|
||||
|
||||
return {
|
||||
groupby,
|
||||
labelMap,
|
||||
setDataMask,
|
||||
selectedValues,
|
||||
emitCrossFilters,
|
||||
onContextMenu,
|
||||
coltypeMapping,
|
||||
// Also include formData for context menu formatting
|
||||
formData,
|
||||
} as CrossFilterRenderProps & { formData: unknown };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a data point is currently filtered (should be dimmed).
|
||||
*
|
||||
* Use this in the transform function to apply opacity/styling to
|
||||
* data points that are not part of the current filter selection.
|
||||
*
|
||||
* @param filterState - Current filter state from chartProps
|
||||
* @param name - The name/label of the data point to check
|
||||
* @returns true if the data point should be dimmed, false otherwise
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const isFiltered = isDataPointFiltered(filterState, datum.name);
|
||||
* const opacity = isFiltered ? OpacityEnum.SemiTransparent : OpacityEnum.NonTransparent;
|
||||
* ```
|
||||
*/
|
||||
export function isDataPointFiltered(
|
||||
filterState: FilterState | undefined,
|
||||
name: string,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
filterState?.selectedValues &&
|
||||
filterState.selectedValues.length > 0 &&
|
||||
!filterState.selectedValues.includes(name),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a labelMap from data records.
|
||||
*
|
||||
* The labelMap maps series names (like "USA" or "2024-01") to their
|
||||
* corresponding groupby column values. This is needed for the cross-filter
|
||||
* event handlers to construct proper filter clauses.
|
||||
*
|
||||
* @param data - Array of data records
|
||||
* @param groupbyLabels - Array of groupby column labels
|
||||
* @param extractLabel - Function to extract the series label from a datum
|
||||
* @returns Map of label -> groupby values
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const labelMap = createLabelMap(
|
||||
* data,
|
||||
* groupbyLabels,
|
||||
* datum => extractGroupbyLabel({ datum, groupby: groupbyLabels, coltypeMapping }),
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function createLabelMap<T extends Record<string, unknown>>(
|
||||
data: T[],
|
||||
groupbyLabels: string[],
|
||||
extractLabel: (datum: T) => string,
|
||||
): Record<string, string[]> {
|
||||
return data.reduce((acc: Record<string, string[]>, datum: T) => {
|
||||
const label = extractLabel(datum);
|
||||
return {
|
||||
...acc,
|
||||
[label]: groupbyLabels.map(col => datum[col] as string),
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,419 @@
|
||||
/**
|
||||
* 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 { t } from '@apache-superset/core/translation';
|
||||
import type {
|
||||
ControlPanelConfig,
|
||||
ControlSetRow,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import type { ChartProps } from '@superset-ui/core';
|
||||
import {
|
||||
Argument,
|
||||
Select,
|
||||
Text,
|
||||
Checkbox,
|
||||
Int,
|
||||
Color,
|
||||
isSelectArg,
|
||||
isCheckboxArg,
|
||||
isIntArg,
|
||||
isColorArg,
|
||||
isMetricArg,
|
||||
isDimensionArg,
|
||||
isTemporalArg,
|
||||
} from './arguments';
|
||||
import type { VisibilityFn, RgbaColor } from './types';
|
||||
|
||||
/**
|
||||
* Configuration for a glyph argument with optional visibility control
|
||||
*/
|
||||
export interface GlyphArgConfig {
|
||||
arg: typeof Argument;
|
||||
visibility?: VisibilityFn;
|
||||
resetOnHide?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Arguments map - parameter name to argument class or config
|
||||
*/
|
||||
export type GlyphArguments = Map<string, typeof Argument | GlyphArgConfig>;
|
||||
|
||||
/**
|
||||
* Convert hex color string to RGBA object for Superset's ColorPickerControl
|
||||
*/
|
||||
function hexToRgba(hex: string): RgbaColor {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
if (result && result[1] && result[2] && result[3]) {
|
||||
return {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
a: 1,
|
||||
};
|
||||
}
|
||||
return { r: 0, g: 0, b: 0, a: 1 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert RGBA object to hex color string
|
||||
*/
|
||||
function rgbaToHex(rgba: RgbaColor): string {
|
||||
const toHex = (n: number) => n.toString(16).padStart(2, '0');
|
||||
return `#${toHex(rgba.r)}${toHex(rgba.g)}${toHex(rgba.b)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the argument class from a config (handles both direct class and config object)
|
||||
*/
|
||||
function getArgClass(
|
||||
argOrConfig: typeof Argument | GlyphArgConfig,
|
||||
): typeof Argument {
|
||||
return 'arg' in argOrConfig ? argOrConfig.arg : argOrConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visibility config if present
|
||||
*/
|
||||
function getVisibilityConfig(argOrConfig: typeof Argument | GlyphArgConfig): {
|
||||
visibility?: VisibilityFn;
|
||||
resetOnHide?: boolean;
|
||||
} {
|
||||
if ('arg' in argOrConfig) {
|
||||
return {
|
||||
visibility: argOrConfig.visibility,
|
||||
resetOnHide: argOrConfig.resetOnHide,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Superset control config from a glyph Argument class
|
||||
*/
|
||||
export function getControlConfig(
|
||||
argClass: typeof Argument,
|
||||
paramName: string,
|
||||
): Record<string, unknown> & { type: string } {
|
||||
const label = argClass.label || paramName;
|
||||
const description = argClass.description || '';
|
||||
|
||||
// Select control
|
||||
if (isSelectArg(argClass)) {
|
||||
return {
|
||||
type: 'SelectControl',
|
||||
label,
|
||||
description,
|
||||
default: argClass.default,
|
||||
options: argClass.options,
|
||||
clearable: argClass.clearable ?? false,
|
||||
renderTrigger: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Checkbox control
|
||||
if (isCheckboxArg(argClass)) {
|
||||
return {
|
||||
type: 'CheckboxControl',
|
||||
label,
|
||||
description,
|
||||
default: argClass.default,
|
||||
renderTrigger: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Int/Slider control
|
||||
if (isIntArg(argClass)) {
|
||||
return {
|
||||
type: 'SliderControl',
|
||||
label,
|
||||
description,
|
||||
default: argClass.default,
|
||||
min: argClass.min,
|
||||
max: argClass.max,
|
||||
step: argClass.step ?? 1,
|
||||
renderTrigger: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Color control
|
||||
if (isColorArg(argClass)) {
|
||||
// eslint-disable-next-line theme-colors/no-literal-colors
|
||||
const hexDefault = argClass.default ?? '#000000';
|
||||
return {
|
||||
type: 'ColorPickerControl',
|
||||
label,
|
||||
description,
|
||||
default: hexToRgba(hexDefault),
|
||||
renderTrigger: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Default to TextControl
|
||||
const textClass = argClass as typeof Text;
|
||||
return {
|
||||
type: 'TextControl',
|
||||
label,
|
||||
description,
|
||||
default: textClass.default ?? '',
|
||||
placeholder: textClass.placeholder ?? '',
|
||||
renderTrigger: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for control panel generation
|
||||
*/
|
||||
export interface ControlPanelOptions {
|
||||
/** Additional control rows for the query section */
|
||||
queryControls?: ControlSetRow[];
|
||||
/** Additional control rows for the chart options section */
|
||||
chartOptionsControls?: ControlSetRow[];
|
||||
/** Control overrides */
|
||||
controlOverrides?: Record<string, Record<string, unknown>>;
|
||||
/** Form data overrides function */
|
||||
formDataOverrides?: (
|
||||
formData: Record<string, unknown>,
|
||||
) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a complete ControlPanelConfig from glyph arguments
|
||||
*
|
||||
* This is the core function that converts semantic argument definitions
|
||||
* into Superset's control panel format.
|
||||
*/
|
||||
export function generateControlPanel(
|
||||
glyphArguments: GlyphArguments,
|
||||
options: ControlPanelOptions = {},
|
||||
): ControlPanelConfig {
|
||||
const queryControls: ControlSetRow[] = [];
|
||||
const chartOptionsControls: ControlSetRow[] = [];
|
||||
|
||||
// Process each argument
|
||||
for (const [paramName, argOrConfig] of glyphArguments) {
|
||||
const argClass = getArgClass(argOrConfig);
|
||||
const { visibility, resetOnHide } = getVisibilityConfig(argOrConfig);
|
||||
|
||||
// Data arguments go in Query section
|
||||
if (isMetricArg(argClass)) {
|
||||
queryControls.push(['metric']);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isDimensionArg(argClass)) {
|
||||
queryControls.push(['groupby']);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isTemporalArg(argClass)) {
|
||||
queryControls.push(['x_axis'], ['time_grain_sqla']);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Style/visual arguments go in Chart Options section
|
||||
const controlConfig = getControlConfig(argClass, paramName);
|
||||
|
||||
// Add visibility if specified
|
||||
if (visibility) {
|
||||
controlConfig.visibility = visibility;
|
||||
controlConfig.resetOnHide = resetOnHide ?? false;
|
||||
}
|
||||
|
||||
chartOptionsControls.push([
|
||||
{
|
||||
name: paramName,
|
||||
config: controlConfig,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// Add adhoc_filters to query section
|
||||
queryControls.push(['adhoc_filters']);
|
||||
|
||||
// Merge with additional controls from options
|
||||
const finalQueryControls = [
|
||||
...queryControls,
|
||||
...(options.queryControls || []),
|
||||
];
|
||||
const finalChartOptionsControls = [
|
||||
...chartOptionsControls,
|
||||
...(options.chartOptionsControls || []),
|
||||
];
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: finalQueryControls,
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
controlSetRows: finalChartOptionsControls,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (options.controlOverrides) {
|
||||
config.controlOverrides = options.controlOverrides;
|
||||
}
|
||||
|
||||
if (options.formDataOverrides) {
|
||||
// Type assertion needed because SqlaFormData is more specific than Record<string, unknown>
|
||||
config.formDataOverrides =
|
||||
options.formDataOverrides as ControlPanelConfig['formDataOverrides'];
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for transformProps generation
|
||||
*/
|
||||
export interface TransformPropsOptions<TResult> {
|
||||
/** Custom transformation function that receives extracted values */
|
||||
transform?: (
|
||||
values: Record<string, unknown>,
|
||||
chartProps: ChartProps,
|
||||
) => TResult;
|
||||
/** Additional props to pass through from chartProps */
|
||||
passthrough?: (keyof ChartProps)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a transformProps function from glyph arguments
|
||||
*
|
||||
* This extracts values from formData based on argument definitions,
|
||||
* applying type conversions as needed (e.g., RGBA to hex for colors).
|
||||
*/
|
||||
export function generateTransformProps<TResult = Record<string, unknown>>(
|
||||
glyphArguments: GlyphArguments,
|
||||
options: TransformPropsOptions<TResult> = {},
|
||||
): (chartProps: ChartProps) => TResult {
|
||||
return (chartProps: ChartProps) => {
|
||||
const { formData, width, height, queriesData } = chartProps;
|
||||
const values: Record<string, unknown> = {
|
||||
width,
|
||||
height,
|
||||
queriesData,
|
||||
};
|
||||
|
||||
// Add passthrough props
|
||||
if (options.passthrough) {
|
||||
for (const key of options.passthrough) {
|
||||
values[key] = chartProps[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Extract values from formData based on argument definitions
|
||||
for (const [paramName, argOrConfig] of glyphArguments) {
|
||||
const argClass = getArgClass(argOrConfig);
|
||||
|
||||
// Skip data arguments (metric, dimension, temporal) - these are handled differently
|
||||
if (
|
||||
isMetricArg(argClass) ||
|
||||
isDimensionArg(argClass) ||
|
||||
isTemporalArg(argClass)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get value from formData, using default if not present
|
||||
let value = formData[paramName];
|
||||
|
||||
// Color control: convert RGBA object to hex string
|
||||
if (isColorArg(argClass)) {
|
||||
const colorClass = argClass as typeof Color;
|
||||
// eslint-disable-next-line theme-colors/no-literal-colors
|
||||
const defaultRgba = hexToRgba(colorClass.default ?? '#000000');
|
||||
const colorValue = value ?? defaultRgba;
|
||||
|
||||
if (
|
||||
typeof colorValue === 'object' &&
|
||||
colorValue !== null &&
|
||||
'r' in colorValue
|
||||
) {
|
||||
value = rgbaToHex(colorValue as RgbaColor);
|
||||
} else if (typeof colorValue === 'string') {
|
||||
value = colorValue;
|
||||
} else {
|
||||
// eslint-disable-next-line theme-colors/no-literal-colors
|
||||
value = colorClass.default ?? '#000000';
|
||||
}
|
||||
}
|
||||
// Select control: use default if no value
|
||||
else if (isSelectArg(argClass)) {
|
||||
const selectClass = argClass as typeof Select;
|
||||
value = value ?? selectClass.default;
|
||||
}
|
||||
// Checkbox control: use default if no value
|
||||
else if (isCheckboxArg(argClass)) {
|
||||
const checkboxClass = argClass as typeof Checkbox;
|
||||
value = value ?? checkboxClass.default ?? false;
|
||||
}
|
||||
// Int control: use default if no value
|
||||
else if (isIntArg(argClass)) {
|
||||
const intClass = argClass as typeof Int;
|
||||
value = value ?? intClass.default ?? 0;
|
||||
}
|
||||
// Text control: use default if no value
|
||||
else {
|
||||
const textClass = argClass as typeof Text;
|
||||
value = value ?? textClass.default ?? '';
|
||||
}
|
||||
|
||||
values[paramName] = value;
|
||||
}
|
||||
|
||||
// Apply custom transformation if provided
|
||||
if (options.transform) {
|
||||
return options.transform(values, chartProps);
|
||||
}
|
||||
|
||||
return values as TResult;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined result of creating a glyph plugin
|
||||
*/
|
||||
export interface GlyphPluginDef<TProps> {
|
||||
controlPanel: ControlPanelConfig;
|
||||
transformProps: (chartProps: ChartProps) => TProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create both controlPanel and transformProps from a single argument definition
|
||||
*
|
||||
* This is the main entry point for the single-file viz pattern.
|
||||
*/
|
||||
export function createGlyphPlugin<TProps = Record<string, unknown>>(
|
||||
glyphArguments: GlyphArguments,
|
||||
controlPanelOptions: ControlPanelOptions = {},
|
||||
transformPropsOptions: TransformPropsOptions<TProps> = {},
|
||||
): GlyphPluginDef<TProps> {
|
||||
return {
|
||||
controlPanel: generateControlPanel(glyphArguments, controlPanelOptions),
|
||||
transformProps: generateTransformProps(
|
||||
glyphArguments,
|
||||
transformPropsOptions,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Glyph Core - A declarative visualization plugin framework
|
||||
*
|
||||
* This module enables single-file visualization plugins where:
|
||||
* 1. Arguments define both the chart's inputs AND the control panel
|
||||
* 2. transformProps is auto-generated from argument definitions
|
||||
* 3. The chart component is a simple function receiving typed arguments
|
||||
*
|
||||
* Features:
|
||||
* - Single-file chart definitions with defineChart()
|
||||
* - Declarative argument types (Metric, Dimension, Select, Checkbox, etc.)
|
||||
* - Conditional visibility with visibleWhen/disabledWhen
|
||||
* - Cross-filtering support with extractCrossFilterProps() and allEventHandlers()
|
||||
* - Reusable presets (ShowLegend, HeaderFontSize, etc.)
|
||||
*
|
||||
* Example usage:
|
||||
* ```typescript
|
||||
* export default defineChart({
|
||||
* metadata: {
|
||||
* name: 'My Chart',
|
||||
* thumbnail,
|
||||
* behaviors: [Behavior.InteractiveChart, Behavior.DrillToDetail, Behavior.DrillBy],
|
||||
* },
|
||||
* arguments: {
|
||||
* metric: Metric.with({ label: 'Metric' }),
|
||||
* groupby: Dimension.with({ label: 'Breakdowns' }),
|
||||
* fontSize: Select.with({
|
||||
* label: 'Font Size',
|
||||
* options: [{ label: 'Small', value: 0.2 }, { label: 'Large', value: 0.4 }],
|
||||
* default: 0.3,
|
||||
* }),
|
||||
* },
|
||||
* transform: (chartProps, argValues) => {
|
||||
* // Extract cross-filter props for interactive filtering
|
||||
* const crossFilterProps = extractCrossFilterProps(chartProps, groupby, labelMap, seriesNames);
|
||||
* return { transformedProps: { echartOptions, ...crossFilterProps } };
|
||||
* },
|
||||
* render: ({ transformedProps }) => {
|
||||
* const eventHandlers = allEventHandlers(transformedProps);
|
||||
* return <Echart eventHandlers={eventHandlers} ... />;
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Re-export everything
|
||||
export * from './types';
|
||||
export * from './arguments';
|
||||
export * from './generators';
|
||||
export * from './defineChart';
|
||||
export * from './presets';
|
||||
export * from './crossFilter';
|
||||
406
superset-frontend/packages/superset-ui-glyph-core/src/presets.ts
Normal file
406
superset-frontend/packages/superset-ui-glyph-core/src/presets.ts
Normal file
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Glyph Presets - Reusable argument configurations
|
||||
*
|
||||
* This module contains pre-configured arguments that are commonly
|
||||
* used across multiple visualization types. Charts can import these
|
||||
* directly or use .with() to customize them further.
|
||||
*
|
||||
* Example usage:
|
||||
* ```typescript
|
||||
* import { HeaderFontSize, Subtitle } from '../../glyph-core/presets';
|
||||
*
|
||||
* arguments: {
|
||||
* headerFontSize: HeaderFontSize,
|
||||
* subtitle: Subtitle,
|
||||
* // Override defaults when needed:
|
||||
* customSize: HeaderFontSize.with({ default: 0.5 }),
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { Select, Text, Checkbox } from './arguments';
|
||||
import { SelectOption } from './types';
|
||||
|
||||
// ============================================================================
|
||||
// Font Size Options
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Large font size options - for primary/header text elements
|
||||
* Values are multipliers of container height (0.2 = 20% of height)
|
||||
*/
|
||||
export const FONT_SIZE_OPTIONS_LARGE: SelectOption[] = [
|
||||
{ label: t('Tiny'), value: 0.2 },
|
||||
{ label: t('Small'), value: 0.3 },
|
||||
{ label: t('Normal'), value: 0.4 },
|
||||
{ label: t('Large'), value: 0.5 },
|
||||
{ label: t('Huge'), value: 0.6 },
|
||||
];
|
||||
|
||||
/**
|
||||
* Small font size options - for secondary text elements (subtitles, labels)
|
||||
* Values are multipliers of container height
|
||||
*/
|
||||
export const FONT_SIZE_OPTIONS_SMALL: SelectOption[] = [
|
||||
{ label: t('Tiny'), value: 0.125 },
|
||||
{ label: t('Small'), value: 0.15 },
|
||||
{ label: t('Normal'), value: 0.2 },
|
||||
{ label: t('Large'), value: 0.3 },
|
||||
{ label: t('Huge'), value: 0.4 },
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// Pre-configured Arguments
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Header/primary font size selector
|
||||
* Used for main display elements like big numbers, titles
|
||||
*/
|
||||
export const HeaderFontSize = Select.with({
|
||||
label: t('Font Size'),
|
||||
description: t('Font size for the primary display element'),
|
||||
options: FONT_SIZE_OPTIONS_LARGE,
|
||||
default: 0.4,
|
||||
});
|
||||
|
||||
/**
|
||||
* Subheader/secondary font size selector
|
||||
* Used for subtitles, labels, secondary text
|
||||
*/
|
||||
export const SubheaderFontSize = Select.with({
|
||||
label: t('Subheader Font Size'),
|
||||
description: t('Font size for secondary text elements'),
|
||||
options: FONT_SIZE_OPTIONS_SMALL,
|
||||
default: 0.15,
|
||||
});
|
||||
|
||||
/**
|
||||
* Subtitle text input
|
||||
* Generic subtitle/description field used by many chart types
|
||||
*/
|
||||
export const Subtitle = Text.with({
|
||||
label: t('Subtitle'),
|
||||
description: t('Description text displayed below the main content'),
|
||||
default: '',
|
||||
});
|
||||
|
||||
/**
|
||||
* Show legend toggle
|
||||
* Common toggle for charts with legends
|
||||
*/
|
||||
export const ShowLegend = Checkbox.with({
|
||||
label: t('Show Legend'),
|
||||
description: t('Whether to display the chart legend'),
|
||||
default: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Force timestamp formatting toggle
|
||||
* Used when a value might be a timestamp but isn't auto-detected
|
||||
*/
|
||||
export const ForceTimestampFormatting = Checkbox.with({
|
||||
label: t('Force Date Format'),
|
||||
description: t(
|
||||
'Use date formatting even when the value is not detected as a timestamp',
|
||||
),
|
||||
default: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Legend Options
|
||||
// ============================================================================
|
||||
|
||||
export const LEGEND_TYPE_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Scroll'), value: 'scroll' },
|
||||
{ label: t('List'), value: 'plain' },
|
||||
];
|
||||
|
||||
export const LEGEND_ORIENTATION_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Top'), value: 'top' },
|
||||
{ label: t('Bottom'), value: 'bottom' },
|
||||
{ label: t('Left'), value: 'left' },
|
||||
{ label: t('Right'), value: 'right' },
|
||||
];
|
||||
|
||||
export const LEGEND_SORT_OPTIONS: SelectOption[] = [
|
||||
{ label: t('No sort'), value: '' },
|
||||
{ label: t('Ascending'), value: 'asc' },
|
||||
{ label: t('Descending'), value: 'desc' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Legend type selector
|
||||
* Choose between scrollable or plain list legend
|
||||
*/
|
||||
export const LegendType = Select.with({
|
||||
label: t('Legend Type'),
|
||||
description: t('Type of legend display'),
|
||||
options: LEGEND_TYPE_OPTIONS,
|
||||
default: 'scroll',
|
||||
});
|
||||
|
||||
/**
|
||||
* Legend orientation selector
|
||||
* Position the legend relative to the chart
|
||||
*/
|
||||
export const LegendOrientation = Select.with({
|
||||
label: t('Legend Orientation'),
|
||||
description: t('Position of the legend'),
|
||||
options: LEGEND_ORIENTATION_OPTIONS,
|
||||
default: 'top',
|
||||
});
|
||||
|
||||
/**
|
||||
* Legend sort selector
|
||||
* Sort legend items alphabetically
|
||||
*/
|
||||
export const LegendSort = Select.with({
|
||||
label: t('Legend Sort'),
|
||||
description: t('Sort order for legend items'),
|
||||
options: LEGEND_SORT_OPTIONS,
|
||||
default: '',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Label Presets
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Show labels toggle
|
||||
* Common toggle for chart labels
|
||||
*/
|
||||
export const ShowLabels = Checkbox.with({
|
||||
label: t('Show Labels'),
|
||||
description: t('Whether to display labels on the chart'),
|
||||
default: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Show value toggle
|
||||
* Common toggle for showing values on chart elements
|
||||
*/
|
||||
export const ShowValue = Checkbox.with({
|
||||
label: t('Show Value'),
|
||||
description: t('Whether to display values on the chart'),
|
||||
default: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Metric Name Presets
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Show metric name toggle
|
||||
* Used in BigNumber charts to optionally show the metric name
|
||||
*/
|
||||
export const ShowMetricName = Checkbox.with({
|
||||
label: t('Show Metric Name'),
|
||||
description: t('Whether to display the metric name as a title'),
|
||||
default: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Metric name font size selector
|
||||
* Typically used with visibility tied to ShowMetricName
|
||||
*/
|
||||
export const MetricNameFontSize = Select.with({
|
||||
label: t('Metric Name Font Size'),
|
||||
description: t('Font size for the metric name'),
|
||||
options: FONT_SIZE_OPTIONS_SMALL,
|
||||
default: 0.15,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Label Type Options (shared by Pie, Funnel, etc.)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Standard label content type options
|
||||
* Used by Pie, Funnel, and other category-based charts
|
||||
*/
|
||||
export const LABEL_TYPE_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Category Name'), value: 'key' },
|
||||
{ label: t('Value'), value: 'value' },
|
||||
{ label: t('Percentage'), value: 'percent' },
|
||||
{ label: t('Category and Value'), value: 'key_value' },
|
||||
{ label: t('Category and Percentage'), value: 'key_percent' },
|
||||
{ label: t('Category, Value and Percentage'), value: 'key_value_percent' },
|
||||
{ label: t('Value and Percentage'), value: 'value_percent' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Label type selector for category-based charts
|
||||
*/
|
||||
export const LabelType = Select.with({
|
||||
label: t('Label Type'),
|
||||
description: t('What should be shown on the label?'),
|
||||
options: LABEL_TYPE_OPTIONS,
|
||||
default: 'key',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Sort Options
|
||||
// ============================================================================
|
||||
|
||||
export const SORT_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Descending'), value: 'descending' },
|
||||
{ label: t('Ascending'), value: 'ascending' },
|
||||
{ label: t('None'), value: 'none' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Sort by metric toggle
|
||||
* Common for charts that need to sort data by metric value
|
||||
*/
|
||||
export const SortByMetric = Checkbox.with({
|
||||
label: t('Sort by Metric'),
|
||||
description: t('Sort results by the selected metric'),
|
||||
default: true,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Label Position Options
|
||||
// ============================================================================
|
||||
|
||||
export const LABEL_POSITION_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Top'), value: 'top' },
|
||||
{ label: t('Left'), value: 'left' },
|
||||
{ label: t('Right'), value: 'right' },
|
||||
{ label: t('Bottom'), value: 'bottom' },
|
||||
{ label: t('Inside'), value: 'inside' },
|
||||
{ label: t('Inside Left'), value: 'insideLeft' },
|
||||
{ label: t('Inside Right'), value: 'insideRight' },
|
||||
{ label: t('Inside Top'), value: 'insideTop' },
|
||||
{ label: t('Inside Bottom'), value: 'insideBottom' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Label position selector
|
||||
* Position labels relative to chart elements
|
||||
*/
|
||||
export const LabelPosition = Select.with({
|
||||
label: t('Label Position'),
|
||||
description: t('Position of labels on the chart'),
|
||||
options: LABEL_POSITION_OPTIONS,
|
||||
default: 'top',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Simple Label Type (key/value variants only)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Simple label type options - for charts with fewer label display options
|
||||
* Used by Radar, Sunburst, etc.
|
||||
*/
|
||||
export const SIMPLE_LABEL_TYPE_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Category Name'), value: 'key' },
|
||||
{ label: t('Value'), value: 'value' },
|
||||
{ label: t('Category and Value'), value: 'key_value' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Simple label type selector
|
||||
* For charts that only need key/value/key_value options
|
||||
*/
|
||||
export const SimpleLabelType = Select.with({
|
||||
label: t('Label Type'),
|
||||
description: t('What should be shown on the label?'),
|
||||
options: SIMPLE_LABEL_TYPE_OPTIONS,
|
||||
default: 'key',
|
||||
});
|
||||
|
||||
/**
|
||||
* Value-only label type options - for charts like Radar
|
||||
*/
|
||||
export const VALUE_LABEL_TYPE_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Value'), value: 'value' },
|
||||
{ label: t('Category and Value'), value: 'key_value' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Value label type selector
|
||||
* For charts that show value or category+value
|
||||
*/
|
||||
export const ValueLabelType = Select.with({
|
||||
label: t('Label Type'),
|
||||
description: t('What should be shown on the label?'),
|
||||
options: VALUE_LABEL_TYPE_OPTIONS,
|
||||
default: 'value',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Totals and Aggregates
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Show total toggle
|
||||
* For charts that can display aggregate totals
|
||||
*/
|
||||
export const ShowTotal = Checkbox.with({
|
||||
label: t('Show Total'),
|
||||
description: t('Whether to display the aggregate total'),
|
||||
default: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Threshold Controls
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Label percentage threshold
|
||||
* Minimum percentage for showing labels (avoids clutter on small slices)
|
||||
*/
|
||||
export const LabelThreshold = Text.with({
|
||||
label: t('Percentage Threshold'),
|
||||
description: t('Minimum threshold in percentage points for showing labels'),
|
||||
default: '5',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Shape Options
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Circle shape toggle (used by Radar)
|
||||
*/
|
||||
export const CircleShape = Checkbox.with({
|
||||
label: t('Circle Shape'),
|
||||
description: t('Use circular shape instead of polygon'),
|
||||
default: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Data Zoom
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Enable data zoom toggle
|
||||
* For charts with zoomable data areas
|
||||
*/
|
||||
export const DataZoom = Checkbox.with({
|
||||
label: t('Data Zoom'),
|
||||
description: t('Enable data zooming controls'),
|
||||
default: false,
|
||||
});
|
||||
306
superset-frontend/packages/superset-ui-glyph-core/src/types.ts
Normal file
306
superset-frontend/packages/superset-ui-glyph-core/src/types.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import type { ControlPanelConfig } from '@superset-ui/chart-controls';
|
||||
import type { ChartProps } from '@superset-ui/core';
|
||||
|
||||
/**
|
||||
* Option for Select controls
|
||||
*/
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Select argument type
|
||||
*/
|
||||
export interface SelectOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string | number;
|
||||
options?: SelectOption[];
|
||||
clearable?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Text argument type
|
||||
*/
|
||||
export interface TextOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Checkbox argument type
|
||||
*/
|
||||
export interface CheckboxOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Int argument type (slider)
|
||||
*/
|
||||
export interface IntOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Color argument type
|
||||
*/
|
||||
export interface ColorOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Metric argument type
|
||||
*/
|
||||
export interface MetricOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
multi?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Dimension argument type
|
||||
*/
|
||||
export interface DimensionOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
multi?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for NumberFormat argument type
|
||||
*/
|
||||
export interface NumberFormatOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Currency value structure
|
||||
*/
|
||||
export interface CurrencyValue {
|
||||
symbol?: string;
|
||||
symbolPosition?: 'prefix' | 'suffix';
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Currency argument type
|
||||
*/
|
||||
export interface CurrencyOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: CurrencyValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for TimeFormat argument type
|
||||
*/
|
||||
export interface TimeFormatOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for ConditionalFormatting argument type
|
||||
*/
|
||||
export interface ConditionalFormattingOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Slider argument type (continuous float values)
|
||||
*/
|
||||
export interface SliderOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Bounds argument type (min/max pairs)
|
||||
*/
|
||||
export interface BoundsOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: [number | null, number | null];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounds value type - tuple of [min, max] where either can be null
|
||||
*/
|
||||
export type BoundsValue = [number | null, number | null];
|
||||
|
||||
/**
|
||||
* Configuration options for ColorPicker argument type (RGBA colors)
|
||||
*/
|
||||
export interface ColorPickerOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: RgbaColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for RadioButton argument type
|
||||
*/
|
||||
export interface RadioButtonOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string | boolean;
|
||||
options: RadioOption[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Option for RadioButton controls
|
||||
*/
|
||||
export interface RadioOption {
|
||||
label: string;
|
||||
value: string | boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditional formatting rule value
|
||||
*/
|
||||
export interface ConditionalFormattingRule {
|
||||
column?: string;
|
||||
operator?: '<' | '<=' | '>' | '>=' | '==' | '!=' | 'between';
|
||||
targetValue?: number;
|
||||
targetValueLeft?: number;
|
||||
targetValueRight?: number;
|
||||
colorScheme?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Column type enum for data arguments
|
||||
*/
|
||||
export enum ColumnType {
|
||||
Metric = 'metric',
|
||||
Dimension = 'dimension',
|
||||
Temporal = 'temporal',
|
||||
Argument = 'argument',
|
||||
}
|
||||
|
||||
/**
|
||||
* Base argument class interface
|
||||
*/
|
||||
export interface ArgumentClass {
|
||||
label: string | null;
|
||||
description: string | null;
|
||||
columnType?: ColumnType;
|
||||
controlType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* RGBA color format used by Superset's ColorPickerControl
|
||||
*/
|
||||
export interface RgbaColor {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visibility function for conditional control display (legacy)
|
||||
*/
|
||||
export type VisibilityFn = (state: {
|
||||
controls: Record<string, { value: unknown }>;
|
||||
}) => boolean;
|
||||
|
||||
/**
|
||||
* Declarative condition for argument visibility/disabled state.
|
||||
*
|
||||
* Keys are argument names, values define the condition:
|
||||
* - Literal value: equality check (e.g., { showMetricName: true })
|
||||
* - Function: custom check (e.g., { subtitle: (val) => !!val })
|
||||
*
|
||||
* Multiple keys are AND'd together.
|
||||
*
|
||||
* @example
|
||||
* // Visible when showMetricName is true
|
||||
* visibleWhen: { showMetricName: true }
|
||||
*
|
||||
* @example
|
||||
* // Visible when subtitle is not empty
|
||||
* visibleWhen: { subtitle: (val) => !!val }
|
||||
*
|
||||
* @example
|
||||
* // Visible when showMetricName is true AND subtitle is not empty
|
||||
* visibleWhen: { showMetricName: true, subtitle: (val) => !!val }
|
||||
*/
|
||||
export type ArgumentCondition = Record<
|
||||
string,
|
||||
unknown | ((value: unknown) => boolean)
|
||||
>;
|
||||
|
||||
/**
|
||||
* Extended control configuration with visibility
|
||||
*/
|
||||
export interface ControlConfig {
|
||||
name: string;
|
||||
config: Record<string, unknown>;
|
||||
visibility?: VisibilityFn;
|
||||
resetOnHide?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Glyph chart definition
|
||||
*/
|
||||
export interface GlyphChartDef<TArgs extends Record<string, ArgumentClass>> {
|
||||
arguments: TArgs;
|
||||
sections?: {
|
||||
query?: ControlConfig[][];
|
||||
chartOptions?: ControlConfig[][];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of createGlyphPlugin
|
||||
*/
|
||||
export interface GlyphPluginResult<TFormData> {
|
||||
controlPanel: ControlPanelConfig;
|
||||
transformProps: (chartProps: ChartProps) => TFormData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type helper to extract form data types from argument definitions
|
||||
*/
|
||||
export type ArgumentsToFormData<TArgs extends Record<string, ArgumentClass>> = {
|
||||
[K in keyof TArgs]: TArgs[K] extends { default: infer D } ? D : unknown;
|
||||
};
|
||||
@@ -0,0 +1,475 @@
|
||||
/**
|
||||
* 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 {
|
||||
Argument,
|
||||
Bounds,
|
||||
Checkbox,
|
||||
Color,
|
||||
ColorPicker,
|
||||
ConditionalFormatting,
|
||||
Currency,
|
||||
Dimension,
|
||||
Int,
|
||||
isBoundsArg,
|
||||
isCheckboxArg,
|
||||
isColorArg,
|
||||
isColorPickerArg,
|
||||
isConditionalFormattingArg,
|
||||
isCurrencyArg,
|
||||
isDimensionArg,
|
||||
isIntArg,
|
||||
isMetricArg,
|
||||
isNumberFormatArg,
|
||||
isRadioButtonArg,
|
||||
isSelectArg,
|
||||
isSliderArg,
|
||||
isTemporalArg,
|
||||
isTextArg,
|
||||
isTimeFormatArg,
|
||||
Metric,
|
||||
NumberFormat,
|
||||
RadioButton,
|
||||
Select,
|
||||
Slider,
|
||||
Temporal,
|
||||
Text,
|
||||
TimeFormat,
|
||||
} from '@superset-ui/glyph-core';
|
||||
import { ColumnType } from '@superset-ui/glyph-core/types';
|
||||
|
||||
describe('Argument base class', () => {
|
||||
test('stores its constructor value', () => {
|
||||
const a = new Argument(42);
|
||||
expect(a.value).toBe(42);
|
||||
});
|
||||
|
||||
test('has expected static defaults', () => {
|
||||
expect(Argument.label).toBeNull();
|
||||
expect(Argument.description).toBeNull();
|
||||
expect(Argument.columnType).toBe(ColumnType.Argument);
|
||||
expect(Argument.controlType).toBe('TextControl');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Metric', () => {
|
||||
test('has expected static metadata', () => {
|
||||
expect(Metric.label).toBe('Metric');
|
||||
expect(Metric.columnType).toBe(ColumnType.Metric);
|
||||
expect(Metric.controlType).toBe('MetricsControl');
|
||||
expect(Metric.multi).toBe(false);
|
||||
});
|
||||
|
||||
test('.with() overrides label, description, multi', () => {
|
||||
const M = Metric.with({
|
||||
label: 'Sales',
|
||||
description: 'Total sales',
|
||||
multi: true,
|
||||
});
|
||||
expect(M.label).toBe('Sales');
|
||||
expect(M.description).toBe('Total sales');
|
||||
expect(M.multi).toBe(true);
|
||||
// unaltered ancestor metadata still present
|
||||
expect(M.columnType).toBe(ColumnType.Metric);
|
||||
expect(M.controlType).toBe('MetricsControl');
|
||||
});
|
||||
|
||||
test('.with() falls back to parent defaults when option omitted', () => {
|
||||
const M = Metric.with({ label: 'X' });
|
||||
expect(M.label).toBe('X');
|
||||
expect(M.multi).toBe(Metric.multi);
|
||||
expect(M.description).toBe(Metric.description);
|
||||
});
|
||||
|
||||
test('isMetricArg type guard', () => {
|
||||
expect(isMetricArg(Metric)).toBe(true);
|
||||
expect(isMetricArg(Metric.with({ label: 'X' }))).toBe(true);
|
||||
expect(isMetricArg(Dimension)).toBe(false);
|
||||
expect(isMetricArg(Select)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dimension', () => {
|
||||
test('has expected static metadata', () => {
|
||||
expect(Dimension.label).toBe('Dimension');
|
||||
expect(Dimension.columnType).toBe(ColumnType.Dimension);
|
||||
expect(Dimension.controlType).toBe('GroupByControl');
|
||||
expect(Dimension.multi).toBe(true);
|
||||
});
|
||||
|
||||
test('.with() overrides label, description, multi', () => {
|
||||
const D = Dimension.with({
|
||||
label: 'Region',
|
||||
multi: false,
|
||||
});
|
||||
expect(D.label).toBe('Region');
|
||||
expect(D.multi).toBe(false);
|
||||
});
|
||||
|
||||
test('isDimensionArg type guard', () => {
|
||||
expect(isDimensionArg(Dimension)).toBe(true);
|
||||
expect(isDimensionArg(Dimension.with({ label: 'X' }))).toBe(true);
|
||||
expect(isDimensionArg(Metric)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Temporal', () => {
|
||||
test('has expected static metadata', () => {
|
||||
expect(Temporal.label).toBe('Time Column');
|
||||
expect(Temporal.columnType).toBe(ColumnType.Temporal);
|
||||
expect(Temporal.controlType).toBe('TemporalControl');
|
||||
});
|
||||
|
||||
test('.with() overrides label and description', () => {
|
||||
const T = Temporal.with({ label: 'Order Date' });
|
||||
expect(T.label).toBe('Order Date');
|
||||
});
|
||||
|
||||
test('isTemporalArg type guard', () => {
|
||||
expect(isTemporalArg(Temporal)).toBe(true);
|
||||
expect(isTemporalArg(Metric)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Select', () => {
|
||||
const OPTIONS = [
|
||||
{ label: 'A', value: 'a' },
|
||||
{ label: 'B', value: 'b' },
|
||||
];
|
||||
|
||||
test('has expected static defaults', () => {
|
||||
expect(Select.label).toBe('Select');
|
||||
expect(Select.controlType).toBe('SelectControl');
|
||||
expect(Select.options).toEqual([]);
|
||||
expect(Select.default).toBe('');
|
||||
});
|
||||
|
||||
test('.with() applies label, default, options', () => {
|
||||
const S = Select.with({
|
||||
label: 'Choice',
|
||||
default: 'a',
|
||||
options: OPTIONS,
|
||||
});
|
||||
expect(S.label).toBe('Choice');
|
||||
expect(S.default).toBe('a');
|
||||
expect(S.options).toEqual(OPTIONS);
|
||||
});
|
||||
|
||||
test('isSelectArg type guard', () => {
|
||||
expect(isSelectArg(Select.with({ options: OPTIONS }))).toBe(true);
|
||||
expect(isSelectArg(Checkbox)).toBe(false);
|
||||
expect(isSelectArg(Metric)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Text', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Text.controlType).toBe('TextControl');
|
||||
expect(Text.default).toBe('');
|
||||
expect(Text.placeholder).toBe('');
|
||||
});
|
||||
|
||||
test('.with() applies label, default, placeholder', () => {
|
||||
const T = Text.with({
|
||||
label: 'Title',
|
||||
default: 'Untitled',
|
||||
placeholder: 'Enter title',
|
||||
});
|
||||
expect(T.label).toBe('Title');
|
||||
expect(T.default).toBe('Untitled');
|
||||
expect(T.placeholder).toBe('Enter title');
|
||||
});
|
||||
|
||||
test('isTextArg type guard accepts Text but not Select/Checkbox', () => {
|
||||
expect(isTextArg(Text)).toBe(true);
|
||||
expect(isTextArg(Text.with({ label: 'X' }))).toBe(true);
|
||||
expect(isTextArg(Checkbox)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Checkbox', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Checkbox.controlType).toBe('CheckboxControl');
|
||||
expect(Checkbox.default).toBe(false);
|
||||
});
|
||||
|
||||
test('.with() applies label, description, default', () => {
|
||||
const C = Checkbox.with({
|
||||
label: 'Show legend',
|
||||
default: true,
|
||||
});
|
||||
expect(C.label).toBe('Show legend');
|
||||
expect(C.default).toBe(true);
|
||||
});
|
||||
|
||||
test('isCheckboxArg type guard', () => {
|
||||
expect(isCheckboxArg(Checkbox)).toBe(true);
|
||||
expect(isCheckboxArg(Checkbox.with({ default: true }))).toBe(true);
|
||||
expect(isCheckboxArg(Text)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Int', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Int.controlType).toBe('SliderControl');
|
||||
expect(Int.default).toBe(0);
|
||||
expect(Int.min).toBe(0);
|
||||
expect(Int.max).toBe(100);
|
||||
expect(Int.step).toBe(1);
|
||||
});
|
||||
|
||||
test('.with() applies label, default, min, max, step', () => {
|
||||
const I = Int.with({
|
||||
label: 'Limit',
|
||||
default: 50,
|
||||
min: 10,
|
||||
max: 1000,
|
||||
step: 5,
|
||||
});
|
||||
expect(I.label).toBe('Limit');
|
||||
expect(I.default).toBe(50);
|
||||
expect(I.min).toBe(10);
|
||||
expect(I.max).toBe(1000);
|
||||
expect(I.step).toBe(5);
|
||||
});
|
||||
|
||||
test('isIntArg type guard', () => {
|
||||
expect(isIntArg(Int)).toBe(true);
|
||||
expect(isIntArg(Slider)).toBe(true); // Slider also has min/max
|
||||
expect(isIntArg(Checkbox)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Color', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Color.controlType).toBe('ColorPickerControl');
|
||||
expect(Color.default).toBe('#000000');
|
||||
});
|
||||
|
||||
test('.with() applies label, default', () => {
|
||||
const C = Color.with({ label: 'Fill', default: '#ff0000' });
|
||||
expect(C.label).toBe('Fill');
|
||||
expect(C.default).toBe('#ff0000');
|
||||
});
|
||||
|
||||
test('isColorArg type guard', () => {
|
||||
expect(isColorArg(Color)).toBe(true);
|
||||
expect(isColorArg(Color.with({ default: '#ff0000' }))).toBe(true);
|
||||
expect(isColorArg(Metric)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NumberFormat', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(NumberFormat.controlType).toBe('NumberFormatControl');
|
||||
expect(NumberFormat.default).toBe('SMART_NUMBER');
|
||||
expect(NumberFormat.FORMAT_OPTIONS.length).toBeGreaterThan(10);
|
||||
expect(
|
||||
NumberFormat.FORMAT_OPTIONS.some(o => o.value === 'SMART_NUMBER'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('.with() applies label, default', () => {
|
||||
const N = NumberFormat.with({ label: 'Amount', default: '.2f' });
|
||||
expect(N.label).toBe('Amount');
|
||||
expect(N.default).toBe('.2f');
|
||||
});
|
||||
|
||||
test('isNumberFormatArg type guard', () => {
|
||||
expect(isNumberFormatArg(NumberFormat)).toBe(true);
|
||||
expect(isNumberFormatArg(TimeFormat)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Currency', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Currency.controlType).toBe('CurrencyControl');
|
||||
expect(Currency.default).toEqual({});
|
||||
});
|
||||
|
||||
test('.with() applies label, default', () => {
|
||||
const C = Currency.with({
|
||||
label: 'Money',
|
||||
default: { symbol: 'USD', symbolPosition: 'prefix' },
|
||||
});
|
||||
expect(C.label).toBe('Money');
|
||||
expect(C.default).toEqual({ symbol: 'USD', symbolPosition: 'prefix' });
|
||||
});
|
||||
|
||||
test('isCurrencyArg type guard', () => {
|
||||
expect(isCurrencyArg(Currency)).toBe(true);
|
||||
expect(isCurrencyArg(NumberFormat)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TimeFormat', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(TimeFormat.controlType).toBe('TimeFormatControl');
|
||||
expect(TimeFormat.default).toBe('smart_date');
|
||||
expect(
|
||||
TimeFormat.FORMAT_OPTIONS.some(o => o.value === 'smart_date'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('.with() applies label, default', () => {
|
||||
const T = TimeFormat.with({ label: 'When', default: '%Y-%m-%d' });
|
||||
expect(T.label).toBe('When');
|
||||
expect(T.default).toBe('%Y-%m-%d');
|
||||
});
|
||||
|
||||
test('isTimeFormatArg type guard', () => {
|
||||
expect(isTimeFormatArg(TimeFormat)).toBe(true);
|
||||
expect(isTimeFormatArg(NumberFormat)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConditionalFormatting', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(ConditionalFormatting.controlType).toBe(
|
||||
'ConditionalFormattingControl',
|
||||
);
|
||||
expect(ConditionalFormatting.default).toEqual([]);
|
||||
});
|
||||
|
||||
test('.with() applies label and description (not default)', () => {
|
||||
const CF = ConditionalFormatting.with({ label: 'Format' });
|
||||
expect(CF.label).toBe('Format');
|
||||
});
|
||||
|
||||
test('isConditionalFormattingArg type guard', () => {
|
||||
expect(isConditionalFormattingArg(ConditionalFormatting)).toBe(true);
|
||||
expect(isConditionalFormattingArg(Select)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Slider', () => {
|
||||
test('has expected float-friendly defaults', () => {
|
||||
expect(Slider.controlType).toBe('SliderControl');
|
||||
expect(Slider.default).toBe(0);
|
||||
expect(Slider.min).toBe(0);
|
||||
expect(Slider.max).toBe(1);
|
||||
expect(Slider.step).toBe(0.1);
|
||||
});
|
||||
|
||||
test('.with() applies all numeric fields', () => {
|
||||
const S = Slider.with({
|
||||
label: 'Opacity',
|
||||
default: 0.8,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
});
|
||||
expect(S.label).toBe('Opacity');
|
||||
expect(S.default).toBe(0.8);
|
||||
expect(S.step).toBe(0.05);
|
||||
});
|
||||
|
||||
test('isSliderArg type guard requires float step', () => {
|
||||
expect(isSliderArg(Slider)).toBe(true);
|
||||
// Int is also SliderControl + has step but step is integer-valued — still
|
||||
// numeric so the guard recognizes it (current behavior); document it.
|
||||
expect(isSliderArg(Int)).toBe(true);
|
||||
expect(isSliderArg(Checkbox)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bounds', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Bounds.controlType).toBe('BoundsControl');
|
||||
expect(Bounds.default).toEqual([null, null]);
|
||||
});
|
||||
|
||||
test('.with() applies default', () => {
|
||||
const B = Bounds.with({ label: 'Range', default: [0, 100] });
|
||||
expect(B.label).toBe('Range');
|
||||
expect(B.default).toEqual([0, 100]);
|
||||
});
|
||||
|
||||
test('isBoundsArg type guard', () => {
|
||||
expect(isBoundsArg(Bounds)).toBe(true);
|
||||
expect(isBoundsArg(Int)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ColorPicker', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(ColorPicker.controlType).toBe('ColorPickerControl');
|
||||
expect(ColorPicker.default).toEqual({ r: 0, g: 0, b: 0, a: 1 });
|
||||
});
|
||||
|
||||
test('.with() applies default', () => {
|
||||
const CP = ColorPicker.with({
|
||||
label: 'Pick',
|
||||
default: { r: 255, g: 0, b: 0, a: 0.5 },
|
||||
});
|
||||
expect(CP.label).toBe('Pick');
|
||||
expect(CP.default).toEqual({ r: 255, g: 0, b: 0, a: 0.5 });
|
||||
});
|
||||
|
||||
test('isColorPickerArg distinguishes from Color (string)', () => {
|
||||
expect(isColorPickerArg(ColorPicker)).toBe(true);
|
||||
expect(isColorPickerArg(Color)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RadioButton', () => {
|
||||
const RADIO_OPTIONS = [
|
||||
{ label: 'Yes', value: true },
|
||||
{ label: 'No', value: false },
|
||||
];
|
||||
|
||||
test('has expected static defaults', () => {
|
||||
expect(RadioButton.controlType).toBe('RadioButtonControl');
|
||||
expect(RadioButton.default).toBe('');
|
||||
expect(RadioButton.options).toEqual([]);
|
||||
});
|
||||
|
||||
test('.with() applies all fields', () => {
|
||||
const RB = RadioButton.with({
|
||||
label: 'Toggle',
|
||||
default: true,
|
||||
options: RADIO_OPTIONS,
|
||||
});
|
||||
expect(RB.label).toBe('Toggle');
|
||||
expect(RB.default).toBe(true);
|
||||
expect(RB.options).toEqual(RADIO_OPTIONS);
|
||||
});
|
||||
|
||||
test('isRadioButtonArg type guard', () => {
|
||||
expect(isRadioButtonArg(RadioButton)).toBe(true);
|
||||
expect(isRadioButtonArg(Select)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Argument inheritance via .with()', () => {
|
||||
test('chained .with() calls compose overrides', () => {
|
||||
const Base = Select.with({
|
||||
label: 'Pick one',
|
||||
options: [{ label: 'A', value: 'a' }],
|
||||
});
|
||||
const Tighter = Base.with({ label: 'Pick exactly one' });
|
||||
expect(Tighter.label).toBe('Pick exactly one');
|
||||
expect(Tighter.options).toEqual([{ label: 'A', value: 'a' }]);
|
||||
});
|
||||
|
||||
test('original class is unmodified after .with()', () => {
|
||||
const before = Metric.multi;
|
||||
Metric.with({ multi: !before });
|
||||
expect(Metric.multi).toBe(before);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { ChartProps, FilterState } from '@superset-ui/core';
|
||||
import {
|
||||
createLabelMap,
|
||||
createSelectedValuesMap,
|
||||
extractCrossFilterProps,
|
||||
isDataPointFiltered,
|
||||
} from '@superset-ui/glyph-core';
|
||||
|
||||
describe('createSelectedValuesMap', () => {
|
||||
test('returns empty object when filterState is undefined', () => {
|
||||
expect(createSelectedValuesMap(undefined, ['a', 'b'])).toEqual({});
|
||||
});
|
||||
|
||||
test('returns empty object when selectedValues is undefined', () => {
|
||||
expect(
|
||||
createSelectedValuesMap({} as FilterState, ['a', 'b']),
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
test('returns empty object when selectedValues is empty', () => {
|
||||
expect(
|
||||
createSelectedValuesMap(
|
||||
{ selectedValues: [] } as unknown as FilterState,
|
||||
['a', 'b'],
|
||||
),
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
test('maps selected value to its index in seriesNames', () => {
|
||||
const result = createSelectedValuesMap(
|
||||
{ selectedValues: ['b'] } as unknown as FilterState,
|
||||
['a', 'b', 'c'],
|
||||
);
|
||||
expect(result).toEqual({ 1: 'b' });
|
||||
});
|
||||
|
||||
test('maps multiple selected values to their indices', () => {
|
||||
const result = createSelectedValuesMap(
|
||||
{ selectedValues: ['a', 'c'] } as unknown as FilterState,
|
||||
['a', 'b', 'c'],
|
||||
);
|
||||
expect(result).toEqual({ 0: 'a', 2: 'c' });
|
||||
});
|
||||
|
||||
test('ignores selected values not in seriesNames', () => {
|
||||
const result = createSelectedValuesMap(
|
||||
{ selectedValues: ['x', 'a'] } as unknown as FilterState,
|
||||
['a', 'b', 'c'],
|
||||
);
|
||||
expect(result).toEqual({ 0: 'a' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDataPointFiltered', () => {
|
||||
test('returns false when no filterState', () => {
|
||||
expect(isDataPointFiltered(undefined, 'a')).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false when selectedValues is empty', () => {
|
||||
expect(
|
||||
isDataPointFiltered(
|
||||
{ selectedValues: [] } as unknown as FilterState,
|
||||
'a',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false when name is in selectedValues', () => {
|
||||
expect(
|
||||
isDataPointFiltered(
|
||||
{ selectedValues: ['a', 'b'] } as unknown as FilterState,
|
||||
'a',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true when name is NOT in non-empty selectedValues', () => {
|
||||
expect(
|
||||
isDataPointFiltered(
|
||||
{ selectedValues: ['a', 'b'] } as unknown as FilterState,
|
||||
'c',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createLabelMap', () => {
|
||||
test('returns empty object for empty data', () => {
|
||||
expect(createLabelMap([], ['col1'], () => 'label')).toEqual({});
|
||||
});
|
||||
|
||||
test('maps each record to its label and groupby column values', () => {
|
||||
const data = [
|
||||
{ country: 'USA', region: 'North' },
|
||||
{ country: 'Brazil', region: 'South' },
|
||||
];
|
||||
const result = createLabelMap(
|
||||
data,
|
||||
['country', 'region'],
|
||||
d => d.country as string,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
USA: ['USA', 'North'],
|
||||
Brazil: ['Brazil', 'South'],
|
||||
});
|
||||
});
|
||||
|
||||
test('last record wins when extractLabel collides', () => {
|
||||
const data = [
|
||||
{ name: 'X', value: 1 },
|
||||
{ name: 'X', value: 2 },
|
||||
];
|
||||
const result = createLabelMap(data, ['value'], d => d.name as string);
|
||||
// collision: later entry overwrites
|
||||
expect(result).toEqual({ X: [2] });
|
||||
});
|
||||
|
||||
test('groupbyLabels controls the columns extracted, not the label', () => {
|
||||
const data = [{ a: 1, b: 2, c: 3 }];
|
||||
const result = createLabelMap(data, ['c'], () => 'only-key');
|
||||
expect(result).toEqual({ 'only-key': [3] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractCrossFilterProps', () => {
|
||||
const baseChartProps = {
|
||||
hooks: { setDataMask: jest.fn(), onContextMenu: jest.fn() },
|
||||
filterState: {
|
||||
selectedValues: ['USA'],
|
||||
} as unknown as FilterState,
|
||||
emitCrossFilters: true,
|
||||
formData: { viz_type: 'test' },
|
||||
} as unknown as ChartProps;
|
||||
|
||||
test('returns all expected fields', () => {
|
||||
const result = extractCrossFilterProps(
|
||||
baseChartProps,
|
||||
['country'],
|
||||
{ USA: ['USA'] },
|
||||
['USA', 'Brazil'],
|
||||
);
|
||||
expect(result.groupby).toEqual(['country']);
|
||||
expect(result.labelMap).toEqual({ USA: ['USA'] });
|
||||
expect(result.selectedValues).toEqual({ 0: 'USA' });
|
||||
expect(result.emitCrossFilters).toBe(true);
|
||||
expect(result.setDataMask).toBe(baseChartProps.hooks!.setDataMask);
|
||||
expect(result.onContextMenu).toBe(baseChartProps.hooks!.onContextMenu);
|
||||
});
|
||||
|
||||
test('coltypeMapping pass-through when provided', () => {
|
||||
const result = extractCrossFilterProps(
|
||||
baseChartProps,
|
||||
['country'],
|
||||
{},
|
||||
[],
|
||||
{ country: 1 },
|
||||
);
|
||||
expect(result.coltypeMapping).toEqual({ country: 1 });
|
||||
});
|
||||
|
||||
test('defaults setDataMask to a no-op when hooks omits it', () => {
|
||||
const chartProps = {
|
||||
...baseChartProps,
|
||||
hooks: {},
|
||||
} as unknown as ChartProps;
|
||||
const result = extractCrossFilterProps(chartProps, [], {}, []);
|
||||
expect(typeof result.setDataMask).toBe('function');
|
||||
// No throw when invoked
|
||||
expect(() =>
|
||||
result.setDataMask({ filterState: {} } as unknown as Parameters<
|
||||
typeof result.setDataMask
|
||||
>[0]),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
test('formData is included in the returned shape (for context menu formatting)', () => {
|
||||
const result = extractCrossFilterProps(
|
||||
baseChartProps,
|
||||
['country'],
|
||||
{},
|
||||
[],
|
||||
) as ReturnType<typeof extractCrossFilterProps> & { formData: unknown };
|
||||
expect(result.formData).toEqual({ viz_type: 'test' });
|
||||
});
|
||||
|
||||
test('selectedValues is empty when filterState has none', () => {
|
||||
const chartProps = {
|
||||
...baseChartProps,
|
||||
filterState: {} as FilterState,
|
||||
} as unknown as ChartProps;
|
||||
const result = extractCrossFilterProps(chartProps, [], {}, ['x']);
|
||||
expect(result.selectedValues).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,563 @@
|
||||
/**
|
||||
* 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 {
|
||||
Behavior,
|
||||
ChartLabel,
|
||||
ChartMetadata,
|
||||
ChartPlugin,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
Checkbox,
|
||||
defineChart,
|
||||
Dimension,
|
||||
evaluateGlyphCondition,
|
||||
getArgVisibleWhen,
|
||||
Metric,
|
||||
resolveArgClass,
|
||||
Select,
|
||||
Text,
|
||||
} from '@superset-ui/glyph-core';
|
||||
|
||||
// Helper: instantiate a plugin and reach its controlPanel config.
|
||||
function instantiate(PluginClass: ReturnType<typeof defineChart>) {
|
||||
const plugin = new PluginClass();
|
||||
// ChartPlugin internals expose the panel under .controlPanel
|
||||
// (set via super({ controlPanel }) in defineChart's GlyphChartPlugin).
|
||||
return {
|
||||
plugin,
|
||||
controlPanel: (plugin as unknown as { controlPanel: Record<string, unknown> })
|
||||
.controlPanel,
|
||||
metadata: (plugin as unknown as { metadata: ChartMetadata }).metadata,
|
||||
};
|
||||
}
|
||||
|
||||
const MIN_THUMBNAIL = 'thumb.png';
|
||||
|
||||
describe('resolveArgClass', () => {
|
||||
test('returns the bare class form unchanged', () => {
|
||||
expect(resolveArgClass(Metric)).toBe(Metric);
|
||||
});
|
||||
|
||||
test('unwraps the { arg, visibleWhen } object form', () => {
|
||||
const M = Metric.with({ label: 'Sales' });
|
||||
const argDef = { arg: M, visibleWhen: { show: true } };
|
||||
expect(resolveArgClass(argDef)).toBe(M);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getArgVisibleWhen', () => {
|
||||
test('returns undefined for bare class form', () => {
|
||||
expect(getArgVisibleWhen(Metric)).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns the condition for object form', () => {
|
||||
const argDef = { arg: Metric, visibleWhen: { show: true } };
|
||||
expect(getArgVisibleWhen(argDef)).toEqual({ show: true });
|
||||
});
|
||||
|
||||
test('returns undefined when object form has no visibleWhen', () => {
|
||||
expect(getArgVisibleWhen({ arg: Metric })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluateGlyphCondition', () => {
|
||||
test('returns true for empty condition', () => {
|
||||
expect(evaluateGlyphCondition({}, { foo: 1 })).toBe(true);
|
||||
});
|
||||
|
||||
test('returns true when equality check matches', () => {
|
||||
expect(evaluateGlyphCondition({ show: true }, { show: true })).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false when equality check fails', () => {
|
||||
expect(evaluateGlyphCondition({ show: true }, { show: false })).toBe(false);
|
||||
});
|
||||
|
||||
test('handles missing formData keys as undefined', () => {
|
||||
expect(evaluateGlyphCondition({ show: true }, {})).toBe(false);
|
||||
});
|
||||
|
||||
test('supports function-valued conditions', () => {
|
||||
const cond = { subtitle: (val: unknown) => !!val };
|
||||
expect(evaluateGlyphCondition(cond, { subtitle: 'hi' })).toBe(true);
|
||||
expect(evaluateGlyphCondition(cond, { subtitle: '' })).toBe(false);
|
||||
});
|
||||
|
||||
test('requires all keys in the condition to pass (AND semantics)', () => {
|
||||
const cond = { a: true, b: 'x' };
|
||||
expect(evaluateGlyphCondition(cond, { a: true, b: 'x' })).toBe(true);
|
||||
expect(evaluateGlyphCondition(cond, { a: true, b: 'y' })).toBe(false);
|
||||
expect(evaluateGlyphCondition(cond, { a: false, b: 'x' })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - basic plugin construction', () => {
|
||||
test('returns a ChartPlugin subclass', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
|
||||
const p = new Plugin();
|
||||
expect(p).toBeInstanceOf(ChartPlugin);
|
||||
});
|
||||
|
||||
test('plugin metadata is a ChartMetadata instance with required fields', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: {
|
||||
name: 'Test',
|
||||
description: 'A test chart',
|
||||
category: 'Charts',
|
||||
tags: ['test'],
|
||||
thumbnail: MIN_THUMBNAIL,
|
||||
},
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata).toBeInstanceOf(ChartMetadata);
|
||||
expect(metadata.name).toBe('Test');
|
||||
expect(metadata.description).toBe('A test chart');
|
||||
expect(metadata.category).toBe('Charts');
|
||||
expect(metadata.tags).toEqual(['test']);
|
||||
expect(metadata.thumbnail).toBe(MIN_THUMBNAIL);
|
||||
});
|
||||
|
||||
test('metadata defaults Behavior.InteractiveChart when omitted', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata.behaviors).toContain(Behavior.InteractiveChart);
|
||||
});
|
||||
|
||||
test('metadata behaviors override the default when provided', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: {
|
||||
name: 'Test',
|
||||
thumbnail: MIN_THUMBNAIL,
|
||||
behaviors: [Behavior.InteractiveChart, Behavior.DrillToDetail],
|
||||
},
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata.behaviors).toEqual([
|
||||
Behavior.InteractiveChart,
|
||||
Behavior.DrillToDetail,
|
||||
]);
|
||||
});
|
||||
|
||||
test('passes label, canBeAnnotationTypes, useLegacyApi, supportedAnnotationTypes through', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: {
|
||||
name: 'Test',
|
||||
thumbnail: MIN_THUMBNAIL,
|
||||
label: ChartLabel.Deprecated,
|
||||
canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
|
||||
useLegacyApi: true,
|
||||
supportedAnnotationTypes: ['FORMULA'],
|
||||
credits: ['https://example.com'],
|
||||
},
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata.label).toBe(ChartLabel.Deprecated);
|
||||
expect(metadata.canBeAnnotationTypes).toEqual(['EVENT', 'INTERVAL']);
|
||||
expect(metadata.useLegacyApi).toBe(true);
|
||||
expect(metadata.supportedAnnotationTypes).toEqual(['FORMULA']);
|
||||
expect(metadata.credits).toEqual(['https://example.com']);
|
||||
});
|
||||
|
||||
test('exampleGallery + thumbnailDark are preserved', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: {
|
||||
name: 'Test',
|
||||
thumbnail: MIN_THUMBNAIL,
|
||||
thumbnailDark: 'thumb-dark.png',
|
||||
exampleGallery: [{ url: 'a.png', urlDark: 'a-dark.png' }],
|
||||
},
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata.thumbnailDark).toBe('thumb-dark.png');
|
||||
expect(metadata.exampleGallery).toEqual([
|
||||
{ url: 'a.png', urlDark: 'a-dark.png' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - controlPanel generation from arguments', () => {
|
||||
test('Query section is auto-generated from Metric/Dimension arguments', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
groupby: Dimension,
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
// Query section should be auto-generated
|
||||
expect(sections.some(s => s?.label === 'Query')).toBe(true);
|
||||
});
|
||||
|
||||
test('suppressQuerySection: true skips the auto Query section', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
},
|
||||
suppressQuerySection: true,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
// The auto-generated Query section is suppressed.
|
||||
// (Charts using suppressQuerySection typically provide their own via
|
||||
// prependSections — see legacy nvd3 / deckgl consolidations.)
|
||||
const autoQuery = sections.find(s => s?.label === 'Query');
|
||||
expect(autoQuery).toBeUndefined();
|
||||
});
|
||||
|
||||
test('Chart Options section is generated when there are non-data args', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
showLegend: Checkbox.with({ label: 'Show legend', default: true }),
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
expect(sections.some(s => s?.label === 'Chart Options')).toBe(true);
|
||||
});
|
||||
|
||||
test('Chart Options section is hidden when there are no customize args', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
groupby: Dimension,
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
// No Customize-tab content → Chart Options auto-hides.
|
||||
expect(sections.some(s => s?.label === 'Chart Options')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - prependSections / middleSections / additionalSections', () => {
|
||||
test('prependSections appears before the auto Query section', () => {
|
||||
const TIME_SECTION = {
|
||||
label: 'Time',
|
||||
controlSetRows: [],
|
||||
};
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { metric: Metric },
|
||||
prependSections: [TIME_SECTION],
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
const timeIdx = sections.findIndex(s => s?.label === 'Time');
|
||||
const queryIdx = sections.findIndex(s => s?.label === 'Query');
|
||||
expect(timeIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(queryIdx).toBeGreaterThan(timeIdx);
|
||||
});
|
||||
|
||||
test('additionalSections appears after Chart Options', () => {
|
||||
const TIME_COMP = {
|
||||
label: 'Time Comparison',
|
||||
controlSetRows: [],
|
||||
};
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
showLegend: Checkbox.with({ label: 'Show', default: true }),
|
||||
},
|
||||
additionalSections: [TIME_COMP],
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
const chartOptsIdx = sections.findIndex(s => s?.label === 'Chart Options');
|
||||
const timeCompIdx = sections.findIndex(s => s?.label === 'Time Comparison');
|
||||
expect(chartOptsIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(timeCompIdx).toBeGreaterThan(chartOptsIdx);
|
||||
});
|
||||
|
||||
test('middleSections appears between Query and Chart Options', () => {
|
||||
const MIDDLE = {
|
||||
label: 'Middle',
|
||||
controlSetRows: [],
|
||||
};
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
showLegend: Checkbox.with({ label: 'Show', default: true }),
|
||||
},
|
||||
middleSections: [MIDDLE],
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
const queryIdx = sections.findIndex(s => s?.label === 'Query');
|
||||
const middleIdx = sections.findIndex(s => s?.label === 'Middle');
|
||||
const chartOptsIdx = sections.findIndex(s => s?.label === 'Chart Options');
|
||||
expect(queryIdx).toBeLessThan(middleIdx);
|
||||
expect(middleIdx).toBeLessThan(chartOptsIdx);
|
||||
});
|
||||
|
||||
test('chartOptionsTabOverride sets tabOverride on the generated section', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
showLegend: Checkbox.with({ label: 'Show', default: true }),
|
||||
},
|
||||
chartOptionsTabOverride: 'data',
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
tabOverride?: string;
|
||||
}>;
|
||||
const chartOpts = sections.find(s => s?.label === 'Chart Options');
|
||||
expect(chartOpts?.tabOverride).toBe('data');
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - overrides + formDataOverrides + onInit', () => {
|
||||
test('additionalControlOverrides land on controlPanel.controlOverrides', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { metric: Metric },
|
||||
additionalControlOverrides: {
|
||||
size: { label: 'Custom Size Label' },
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(
|
||||
(controlPanel.controlOverrides as Record<string, unknown>)?.size,
|
||||
).toEqual({ label: 'Custom Size Label' });
|
||||
});
|
||||
|
||||
test('controlOverrides + additionalControlOverrides merge', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { metric: Metric },
|
||||
controlOverrides: {
|
||||
a: { label: 'A' },
|
||||
},
|
||||
additionalControlOverrides: {
|
||||
b: { label: 'B' },
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const merged = controlPanel.controlOverrides as Record<string, unknown>;
|
||||
expect(merged.a).toEqual({ label: 'A' });
|
||||
expect(merged.b).toEqual({ label: 'B' });
|
||||
});
|
||||
|
||||
test('formDataOverrides is preserved on controlPanel', () => {
|
||||
const fdo = (formData: Record<string, unknown>) => ({
|
||||
...formData,
|
||||
custom: 'extra',
|
||||
});
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
formDataOverrides: fdo,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(controlPanel.formDataOverrides).toBe(fdo);
|
||||
});
|
||||
|
||||
test('onInit is preserved on controlPanel', () => {
|
||||
const onInit = (state: unknown) => state;
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
onInit,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(controlPanel.onInit).toBe(onInit);
|
||||
});
|
||||
|
||||
test('_glyphArgs is attached to the controlPanel for native rendering', () => {
|
||||
const args = {
|
||||
metric: Metric,
|
||||
showLegend: Checkbox.with({ label: 'Show', default: true }),
|
||||
};
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: args,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(controlPanel._glyphArgs).toEqual(args);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - custom buildQuery / transform', () => {
|
||||
test('custom buildQuery is invoked via the plugin loader', async () => {
|
||||
const customBuildQuery = jest.fn(() => ({ queries: [{ marker: 1 }] }));
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
buildQuery: customBuildQuery as unknown as never,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const p = new Plugin();
|
||||
// ChartPlugin stores it as a sanitized loader
|
||||
const loader = (p as unknown as { loadBuildQuery?: () => Promise<Function> })
|
||||
.loadBuildQuery;
|
||||
expect(loader).toBeDefined();
|
||||
const fn = await (loader as () => Promise<Function>)();
|
||||
fn({ viz_type: 'test', datasource: '1__table' });
|
||||
expect(customBuildQuery).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('transform receives chartProps and argValues', async () => {
|
||||
const captured: unknown[] = [];
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { metric: Metric },
|
||||
transform: (chartProps, argValues) => {
|
||||
captured.push({ chartProps, argValues });
|
||||
return { transformed: true };
|
||||
},
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const p = new Plugin();
|
||||
const loader = (
|
||||
p as unknown as { loadTransformProps: () => Promise<Function> }
|
||||
).loadTransformProps;
|
||||
const transformProps = await loader();
|
||||
transformProps({
|
||||
width: 100,
|
||||
height: 100,
|
||||
formData: { metric: 'count' },
|
||||
queriesData: [{ data: [] }],
|
||||
});
|
||||
expect(captured).toHaveLength(1);
|
||||
expect((captured[0] as { chartProps: unknown }).chartProps).toBeDefined();
|
||||
expect((captured[0] as { argValues: unknown }).argValues).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - Text-only argument behavior', () => {
|
||||
test('a Text-only chart still wires up a working plugin', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'TextOnly', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
title: Text.with({ label: 'Title', default: 'Hi' }),
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel, metadata } = instantiate(Plugin);
|
||||
expect(metadata.name).toBe('TextOnly');
|
||||
// Customize args present → Chart Options shows up
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
expect(sections.some(s => s?.label === 'Chart Options')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - visibleWhen with object-form ArgDef', () => {
|
||||
test('attaches a visibility derivation to the underlying control', () => {
|
||||
// Build a plugin where one arg is visibleWhen another is true.
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'V', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
showLegend: Checkbox.with({ label: 'Show legend', default: true }),
|
||||
legendPosition: {
|
||||
arg: Select.with({
|
||||
label: 'Position',
|
||||
default: 'right',
|
||||
options: [{ label: 'R', value: 'right' }],
|
||||
}),
|
||||
visibleWhen: { showLegend: true },
|
||||
},
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(controlPanel._glyphArgs).toBeDefined();
|
||||
const glyphArgs = controlPanel._glyphArgs as Record<string, unknown>;
|
||||
// The visibleWhen is preserved on the glyph args
|
||||
const lp = glyphArgs.legendPosition as { visibleWhen?: unknown };
|
||||
expect(lp.visibleWhen).toEqual({ showLegend: true });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,401 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { ChartProps } from '@superset-ui/core';
|
||||
import {
|
||||
Checkbox,
|
||||
Color,
|
||||
createGlyphPlugin,
|
||||
Dimension,
|
||||
generateControlPanel,
|
||||
generateTransformProps,
|
||||
getControlConfig,
|
||||
Int,
|
||||
Metric,
|
||||
Select,
|
||||
Temporal,
|
||||
Text,
|
||||
} from '@superset-ui/glyph-core';
|
||||
import type { GlyphArguments } from '@superset-ui/glyph-core/generators';
|
||||
|
||||
describe('getControlConfig - per argument type', () => {
|
||||
test('Select → SelectControl with options and clearable=false', () => {
|
||||
const S = Select.with({
|
||||
label: 'Choose',
|
||||
default: 'a',
|
||||
options: [{ label: 'A', value: 'a' }],
|
||||
});
|
||||
const cfg = getControlConfig(S, 'myParam');
|
||||
expect(cfg.type).toBe('SelectControl');
|
||||
expect(cfg.label).toBe('Choose');
|
||||
expect(cfg.default).toBe('a');
|
||||
expect(cfg.options).toEqual([{ label: 'A', value: 'a' }]);
|
||||
expect(cfg.clearable).toBe(false);
|
||||
expect(cfg.renderTrigger).toBe(true);
|
||||
});
|
||||
|
||||
test('Checkbox → CheckboxControl with default', () => {
|
||||
const C = Checkbox.with({ label: 'Show', default: true });
|
||||
const cfg = getControlConfig(C, 'show');
|
||||
expect(cfg.type).toBe('CheckboxControl');
|
||||
expect(cfg.label).toBe('Show');
|
||||
expect(cfg.default).toBe(true);
|
||||
expect(cfg.renderTrigger).toBe(true);
|
||||
});
|
||||
|
||||
test('Int → SliderControl with min/max/step', () => {
|
||||
const I = Int.with({ label: 'Limit', default: 50, min: 0, max: 1000, step: 5 });
|
||||
const cfg = getControlConfig(I, 'limit');
|
||||
expect(cfg.type).toBe('SliderControl');
|
||||
expect(cfg.label).toBe('Limit');
|
||||
expect(cfg.default).toBe(50);
|
||||
expect(cfg.min).toBe(0);
|
||||
expect(cfg.max).toBe(1000);
|
||||
expect(cfg.step).toBe(5);
|
||||
});
|
||||
|
||||
test('Color (hex) → ColorPickerControl with RGBA default', () => {
|
||||
const C = Color.with({ label: 'Fill', default: '#ff0000' });
|
||||
const cfg = getControlConfig(C, 'fill');
|
||||
expect(cfg.type).toBe('ColorPickerControl');
|
||||
expect(cfg.label).toBe('Fill');
|
||||
expect(cfg.default).toEqual({ r: 255, g: 0, b: 0, a: 1 });
|
||||
});
|
||||
|
||||
test('Text → TextControl with placeholder', () => {
|
||||
const T = Text.with({
|
||||
label: 'Title',
|
||||
default: 'Untitled',
|
||||
placeholder: 'Enter…',
|
||||
});
|
||||
const cfg = getControlConfig(T, 'title');
|
||||
expect(cfg.type).toBe('TextControl');
|
||||
expect(cfg.label).toBe('Title');
|
||||
expect(cfg.default).toBe('Untitled');
|
||||
expect(cfg.placeholder).toBe('Enter…');
|
||||
});
|
||||
|
||||
test('falls back to paramName when label is unset', () => {
|
||||
// Use the raw Text class (label: null on Argument)
|
||||
class Bare extends Text {
|
||||
static override label = null;
|
||||
}
|
||||
const cfg = getControlConfig(Bare, 'fallback_name');
|
||||
expect(cfg.label).toBe('fallback_name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateControlPanel', () => {
|
||||
test('produces Query and Chart Options sections', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['metric', Metric],
|
||||
['showLegend', Checkbox.with({ label: 'Legend', default: true })],
|
||||
]);
|
||||
const cp = generateControlPanel(args);
|
||||
const labels = cp.controlPanelSections.map(s =>
|
||||
s && 'label' in s ? s.label : undefined,
|
||||
);
|
||||
expect(labels).toContain('Query');
|
||||
expect(labels).toContain('Chart Options');
|
||||
});
|
||||
|
||||
test('Metric args produce a [metric] row in Query', () => {
|
||||
const args: GlyphArguments = new Map([['m', Metric]]);
|
||||
const cp = generateControlPanel(args);
|
||||
const querySection = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Query',
|
||||
);
|
||||
expect(querySection).toBeDefined();
|
||||
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(rows).toContainEqual(['metric']);
|
||||
});
|
||||
|
||||
test('Dimension args produce a [groupby] row in Query', () => {
|
||||
const args: GlyphArguments = new Map([['d', Dimension]]);
|
||||
const cp = generateControlPanel(args);
|
||||
const querySection = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Query',
|
||||
);
|
||||
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(rows).toContainEqual(['groupby']);
|
||||
});
|
||||
|
||||
test('Temporal args produce [x_axis] and [time_grain_sqla] rows in Query', () => {
|
||||
const args: GlyphArguments = new Map([['t', Temporal]]);
|
||||
const cp = generateControlPanel(args);
|
||||
const querySection = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Query',
|
||||
);
|
||||
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(rows).toContainEqual(['x_axis']);
|
||||
expect(rows).toContainEqual(['time_grain_sqla']);
|
||||
});
|
||||
|
||||
test('adhoc_filters is always added to Query', () => {
|
||||
const args: GlyphArguments = new Map();
|
||||
const cp = generateControlPanel(args);
|
||||
const querySection = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Query',
|
||||
);
|
||||
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(rows).toContainEqual(['adhoc_filters']);
|
||||
});
|
||||
|
||||
test('Non-data args become controls in Chart Options', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['showLegend', Checkbox.with({ label: 'Legend', default: true })],
|
||||
]);
|
||||
const cp = generateControlPanel(args);
|
||||
const chartOpts = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Chart Options',
|
||||
);
|
||||
const rows = (chartOpts as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toEqual([
|
||||
{
|
||||
name: 'showLegend',
|
||||
config: expect.objectContaining({ type: 'CheckboxControl' }),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('GlyphArgConfig with visibility wires onto control', () => {
|
||||
const visibility = jest.fn(() => true);
|
||||
const args: GlyphArguments = new Map([
|
||||
[
|
||||
'subtitleSize',
|
||||
{
|
||||
arg: Select.with({
|
||||
label: 'Size',
|
||||
default: 'm',
|
||||
options: [{ label: 'M', value: 'm' }],
|
||||
}),
|
||||
visibility,
|
||||
resetOnHide: true,
|
||||
},
|
||||
],
|
||||
]);
|
||||
const cp = generateControlPanel(args);
|
||||
const chartOpts = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Chart Options',
|
||||
);
|
||||
const row = (chartOpts as { controlSetRows: unknown[][] }).controlSetRows[0];
|
||||
const item = (row as Array<{ config: Record<string, unknown> }>)[0];
|
||||
expect(item.config.visibility).toBe(visibility);
|
||||
expect(item.config.resetOnHide).toBe(true);
|
||||
});
|
||||
|
||||
test('controlOverrides and formDataOverrides options pass through', () => {
|
||||
const fdo = (fd: Record<string, unknown>) => ({ ...fd, x: 1 });
|
||||
const cp = generateControlPanel(new Map(), {
|
||||
controlOverrides: { metric: { label: 'M' } },
|
||||
formDataOverrides: fdo,
|
||||
});
|
||||
expect(cp.controlOverrides).toEqual({ metric: { label: 'M' } });
|
||||
expect(cp.formDataOverrides).toBe(fdo);
|
||||
});
|
||||
|
||||
test('extra queryControls and chartOptionsControls are appended', () => {
|
||||
const args: GlyphArguments = new Map();
|
||||
const cp = generateControlPanel(args, {
|
||||
queryControls: [['custom_filter']] as never,
|
||||
chartOptionsControls: [['custom_chart_opt']] as never,
|
||||
});
|
||||
const querySection = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Query',
|
||||
);
|
||||
const queryRows = (querySection as { controlSetRows: unknown[][] })
|
||||
.controlSetRows;
|
||||
expect(queryRows).toContainEqual(['custom_filter']);
|
||||
|
||||
const chartOpts = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Chart Options',
|
||||
);
|
||||
const optRows = (chartOpts as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(optRows).toContainEqual(['custom_chart_opt']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateTransformProps', () => {
|
||||
function makeChartProps(formData: Record<string, unknown>): ChartProps {
|
||||
return {
|
||||
width: 400,
|
||||
height: 300,
|
||||
queriesData: [{ data: [] }],
|
||||
formData,
|
||||
} as unknown as ChartProps;
|
||||
}
|
||||
|
||||
test('returns width/height/queriesData passthrough', () => {
|
||||
const transform = generateTransformProps(new Map());
|
||||
const out = transform(makeChartProps({}));
|
||||
expect(out).toMatchObject({ width: 400, height: 300 });
|
||||
});
|
||||
|
||||
test('extracts Select value from formData', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
[
|
||||
'size',
|
||||
Select.with({
|
||||
label: 'Size',
|
||||
default: 'm',
|
||||
options: [
|
||||
{ label: 'S', value: 's' },
|
||||
{ label: 'M', value: 'm' },
|
||||
],
|
||||
}),
|
||||
],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({ size: 's' }));
|
||||
expect((out as { size: unknown }).size).toBe('s');
|
||||
});
|
||||
|
||||
test('Select falls back to default when value missing', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['size', Select.with({ label: 'Size', default: 'm', options: [] })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({}));
|
||||
expect((out as { size: unknown }).size).toBe('m');
|
||||
});
|
||||
|
||||
test('Checkbox uses formData value when present', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['flag', Checkbox.with({ label: 'F', default: false })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({ flag: true }));
|
||||
expect((out as { flag: unknown }).flag).toBe(true);
|
||||
});
|
||||
|
||||
test('Checkbox falls back to default when value missing', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['flag', Checkbox.with({ label: 'F', default: true })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({}));
|
||||
expect((out as { flag: unknown }).flag).toBe(true);
|
||||
});
|
||||
|
||||
test('Color: RGBA in formData → hex string', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['fill', Color.with({ label: 'Fill', default: '#000000' })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(
|
||||
makeChartProps({ fill: { r: 255, g: 0, b: 0, a: 1 } }),
|
||||
);
|
||||
expect((out as { fill: unknown }).fill).toBe('#ff0000');
|
||||
});
|
||||
|
||||
test('Color: string value in formData passes through', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['fill', Color.with({ label: 'Fill', default: '#000000' })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({ fill: '#abcdef' }));
|
||||
expect((out as { fill: unknown }).fill).toBe('#abcdef');
|
||||
});
|
||||
|
||||
test('Color: falls back to class default when value missing', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['fill', Color.with({ label: 'Fill', default: '#112233' })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({}));
|
||||
// default is hex string; transform converts the RGBA-formatted default back to hex
|
||||
expect((out as { fill: unknown }).fill).toBe('#112233');
|
||||
});
|
||||
|
||||
test('Int uses default when value missing', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['n', Int.with({ label: 'N', default: 7, min: 0, max: 100 })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({}));
|
||||
expect((out as { n: unknown }).n).toBe(7);
|
||||
});
|
||||
|
||||
test('Text uses default when value missing', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['s', Text.with({ label: 'S', default: 'hi' })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({}));
|
||||
expect((out as { s: unknown }).s).toBe('hi');
|
||||
});
|
||||
|
||||
test('Metric/Dimension/Temporal args are NOT extracted (handled elsewhere)', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['metric', Metric],
|
||||
['groupby', Dimension],
|
||||
['t', Temporal],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(
|
||||
makeChartProps({ metric: 'count', groupby: 'a', t: 'date' }),
|
||||
);
|
||||
expect((out as Record<string, unknown>).metric).toBeUndefined();
|
||||
expect((out as Record<string, unknown>).groupby).toBeUndefined();
|
||||
expect((out as Record<string, unknown>).t).toBeUndefined();
|
||||
});
|
||||
|
||||
test('passthrough option copies named ChartProps fields onto the result', () => {
|
||||
const args: GlyphArguments = new Map();
|
||||
const transform = generateTransformProps(args, {
|
||||
passthrough: ['formData'],
|
||||
});
|
||||
const out = transform(makeChartProps({ marker: 1 })) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(out.formData).toEqual({ marker: 1 });
|
||||
});
|
||||
|
||||
test('custom transform option receives extracted values and chartProps', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['flag', Checkbox.with({ label: 'F', default: false })],
|
||||
]);
|
||||
const transform = generateTransformProps(args, {
|
||||
transform: (values, chartProps) => ({
|
||||
values,
|
||||
gotChartProps: !!chartProps,
|
||||
}),
|
||||
});
|
||||
const out = transform(makeChartProps({ flag: true })) as {
|
||||
values: { flag: boolean };
|
||||
gotChartProps: boolean;
|
||||
};
|
||||
expect(out.values.flag).toBe(true);
|
||||
expect(out.gotChartProps).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createGlyphPlugin', () => {
|
||||
test('returns both controlPanel and transformProps', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['metric', Metric],
|
||||
['show', Checkbox.with({ label: 'S', default: true })],
|
||||
]);
|
||||
const plugin = createGlyphPlugin(args);
|
||||
expect(plugin.controlPanel).toBeDefined();
|
||||
expect(plugin.controlPanel.controlPanelSections.length).toBe(2);
|
||||
expect(typeof plugin.transformProps).toBe('function');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* 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 {
|
||||
Checkbox,
|
||||
CircleShape,
|
||||
DataZoom,
|
||||
ForceTimestampFormatting,
|
||||
HeaderFontSize,
|
||||
isCheckboxArg,
|
||||
isSelectArg,
|
||||
isTextArg,
|
||||
LabelPosition,
|
||||
LabelType,
|
||||
LabelThreshold,
|
||||
LegendOrientation,
|
||||
LegendSort,
|
||||
LegendType,
|
||||
MetricNameFontSize,
|
||||
Select,
|
||||
ShowLabels,
|
||||
ShowLegend,
|
||||
ShowMetricName,
|
||||
ShowTotal,
|
||||
ShowValue,
|
||||
SimpleLabelType,
|
||||
SortByMetric,
|
||||
Subtitle,
|
||||
SubheaderFontSize,
|
||||
Text,
|
||||
ValueLabelType,
|
||||
} from '@superset-ui/glyph-core';
|
||||
import {
|
||||
FONT_SIZE_OPTIONS_LARGE,
|
||||
FONT_SIZE_OPTIONS_SMALL,
|
||||
LABEL_TYPE_OPTIONS,
|
||||
LEGEND_ORIENTATION_OPTIONS,
|
||||
LEGEND_SORT_OPTIONS,
|
||||
LEGEND_TYPE_OPTIONS,
|
||||
SORT_OPTIONS,
|
||||
} from '@superset-ui/glyph-core/presets';
|
||||
|
||||
describe('Font-size presets', () => {
|
||||
test('HeaderFontSize is a Select with large font options', () => {
|
||||
expect(isSelectArg(HeaderFontSize)).toBe(true);
|
||||
expect((HeaderFontSize as unknown as typeof Select).options).toBe(
|
||||
FONT_SIZE_OPTIONS_LARGE,
|
||||
);
|
||||
});
|
||||
|
||||
test('SubheaderFontSize is a Select with small font options', () => {
|
||||
expect(isSelectArg(SubheaderFontSize)).toBe(true);
|
||||
expect((SubheaderFontSize as unknown as typeof Select).options).toBe(
|
||||
FONT_SIZE_OPTIONS_SMALL,
|
||||
);
|
||||
});
|
||||
|
||||
test('FONT_SIZE_OPTIONS_LARGE and _SMALL are non-empty option arrays', () => {
|
||||
expect(FONT_SIZE_OPTIONS_LARGE.length).toBeGreaterThan(0);
|
||||
expect(FONT_SIZE_OPTIONS_SMALL.length).toBeGreaterThan(0);
|
||||
expect(FONT_SIZE_OPTIONS_LARGE[0]).toHaveProperty('label');
|
||||
expect(FONT_SIZE_OPTIONS_LARGE[0]).toHaveProperty('value');
|
||||
});
|
||||
|
||||
test('MetricNameFontSize is a Select preset', () => {
|
||||
expect(isSelectArg(MetricNameFontSize)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Text presets', () => {
|
||||
test('Subtitle is a Text preset', () => {
|
||||
expect(isTextArg(Subtitle)).toBe(true);
|
||||
expect(Subtitle.prototype).toBeInstanceOf(Text);
|
||||
});
|
||||
|
||||
test('LabelThreshold is a Text preset', () => {
|
||||
expect(isTextArg(LabelThreshold)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Checkbox presets', () => {
|
||||
test.each([
|
||||
['ShowLegend', ShowLegend],
|
||||
['ShowLabels', ShowLabels],
|
||||
['ShowValue', ShowValue],
|
||||
['ShowMetricName', ShowMetricName],
|
||||
['ShowTotal', ShowTotal],
|
||||
['SortByMetric', SortByMetric],
|
||||
['CircleShape', CircleShape],
|
||||
['DataZoom', DataZoom],
|
||||
['ForceTimestampFormatting', ForceTimestampFormatting],
|
||||
])('%s is a Checkbox preset', (_name, preset) => {
|
||||
expect(isCheckboxArg(preset)).toBe(true);
|
||||
expect(preset.prototype).toBeInstanceOf(Checkbox);
|
||||
});
|
||||
|
||||
test('Checkbox presets have a label and a description', () => {
|
||||
[ShowLegend, ShowLabels, ShowValue, ShowMetricName, ShowTotal].forEach(
|
||||
preset => {
|
||||
expect(preset.label).toBeTruthy();
|
||||
expect(preset.description).toBeTruthy();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Legend Select presets', () => {
|
||||
test('LegendType uses LEGEND_TYPE_OPTIONS', () => {
|
||||
expect(isSelectArg(LegendType)).toBe(true);
|
||||
expect((LegendType as unknown as typeof Select).options).toBe(
|
||||
LEGEND_TYPE_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
test('LegendOrientation uses LEGEND_ORIENTATION_OPTIONS', () => {
|
||||
expect(isSelectArg(LegendOrientation)).toBe(true);
|
||||
expect((LegendOrientation as unknown as typeof Select).options).toBe(
|
||||
LEGEND_ORIENTATION_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
test('LegendSort uses LEGEND_SORT_OPTIONS', () => {
|
||||
expect(isSelectArg(LegendSort)).toBe(true);
|
||||
expect((LegendSort as unknown as typeof Select).options).toBe(
|
||||
LEGEND_SORT_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
test('legend option sets are non-empty', () => {
|
||||
expect(LEGEND_TYPE_OPTIONS.length).toBeGreaterThan(0);
|
||||
expect(LEGEND_ORIENTATION_OPTIONS.length).toBeGreaterThan(0);
|
||||
expect(LEGEND_SORT_OPTIONS.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Label / value-label Select presets', () => {
|
||||
test('LabelType is a Select with LABEL_TYPE_OPTIONS', () => {
|
||||
expect(isSelectArg(LabelType)).toBe(true);
|
||||
expect((LabelType as unknown as typeof Select).options).toBe(
|
||||
LABEL_TYPE_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
test('SimpleLabelType is a Select preset', () => {
|
||||
expect(isSelectArg(SimpleLabelType)).toBe(true);
|
||||
});
|
||||
|
||||
test('ValueLabelType is a Select preset', () => {
|
||||
expect(isSelectArg(ValueLabelType)).toBe(true);
|
||||
});
|
||||
|
||||
test('LabelPosition is a Select preset', () => {
|
||||
expect(isSelectArg(LabelPosition)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sort options', () => {
|
||||
test('SORT_OPTIONS is non-empty', () => {
|
||||
expect(SORT_OPTIONS.length).toBeGreaterThan(0);
|
||||
expect(SORT_OPTIONS[0]).toHaveProperty('label');
|
||||
expect(SORT_OPTIONS[0]).toHaveProperty('value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Preset extensibility', () => {
|
||||
test('ShowLegend.with() overrides label while keeping the Checkbox shape', () => {
|
||||
const Custom = ShowLegend.with({
|
||||
label: 'Display legend',
|
||||
default: false,
|
||||
});
|
||||
expect(isCheckboxArg(Custom)).toBe(true);
|
||||
expect(Custom.label).toBe('Display legend');
|
||||
expect(Custom.default).toBe(false);
|
||||
});
|
||||
|
||||
test('HeaderFontSize.with() overrides label, default keeps options', () => {
|
||||
const Custom = HeaderFontSize.with({
|
||||
label: 'Title size',
|
||||
default: 0.4,
|
||||
});
|
||||
expect(isSelectArg(Custom)).toBe(true);
|
||||
expect(Custom.label).toBe('Title size');
|
||||
expect(Custom.default).toBe(0.4);
|
||||
expect((Custom as unknown as typeof Select).options).toBe(
|
||||
FONT_SIZE_OPTIONS_LARGE,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// Path Resolution: Override baseUrl to maintain correct path mappings from parent config
|
||||
// (e.g., "@apache-superset/core" -> "./packages/superset-core/src")
|
||||
"baseUrl": "../..",
|
||||
|
||||
// Directory Overrides: Parent config paths are relative to frontend root,
|
||||
// but packages need paths relative to their own directory
|
||||
"outDir": "lib",
|
||||
"rootDir": "src",
|
||||
"declarationDir": "lib"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
|
||||
"exclude": ["src/**/*.test.*", "src/**/*.stories.*"],
|
||||
"references": [
|
||||
{ "path": "../superset-core" },
|
||||
{ "path": "../superset-ui-core" },
|
||||
{ "path": "../superset-ui-chart-controls" }
|
||||
]
|
||||
}
|
||||
@@ -32,6 +32,7 @@
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { legacyValidateInteger } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
D3_FORMAT_DOCS,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Time'),
|
||||
expanded: true,
|
||||
description: t('Time related form attributes'),
|
||||
controlSetRows: [['granularity_sqla'], ['time_range']],
|
||||
},
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'domain_granularity',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Domain'),
|
||||
default: 'month',
|
||||
choices: [
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
['year', t('year')],
|
||||
],
|
||||
description: t('The time unit used for the grouping of blocks'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'subdomain_granularity',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Subdomain'),
|
||||
default: 'day',
|
||||
choices: [
|
||||
['min', t('min')],
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
],
|
||||
description: t(
|
||||
'The time unit for each block. Should be a smaller unit than ' +
|
||||
'domain_granularity. Should be larger or equal to Time Grain',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
['metrics'],
|
||||
['adhoc_filters'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
tabOverride: 'customize',
|
||||
controlSetRows: [
|
||||
['linear_color_scheme'],
|
||||
[
|
||||
{
|
||||
name: 'cell_size',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
default: 10,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
label: t('Cell Size'),
|
||||
description: t('The size of the square cell, in pixels'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'cell_padding',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
default: 2,
|
||||
label: t('Cell Padding'),
|
||||
description: t('The distance between cells, in pixels'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'cell_radius',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
default: 0,
|
||||
label: t('Cell Radius'),
|
||||
description: t('The pixel radius'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'steps',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
default: 10,
|
||||
label: t('Color Steps'),
|
||||
description: t('The number color "steps"'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'y_axis_format',
|
||||
{
|
||||
name: 'x_axis_time_format',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Time Format'),
|
||||
renderTrigger: true,
|
||||
default: 'smart_date',
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'show_legend',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Legend'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t('Whether to display the legend (toggles)'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'show_values',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Values'),
|
||||
renderTrigger: true,
|
||||
default: false,
|
||||
description: t(
|
||||
'Whether to display the numerical values within the cells',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'show_metric_name',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Metric Names'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t('Whether to display the metric name as a title'),
|
||||
},
|
||||
},
|
||||
null,
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
y_axis_format: {
|
||||
label: t('Number Format'),
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
metrics: getStandardizedControls().popAllMetrics(),
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,58 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import transformProps from './transformProps';
|
||||
import example from './images/example.jpg';
|
||||
import exampleDark from './images/example-dark.jpg';
|
||||
import controlPanel from './controlPanel';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Correlation'),
|
||||
credits: ['https://github.com/wa0x6e/cal-heatmap'],
|
||||
description: t(
|
||||
"Visualizes how a metric has changed over a time using a color scale and a calendar view. Gray values are used to indicate missing values and the linear color scheme is used to encode the magnitude of each day's value.",
|
||||
),
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
name: t('Calendar Heatmap'),
|
||||
tags: [
|
||||
t('Business'),
|
||||
t('Comparison'),
|
||||
t('Intensity'),
|
||||
t('Pattern'),
|
||||
t('Report'),
|
||||
t('Trend'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class CalendarChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./ReactCalendar'),
|
||||
metadata,
|
||||
transformProps,
|
||||
controlPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* 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 { t } from '@apache-superset/core/translation';
|
||||
import { getNumberFormatter } from '@superset-ui/core';
|
||||
import {
|
||||
D3_FORMAT_DOCS,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import {
|
||||
defineChart,
|
||||
Int,
|
||||
Checkbox,
|
||||
TimeFormat,
|
||||
} from '@superset-ui/glyph-core';
|
||||
import example from './images/example.jpg';
|
||||
import exampleDark from './images/example-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import { getFormattedUTCTime } from './utils';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const ReactCalendar = require('./ReactCalendar').default;
|
||||
|
||||
type CalendarExtra = {
|
||||
timeFormatter: (ts: number | string) => string;
|
||||
valueFormatter: (val: unknown) => string;
|
||||
verboseMap: Record<string, string>;
|
||||
domainGranularity: string;
|
||||
subdomainGranularity: string;
|
||||
linearColorScheme: string;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default defineChart<any, CalendarExtra>({
|
||||
metadata: {
|
||||
name: t('Calendar Heatmap'),
|
||||
description: t(
|
||||
"Visualizes how a metric has changed over a time using a color scale and a calendar view. Gray values are used to indicate missing values and the linear color scheme is used to encode the magnitude of each day's value.",
|
||||
),
|
||||
category: t('Correlation'),
|
||||
credits: ['https://github.com/wa0x6e/cal-heatmap'],
|
||||
tags: [
|
||||
t('Business'),
|
||||
t('Comparison'),
|
||||
t('Intensity'),
|
||||
t('Pattern'),
|
||||
t('Report'),
|
||||
t('Trend'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
},
|
||||
arguments: {
|
||||
cell_size: Int.with({
|
||||
label: 'Cell Size',
|
||||
description: 'The size of the square cell, in pixels',
|
||||
default: 10,
|
||||
min: 1,
|
||||
max: 100,
|
||||
}),
|
||||
cell_padding: Int.with({
|
||||
label: 'Cell Padding',
|
||||
description: 'The distance between cells, in pixels',
|
||||
default: 2,
|
||||
min: 0,
|
||||
max: 20,
|
||||
}),
|
||||
cell_radius: Int.with({
|
||||
label: 'Cell Radius',
|
||||
description: 'The pixel radius',
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 50,
|
||||
}),
|
||||
steps: Int.with({
|
||||
label: 'Color Steps',
|
||||
description: 'The number color "steps"',
|
||||
default: 10,
|
||||
min: 1,
|
||||
max: 50,
|
||||
}),
|
||||
x_axis_time_format: TimeFormat.with({
|
||||
label: 'Time Format',
|
||||
description: D3_FORMAT_DOCS,
|
||||
default: 'smart_date',
|
||||
}),
|
||||
show_legend: Checkbox.with({
|
||||
label: 'Legend',
|
||||
description: 'Whether to display the legend (toggles)',
|
||||
default: true,
|
||||
}),
|
||||
show_values: Checkbox.with({
|
||||
label: 'Show Values',
|
||||
description: 'Whether to display the numerical values within the cells',
|
||||
default: false,
|
||||
}),
|
||||
show_metric_name: Checkbox.with({
|
||||
label: 'Show Metric Names',
|
||||
description: 'Whether to display the metric name as a title',
|
||||
default: true,
|
||||
}),
|
||||
},
|
||||
prependSections: [
|
||||
{
|
||||
label: t('Time'),
|
||||
expanded: true,
|
||||
description: t('Time related form attributes'),
|
||||
controlSetRows: [['granularity_sqla'], ['time_range']],
|
||||
},
|
||||
],
|
||||
additionalControls: {
|
||||
queryBefore: [
|
||||
[
|
||||
{
|
||||
name: 'domain_granularity',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Domain'),
|
||||
default: 'month',
|
||||
choices: [
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
['year', t('year')],
|
||||
],
|
||||
description: t('The time unit used for the grouping of blocks'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'subdomain_granularity',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Subdomain'),
|
||||
default: 'day',
|
||||
choices: [
|
||||
['min', t('min')],
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
],
|
||||
description: t(
|
||||
'The time unit for each block. Should be a smaller unit than ' +
|
||||
'domain_granularity. Should be larger or equal to Time Grain',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
['metrics'],
|
||||
],
|
||||
chartOptions: [['linear_color_scheme'], ['y_axis_format']],
|
||||
},
|
||||
chartOptionsTabOverride: 'customize',
|
||||
additionalControlOverrides: {
|
||||
y_axis_format: {
|
||||
label: t('Number Format'),
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
metrics: getStandardizedControls().popAllMetrics(),
|
||||
}),
|
||||
transform: (chartProps, { x_axis_time_format }) => {
|
||||
const { formData, datasource } = chartProps;
|
||||
const {
|
||||
domainGranularity,
|
||||
subdomainGranularity,
|
||||
linearColorScheme,
|
||||
yAxisFormat,
|
||||
} = formData as Record<string, string>;
|
||||
|
||||
const verboseMap =
|
||||
(datasource as { verboseMap?: Record<string, string> })?.verboseMap ?? {};
|
||||
const timeFormatter = (ts: number | string) =>
|
||||
getFormattedUTCTime(ts, x_axis_time_format as string);
|
||||
const valueFormatter = getNumberFormatter(yAxisFormat);
|
||||
|
||||
return {
|
||||
timeFormatter,
|
||||
valueFormatter: valueFormatter as (val: unknown) => string,
|
||||
verboseMap,
|
||||
domainGranularity: domainGranularity ?? 'month',
|
||||
subdomainGranularity: subdomainGranularity ?? 'day',
|
||||
linearColorScheme: linearColorScheme ?? '',
|
||||
};
|
||||
},
|
||||
render: ({
|
||||
height,
|
||||
data,
|
||||
cell_size: cellSize,
|
||||
cell_padding: cellPadding,
|
||||
cell_radius: cellRadius,
|
||||
steps,
|
||||
show_legend: showLegend,
|
||||
show_values: showValues,
|
||||
show_metric_name: showMetricName,
|
||||
timeFormatter,
|
||||
valueFormatter,
|
||||
verboseMap,
|
||||
domainGranularity,
|
||||
subdomainGranularity,
|
||||
linearColorScheme,
|
||||
}) => (
|
||||
<ReactCalendar
|
||||
height={height}
|
||||
data={data}
|
||||
cellSize={cellSize}
|
||||
cellPadding={cellPadding}
|
||||
cellRadius={cellRadius}
|
||||
steps={steps}
|
||||
showLegend={showLegend}
|
||||
showValues={showValues}
|
||||
showMetricName={showMetricName}
|
||||
timeFormatter={timeFormatter}
|
||||
valueFormatter={valueFormatter}
|
||||
verboseMap={verboseMap}
|
||||
domainGranularity={domainGranularity}
|
||||
subdomainGranularity={subdomainGranularity}
|
||||
linearColorScheme={linearColorScheme}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ChartProps, getNumberFormatter } from '@superset-ui/core';
|
||||
import { getFormattedUTCTime } from './utils';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { height, formData, queriesData, datasource } = chartProps;
|
||||
const {
|
||||
cellPadding,
|
||||
cellRadius,
|
||||
cellSize,
|
||||
domainGranularity,
|
||||
linearColorScheme,
|
||||
showLegend,
|
||||
showMetricName,
|
||||
showValues,
|
||||
steps,
|
||||
subdomainGranularity,
|
||||
xAxisTimeFormat,
|
||||
yAxisFormat,
|
||||
} = formData;
|
||||
|
||||
const { verboseMap } = datasource;
|
||||
const timeFormatter = (ts: number | string) =>
|
||||
getFormattedUTCTime(ts, xAxisTimeFormat);
|
||||
const valueFormatter = getNumberFormatter(yAxisFormat);
|
||||
|
||||
return {
|
||||
height,
|
||||
data: queriesData[0].data,
|
||||
cellPadding,
|
||||
cellRadius,
|
||||
cellSize,
|
||||
domainGranularity,
|
||||
linearColorScheme,
|
||||
showLegend,
|
||||
showMetricName,
|
||||
showValues,
|
||||
steps,
|
||||
subdomainGranularity,
|
||||
timeFormatter,
|
||||
valueFormatter,
|
||||
verboseMap,
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"references": [
|
||||
{ "path": "../../packages/superset-core" },
|
||||
{ "path": "../../packages/superset-ui-core" },
|
||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||
{ "path": "../../packages/superset-ui-chart-controls" },
|
||||
{ "path": "../../packages/superset-ui-glyph-core" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*"
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/glyph-core": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ensureIsArray, validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['groupby'],
|
||||
['columns'],
|
||||
['metric'],
|
||||
['adhoc_filters'],
|
||||
['row_limit'],
|
||||
['sort_by_metric'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [['y_axis_format', null], ['color_scheme']],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
y_axis_format: {
|
||||
label: t('Number format'),
|
||||
description: t('Choose a number format'),
|
||||
},
|
||||
groupby: {
|
||||
label: t('Source'),
|
||||
multi: false,
|
||||
validators: [validateNonEmpty],
|
||||
description: t('Choose a source'),
|
||||
},
|
||||
columns: {
|
||||
label: t('Target'),
|
||||
multi: false,
|
||||
validators: [validateNonEmpty],
|
||||
description: t('Choose a target'),
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => {
|
||||
const groupby = getStandardizedControls()
|
||||
.popAllColumns()
|
||||
.filter(col => !ensureIsArray(formData.columns).includes(col));
|
||||
return {
|
||||
...formData,
|
||||
groupby,
|
||||
metric: getStandardizedControls().shiftMetric(),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import example from './images/chord.jpg';
|
||||
import exampleDark from './images/chord-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Flow'),
|
||||
credits: ['https://github.com/d3/d3-chord'],
|
||||
description: t(
|
||||
'Showcases the flow or link between categories using thickness of chords. The value and corresponding thickness can be different for each side.',
|
||||
),
|
||||
exampleGallery: [
|
||||
{
|
||||
url: example,
|
||||
urlDark: exampleDark,
|
||||
caption: t('Relationships between community channels'),
|
||||
},
|
||||
],
|
||||
name: t('Chord Diagram'),
|
||||
tags: [t('Circular'), t('Legacy'), t('Proportional'), t('Relational')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class ChordChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./ReactChord'),
|
||||
metadata,
|
||||
transformProps,
|
||||
controlPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 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 { t } from '@apache-superset/core/translation';
|
||||
import { ensureIsArray, validateNonEmpty } from '@superset-ui/core';
|
||||
import { getStandardizedControls } from '@superset-ui/chart-controls';
|
||||
import { defineChart } from '@superset-ui/glyph-core';
|
||||
import example from './images/chord.jpg';
|
||||
import exampleDark from './images/chord-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const ReactChord = require('./ReactChord').default;
|
||||
|
||||
type ChordExtra = {
|
||||
colorScheme: string;
|
||||
numberFormat: string;
|
||||
sliceId: number;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default defineChart<any, ChordExtra>({
|
||||
metadata: {
|
||||
name: t('Chord Diagram'),
|
||||
description: t(
|
||||
'Showcases the flow or link between categories using thickness of chords. The value and corresponding thickness can be different for each side.',
|
||||
),
|
||||
category: t('Flow'),
|
||||
credits: ['https://github.com/d3/d3-chord'],
|
||||
tags: [t('Circular'), t('Legacy'), t('Proportional'), t('Relational')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [
|
||||
{
|
||||
url: example,
|
||||
urlDark: exampleDark,
|
||||
caption: t('Relationships between community channels'),
|
||||
},
|
||||
],
|
||||
useLegacyApi: true,
|
||||
},
|
||||
arguments: {},
|
||||
additionalControls: {
|
||||
queryBefore: [['groupby'], ['columns'], ['metric']],
|
||||
query: [['row_limit'], ['sort_by_metric']],
|
||||
chartOptions: [['y_axis_format', null], ['color_scheme']],
|
||||
},
|
||||
additionalControlOverrides: {
|
||||
y_axis_format: {
|
||||
label: t('Number format'),
|
||||
description: t('Choose a number format'),
|
||||
},
|
||||
groupby: {
|
||||
label: t('Source'),
|
||||
multi: false,
|
||||
validators: [validateNonEmpty],
|
||||
description: t('Choose a source'),
|
||||
},
|
||||
columns: {
|
||||
label: t('Target'),
|
||||
multi: false,
|
||||
validators: [validateNonEmpty],
|
||||
description: t('Choose a target'),
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => {
|
||||
const groupby = getStandardizedControls()
|
||||
.popAllColumns()
|
||||
.filter(
|
||||
(col: string) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
!ensureIsArray((formData as any).columns).includes(col),
|
||||
);
|
||||
return {
|
||||
...formData,
|
||||
groupby,
|
||||
metric: getStandardizedControls().shiftMetric(),
|
||||
};
|
||||
},
|
||||
transform: chartProps => {
|
||||
const { formData } = chartProps;
|
||||
const { yAxisFormat, colorScheme, sliceId } = formData as Record<
|
||||
string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
any
|
||||
>;
|
||||
return {
|
||||
colorScheme: colorScheme ?? '',
|
||||
numberFormat: yAxisFormat ?? '',
|
||||
sliceId: sliceId ?? 0,
|
||||
};
|
||||
},
|
||||
render: ({ width, height, data, colorScheme, numberFormat, sliceId }) => (
|
||||
<ReactChord
|
||||
width={width}
|
||||
height={height}
|
||||
data={data}
|
||||
colorScheme={colorScheme}
|
||||
numberFormat={numberFormat}
|
||||
sliceId={sliceId}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartProps } from '@superset-ui/core';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { width, height, formData, queriesData } = chartProps;
|
||||
const { yAxisFormat, colorScheme, sliceId } = formData;
|
||||
|
||||
return {
|
||||
colorScheme,
|
||||
data: queriesData[0].data,
|
||||
height,
|
||||
numberFormat: yAxisFormat,
|
||||
width,
|
||||
sliceId,
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"references": [
|
||||
{ "path": "../../packages/superset-core" },
|
||||
{ "path": "../../packages/superset-ui-core" },
|
||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||
{ "path": "../../packages/superset-ui-chart-controls" },
|
||||
{ "path": "../../packages/superset-ui-glyph-core" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
D3_FORMAT_OPTIONS,
|
||||
D3_FORMAT_DOCS,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { countryOptions } from './countries';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'select_country',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Country'),
|
||||
default: null,
|
||||
choices: countryOptions,
|
||||
description: t('Which country to plot the map for?'),
|
||||
validators: [validateNonEmpty],
|
||||
},
|
||||
},
|
||||
],
|
||||
['entity'],
|
||||
['metric'],
|
||||
['adhoc_filters'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
tabOverride: 'customize',
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'number_format',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Number format'),
|
||||
renderTrigger: true,
|
||||
default: 'SMART_NUMBER',
|
||||
choices: D3_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
],
|
||||
['currency_format'],
|
||||
['linear_color_scheme'],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
entity: {
|
||||
label: t('ISO 3166-2 Codes'),
|
||||
description: t(
|
||||
'Column containing ISO 3166-2 codes of region/province/department in your table.',
|
||||
),
|
||||
},
|
||||
metric: {
|
||||
label: t('Metric'),
|
||||
description: t('Metric to display bottom title'),
|
||||
},
|
||||
linear_color_scheme: {
|
||||
renderTrigger: false,
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
entity: getStandardizedControls().shiftColumn(),
|
||||
metric: getStandardizedControls().shiftMetric(),
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import exampleUsa from './images/exampleUsa.jpg';
|
||||
import exampleUsaDark from './images/exampleUsa-dark.jpg';
|
||||
import exampleGermany from './images/exampleGermany.jpg';
|
||||
import exampleGermanyDark from './images/exampleGermany-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Map'),
|
||||
credits: ['https://bl.ocks.org/john-guerra'],
|
||||
description: t(
|
||||
"Visualizes how a single metric varies across a country's principal subdivisions (states, provinces, etc) on a choropleth map. Each subdivision's value is elevated when you hover over the corresponding geographic boundary.",
|
||||
),
|
||||
exampleGallery: [
|
||||
{ url: exampleUsa, urlDark: exampleUsaDark },
|
||||
{ url: exampleGermany, urlDark: exampleGermanyDark },
|
||||
],
|
||||
name: t('Country Map'),
|
||||
tags: [
|
||||
t('2D'),
|
||||
t('Comparison'),
|
||||
t('Geo'),
|
||||
t('Range'),
|
||||
t('Report'),
|
||||
t('Stacked'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class CountryMapChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./ReactCountryMap'),
|
||||
metadata,
|
||||
transformProps,
|
||||
controlPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { default as countries } from './countries';
|
||||
@@ -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 { t } from '@apache-superset/core/translation';
|
||||
import { validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
D3_FORMAT_OPTIONS,
|
||||
D3_FORMAT_DOCS,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { defineChart } from '@superset-ui/glyph-core';
|
||||
import exampleUsa from './images/exampleUsa.jpg';
|
||||
import exampleUsaDark from './images/exampleUsa-dark.jpg';
|
||||
import exampleGermany from './images/exampleGermany.jpg';
|
||||
import exampleGermanyDark from './images/exampleGermany-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import { countryOptions } from './countries';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const ReactCountryMap = require('./ReactCountryMap').default;
|
||||
|
||||
export { default as countries } from './countries';
|
||||
|
||||
type CountryMapExtra = {
|
||||
country: string | null;
|
||||
linearColorScheme: string;
|
||||
numberFormat: string;
|
||||
colorScheme: string;
|
||||
sliceId: number;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default defineChart<any, CountryMapExtra>({
|
||||
metadata: {
|
||||
name: t('Country Map'),
|
||||
description: t(
|
||||
"Visualizes how a single metric varies across a country's principal subdivisions (states, provinces, etc) on a choropleth map. Each subdivision's value is elevated when you hover over the corresponding geographic boundary.",
|
||||
),
|
||||
category: t('Map'),
|
||||
credits: ['https://bl.ocks.org/john-guerra'],
|
||||
tags: [
|
||||
t('2D'),
|
||||
t('Comparison'),
|
||||
t('Geo'),
|
||||
t('Range'),
|
||||
t('Report'),
|
||||
t('Stacked'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [
|
||||
{ url: exampleUsa, urlDark: exampleUsaDark },
|
||||
{ url: exampleGermany, urlDark: exampleGermanyDark },
|
||||
],
|
||||
useLegacyApi: true,
|
||||
},
|
||||
arguments: {},
|
||||
additionalControls: {
|
||||
queryBefore: [
|
||||
[
|
||||
{
|
||||
name: 'select_country',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Country'),
|
||||
default: null,
|
||||
choices: countryOptions,
|
||||
description: t('Which country to plot the map for?'),
|
||||
validators: [validateNonEmpty],
|
||||
},
|
||||
},
|
||||
],
|
||||
['entity'],
|
||||
['metric'],
|
||||
],
|
||||
chartOptions: [
|
||||
[
|
||||
{
|
||||
name: 'number_format',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Number format'),
|
||||
renderTrigger: true,
|
||||
default: 'SMART_NUMBER',
|
||||
choices: D3_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
],
|
||||
['linear_color_scheme'],
|
||||
],
|
||||
},
|
||||
chartOptionsTabOverride: 'customize',
|
||||
additionalControlOverrides: {
|
||||
entity: {
|
||||
label: t('ISO 3166-2 Codes'),
|
||||
description: t(
|
||||
'Column containing ISO 3166-2 codes of region/province/department in your table.',
|
||||
),
|
||||
},
|
||||
metric: {
|
||||
label: t('Metric'),
|
||||
description: t('Metric to display bottom title'),
|
||||
},
|
||||
linear_color_scheme: {
|
||||
renderTrigger: false,
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
entity: getStandardizedControls().shiftColumn(),
|
||||
metric: getStandardizedControls().shiftMetric(),
|
||||
}),
|
||||
transform: chartProps => {
|
||||
const { formData } = chartProps;
|
||||
const {
|
||||
linearColorScheme,
|
||||
numberFormat,
|
||||
selectCountry,
|
||||
colorScheme,
|
||||
sliceId,
|
||||
} = formData as Record<string, unknown>;
|
||||
return {
|
||||
country: selectCountry ? String(selectCountry).toLowerCase() : null,
|
||||
linearColorScheme: (linearColorScheme as string) ?? '',
|
||||
numberFormat: (numberFormat as string) ?? '',
|
||||
colorScheme: (colorScheme as string) ?? '',
|
||||
sliceId: (sliceId as number) ?? 0,
|
||||
};
|
||||
},
|
||||
render: ({
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
country,
|
||||
linearColorScheme,
|
||||
numberFormat,
|
||||
colorScheme,
|
||||
sliceId,
|
||||
}) => (
|
||||
<ReactCountryMap
|
||||
width={width}
|
||||
height={height}
|
||||
data={data}
|
||||
country={country}
|
||||
linearColorScheme={linearColorScheme}
|
||||
numberFormat={numberFormat}
|
||||
colorScheme={colorScheme}
|
||||
sliceId={sliceId}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartProps, getValueFormatter } from '@superset-ui/core';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { width, height, formData, queriesData, datasource } = chartProps;
|
||||
const {
|
||||
linearColorScheme,
|
||||
numberFormat,
|
||||
currencyFormat,
|
||||
selectCountry,
|
||||
colorScheme,
|
||||
sliceId,
|
||||
metric,
|
||||
} = formData;
|
||||
|
||||
const {
|
||||
currencyFormats = {},
|
||||
columnFormats = {},
|
||||
currencyCodeColumn,
|
||||
} = datasource;
|
||||
const { data, detected_currency: detectedCurrency } = queriesData[0];
|
||||
|
||||
const formatter = getValueFormatter(
|
||||
metric,
|
||||
currencyFormats,
|
||||
columnFormats,
|
||||
numberFormat,
|
||||
currencyFormat,
|
||||
undefined, // key - not needed for single-metric charts
|
||||
data,
|
||||
currencyCodeColumn,
|
||||
detectedCurrency,
|
||||
);
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
data: queriesData[0].data,
|
||||
country: selectCountry ? String(selectCountry).toLowerCase() : null,
|
||||
linearColorScheme,
|
||||
numberFormat, // left for backward compatibility
|
||||
colorScheme,
|
||||
sliceId,
|
||||
formatter,
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"references": [
|
||||
{ "path": "../../packages/superset-core" },
|
||||
{ "path": "../../packages/superset-ui-core" },
|
||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||
{ "path": "../../packages/superset-ui-chart-controls" },
|
||||
{ "path": "../../packages/superset-ui-glyph-core" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
formatSelectOptions,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Time'),
|
||||
expanded: true,
|
||||
description: t('Time related form attributes'),
|
||||
controlSetRows: [['granularity_sqla'], ['time_range']],
|
||||
},
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['metrics'],
|
||||
['adhoc_filters'],
|
||||
['groupby'],
|
||||
['limit', 'timeseries_limit_metric'],
|
||||
['order_desc'],
|
||||
[
|
||||
{
|
||||
name: 'contribution',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Contribution'),
|
||||
default: false,
|
||||
description: t('Compute the contribution to the total'),
|
||||
},
|
||||
},
|
||||
],
|
||||
['row_limit', null],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'series_height',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
renderTrigger: true,
|
||||
freeForm: true,
|
||||
label: t('Series Height'),
|
||||
default: '25',
|
||||
choices: formatSelectOptions([
|
||||
'10',
|
||||
'25',
|
||||
'40',
|
||||
'50',
|
||||
'75',
|
||||
'100',
|
||||
'150',
|
||||
'200',
|
||||
]),
|
||||
description: t('Pixel height of each series'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'horizon_color_scale',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
renderTrigger: true,
|
||||
label: t('Value Domain'),
|
||||
choices: [
|
||||
['series', t('series')],
|
||||
['overall', t('overall')],
|
||||
['change', t('change')],
|
||||
],
|
||||
default: 'series',
|
||||
description: t(
|
||||
'series: Treat each series independently; overall: All series use the same scale; change: Show changes compared to the first data point in each series',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,51 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import example from './images/Horizon_Chart.jpg';
|
||||
import exampleDark from './images/Horizon_Chart-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Distribution'),
|
||||
credits: ['http://kmandov.github.io/d3-horizon-chart/'],
|
||||
description: t(
|
||||
'Compares how a metric changes over time between different groups. Each group is mapped to a row and change over time is visualized bar lengths and color.',
|
||||
),
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
name: t('Horizon Chart'),
|
||||
tags: [t('Legacy')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class HorizonChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./HorizonChart'),
|
||||
metadata,
|
||||
transformProps,
|
||||
controlPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 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 { t } from '@apache-superset/core/translation';
|
||||
import { formatSelectOptions } from '@superset-ui/chart-controls';
|
||||
import { defineChart } from '@superset-ui/glyph-core';
|
||||
import example from './images/Horizon_Chart.jpg';
|
||||
import exampleDark from './images/Horizon_Chart-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const HorizonChart = require('./HorizonChart').default;
|
||||
|
||||
type HorizonExtra = {
|
||||
colorScale: string;
|
||||
seriesHeight: number;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default defineChart<any, HorizonExtra>({
|
||||
metadata: {
|
||||
name: t('Horizon Chart'),
|
||||
description: t(
|
||||
'Compares how a metric changes over time between different groups. Each group is mapped to a row and change over time is visualized bar lengths and color.',
|
||||
),
|
||||
category: t('Distribution'),
|
||||
credits: ['http://kmandov.github.io/d3-horizon-chart/'],
|
||||
tags: [t('Legacy')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
useLegacyApi: true,
|
||||
},
|
||||
arguments: {},
|
||||
prependSections: [
|
||||
{
|
||||
label: t('Time'),
|
||||
expanded: true,
|
||||
description: t('Time related form attributes'),
|
||||
controlSetRows: [['granularity_sqla'], ['time_range']],
|
||||
},
|
||||
],
|
||||
additionalControls: {
|
||||
queryBefore: [['metrics']],
|
||||
query: [
|
||||
['groupby'],
|
||||
['limit', 'timeseries_limit_metric'],
|
||||
['order_desc'],
|
||||
[
|
||||
{
|
||||
name: 'contribution',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Contribution'),
|
||||
default: false,
|
||||
description: t('Compute the contribution to the total'),
|
||||
},
|
||||
},
|
||||
],
|
||||
['row_limit', null],
|
||||
],
|
||||
chartOptions: [
|
||||
[
|
||||
{
|
||||
name: 'series_height',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
renderTrigger: true,
|
||||
freeForm: true,
|
||||
label: t('Series Height'),
|
||||
default: '25',
|
||||
choices: formatSelectOptions([
|
||||
'10',
|
||||
'25',
|
||||
'40',
|
||||
'50',
|
||||
'75',
|
||||
'100',
|
||||
'150',
|
||||
'200',
|
||||
]),
|
||||
description: t('Pixel height of each series'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'horizon_color_scale',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
renderTrigger: true,
|
||||
label: t('Value Domain'),
|
||||
choices: [
|
||||
['series', t('series')],
|
||||
['overall', t('overall')],
|
||||
['change', t('change')],
|
||||
],
|
||||
default: 'series',
|
||||
description: t(
|
||||
'series: Treat each series independently; overall: All series use the same scale; change: Show changes compared to the first data point in each series',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
transform: chartProps => {
|
||||
const { formData } = chartProps;
|
||||
const { horizonColorScale, seriesHeight } = formData as Record<
|
||||
string,
|
||||
string
|
||||
>;
|
||||
return {
|
||||
colorScale: horizonColorScale ?? 'series',
|
||||
seriesHeight: parseInt(seriesHeight ?? '25', 10),
|
||||
};
|
||||
},
|
||||
render: ({ width, height, data, colorScale, seriesHeight }) => (
|
||||
<HorizonChart
|
||||
width={width}
|
||||
height={height}
|
||||
data={data}
|
||||
colorScale={colorScale}
|
||||
seriesHeight={seriesHeight}
|
||||
/>
|
||||
),
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user