Compare commits
219 Commits
extensions
...
fix-timezo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b8f2f3f94 | ||
|
|
a3d876c64f | ||
|
|
c0763fe58f | ||
|
|
2f2ce330b2 | ||
|
|
f4708a5648 | ||
|
|
b9ab03994a | ||
|
|
df253f6aa4 | ||
|
|
5cea4fb7fe | ||
|
|
76a27d5360 | ||
|
|
174e3c26d3 | ||
|
|
9ea5ded988 | ||
|
|
9086ae8e6c | ||
|
|
fc5506e466 | ||
|
|
e9ae212c1c | ||
|
|
46bca32677 | ||
|
|
a04571fa20 | ||
|
|
fc26dbfebf | ||
|
|
0415118544 | ||
|
|
935bbe6061 | ||
|
|
ec6eaf4898 | ||
|
|
87d15d32c4 | ||
|
|
7d9a8a0c5a | ||
|
|
ddba88ffad | ||
|
|
1e50422a66 | ||
|
|
246dbd7f5c | ||
|
|
9b861b2848 | ||
|
|
1c35c3f6d0 | ||
|
|
b71654877f | ||
|
|
cd447ca1fd | ||
|
|
01ac966b83 | ||
|
|
97e5f0631d | ||
|
|
b7acb7984f | ||
|
|
d3919cf24f | ||
|
|
27889651b3 | ||
|
|
361fe6fe89 | ||
|
|
8506d70242 | ||
|
|
00a53eec2d | ||
|
|
5040db859c | ||
|
|
ef4f7afa90 | ||
|
|
47db185e3b | ||
|
|
2e463078a2 | ||
|
|
4f42928b34 | ||
|
|
75fa474fce | ||
|
|
fd8c21591a | ||
|
|
4147d877fc | ||
|
|
a9dca529c1 | ||
|
|
20f1918dd6 | ||
|
|
c09a4f6f47 | ||
|
|
4e4fa53c8d | ||
|
|
07ff82f189 | ||
|
|
b7b9bfd3fe | ||
|
|
b968d1095c | ||
|
|
e10237fcc1 | ||
|
|
92438322c0 | ||
|
|
f96e90b979 | ||
|
|
b464979db1 | ||
|
|
45f883c9cd | ||
|
|
8fd3401077 | ||
|
|
89a98ab9a4 | ||
|
|
2dfc770b0f | ||
|
|
6b7b23ed78 | ||
|
|
5ac5480f35 | ||
|
|
76889c1a69 | ||
|
|
569606635b | ||
|
|
66264856a7 | ||
|
|
3eb860a663 | ||
|
|
a44980da65 | ||
|
|
7112bce961 | ||
|
|
568486a304 | ||
|
|
fea135b46c | ||
|
|
601fcb3382 | ||
|
|
0d7cc88b2b | ||
|
|
32ee160c75 | ||
|
|
5914e83436 | ||
|
|
0b5e4dd5de | ||
|
|
3a565a6c16 | ||
|
|
f60c82e4a6 | ||
|
|
91131d5996 | ||
|
|
4b0d497513 | ||
|
|
86f690d17f | ||
|
|
e9b494163b | ||
|
|
be404f9b84 | ||
|
|
11257c0536 | ||
|
|
f2b6c395cd | ||
|
|
2d35ed2391 | ||
|
|
bd65469091 | ||
|
|
a6a66ca483 | ||
|
|
4a7cdccdad | ||
|
|
61bd8f0cf2 | ||
|
|
ae10e105c2 | ||
|
|
901dca58f7 | ||
|
|
d95a3d8426 | ||
|
|
70b95ca1b9 | ||
|
|
004f02746f | ||
|
|
5d20dc57d7 | ||
|
|
05c2354997 | ||
|
|
6043e7e7e3 | ||
|
|
1ee14c5993 | ||
|
|
9764a84402 | ||
|
|
570cc3e5f8 | ||
|
|
66519c3a85 | ||
|
|
1f43138888 | ||
|
|
652d029a2d | ||
|
|
e67b1f5326 | ||
|
|
fa79a467e4 | ||
|
|
2cce0308d4 | ||
|
|
c7fd1a2f65 | ||
|
|
ab4f646ef6 | ||
|
|
d6029f5c8a | ||
|
|
c16e8f747c | ||
|
|
9c0337d092 | ||
|
|
3ef33dcb76 | ||
|
|
5a99588f57 | ||
|
|
0b34363654 | ||
|
|
55ec1152ec | ||
|
|
810d6ff480 | ||
|
|
1501af06fe | ||
|
|
6cb3cea960 | ||
|
|
675a4c7a66 | ||
|
|
7110fc9cde | ||
|
|
73e095db8e | ||
|
|
5fedb65bc0 | ||
|
|
b3526fc4ca | ||
|
|
042229bf80 | ||
|
|
06e4f4ff4c | ||
|
|
bb5be6cf54 | ||
|
|
f6f9e083ac | ||
|
|
ad0186093f | ||
|
|
073c3c72b4 | ||
|
|
d4b89de001 | ||
|
|
912538d176 | ||
|
|
cfeb7ccd31 | ||
|
|
abf90de0ca | ||
|
|
ec2509a8b4 | ||
|
|
43653d1fa1 | ||
|
|
da56bddada | ||
|
|
3347b9bf6c | ||
|
|
6663709a23 | ||
|
|
e6d0f97aab | ||
|
|
3bcd3b1683 | ||
|
|
b223f10ab5 | ||
|
|
f787aec567 | ||
|
|
2ec3aaaeea | ||
|
|
20da4eb86e | ||
|
|
27a4575f3e | ||
|
|
5fa6925522 | ||
|
|
a7e7cc30a9 | ||
|
|
e4d71c2a55 | ||
|
|
10a8d8b8ee | ||
|
|
1681f74b2e | ||
|
|
58ab4e78ff | ||
|
|
8f6dd4aba0 | ||
|
|
dba75bd897 | ||
|
|
e28d2782f1 | ||
|
|
97aea5d128 | ||
|
|
bd419d19af | ||
|
|
56ad429200 | ||
|
|
7fc9974a7c | ||
|
|
73d4332b51 | ||
|
|
10a9b4bb94 | ||
|
|
290bcc1dbb | ||
|
|
26ac832138 | ||
|
|
4db6f9e04c | ||
|
|
0fd528c7af | ||
|
|
647f21c26a | ||
|
|
89b998d6b7 | ||
|
|
695e295333 | ||
|
|
f2fc5dec11 | ||
|
|
95a465ad7c | ||
|
|
8aebfe1105 | ||
|
|
c7cec19827 | ||
|
|
ce84ab4ce2 | ||
|
|
470c593c3d | ||
|
|
04a9be04ab | ||
|
|
09b5af5945 | ||
|
|
19d5fa86fc | ||
|
|
b09e60c1ec | ||
|
|
2f81720603 | ||
|
|
0ecc69d2f1 | ||
|
|
319a131ec9 | ||
|
|
3f37cdbf9c | ||
|
|
3a811d680d | ||
|
|
a60f8d761d | ||
|
|
3580dc6cad | ||
|
|
b99fc582e4 | ||
|
|
e4f649e49c | ||
|
|
d54e227e25 | ||
|
|
39ebf7a7ad | ||
|
|
34418d7e0b | ||
|
|
d6328fcb42 | ||
|
|
e8363cf606 | ||
|
|
5747fb1e85 | ||
|
|
d823dfd2b9 | ||
|
|
baaa8c5f54 | ||
|
|
429d9b27f6 | ||
|
|
56cf7a810b | ||
|
|
bbab86a0b1 | ||
|
|
f83f952221 | ||
|
|
e14931c368 | ||
|
|
8951362852 | ||
|
|
eeb4065d7d | ||
|
|
6a46700721 | ||
|
|
790b79541b | ||
|
|
7c69ec7f24 | ||
|
|
ef395662aa | ||
|
|
e1ce553b2b | ||
|
|
b81543c18c | ||
|
|
5f67fa45ce | ||
|
|
8e0c584a92 | ||
|
|
01a9541a0e | ||
|
|
5e3acc2041 | ||
|
|
f2b54e882d | ||
|
|
54919c942a | ||
|
|
04c5517206 | ||
|
|
b1ad54220b | ||
|
|
c6821cac6f | ||
|
|
b7a5b24a54 | ||
|
|
760227d630 | ||
|
|
3ca8c998ab |
2
.github/CODEOWNERS
vendored
@@ -20,7 +20,7 @@
|
||||
|
||||
# Notify PMC members of changes to GitHub Actions
|
||||
|
||||
/.github/ @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @craig-rueda @kgabryje @dpgaspar @sadpandajoe
|
||||
/.github/ @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @craig-rueda @kgabryje @dpgaspar @sadpandajoe @hainenber
|
||||
|
||||
# Notify PMC members of changes to required GitHub Actions
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
steps:
|
||||
- name: Check if the PR is a draft
|
||||
id: check-draft
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const isDraft = context.payload.pull_request.draft;
|
||||
|
||||
28
.github/dependabot.yml
vendored
@@ -9,9 +9,12 @@ updates:
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
ignore:
|
||||
# not until React >= 18.0.0
|
||||
# TODO: remove below entries until React >= 18.0.0
|
||||
- dependency-name: "storybook"
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||
- dependency-name: "@storybook*"
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||
- dependency-name: "eslint-plugin-storybook"
|
||||
# remark-gfm v4+ requires react-markdown v9+, which needs React 18
|
||||
- dependency-name: "remark-gfm"
|
||||
- dependency-name: "react-markdown"
|
||||
@@ -19,6 +22,18 @@ updates:
|
||||
# Source: https://jestjs.io/blog#known-issues
|
||||
# GH thread: https://github.com/jsdom/jsdom/issues/3492
|
||||
- dependency-name: "jest-environment-jsdom"
|
||||
# `@swc/plugin-transform-imports` doesn't work with current Webpack-SWC hybrid setup
|
||||
# See https://github.com/apache/superset/pull/37384#issuecomment-3793991389
|
||||
# TODO: remove the plugin once Lodash usage has been migrated to a more readily tree-shakeable alternative
|
||||
- dependency-name: "@swc/plugin-transform-imports"
|
||||
groups:
|
||||
storybook:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "@storybook*"
|
||||
- "storybook"
|
||||
update-types:
|
||||
- "patch"
|
||||
directory: "/superset-frontend/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -351,6 +366,17 @@ updates:
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/packages/superset-ui-demo/"
|
||||
ignore:
|
||||
# TODO: remove below entries until React >= 18.0.0
|
||||
- dependency-name: "@storybook*"
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||
groups:
|
||||
storybook:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "@storybook*"
|
||||
update-types:
|
||||
- "patch"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
|
||||
2
.github/workflows/ephemeral-env-pr-close.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
4
.github/workflows/ephemeral-env.yml
vendored
@@ -189,7 +189,7 @@ jobs:
|
||||
--extra-flags "--build-arg INCLUDE_CHROMIUM=false"
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
@@ -225,7 +225,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
70
.github/workflows/prefer-typescript.yml
vendored
@@ -1,70 +0,0 @@
|
||||
name: Prefer TypeScript
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "[0-9].[0-9]*"
|
||||
paths:
|
||||
- "superset-frontend/src/**"
|
||||
pull_request:
|
||||
types: [synchronize, opened, reopened, ready_for_review]
|
||||
paths:
|
||||
- "superset-frontend/src/**"
|
||||
|
||||
# cancel previous workflow jobs for PRs
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
prefer_typescript:
|
||||
if: github.ref == 'ref/heads/master' && github.event_name == 'pull_request'
|
||||
name: Prefer TypeScript
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Get changed files
|
||||
id: changed
|
||||
uses: ./.github/actions/file-changes-action
|
||||
with:
|
||||
githubToken: ${{ github.token }}
|
||||
|
||||
- name: Determine if a .js or .jsx file was added
|
||||
id: check
|
||||
run: |
|
||||
js_files_added() {
|
||||
jq -r '
|
||||
map(
|
||||
select(
|
||||
endswith(".js") or endswith(".jsx")
|
||||
)
|
||||
) | join("\n")
|
||||
' ${HOME}/files_added.json
|
||||
}
|
||||
echo "js_files_added=$(js_files_added)" >> $GITHUB_OUTPUT
|
||||
|
||||
- if: steps.check.outputs.js_files_added
|
||||
name: Add Comment to PR
|
||||
uses: ./.github/actions/comment-on-pr
|
||||
continue-on-error: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
msg: |
|
||||
### WARNING: Prefer TypeScript
|
||||
|
||||
Looks like your PR contains new `.js` or `.jsx` files:
|
||||
|
||||
```
|
||||
${{steps.check.outputs.js_files_added}}
|
||||
```
|
||||
|
||||
As decided in [SIP-36](https://github.com/apache/superset/issues/9101), all new frontend code should be written in TypeScript. Please convert above files to TypeScript then re-request review.
|
||||
4
.github/workflows/superset-docs-deploy.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
yarn install --check-cache
|
||||
- name: Download database diagnostics (if triggered by integration tests)
|
||||
if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success'
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
uses: dawidd6/action-download-artifact@v14
|
||||
continue-on-error: true
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
path: docs/src/data/
|
||||
- name: Try to download latest diagnostics (for push/dispatch triggers)
|
||||
if: github.event_name != 'workflow_run'
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
uses: dawidd6/action-download-artifact@v14
|
||||
continue-on-error: true
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
|
||||
4
.github/workflows/superset-docs-verify.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
# Do not bump this linkinator-action version without opening
|
||||
# an ASF Infra ticket to allow the new version first!
|
||||
- uses: JustinBeckwith/linkinator-action@af984b9f30f63e796ae2ea5be5e07cb587f1bbd9 # v2.3
|
||||
- uses: JustinBeckwith/linkinator-action@f62ba0c110a76effb2ee6022cc6ce4ab161085e3 # v2.4
|
||||
continue-on-error: true # This will make the job advisory (non-blocking, no red X)
|
||||
with:
|
||||
paths: "**/*.md, **/*.mdx"
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
run: |
|
||||
yarn install --check-cache
|
||||
- name: Download database diagnostics from integration tests
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
uses: dawidd6/action-download-artifact@v14
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
"
|
||||
- name: Upload database diagnostics artifact
|
||||
if: steps.check.outputs.python
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: database-diagnostics
|
||||
path: databases-diagnostics.json
|
||||
|
||||
5
.gitignore
vendored
@@ -61,6 +61,7 @@ tmp
|
||||
rat-results.txt
|
||||
superset/app/
|
||||
superset-websocket/config.json
|
||||
.direnv
|
||||
|
||||
# Node.js, webpack artifacts, storybook
|
||||
*.entry.js
|
||||
@@ -72,10 +73,6 @@ superset/static/assets/*
|
||||
superset/static/uploads/*
|
||||
!superset/static/uploads/.gitkeep
|
||||
superset/static/version_info.json
|
||||
superset-frontend/**/esm/*
|
||||
superset-frontend/**/lib/*
|
||||
superset-frontend/**/storybook-static/*
|
||||
superset-frontend/migration-storybook.log
|
||||
yarn-error.log
|
||||
*.map
|
||||
*.min.js
|
||||
|
||||
@@ -27,6 +27,7 @@ repos:
|
||||
args: [--check-untyped-defs]
|
||||
exclude: ^superset-extensions-cli/
|
||||
additional_dependencies: [
|
||||
types-cachetools,
|
||||
types-simplejson,
|
||||
types-python-dateutil,
|
||||
types-requests,
|
||||
|
||||
@@ -67,25 +67,15 @@ temporary_superset_ui/*
|
||||
# skip license checks for auto-generated test snapshots
|
||||
.*snap
|
||||
|
||||
# docs overrides for third party logos we don't have the rights to
|
||||
google-big-query.svg
|
||||
google-sheets.svg
|
||||
ibm-db2.svg
|
||||
postgresql.svg
|
||||
snowflake.svg
|
||||
ydb.svg
|
||||
loading.svg
|
||||
apache-solr.svg
|
||||
azure.svg
|
||||
superset.svg
|
||||
|
||||
# docs third-party logos, i.e. docs/static/img/logos/*
|
||||
# docs third-party logos (database logos, org logos, etc.)
|
||||
databases/*
|
||||
logos/*
|
||||
|
||||
# docs-related
|
||||
erd.puml
|
||||
erd.svg
|
||||
intro_header.txt
|
||||
TODO.md
|
||||
|
||||
# for LLMs
|
||||
llm-context.md
|
||||
|
||||
24
AGENTS.md
@@ -101,6 +101,30 @@ superset/
|
||||
- **UPDATING.md**: Add breaking changes here
|
||||
- **Docstrings**: Required for new functions/classes
|
||||
|
||||
## Developer Portal: Storybook-to-MDX Documentation
|
||||
|
||||
The Developer Portal auto-generates MDX documentation from Storybook stories. **Stories are the single source of truth.**
|
||||
|
||||
### Core Philosophy
|
||||
- **Fix issues in the STORY, not the generator** - When something doesn't render correctly, update the story file first
|
||||
- **Generator should be lightweight** - It extracts and passes through data; avoid special cases
|
||||
- **Stories define everything** - Props, controls, galleries, examples all come from story metadata
|
||||
|
||||
### Story Requirements for Docs Generation
|
||||
- Use `export default { title: '...' }` (inline), not `const meta = ...; export default meta;`
|
||||
- Name interactive stories `Interactive${ComponentName}` (e.g., `InteractiveButton`)
|
||||
- Define `args` for default prop values
|
||||
- Define `argTypes` at the story level (not meta level) with control types and descriptions
|
||||
- Use `parameters.docs.gallery` for size×style variant grids
|
||||
- Use `parameters.docs.sampleChildren` for components that need children
|
||||
- Use `parameters.docs.liveExample` for custom live code blocks
|
||||
- Use `parameters.docs.staticProps` for complex object props that can't be parsed inline
|
||||
|
||||
### Generator Location
|
||||
- Script: `docs/scripts/generate-superset-components.mjs`
|
||||
- Wrapper: `docs/src/components/StorybookWrapper.jsx`
|
||||
- Output: `docs/developer_portal/components/`
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Security & Features
|
||||
|
||||
101
README.md
@@ -89,7 +89,7 @@ Superset provides:
|
||||
|
||||
**Craft Beautiful, Dynamic Dashboards**
|
||||
|
||||
<kbd><img title="View Dashboards" src="https://superset.apache.org/img/screenshots/slack_dash.jpg"/></kbd><br/>
|
||||
<kbd><img title="View Dashboards" src="https://superset.apache.org/img/screenshots/dashboard.jpg"/></kbd><br/>
|
||||
|
||||
**No-Code Chart Builder**
|
||||
|
||||
@@ -107,44 +107,67 @@ Here are some of the major database solutions that are supported:
|
||||
|
||||
<!-- SUPPORTED_DATABASES_START -->
|
||||
<p align="center">
|
||||
<img src="https://superset.apache.org/img/databases/doris.png" alt="apache-doris" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/apache-drill.png" alt="apache-drill" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/druid.png" alt="apache-druid" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/apache-hive.svg" alt="apache-hive" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/apache-impala.png" alt="apache-impala" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/apache-kylin.png" alt="apache-kylin" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/apache-pinot.svg" alt="apache-pinot" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/amazon-athena.jpg" alt="aws-athena" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/redshift.png" alt="aws-redshift" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/clickhouse.png" alt="clickhouse" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/couchbase.svg" alt="couchbase" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/databend.png" alt="databend" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/databricks.png" alt="databricks" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/denodo.png" alt="denodo" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/dremio.png" alt="dremio" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/exasol.png" alt="exasol" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/firebird.png" alt="firebird" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/firebolt.png" alt="firebolt" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/google-big-query.svg" alt="google-bigquery" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/google-sheets.svg" alt="google-sheets" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/ibm-db2.svg" alt="ibm-db2" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/netezza.png" alt="ibm-netezza" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/mariadb.png" alt="mariadb" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/msql.png" alt="microsoft-sql-server" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/mysql.png" alt="mysql" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/oceanbase.svg" alt="oceanbase" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/oraclelogo.png" alt="oracle" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/postgresql.svg" alt="postgresql" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/presto-og.png" alt="presto" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/sap-hana.png" alt="sap-hana" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/snowflake.svg" alt="snowflake" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/sqlite.png" alt="sqlite" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/starrocks.png" alt="starrocks" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/tdengine.png" alt="tdengine" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/teradata.png" alt="teradata" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/trino.png" alt="trino" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/vertica.png" alt="vertica" border="0" width="120" height="60" class="database-logo" />
|
||||
<img src="https://superset.apache.org/img/databases/ydb.svg" alt="ydb" border="0" width="120" height="60" class="database-logo" />
|
||||
<a href="https://superset.apache.org/docs/databases/supported/amazon-athena" title="Amazon Athena"><img src="docs/static/img/databases/amazon-athena.jpg" alt="Amazon Athena" width="76" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/amazon-dynamodb" title="Amazon DynamoDB"><img src="docs/static/img/databases/aws.png" alt="Amazon DynamoDB" width="40" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/amazon-redshift" title="Amazon Redshift"><img src="docs/static/img/databases/redshift.png" alt="Amazon Redshift" width="100" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/apache-doris" title="Apache Doris"><img src="docs/static/img/databases/doris.png" alt="Apache Doris" width="103" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/apache-drill" title="Apache Drill"><img src="docs/static/img/databases/apache-drill.png" alt="Apache Drill" width="81" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/apache-druid" title="Apache Druid"><img src="docs/static/img/databases/druid.png" alt="Apache Druid" width="117" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/apache-hive" title="Apache Hive"><img src="docs/static/img/databases/apache-hive.svg" alt="Apache Hive" width="44" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/apache-impala" title="Apache Impala"><img src="docs/static/img/databases/apache-impala.png" alt="Apache Impala" width="21" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/apache-kylin" title="Apache Kylin"><img src="docs/static/img/databases/apache-kylin.png" alt="Apache Kylin" width="44" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/apache-pinot" title="Apache Pinot"><img src="docs/static/img/databases/apache-pinot.svg" alt="Apache Pinot" width="76" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/apache-solr" title="Apache Solr"><img src="docs/static/img/databases/apache-solr.png" alt="Apache Solr" width="79" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/apache-spark-sql" title="Apache Spark SQL"><img src="docs/static/img/databases/apache-spark.png" alt="Apache Spark SQL" width="75" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/ascend" title="Ascend"><img src="docs/static/img/databases/ascend.webp" alt="Ascend" width="117" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/aurora-mysql-data-api" title="Aurora MySQL (Data API)"><img src="docs/static/img/databases/mysql.png" alt="Aurora MySQL (Data API)" width="77" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/aurora-postgresql-data-api" title="Aurora PostgreSQL (Data API)"><img src="docs/static/img/databases/postgresql.svg" alt="Aurora PostgreSQL (Data API)" width="76" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/azure-data-explorer" title="Azure Data Explorer"><img src="docs/static/img/databases/kusto.png" alt="Azure Data Explorer" width="40" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/azure-synapse" title="Azure Synapse"><img src="docs/static/img/databases/azure.svg" alt="Azure Synapse" width="40" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/clickhouse" title="ClickHouse"><img src="docs/static/img/databases/clickhouse.png" alt="ClickHouse" width="150" height="37" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/cloudflare-d1" title="Cloudflare D1"><img src="docs/static/img/databases/cloudflare.png" alt="Cloudflare D1" width="40" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/cockroachdb" title="CockroachDB"><img src="docs/static/img/databases/cockroachdb.png" alt="CockroachDB" width="150" height="24" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/couchbase" title="Couchbase"><img src="docs/static/img/databases/couchbase.svg" alt="Couchbase" width="150" height="35" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/cratedb" title="CrateDB"><img src="docs/static/img/databases/cratedb.svg" alt="CrateDB" width="180" height="24" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/databend" title="Databend"><img src="docs/static/img/databases/databend.png" alt="Databend" width="100" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/databricks" title="Databricks"><img src="docs/static/img/databases/databricks.png" alt="Databricks" width="152" height="24" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/denodo" title="Denodo"><img src="docs/static/img/databases/denodo.png" alt="Denodo" width="138" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/dremio" title="Dremio"><img src="docs/static/img/databases/dremio.png" alt="Dremio" width="126" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/duckdb" title="DuckDB"><img src="docs/static/img/databases/duckdb.png" alt="DuckDB" width="52" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/elasticsearch" title="Elasticsearch"><img src="docs/static/img/databases/elasticsearch.png" alt="Elasticsearch" width="40" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/exasol" title="Exasol"><img src="docs/static/img/databases/exasol.png" alt="Exasol" width="72" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/firebird" title="Firebird"><img src="docs/static/img/databases/firebird.png" alt="Firebird" width="100" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/firebolt" title="Firebolt"><img src="docs/static/img/databases/firebolt.png" alt="Firebolt" width="100" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/google-bigquery" title="Google BigQuery"><img src="docs/static/img/databases/google-big-query.svg" alt="Google BigQuery" width="76" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/google-sheets" title="Google Sheets"><img src="docs/static/img/databases/google-sheets.svg" alt="Google Sheets" width="76" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/greenplum" title="Greenplum"><img src="docs/static/img/databases/greenplum.png" alt="Greenplum" width="124" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/hologres" title="Hologres"><img src="docs/static/img/databases/hologres.png" alt="Hologres" width="44" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/ibm-db2" title="IBM Db2"><img src="docs/static/img/databases/ibm-db2.svg" alt="IBM Db2" width="91" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/ibm-netezza-performance-server" title="IBM Netezza Performance Server"><img src="docs/static/img/databases/netezza.png" alt="IBM Netezza Performance Server" width="40" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/mariadb" title="MariaDB"><img src="docs/static/img/databases/mariadb.png" alt="MariaDB" width="150" height="37" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/microsoft-sql-server" title="Microsoft SQL Server"><img src="docs/static/img/databases/msql.png" alt="Microsoft SQL Server" width="50" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/monetdb" title="MonetDB"><img src="docs/static/img/databases/monet-db.png" alt="MonetDB" width="100" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/mongodb" title="MongoDB"><img src="docs/static/img/databases/mongodb.png" alt="MongoDB" width="150" height="38" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/motherduck" title="MotherDuck"><img src="docs/static/img/databases/motherduck.png" alt="MotherDuck" width="40" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/oceanbase" title="OceanBase"><img src="docs/static/img/databases/oceanbase.svg" alt="OceanBase" width="175" height="24" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/oracle" title="Oracle"><img src="docs/static/img/databases/oraclelogo.png" alt="Oracle" width="111" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/presto" title="Presto"><img src="docs/static/img/databases/presto-og.png" alt="Presto" width="127" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/risingwave" title="RisingWave"><img src="docs/static/img/databases/risingwave.svg" alt="RisingWave" width="147" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/sap-hana" title="SAP HANA"><img src="docs/static/img/databases/sap-hana.png" alt="SAP HANA" width="137" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/sap-sybase" title="SAP Sybase"><img src="docs/static/img/databases/sybase.png" alt="SAP Sybase" width="100" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/shillelagh" title="Shillelagh"><img src="docs/static/img/databases/shillelagh.png" alt="Shillelagh" width="40" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/singlestore" title="SingleStore"><img src="docs/static/img/databases/singlestore.png" alt="SingleStore" width="150" height="31" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/snowflake" title="Snowflake"><img src="docs/static/img/databases/snowflake.svg" alt="Snowflake" width="76" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/sqlite" title="SQLite"><img src="docs/static/img/databases/sqlite.png" alt="SQLite" width="84" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/starrocks" title="StarRocks"><img src="docs/static/img/databases/starrocks.png" alt="StarRocks" width="149" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/superset-meta-database" title="Superset meta database"><img src="docs/static/img/databases/superset.svg" alt="Superset meta database" width="150" height="39" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/tdengine" title="TDengine"><img src="docs/static/img/databases/tdengine.png" alt="TDengine" width="140" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/teradata" title="Teradata"><img src="docs/static/img/databases/teradata.png" alt="Teradata" width="124" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/timescaledb" title="TimescaleDB"><img src="docs/static/img/databases/timescale.png" alt="TimescaleDB" width="150" height="36" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/trino" title="Trino"><img src="docs/static/img/databases/trino.png" alt="Trino" width="89" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/vertica" title="Vertica"><img src="docs/static/img/databases/vertica.png" alt="Vertica" width="128" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/ydb" title="YDB"><img src="docs/static/img/databases/ydb.svg" alt="YDB" width="110" height="40" /></a>
|
||||
<a href="https://superset.apache.org/docs/databases/supported/yugabytedb" title="YugabyteDB"><img src="docs/static/img/databases/yugabyte.png" alt="YugabyteDB" width="150" height="26" /></a>
|
||||
</p>
|
||||
<!-- SUPPORTED_DATABASES_END -->
|
||||
|
||||
|
||||
@@ -136,10 +136,6 @@ categories:
|
||||
url: https://www.dropit.shop/
|
||||
contributors: ["@dropit-dev"]
|
||||
|
||||
- name: Fanatics
|
||||
url: https://www.fanatics.com/
|
||||
contributors: ["@coderfender"]
|
||||
|
||||
- name: Fordeal
|
||||
url: https://www.fordeal.com
|
||||
contributors: ["@Renkai"]
|
||||
@@ -291,8 +287,10 @@ categories:
|
||||
url: https://www.gfk.com/home
|
||||
contributors: ["@mherr"]
|
||||
|
||||
# Logo approved by @anmol-hpe on behalf of HPE
|
||||
- name: HPE
|
||||
url: https://www.hpe.com/in/en/home.html
|
||||
logo: hpe.png
|
||||
contributors: ["@anmol-hpe"]
|
||||
|
||||
- name: Hydrolix
|
||||
@@ -432,6 +430,11 @@ categories:
|
||||
url: https://brandct.cn/
|
||||
contributors: ["@wenbinye"]
|
||||
|
||||
- name: XNET
|
||||
url: https://xnetmobile.com/
|
||||
logo: xnet.png
|
||||
contributors: ["@deuspt"]
|
||||
|
||||
- name: Zeta
|
||||
url: https://www.zeta.tech/
|
||||
contributors: ["@shaikidris"]
|
||||
@@ -622,6 +625,20 @@ categories:
|
||||
- name: Stockarea
|
||||
url: https://stockarea.io
|
||||
|
||||
Sports:
|
||||
- name: Club 25 de Agosto (Femenino / Women's Team)
|
||||
url: https://www.instagram.com/25deagosto.basketfemenino/
|
||||
contributors: [ "@lion90" ]
|
||||
logo: club25deagosto.svg
|
||||
|
||||
- name: Fanatics
|
||||
url: https://www.fanatics.com/
|
||||
contributors: [ "@coderfender" ]
|
||||
|
||||
- name: komoot
|
||||
url: https://www.komoot.com/
|
||||
contributors: [ "@christophlingg" ]
|
||||
|
||||
Others:
|
||||
- name: 10Web
|
||||
url: https://10web.io/
|
||||
@@ -657,10 +674,6 @@ categories:
|
||||
url: https://www.increff.com/
|
||||
contributors: ["@ishansinghania"]
|
||||
|
||||
- name: komoot
|
||||
url: https://www.komoot.com/
|
||||
contributors: ["@christophlingg"]
|
||||
|
||||
- name: Let's Roam
|
||||
url: https://www.letsroam.com/
|
||||
|
||||
|
||||
39
UPDATING.md
@@ -24,6 +24,21 @@ assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
### WebSocket config for GAQ with Docker
|
||||
|
||||
[35896](https://github.com/apache/superset/pull/35896) and [37624](https://github.com/apache/superset/pull/37624) updated documentation on how to run and configure Superset with Docker. Specifically for the WebSocket configuration, a new `docker/superset-websocket/config.example.json` was added to the repo, so that users could copy it to create a `docker/superset-websocket/config.json` file. The existing `docker/superset-websocket/config.json` was removed and git-ignored, so if you're using GAQ / WebSocket make sure to:
|
||||
- Stash/backup your existing `config.json` file, to re-apply it after (will get git-ignored going forward)
|
||||
- Update the `volumes` configuration for the `superset-websocket` service in your `docker-compose.override.yml` file, to include the `docker/superset-websocket/config.json` file. For example:
|
||||
``` yaml
|
||||
services:
|
||||
superset-websocket:
|
||||
volumes:
|
||||
- ./superset-websocket:/home/superset-websocket
|
||||
- /home/superset-websocket/node_modules
|
||||
- /home/superset-websocket/dist
|
||||
- ./docker/superset-websocket/config.json:/home/superset-websocket/config.json:ro
|
||||
```
|
||||
|
||||
### Example Data Loading Improvements
|
||||
|
||||
#### New Directory Structure
|
||||
@@ -163,6 +178,28 @@ See `superset/mcp_service/PRODUCTION.md` for deployment guides.
|
||||
- [35062](https://github.com/apache/superset/pull/35062): Changed the function signature of `setupExtensions` to `setupCodeOverrides` with options as arguments.
|
||||
|
||||
### Breaking Changes
|
||||
- [37370](https://github.com/apache/superset/pull/37370): The `APP_NAME` configuration variable no longer controls the browser window/tab title or other frontend branding. Application names should now be configured using the theme system with the `brandAppName` token. The `APP_NAME` config is still used for backend contexts (MCP service, logs, etc.) and serves as a fallback if `brandAppName` is not set.
|
||||
- **Migration:**
|
||||
```python
|
||||
# Before (Superset 5.x)
|
||||
APP_NAME = "My Custom App"
|
||||
|
||||
# After (Superset 6.x) - Option 1: Use theme system (recommended)
|
||||
THEME_DEFAULT = {
|
||||
"token": {
|
||||
"brandAppName": "My Custom App", # Window titles
|
||||
"brandLogoAlt": "My Custom App", # Logo alt text
|
||||
"brandLogoUrl": "/static/assets/images/custom_logo.png"
|
||||
}
|
||||
}
|
||||
|
||||
# After (Superset 6.x) - Option 2: Temporary fallback
|
||||
# Keep APP_NAME for now (will be used as fallback for brandAppName)
|
||||
APP_NAME = "My Custom App"
|
||||
# But you should migrate to THEME_DEFAULT.token.brandAppName
|
||||
```
|
||||
- **Note:** For dark mode, set the same tokens in `THEME_DARK` configuration.
|
||||
|
||||
- [36317](https://github.com/apache/superset/pull/36317): The `CUSTOM_FONT_URLS` configuration option has been removed. Use the new per-theme `fontUrls` token in `THEME_DEFAULT` or database-managed themes instead.
|
||||
- **Before:**
|
||||
```python
|
||||
@@ -177,7 +214,7 @@ See `superset/mcp_service/PRODUCTION.md` for deployment guides.
|
||||
"fontUrls": [
|
||||
"https://fonts.example.com/myfont.css",
|
||||
],
|
||||
# ... other tokens
|
||||
# ... other tokens
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -159,8 +159,8 @@ services:
|
||||
SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}"
|
||||
# configuring the dev-server to use the host.docker.internal to connect to the backend
|
||||
superset: "http://superset-light:8088"
|
||||
# Webpack dev server configuration
|
||||
WEBPACK_DEVSERVER_HOST: "${WEBPACK_DEVSERVER_HOST:-127.0.0.1}"
|
||||
# Webpack dev server must bind to 0.0.0.0 to be accessible from outside the container
|
||||
WEBPACK_DEVSERVER_HOST: "${WEBPACK_DEVSERVER_HOST:-0.0.0.0}"
|
||||
WEBPACK_DEVSERVER_PORT: "${WEBPACK_DEVSERVER_PORT:-9000}"
|
||||
ports:
|
||||
- "${NODE_PORT:-9001}:9000" # Parameterized port, accessible on all interfaces
|
||||
|
||||
@@ -175,7 +175,7 @@ services:
|
||||
SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}"
|
||||
# configuring the dev-server to use the host.docker.internal to connect to the backend
|
||||
superset: "http://superset:8088"
|
||||
# Bind to all interfaces so Docker port mapping works
|
||||
# Webpack dev server must bind to 0.0.0.0 to be accessible from outside the container
|
||||
WEBPACK_DEVSERVER_HOST: "0.0.0.0"
|
||||
ports:
|
||||
- "127.0.0.1:${NODE_PORT:-9000}:9000" # exposing the dynamic webpack dev server
|
||||
|
||||
@@ -28,11 +28,11 @@ if [ "$BUILD_SUPERSET_FRONTEND_IN_DOCKER" = "true" ]; then
|
||||
cd /app/superset-frontend
|
||||
|
||||
if [ "$NPM_RUN_PRUNE" = "true" ]; then
|
||||
echo "Running `npm run prune`"
|
||||
echo "Running \"npm run prune\""
|
||||
npm run prune
|
||||
fi
|
||||
|
||||
echo "Running `npm install`"
|
||||
echo "Running \"npm install\""
|
||||
npm install
|
||||
|
||||
echo "Start webpack dev server"
|
||||
|
||||
@@ -105,7 +105,7 @@ class CeleryConfig:
|
||||
|
||||
CELERY_CONFIG = CeleryConfig
|
||||
|
||||
FEATURE_FLAGS = {"ALERT_REPORTS": True}
|
||||
FEATURE_FLAGS = {"ALERT_REPORTS": True, "DATASET_FOLDERS": True}
|
||||
ALERT_REPORTS_NOTIFICATION_DRY_RUN = True
|
||||
WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501
|
||||
# The base URL for the email report hyperlinks.
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"port": 8080,
|
||||
"logLevel": "info",
|
||||
"logToFile": false,
|
||||
"logFilename": "app.log",
|
||||
"statsd": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 8125,
|
||||
"globalTags": []
|
||||
},
|
||||
"redis": {
|
||||
"port": 6379,
|
||||
"host": "127.0.0.1",
|
||||
"password": "",
|
||||
"db": 0,
|
||||
"ssl": false
|
||||
},
|
||||
"redisStreamPrefix": "async-events-",
|
||||
"jwtAlgorithms": ["HS256"],
|
||||
"jwtSecret": "CHANGE-ME-IN-PRODUCTION-GOTTA-BE-LONG-AND-SECRET",
|
||||
"jwtCookieName": "async-token"
|
||||
}
|
||||
115
docs/.claude/instructions.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Developer Portal Documentation Instructions
|
||||
|
||||
## Core Principle: Stories Are the Single Source of Truth
|
||||
|
||||
When working on the Storybook-to-MDX documentation system:
|
||||
|
||||
**ALWAYS fix the story first. NEVER add workarounds to the generator.**
|
||||
|
||||
## Why This Matters
|
||||
|
||||
The generator (`scripts/generate-superset-components.mjs`) should be lightweight - it extracts data from stories and passes it through. When you add special cases to the generator:
|
||||
- It becomes harder to maintain
|
||||
- Stories diverge from their docs representation
|
||||
- Future stories need to know about generator quirks
|
||||
|
||||
When you fix stories to match the expected patterns:
|
||||
- Stories work identically in Storybook and Docs
|
||||
- The generator stays simple and predictable
|
||||
- Patterns are consistent and learnable
|
||||
|
||||
## Story Patterns for Docs Generation
|
||||
|
||||
### Required Structure
|
||||
```tsx
|
||||
// Use inline export default (NOT const meta = ...; export default meta)
|
||||
export default {
|
||||
title: 'Components/MyComponent',
|
||||
component: MyComponent,
|
||||
};
|
||||
|
||||
// Name interactive stories with Interactive prefix
|
||||
export const InteractiveMyComponent: Story = {
|
||||
args: {
|
||||
// Default prop values
|
||||
},
|
||||
argTypes: {
|
||||
// Control definitions - MUST be at story level, not meta level
|
||||
propName: {
|
||||
control: { type: 'select' },
|
||||
options: ['a', 'b', 'c'],
|
||||
description: 'What this prop does',
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### For Components with Variants (size × style grids)
|
||||
```tsx
|
||||
const sizes = ['small', 'medium', 'large'];
|
||||
const variants = ['primary', 'secondary', 'danger'];
|
||||
|
||||
InteractiveButton.parameters = {
|
||||
docs: {
|
||||
gallery: {
|
||||
component: 'Button',
|
||||
sizes,
|
||||
styles: variants,
|
||||
sizeProp: 'size',
|
||||
styleProp: 'variant',
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### For Components Requiring Children
|
||||
```tsx
|
||||
InteractiveIconTooltip.parameters = {
|
||||
docs: {
|
||||
// Component descriptors with dot notation for nested components
|
||||
sampleChildren: [{ component: 'Icons.InfoCircleOutlined', props: { iconSize: 'l' } }],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### For Custom Live Code Examples
|
||||
```tsx
|
||||
InteractiveMyComponent.parameters = {
|
||||
docs: {
|
||||
liveExample: `function Demo() {
|
||||
return <MyComponent prop="value">Content</MyComponent>;
|
||||
}`,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### For Complex Props (objects, arrays)
|
||||
```tsx
|
||||
InteractiveMenu.parameters = {
|
||||
docs: {
|
||||
staticProps: {
|
||||
items: [
|
||||
{ key: '1', label: 'Item 1' },
|
||||
{ key: '2', label: 'Item 2' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Common Issues and How to Fix Them (in the Story)
|
||||
|
||||
| Issue | Wrong Approach | Right Approach |
|
||||
|-------|---------------|----------------|
|
||||
| Component not generated | Add pattern to generator | Change story to use inline `export default` |
|
||||
| Control shows as text instead of select | Add special case in generator | Add `argTypes` with `control: { type: 'select' }` |
|
||||
| Missing children/content | Modify StorybookWrapper | Add `parameters.docs.sampleChildren` |
|
||||
| Gallery not showing | Add to generator output | Add `parameters.docs.gallery` config |
|
||||
| Wrong live example | Hardcode in generator | Add `parameters.docs.liveExample` |
|
||||
|
||||
## Files
|
||||
|
||||
- **Generator**: `docs/scripts/generate-superset-components.mjs`
|
||||
- **Wrapper**: `docs/src/components/StorybookWrapper.jsx`
|
||||
- **Output**: `docs/developer_portal/components/`
|
||||
- **Stories**: `superset-frontend/packages/superset-ui-core/src/components/*/`
|
||||
11
docs/.gitignore
vendored
@@ -31,5 +31,16 @@ static/badges/
|
||||
# Source of truth is in superset/db_engine_specs/*.py metadata attributes
|
||||
docs/databases/
|
||||
|
||||
# Generated API documentation (regenerated at build time from openapi.json)
|
||||
# Source of truth is static/resources/openapi.json
|
||||
docs/api/
|
||||
|
||||
# Generated component documentation MDX files (regenerated at build time)
|
||||
# Source of truth is Storybook stories in superset-frontend/packages/superset-ui-core/src/components/
|
||||
developer_portal/components/
|
||||
|
||||
# Generated extension component documentation (regenerated at build time)
|
||||
developer_portal/extensions/components/
|
||||
|
||||
# Note: src/data/databases.json is COMMITTED (not ignored) to preserve feature diagnostics
|
||||
# that require Flask context to generate. Update it locally with: npm run gen-db-docs
|
||||
|
||||
@@ -19,5 +19,14 @@
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
presets: [
|
||||
[
|
||||
require.resolve('@docusaurus/core/lib/babel/preset'),
|
||||
{
|
||||
runtime: 'automatic',
|
||||
importSource: '@emotion/react',
|
||||
},
|
||||
],
|
||||
],
|
||||
plugins: ['@emotion/babel-plugin'],
|
||||
};
|
||||
|
||||
@@ -788,7 +788,7 @@ pytest ./link_to_test.py
|
||||
|
||||
### Frontend Testing
|
||||
|
||||
We use [Jest](https://jestjs.io/) and [Enzyme](https://airbnb.io/enzyme/) to test TypeScript/JavaScript. Tests can be run with:
|
||||
We use [Jest](https://jestjs.io/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) to test TypeScript. Tests can be run with:
|
||||
|
||||
```bash
|
||||
cd superset-frontend
|
||||
|
||||
@@ -100,7 +100,7 @@ npm link superset-plugin-chart-hello-world
|
||||
```
|
||||
|
||||
7. **Import and register in Superset**:
|
||||
Edit `superset-frontend/src/visualizations/presets/MainPreset.js` to include your plugin.
|
||||
Edit `superset-frontend/src/visualizations/presets/MainPreset.ts` to include your plugin.
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -258,19 +258,7 @@ For debugging the Flask backend:
|
||||
|
||||
### Storybook
|
||||
|
||||
Storybook is used for developing and testing UI components in isolation:
|
||||
|
||||
```bash
|
||||
cd superset-frontend
|
||||
|
||||
# Start Storybook
|
||||
npm run storybook
|
||||
|
||||
# Build static Storybook
|
||||
npm run build-storybook
|
||||
```
|
||||
|
||||
Access Storybook at http://localhost:6006
|
||||
See the dedicated [Storybook documentation](../testing/storybook) for information on running Storybook locally and adding new stories.
|
||||
|
||||
## Contributing Translations
|
||||
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
---
|
||||
title: Administrator Configuration
|
||||
sidebar_position: 12
|
||||
---
|
||||
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
# Extension Administrator Configuration
|
||||
|
||||
This guide covers how to configure extension security for production deployments. As an administrator, you control which extensions can run and at what trust level.
|
||||
|
||||
## Trust Configuration
|
||||
|
||||
Configure extension trust in `superset_config.py`:
|
||||
|
||||
```python
|
||||
EXTENSIONS_TRUST_CONFIG = {
|
||||
# Extensions that can run with full privileges ('core' trust level)
|
||||
"trusted_extensions": [
|
||||
"official-parquet-export",
|
||||
"enterprise-sso-plugin",
|
||||
],
|
||||
|
||||
# Allow any extension to run as 'core' without signature verification
|
||||
# WARNING: NEVER enable in production - development use only!
|
||||
"allow_unsigned_core": False,
|
||||
|
||||
# Default sandbox for extensions without explicit trust configuration
|
||||
# Options: 'core', 'iframe', 'worker', 'wasm'
|
||||
"default_trust_level": "iframe",
|
||||
|
||||
# Require valid signatures for extensions requesting 'core' trust
|
||||
# Recommended for production deployments
|
||||
"require_core_signatures": True,
|
||||
|
||||
# Public keys for verified publishers (file paths or PEM strings)
|
||||
"trusted_signers": [
|
||||
"/etc/superset/keys/apache-official.pub",
|
||||
"/etc/superset/keys/enterprise-team.pub",
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### `trusted_extensions`
|
||||
|
||||
A list of extension IDs that are allowed to run as `core` trust level without signature verification. Use this for extensions you've reviewed and trust completely.
|
||||
|
||||
```python
|
||||
"trusted_extensions": [
|
||||
"my-company-plugin",
|
||||
"approved-community-extension",
|
||||
],
|
||||
```
|
||||
|
||||
### `allow_unsigned_core`
|
||||
|
||||
When `True`, allows any extension to run as `core` trust level regardless of signatures or trusted list. **Never enable this in production** - it's intended only for development environments.
|
||||
|
||||
```python
|
||||
# Development only!
|
||||
"allow_unsigned_core": True,
|
||||
```
|
||||
|
||||
### `default_trust_level`
|
||||
|
||||
The trust level assigned to extensions that don't specify one in their manifest. The safest option is `iframe`, which provides browser-enforced isolation.
|
||||
|
||||
| Level | Description |
|
||||
|-------|-------------|
|
||||
| `iframe` | Browser-sandboxed iframe with controlled API access (recommended default) |
|
||||
| `worker` | Web Worker sandbox for command-only extensions |
|
||||
| `wasm` | WASM sandbox with no DOM access (most restrictive) |
|
||||
| `core` | Full access to main context (not recommended as default) |
|
||||
|
||||
```python
|
||||
"default_trust_level": "iframe",
|
||||
```
|
||||
|
||||
### `require_core_signatures`
|
||||
|
||||
When `True`, extensions requesting `core` trust level must have a valid signature from a trusted signer. Extensions without valid signatures are downgraded to `default_trust_level`.
|
||||
|
||||
```python
|
||||
"require_core_signatures": True,
|
||||
```
|
||||
|
||||
### `trusted_signers`
|
||||
|
||||
A list of public keys authorized to sign extensions. Keys can be specified as file paths or inline PEM strings.
|
||||
|
||||
```python
|
||||
"trusted_signers": [
|
||||
# File path to public key
|
||||
"/etc/superset/keys/publisher.pub",
|
||||
|
||||
# Inline PEM string
|
||||
"""-----BEGIN PUBLIC KEY-----
|
||||
MCowBQYDK2VwAyEA...
|
||||
-----END PUBLIC KEY-----""",
|
||||
],
|
||||
```
|
||||
|
||||
## Signature Verification
|
||||
|
||||
### How It Works
|
||||
|
||||
1. Extension developers generate a signing keypair using the CLI
|
||||
2. They sign their extension's manifest during the build process
|
||||
3. The signed bundle includes `manifest.sig` alongside `manifest.json`
|
||||
4. When Superset loads the extension, it verifies the signature against `trusted_signers`
|
||||
5. If verification passes, the extension can run at its requested trust level
|
||||
|
||||
### Configuring Trusted Signers
|
||||
|
||||
1. Obtain the publisher's public key file (`.pub` extension)
|
||||
2. Place it in a secure location on your server (e.g., `/etc/superset/keys/`)
|
||||
3. Add the path to `trusted_signers` in your configuration
|
||||
|
||||
```python
|
||||
EXTENSIONS_TRUST_CONFIG = {
|
||||
"trusted_signers": [
|
||||
"/etc/superset/keys/acme-corp.pub",
|
||||
],
|
||||
"require_core_signatures": True,
|
||||
}
|
||||
```
|
||||
|
||||
### Verifying a Key Fingerprint
|
||||
|
||||
Before adding a public key to your trusted signers, verify its fingerprint with the publisher:
|
||||
|
||||
```bash
|
||||
# On the publisher's machine
|
||||
superset-extensions generate-keys --output my-key.pem
|
||||
# Output: Fingerprint: MCowBQYDK2Vw...
|
||||
```
|
||||
|
||||
Compare this fingerprint with what you receive to ensure authenticity.
|
||||
|
||||
## Security Recommendations
|
||||
|
||||
### Production Deployments
|
||||
|
||||
1. **Set `require_core_signatures: True`** - Ensures core extensions are verified
|
||||
2. **Set `allow_unsigned_core: False`** - Never allow unsigned core extensions
|
||||
3. **Use `iframe` as default** - Provides strong browser isolation
|
||||
4. **Limit `trusted_extensions`** - Only add extensions you've thoroughly reviewed
|
||||
5. **Secure key storage** - Store public keys in protected directories
|
||||
|
||||
### Development Environments
|
||||
|
||||
For local development, you may relax some restrictions:
|
||||
|
||||
```python
|
||||
# Development configuration
|
||||
EXTENSIONS_TRUST_CONFIG = {
|
||||
"trusted_extensions": [],
|
||||
"allow_unsigned_core": True, # OK for development
|
||||
"default_trust_level": "core", # Easier debugging
|
||||
"require_core_signatures": False,
|
||||
"trusted_signers": [],
|
||||
}
|
||||
```
|
||||
|
||||
## Extension Installation
|
||||
|
||||
### From Trusted Sources
|
||||
|
||||
1. Download the `.supx` bundle from a trusted source
|
||||
2. Verify any checksums or signatures provided by the publisher
|
||||
3. Place the bundle in your `EXTENSIONS_PATH` directory
|
||||
4. If the extension requires `core` trust, add it to `trusted_extensions` or configure signature verification
|
||||
|
||||
### From Community Registry
|
||||
|
||||
Extensions from the community registry should be treated as semi-trusted at best. Consider:
|
||||
|
||||
1. Using `iframe` sandbox for community extensions
|
||||
2. Reviewing the extension's source code before installation
|
||||
3. Testing in a staging environment first
|
||||
|
||||
## Monitoring Extensions
|
||||
|
||||
### Logging
|
||||
|
||||
Extension trust decisions are logged at the INFO level:
|
||||
|
||||
```
|
||||
INFO: Extension my-extension granted core trust (trusted + valid signature)
|
||||
WARNING: Extension unknown-ext trust downgraded from core to iframe: Extension not in trusted list
|
||||
```
|
||||
|
||||
Review these logs to monitor extension behavior and identify potential issues.
|
||||
|
||||
### Trust Downgrades
|
||||
|
||||
If an extension's trust is downgraded, you'll see a warning in the logs. Common reasons:
|
||||
|
||||
| Reason | Meaning |
|
||||
|--------|---------|
|
||||
| "Extension not in trusted list" | Extension requests core but isn't in `trusted_extensions` |
|
||||
| "Core trust requires a valid signature" | `require_core_signatures` is enabled but signature is missing |
|
||||
| "Signature verification failed" | Signature doesn't match any trusted signer |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Extension Not Loading as Core
|
||||
|
||||
1. Check if the extension ID is in `trusted_extensions`
|
||||
2. If using signatures, verify the public key is in `trusted_signers`
|
||||
3. Check logs for trust downgrade messages
|
||||
4. Verify the extension bundle contains `manifest.sig`
|
||||
|
||||
### Signature Verification Failing
|
||||
|
||||
1. Ensure the public key file is readable by the Superset process
|
||||
2. Verify the key is in PEM format with correct Ed25519 type
|
||||
3. Check that the manifest wasn't modified after signing
|
||||
4. Confirm the signature was created with the matching private key
|
||||
|
||||
### Permission Denied Errors
|
||||
|
||||
Sandboxed extensions may encounter permission errors if:
|
||||
|
||||
1. The extension's declared permissions don't match its API calls
|
||||
2. The sandbox is blocking access correctly (working as intended)
|
||||
3. The extension was downgraded to a more restrictive sandbox
|
||||
|
||||
Check the extension's `sandbox.permissions` configuration against its actual needs.
|
||||
@@ -1,131 +0,0 @@
|
||||
---
|
||||
title: Alert
|
||||
sidebar_label: Alert
|
||||
---
|
||||
|
||||
<!--
|
||||
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 { StoryWithControls } from '../../../src/components/StorybookWrapper';
|
||||
import { Alert } from '@apache-superset/core/ui';
|
||||
|
||||
# Alert
|
||||
|
||||
Alert component for displaying important messages to users. Wraps Ant Design Alert with sensible defaults and improved accessibility.
|
||||
|
||||
## Live Example
|
||||
|
||||
<StoryWithControls
|
||||
component={Alert}
|
||||
props={{
|
||||
closable: true,
|
||||
type: 'info',
|
||||
message: 'This is a sample alert message.',
|
||||
description: 'Sample description for additional context.',
|
||||
showIcon: true
|
||||
}}
|
||||
controls={[
|
||||
{
|
||||
name: 'type',
|
||||
label: 'Type',
|
||||
type: 'select',
|
||||
options: [
|
||||
'info',
|
||||
'error',
|
||||
'warning',
|
||||
'success'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'closable',
|
||||
label: 'Closable',
|
||||
type: 'boolean'
|
||||
},
|
||||
{
|
||||
name: 'showIcon',
|
||||
label: 'Show Icon',
|
||||
type: 'boolean'
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
label: 'Message',
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
type: 'text'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
## Try It
|
||||
|
||||
Edit the code below to experiment with the component:
|
||||
|
||||
```tsx live
|
||||
function Demo() {
|
||||
return (
|
||||
<Alert
|
||||
closable
|
||||
type="info"
|
||||
message="This is a sample alert message."
|
||||
description="Sample description for additional context."
|
||||
showIcon
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `closable` | `boolean` | `true` | Whether the Alert can be closed with a close button. |
|
||||
| `type` | `string` | `"info"` | Type of the alert (e.g., info, error, warning, success). |
|
||||
| `message` | `string` | `"This is a sample alert message."` | Message |
|
||||
| `description` | `string` | `"Sample description for additional context."` | Description |
|
||||
| `showIcon` | `boolean` | `true` | Whether to display an icon in the Alert. |
|
||||
|
||||
## Usage in Extensions
|
||||
|
||||
This component is available in the `@apache-superset/core/ui` package, which is automatically available to Superset extensions.
|
||||
|
||||
```tsx
|
||||
import { Alert } from '@apache-superset/core/ui';
|
||||
|
||||
function MyExtension() {
|
||||
return (
|
||||
<Alert
|
||||
closable
|
||||
type="info"
|
||||
message="This is a sample alert message."
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Source Links
|
||||
|
||||
- [Story file](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/ui/components/Alert/Alert.stories.tsx)
|
||||
- [Component source](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/ui/components/Alert/index.tsx)
|
||||
|
||||
---
|
||||
|
||||
*This page was auto-generated from the component's Storybook story.*
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
title: Extension Components
|
||||
sidebar_label: Overview
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
# Extension Components
|
||||
|
||||
These UI components are available to Superset extension developers through the `@apache-superset/core/ui` package. They provide a consistent look and feel with the rest of Superset and are designed to be used in extension panels, views, and other UI elements.
|
||||
|
||||
## Available Components
|
||||
|
||||
- [Alert](./alert)
|
||||
|
||||
## Usage
|
||||
|
||||
All components are exported from the `@apache-superset/core/ui` package:
|
||||
|
||||
```tsx
|
||||
import { Alert } from '@apache-superset/core/ui';
|
||||
|
||||
export function MyExtensionPanel() {
|
||||
return (
|
||||
<Alert type="info">
|
||||
Welcome to my extension!
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Adding New Components
|
||||
|
||||
Components in `@apache-superset/core/ui` are automatically documented here. To add a new extension component:
|
||||
|
||||
1. Add the component to `superset-frontend/packages/superset-core/src/ui/components/`
|
||||
2. Export it from `superset-frontend/packages/superset-core/src/ui/components/index.ts`
|
||||
3. Create a Storybook story with an `Interactive` export:
|
||||
|
||||
```tsx
|
||||
export default {
|
||||
title: 'Extension Components/MyComponent',
|
||||
component: MyComponent,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Description of the component...',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const InteractiveMyComponent = (args) => <MyComponent {...args} />;
|
||||
|
||||
InteractiveMyComponent.args = {
|
||||
variant: 'primary',
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
InteractiveMyComponent.argTypes = {
|
||||
variant: {
|
||||
control: { type: 'select' },
|
||||
options: ['primary', 'secondary'],
|
||||
},
|
||||
disabled: {
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
4. Run `yarn start` in `docs/` - the page generates automatically!
|
||||
|
||||
## Interactive Documentation
|
||||
|
||||
For interactive examples with controls, visit the [Storybook](/storybook/?path=/docs/extension-components--docs).
|
||||
@@ -34,7 +34,7 @@ Frontend contribution types allow extensions to extend Superset's user interface
|
||||
|
||||
Extensions can add new views or panels to the host application, such as custom SQL Lab panels, dashboards, or other UI components. Each view is registered with a unique ID and can be activated or deactivated as needed. Contribution areas are uniquely identified (e.g., `sqllab.panels` for SQL Lab panels), enabling seamless integration into specific parts of the application.
|
||||
|
||||
``` json
|
||||
```json
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"views": {
|
||||
@@ -53,7 +53,7 @@ Extensions can add new views or panels to the host application, such as custom S
|
||||
|
||||
Extensions can define custom commands that can be executed within the host application, such as context-aware actions or menu options. Each command can specify properties like a unique command identifier, an icon, a title, and a description. These commands can be invoked by users through menus, keyboard shortcuts, or other UI elements, enabling extensions to add rich, interactive functionality to Superset.
|
||||
|
||||
``` json
|
||||
```json
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"commands": [
|
||||
@@ -72,7 +72,7 @@ Extensions can define custom commands that can be executed within the host appli
|
||||
|
||||
Extensions can contribute new menu items or context menus to the host application, providing users with additional actions and options. Each menu item can specify properties such as the target view, the command to execute, its placement (primary, secondary, or context), and conditions for when it should be displayed. Menu contribution areas are uniquely identified (e.g., `sqllab.editor` for the SQL Lab editor), allowing extensions to seamlessly integrate their functionality into specific menus and workflows within Superset.
|
||||
|
||||
``` json
|
||||
```json
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"menus": {
|
||||
@@ -101,6 +101,27 @@ Extensions can contribute new menu items or context menus to the host applicatio
|
||||
}
|
||||
```
|
||||
|
||||
### Editors
|
||||
|
||||
Extensions can replace Superset's default text editors with custom implementations. This enables enhanced editing experiences using alternative editor frameworks like Monaco, CodeMirror, or custom solutions. When an extension registers an editor for a language, it replaces the default Ace editor in all locations that use that language (SQL Lab, Dashboard Properties, CSS editors, etc.).
|
||||
|
||||
```json
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"editors": [
|
||||
{
|
||||
"id": "my_extension.monaco_sql",
|
||||
"name": "Monaco SQL Editor",
|
||||
"languages": ["sql"],
|
||||
"description": "Monaco-based SQL editor with IntelliSense"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [Editors Extension Point](./extension-points/editors) for implementation details.
|
||||
|
||||
## Backend
|
||||
|
||||
Backend contribution types allow extensions to extend Superset's server-side capabilities with new API endpoints, MCP tools, and MCP prompts.
|
||||
@@ -109,7 +130,7 @@ Backend contribution types allow extensions to extend Superset's server-side cap
|
||||
|
||||
Extensions can register custom REST API endpoints under the `/api/v1/extensions/` namespace. This dedicated namespace prevents conflicts with built-in endpoints and provides a clear separation between core and extension functionality.
|
||||
|
||||
``` json
|
||||
```json
|
||||
"backend": {
|
||||
"entryPoints": ["my_extension.entrypoint"],
|
||||
"files": ["backend/src/my_extension/**/*.py"]
|
||||
@@ -118,7 +139,7 @@ Extensions can register custom REST API endpoints under the `/api/v1/extensions/
|
||||
|
||||
The entry point module registers the API with Superset:
|
||||
|
||||
``` python
|
||||
```python
|
||||
from superset_core.api.rest_api import add_extension_api
|
||||
from .api import MyExtensionAPI
|
||||
|
||||
|
||||
@@ -134,9 +134,9 @@ export const onDidChangeActivePanel: Event<Panel>;
|
||||
|
||||
export const onDidChangeTabTitle: Event<string>;
|
||||
|
||||
export const onDidQueryRun: Event<Editor>;
|
||||
export const onDidQueryRun: Event<QueryContext>;
|
||||
|
||||
export const onDidQueryStop: Event<Editor>;
|
||||
export const onDidQueryStop: Event<QueryContext>;
|
||||
```
|
||||
|
||||
The following code demonstrates more examples of the existing frontend APIs:
|
||||
@@ -150,16 +150,16 @@ export function activate(context) {
|
||||
const panelDisposable = core.registerView('my_extension.panel', <MyPanel><Button/></MyPanel>);
|
||||
|
||||
// Register a custom command
|
||||
const commandDisposable = commands.registerCommand('my_extension.copy_query', {
|
||||
title: 'Copy Query',
|
||||
execute: () => {
|
||||
const commandDisposable = commands.registerCommand(
|
||||
'my_extension.copy_query',
|
||||
() => {
|
||||
// Command logic here
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
// Listen for query run events in SQL Lab
|
||||
const eventDisposable = sqlLab.onDidQueryRun(editor => {
|
||||
// Handle query execution event
|
||||
const eventDisposable = sqlLab.onDidQueryRun(queryContext => {
|
||||
console.log('Query started on database:', queryContext.tab.databaseId);
|
||||
});
|
||||
|
||||
// Access a CSRF token for secure API requests
|
||||
|
||||
245
docs/developer_portal/extensions/extension-points/editors.md
Normal file
@@ -0,0 +1,245 @@
|
||||
---
|
||||
title: Editors
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# Editor Contributions
|
||||
|
||||
Extensions can replace Superset's default text editors with custom implementations. This allows you to provide enhanced editing experiences using alternative editor frameworks like Monaco, CodeMirror, or custom solutions.
|
||||
|
||||
## Overview
|
||||
|
||||
Superset uses text editors in various places throughout the application:
|
||||
|
||||
| Language | Locations |
|
||||
|----------|-----------|
|
||||
| `sql` | SQL Lab, Metric/Filter Popovers |
|
||||
| `json` | Dashboard Properties, Annotation Modal, Theme Modal |
|
||||
| `css` | Dashboard Properties, CSS Template Modal |
|
||||
| `markdown` | Dashboard Markdown component |
|
||||
| `yaml` | Template Params Editor |
|
||||
|
||||
By registering an editor provider for a language, your extension replaces the default Ace editor in **all** locations that use that language.
|
||||
|
||||
## Manifest Configuration
|
||||
|
||||
Declare editor contributions in your `extension.json` manifest:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "monaco-editor",
|
||||
"version": "1.0.0",
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"editors": [
|
||||
{
|
||||
"id": "monaco-editor.sql",
|
||||
"name": "Monaco SQL Editor",
|
||||
"languages": ["sql"],
|
||||
"description": "Monaco-based SQL editor with IntelliSense"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementing an Editor
|
||||
|
||||
Your editor component must implement the `EditorProps` interface and expose an `EditorHandle` via `forwardRef`. For the complete interface definitions, see `@apache-superset/core/api/editors.ts`.
|
||||
|
||||
### Key EditorProps
|
||||
|
||||
```typescript
|
||||
interface EditorProps {
|
||||
/** Controlled value */
|
||||
value: string;
|
||||
/** Content change handler */
|
||||
onChange: (value: string) => void;
|
||||
/** Language mode for syntax highlighting */
|
||||
language: EditorLanguage;
|
||||
/** Keyboard shortcuts to register */
|
||||
hotkeys?: EditorHotkey[];
|
||||
/** Callback when editor is ready with imperative handle */
|
||||
onReady?: (handle: EditorHandle) => void;
|
||||
/** Host-specific context (e.g., database info from SQL Lab) */
|
||||
metadata?: Record<string, unknown>;
|
||||
// ... additional props for styling, annotations, etc.
|
||||
}
|
||||
```
|
||||
|
||||
### Key EditorHandle Methods
|
||||
|
||||
```typescript
|
||||
interface EditorHandle {
|
||||
/** Focus the editor */
|
||||
focus(): void;
|
||||
/** Get the current editor content */
|
||||
getValue(): string;
|
||||
/** Get the current cursor position */
|
||||
getCursorPosition(): Position;
|
||||
/** Move the cursor to a specific position */
|
||||
moveCursorToPosition(position: Position): void;
|
||||
/** Set the selection range */
|
||||
setSelection(selection: Range): void;
|
||||
/** Scroll to a specific line */
|
||||
scrollToLine(line: number): void;
|
||||
// ... additional methods for text manipulation, annotations, etc.
|
||||
}
|
||||
```
|
||||
|
||||
## Example Implementation
|
||||
|
||||
Here's an example of a Monaco-based SQL editor implementing the key interfaces shown above:
|
||||
|
||||
### MonacoSQLEditor.tsx
|
||||
|
||||
```typescript
|
||||
import { forwardRef, useRef, useImperativeHandle, useEffect } from 'react';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import type { editors } from '@apache-superset/core';
|
||||
|
||||
const MonacoSQLEditor = forwardRef<editors.EditorHandle, editors.EditorProps>(
|
||||
(props, ref) => {
|
||||
const { value, onChange, hotkeys, onReady } = props;
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
||||
|
||||
// Implement EditorHandle interface
|
||||
const handle: editors.EditorHandle = {
|
||||
focus: () => editorRef.current?.focus(),
|
||||
getValue: () => editorRef.current?.getValue() ?? '',
|
||||
getCursorPosition: () => {
|
||||
const pos = editorRef.current?.getPosition();
|
||||
return { line: (pos?.lineNumber ?? 1) - 1, column: (pos?.column ?? 1) - 1 };
|
||||
},
|
||||
// ... implement remaining methods
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => handle, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const editor = monaco.editor.create(containerRef.current, { value, language: 'sql' });
|
||||
editorRef.current = editor;
|
||||
|
||||
editor.onDidChangeModelContent(() => onChange(editor.getValue()));
|
||||
|
||||
// Register hotkeys
|
||||
hotkeys?.forEach(hotkey => {
|
||||
editor.addAction({
|
||||
id: hotkey.name,
|
||||
label: hotkey.name,
|
||||
run: () => hotkey.exec(handle),
|
||||
});
|
||||
});
|
||||
|
||||
onReady?.(handle);
|
||||
return () => editor.dispose();
|
||||
}, []);
|
||||
|
||||
return <div ref={containerRef} style={{ height: '100%', width: '100%' }} />;
|
||||
},
|
||||
);
|
||||
|
||||
export default MonacoSQLEditor;
|
||||
```
|
||||
|
||||
### activate.ts
|
||||
|
||||
```typescript
|
||||
import { editors } from '@apache-superset/core';
|
||||
import MonacoSQLEditor from './MonacoSQLEditor';
|
||||
|
||||
export function activate(context) {
|
||||
// Register the Monaco editor for SQL
|
||||
const disposable = editors.registerEditorProvider(
|
||||
{
|
||||
id: 'monaco-sql-editor.sql',
|
||||
name: 'Monaco SQL Editor',
|
||||
languages: ['sql'],
|
||||
},
|
||||
MonacoSQLEditor,
|
||||
);
|
||||
|
||||
context.subscriptions.push(disposable);
|
||||
}
|
||||
```
|
||||
|
||||
## Handling Hotkeys
|
||||
|
||||
Superset passes keyboard shortcuts via the `hotkeys` prop. Each hotkey includes an `exec` function that receives the `EditorHandle`:
|
||||
|
||||
```typescript
|
||||
interface EditorHotkey {
|
||||
name: string;
|
||||
key: string; // e.g., "Ctrl-Enter", "Alt-Shift-F"
|
||||
description?: string;
|
||||
exec: (handle: EditorHandle) => void;
|
||||
}
|
||||
```
|
||||
|
||||
Your editor must register these hotkeys with your editor framework and call `exec(handle)` when triggered.
|
||||
|
||||
## Keywords
|
||||
|
||||
Superset passes static autocomplete suggestions via the `keywords` prop. These include table names, column names, and SQL functions based on the current database context:
|
||||
|
||||
```typescript
|
||||
interface EditorKeyword {
|
||||
name: string;
|
||||
value?: string; // Text to insert (defaults to name)
|
||||
meta?: string; // Category like "table", "column", "function"
|
||||
score?: number; // Sorting priority
|
||||
}
|
||||
```
|
||||
|
||||
Your editor should convert these to your framework's completion format and register them for autocomplete.
|
||||
|
||||
## Completion Providers
|
||||
|
||||
For dynamic autocomplete (e.g., fetching suggestions as the user types), implement and register a `CompletionProvider` via the `EditorHandle`:
|
||||
|
||||
```typescript
|
||||
const provider: CompletionProvider = {
|
||||
id: 'my-sql-completions',
|
||||
triggerCharacters: ['.', ' '],
|
||||
provideCompletions: async (content, position, context) => {
|
||||
// Use context.metadata for database info
|
||||
// Return array of CompletionItem
|
||||
return [
|
||||
{ label: 'SELECT', insertText: 'SELECT', kind: 'keyword' },
|
||||
// ...
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
// Register during editor initialization
|
||||
const disposable = handle.registerCompletionProvider(provider);
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[SQL Lab Extension Points](./sqllab)** - Learn about other SQL Lab customizations
|
||||
- **[Contribution Types](../contribution-types)** - Explore other contribution types
|
||||
- **[Development](../development)** - Set up your development environment
|
||||
@@ -24,7 +24,7 @@ under the License.
|
||||
|
||||
# SQL Lab Extension Points
|
||||
|
||||
SQL Lab provides 5 extension points where extensions can contribute custom UI components. Each area serves a specific purpose and can be customized to add new functionality.
|
||||
SQL Lab provides 4 extension points where extensions can contribute custom UI components. Each area serves a specific purpose and supports different types of customizations. These areas will evolve over time as new features are added to SQL Lab.
|
||||
|
||||
## Layout Overview
|
||||
|
||||
@@ -41,42 +41,44 @@ SQL Lab provides 5 extension points where extensions can contribute custom UI co
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
├──────────┴─────────────────────────────────────────┴─────────────┤
|
||||
│ Status Bar │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
└──────────┴─────────────────────────────────────────┴─────────────┘
|
||||
```
|
||||
|
||||
| Extension Point | ID | Description |
|
||||
| ----------------- | --------------------- | ---------------------------------------------------------- |
|
||||
| **Left Sidebar** | `sqllab.leftSidebar` | Navigation and browsing (database explorer, saved queries) |
|
||||
| **Editor** | `sqllab.editor` | SQL query editor workspace |
|
||||
| **Right Sidebar** | `sqllab.rightSidebar` | Contextual tools (AI assistants, query analysis) |
|
||||
| **Panels** | `sqllab.panels` | Results and related views (visualizations, data profiling) |
|
||||
| **Status Bar** | `sqllab.statusBar` | Connection status and query metrics |
|
||||
| Extension Point | ID | Views | Menus | Description |
|
||||
| ----------------- | --------------------- | ----- | ----- | ---------------------------------------------- |
|
||||
| **Left Sidebar** | `sqllab.leftSidebar` | — | ✓ | Menu actions for the database explorer |
|
||||
| **Editor** | `sqllab.editor` | ✓\* | ✓ | Custom editors + toolbar actions |
|
||||
| **Right Sidebar** | `sqllab.rightSidebar` | ✓ | — | Custom panels (AI assistants, query analysis) |
|
||||
| **Panels** | `sqllab.panels` | ✓ | ✓ | Custom tabs + toolbar actions (data profiling) |
|
||||
|
||||
## Area Customizations
|
||||
\*Editor views are contributed via [Editor Contributions](./editors), not standard view contributions.
|
||||
|
||||
Each extension point area supports three types of action customizations:
|
||||
## Customization Types
|
||||
|
||||
### Views
|
||||
|
||||
Extensions can add custom views (React components) to **Right Sidebar** and **Panels**. Views appear as new panels or tabs in their respective areas.
|
||||
|
||||
### Menus
|
||||
|
||||
Extensions can add toolbar actions to **Left Sidebar**, **Editor**, and **Panels**. Menu contributions support:
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ Area Title [Button] [Button] [•••] │
|
||||
│ [Button] [Button] [•••] │
|
||||
├───────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ │
|
||||
│ Area Content │
|
||||
│ │
|
||||
│ (right-click for context menu) │
|
||||
│ │
|
||||
│ │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| Action Type | Location | Use Case |
|
||||
| --------------------- | ----------------- | ----------------------------------------------------- |
|
||||
| **Primary Actions** | Top-right buttons | Frequently used actions (e.g., run, refresh, add new) |
|
||||
| **Secondary Actions** | 3-dot menu (•••) | Less common actions (e.g., export, settings) |
|
||||
| **Context Actions** | Right-click menu | Context-sensitive actions on content |
|
||||
| Action Type | Location | Use Case |
|
||||
| --------------------- | ---------------- | ----------------------------------------------------- |
|
||||
| **Primary Actions** | Toolbar buttons | Frequently used actions (e.g., run, refresh, add new) |
|
||||
| **Secondary Actions** | 3-dot menu (•••) | Less common actions (e.g., export, settings) |
|
||||
|
||||
### Custom Editors
|
||||
|
||||
Extensions can replace the default SQL editor with custom implementations (Monaco, CodeMirror, etc.). See [Editor Contributions](./editors) for details.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -171,32 +173,38 @@ import { commands, sqlLab } from '@apache-superset/core';
|
||||
|
||||
export function activate(context) {
|
||||
// Register the commands declared in extension.json
|
||||
const formatCommand = commands.registerCommand('query_tools.format', {
|
||||
execute: () => {
|
||||
const formatCommand = commands.registerCommand(
|
||||
'query_tools.format',
|
||||
async () => {
|
||||
const tab = sqlLab.getCurrentTab();
|
||||
if (tab?.editor) {
|
||||
if (tab) {
|
||||
const editor = await tab.getEditor();
|
||||
// Format the SQL query
|
||||
}
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const explainCommand = commands.registerCommand('query_tools.explain', {
|
||||
execute: () => {
|
||||
const explainCommand = commands.registerCommand(
|
||||
'query_tools.explain',
|
||||
async () => {
|
||||
const tab = sqlLab.getCurrentTab();
|
||||
if (tab?.editor) {
|
||||
if (tab) {
|
||||
const editor = await tab.getEditor();
|
||||
// Show query explanation
|
||||
}
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const copyAsCteCommand = commands.registerCommand('query_tools.copy_as_cte', {
|
||||
execute: () => {
|
||||
const copyAsCteCommand = commands.registerCommand(
|
||||
'query_tools.copy_as_cte',
|
||||
async () => {
|
||||
const tab = sqlLab.getCurrentTab();
|
||||
if (tab?.editor) {
|
||||
if (tab) {
|
||||
const editor = await tab.getEditor();
|
||||
// Copy selected text as CTE
|
||||
}
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
context.subscriptions.push(formatCommand, explainCommand, copyAsCteCommand);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ This page serves as a registry of community-created Superset extensions. These e
|
||||
| [SQL Lab Result Stats](https://github.com/michael-s-molina/superset-extensions/tree/main/result_stats) | A SQL Lab extension that automatically computes statistics for query results, providing type-aware analysis including numeric metrics (min, max, mean, median, std dev), string analysis (length, empty counts), and date range information. | Michael S. Molina | <a href="/img/extensions/result-stats.png" target="_blank"><img src="/img/extensions/result-stats.png" alt="Result Stats" width="120" /></a> |
|
||||
| [SQL Snippets](https://github.com/michael-s-molina/superset-extensions/tree/main/sql_snippets) | A SQL Lab extension that provides reusable SQL code snippets, enabling quick insertion of commonly used code blocks such as license headers, author information, and frequently used SQL patterns. | Michael S. Molina | <a href="/img/extensions/sql-snippets.png" target="_blank"><img src="/img/extensions/sql-snippets.png" alt="SQL Snippets" width="120" /></a> |
|
||||
| [SQL Lab Query Estimator](https://github.com/michael-s-molina/superset-extensions/tree/main/query_estimator) | A SQL Lab panel that analyzes query execution plans to estimate resource impact, detect performance issues like Cartesian products and high-cost operations, and visualize the query plan tree. | Michael S. Molina | <a href="/img/extensions/query-estimator.png" target="_blank"><img src="/img/extensions/query-estimator.png" alt="Query Estimator" width="120" /></a> |
|
||||
| [Editors Bundle](https://github.com/michael-s-molina/superset-extensions/tree/main/editors_bundle) | A Superset extension that demonstrates how to provide custom code editors for different languages. This extension showcases the editor contribution system by registering alternative editors that can replace Superset's default Ace editor. | Michael S. Molina | <a href="/img/extensions/editors-bundle.png" target="_blank"><img src="/img/extensions/editors-bundle.png" alt="Editors Bundle" width="120" /></a> |
|
||||
|
||||
## How to Add Your Extension
|
||||
|
||||
|
||||
@@ -1,416 +0,0 @@
|
||||
---
|
||||
title: Extension Sandboxing
|
||||
sidebar_position: 10
|
||||
---
|
||||
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
# Extension Sandboxing
|
||||
|
||||
Superset provides a tiered sandbox architecture for running extensions with varying levels of trust and isolation. This system balances security with functionality, allowing extensions to be safely executed based on their trust level and requirements.
|
||||
|
||||
## Overview
|
||||
|
||||
The sandbox system supports three tiers of trust:
|
||||
|
||||
| Tier | Trust Level | Isolation | Use Case |
|
||||
|------|-------------|-----------|----------|
|
||||
| **Tier 1** | `core` | None (main context) | Official/signed extensions |
|
||||
| **Tier 2** | `iframe` | Browser sandbox | Community UI extensions |
|
||||
| **Tier 3** | `wasm` | WASM sandbox | Logic-only extensions |
|
||||
|
||||
## Trust Levels
|
||||
|
||||
### Tier 1: Core (Trusted)
|
||||
|
||||
Core extensions run in the main JavaScript context with full access to Superset APIs, DOM, and browser capabilities. This is the same behavior as legacy extensions.
|
||||
|
||||
**Requirements:**
|
||||
- Must be in the trusted extensions list, OR
|
||||
- `allowUnsignedCore` configuration must be enabled
|
||||
|
||||
**Use cases:**
|
||||
- Official Apache Superset extensions
|
||||
- Enterprise-verified plugins
|
||||
- Extensions from trusted sources
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "official-extension",
|
||||
"sandbox": {
|
||||
"trustLevel": "core",
|
||||
"requiresSignature": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tier 2: Iframe (Semi-Trusted)
|
||||
|
||||
Iframe-sandboxed extensions run in isolated browser sandboxes with controlled API access via postMessage. This provides strong browser-enforced isolation while still allowing full UI rendering.
|
||||
|
||||
**Security features:**
|
||||
- Browser-enforced same-origin isolation
|
||||
- Content Security Policy (CSP) restrictions
|
||||
- Permission-based API access
|
||||
- No access to parent window's cookies, localStorage, or DOM
|
||||
|
||||
**Use cases:**
|
||||
- Community-contributed extensions
|
||||
- Third-party plugins
|
||||
- Extensions that render custom UI
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "community-extension",
|
||||
"sandbox": {
|
||||
"trustLevel": "iframe",
|
||||
"permissions": ["sqllab:read", "notification:show"],
|
||||
"csp": {
|
||||
"connectSrc": ["https://api.example.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tier 3: WASM (Untrusted)
|
||||
|
||||
WASM-sandboxed extensions run in a QuickJS WebAssembly sandbox with no DOM access. Only explicitly injected APIs are available. This provides the highest level of isolation.
|
||||
|
||||
**Security features:**
|
||||
- Complete isolation from browser APIs
|
||||
- Memory limits to prevent DoS
|
||||
- Execution time limits
|
||||
- No network or DOM access
|
||||
|
||||
**Use cases:**
|
||||
- Custom data transformations
|
||||
- Calculated fields and formatters
|
||||
- Data validation rules
|
||||
- Custom aggregation functions
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "formatter-extension",
|
||||
"sandbox": {
|
||||
"trustLevel": "wasm",
|
||||
"resourceLimits": {
|
||||
"maxMemory": 10485760,
|
||||
"maxExecutionTime": 5000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
Sandboxed extensions (Tier 2 and 3) must declare the permissions they need. Permissions follow a least-privilege model.
|
||||
|
||||
### Available Permissions
|
||||
|
||||
| Permission | Description |
|
||||
|------------|-------------|
|
||||
| `api:read` | Read-only access to Superset APIs |
|
||||
| `api:write` | Write access to Superset APIs |
|
||||
| `sqllab:read` | Read SQL Lab state (queries, results) |
|
||||
| `sqllab:execute` | Execute SQL queries |
|
||||
| `dashboard:read` | Read dashboard data |
|
||||
| `dashboard:write` | Modify dashboards |
|
||||
| `chart:read` | Read chart data |
|
||||
| `chart:write` | Modify charts |
|
||||
| `user:read` | Read current user info |
|
||||
| `notification:show` | Show notifications to user |
|
||||
| `modal:open` | Open modal dialogs |
|
||||
| `navigation:redirect` | Navigate to other pages |
|
||||
| `clipboard:write` | Write to clipboard |
|
||||
| `download:file` | Trigger file downloads |
|
||||
|
||||
### Example Permission Declaration
|
||||
|
||||
```json
|
||||
{
|
||||
"sandbox": {
|
||||
"trustLevel": "iframe",
|
||||
"permissions": [
|
||||
"sqllab:read",
|
||||
"notification:show",
|
||||
"download:file"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sandboxed Extension API
|
||||
|
||||
Extensions running in iframe sandboxes have access to a controlled API through the `window.superset` object.
|
||||
|
||||
### SQL Lab API
|
||||
|
||||
```typescript
|
||||
// Get the current SQL Lab tab (requires sqllab:read)
|
||||
const tab = await window.superset.sqlLab.getCurrentTab();
|
||||
|
||||
// Get query results (requires sqllab:read)
|
||||
const results = await window.superset.sqlLab.getQueryResults(queryId);
|
||||
```
|
||||
|
||||
### Dashboard API
|
||||
|
||||
```typescript
|
||||
// Get dashboard context (requires dashboard:read)
|
||||
const context = await window.superset.dashboard.getContext();
|
||||
|
||||
// Get dashboard filters (requires dashboard:read)
|
||||
const filters = await window.superset.dashboard.getFilters();
|
||||
```
|
||||
|
||||
### Chart API
|
||||
|
||||
```typescript
|
||||
// Get chart data (requires chart:read)
|
||||
const chartData = await window.superset.chart.getData(chartId);
|
||||
```
|
||||
|
||||
### User API
|
||||
|
||||
```typescript
|
||||
// Get current user (requires user:read)
|
||||
const user = await window.superset.user.getCurrentUser();
|
||||
```
|
||||
|
||||
### UI API
|
||||
|
||||
```typescript
|
||||
// Show notification (requires notification:show)
|
||||
window.superset.ui.showNotification('Success!', 'success');
|
||||
|
||||
// Open modal (requires modal:open)
|
||||
const result = await window.superset.ui.openModal({
|
||||
title: 'Confirm',
|
||||
content: 'Are you sure?',
|
||||
type: 'confirm'
|
||||
});
|
||||
|
||||
// Navigate (requires navigation:redirect)
|
||||
window.superset.ui.navigateTo('/dashboard/1');
|
||||
```
|
||||
|
||||
### Utility API
|
||||
|
||||
```typescript
|
||||
// Copy to clipboard (requires clipboard:write)
|
||||
await window.superset.utils.copyToClipboard('text');
|
||||
|
||||
// Download file (requires download:file)
|
||||
window.superset.utils.downloadFile(blob, 'filename.csv');
|
||||
|
||||
// Get CSRF token (no permission required)
|
||||
const token = await window.superset.utils.getCSRFToken();
|
||||
```
|
||||
|
||||
### Event Subscriptions
|
||||
|
||||
```typescript
|
||||
// Subscribe to events
|
||||
const unsubscribe = window.superset.on('dashboard:filterChange', (filters) => {
|
||||
console.log('Filters changed:', filters);
|
||||
});
|
||||
|
||||
// Later, unsubscribe
|
||||
unsubscribe();
|
||||
```
|
||||
|
||||
## Content Security Policy
|
||||
|
||||
Iframe-sandboxed extensions can customize their Content Security Policy through the `csp` configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"sandbox": {
|
||||
"trustLevel": "iframe",
|
||||
"csp": {
|
||||
"defaultSrc": ["'none'"],
|
||||
"scriptSrc": ["'unsafe-inline'"],
|
||||
"styleSrc": ["'unsafe-inline'"],
|
||||
"imgSrc": ["data:", "blob:", "https://cdn.example.com"],
|
||||
"connectSrc": ["https://api.example.com"],
|
||||
"fontSrc": ["data:"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Default CSP
|
||||
|
||||
By default, iframe sandboxes use a restrictive CSP:
|
||||
|
||||
```
|
||||
default-src 'none';
|
||||
script-src 'unsafe-inline';
|
||||
style-src 'unsafe-inline';
|
||||
img-src data: blob:;
|
||||
font-src data:;
|
||||
connect-src 'none';
|
||||
frame-src 'none';
|
||||
```
|
||||
|
||||
## WASM Resource Limits
|
||||
|
||||
WASM-sandboxed extensions can configure resource limits:
|
||||
|
||||
```json
|
||||
{
|
||||
"sandbox": {
|
||||
"trustLevel": "wasm",
|
||||
"resourceLimits": {
|
||||
"maxMemory": 10485760, // 10MB max memory
|
||||
"maxExecutionTime": 5000, // 5 second timeout
|
||||
"maxStackSize": 1000 // Max call stack depth
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Defaults
|
||||
|
||||
- **maxMemory**: 10MB
|
||||
- **maxExecutionTime**: 5000ms (5 seconds)
|
||||
- **maxStackSize**: 1000 calls
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Migrating from Legacy Extensions
|
||||
|
||||
Existing extensions that don't specify a `sandbox` configuration will continue to run as `core` extensions for backward compatibility. To migrate to a sandboxed model:
|
||||
|
||||
1. **Assess your extension's requirements**:
|
||||
- Does it need to render UI? Use `iframe`
|
||||
- Is it logic-only (formatters, validators)? Use `wasm`
|
||||
- Does it need full access? Keep as `core` (requires trust)
|
||||
|
||||
2. **Add sandbox configuration to extension.json**:
|
||||
|
||||
```json
|
||||
{
|
||||
"sandbox": {
|
||||
"trustLevel": "iframe",
|
||||
"permissions": ["sqllab:read"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Update your code to use the sandboxed API**:
|
||||
|
||||
Before (core extension):
|
||||
```typescript
|
||||
import { sqlLab } from '@apache-superset/core';
|
||||
const tab = sqlLab.getCurrentTab();
|
||||
```
|
||||
|
||||
After (sandboxed extension):
|
||||
```typescript
|
||||
const tab = await window.superset.sqlLab.getCurrentTab();
|
||||
```
|
||||
|
||||
4. **Test thoroughly** to ensure all functionality works within the sandbox
|
||||
|
||||
## Security Comparison
|
||||
|
||||
| Aspect | Core | Iframe | WASM |
|
||||
|--------|------|--------|------|
|
||||
| DOM Access | Full | Own iframe only | None |
|
||||
| Network | Full | Restricted (CSP) | None |
|
||||
| Cookies | Full | None | None |
|
||||
| localStorage | Full | None | None |
|
||||
| Superset APIs | Full | Controlled bridge | Injected only |
|
||||
| Performance | Native | Near-native | ~40% slower |
|
||||
| React rendering | Full | Own instance | Via descriptors |
|
||||
|
||||
## Administrator Configuration
|
||||
|
||||
Administrators can configure trust settings for their Superset deployment:
|
||||
|
||||
```python
|
||||
# In superset_config.py
|
||||
EXTENSIONS_TRUST_CONFIG = {
|
||||
# Extensions allowed to run as 'core'
|
||||
"trusted_extensions": [
|
||||
"official-extension-1",
|
||||
"enterprise-plugin",
|
||||
],
|
||||
|
||||
# Allow unsigned extensions to run as core (not recommended for production)
|
||||
"allow_unsigned_core": False,
|
||||
|
||||
# Default trust level for extensions without sandbox config
|
||||
"default_trust_level": "iframe",
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Request minimal permissions** - Only request the permissions your extension actually needs
|
||||
|
||||
2. **Prefer iframe over core** - Unless your extension requires deep integration, use iframe sandboxing
|
||||
|
||||
3. **Use WASM for pure logic** - If your extension doesn't need UI, WASM provides the best isolation
|
||||
|
||||
4. **Handle permission denials gracefully** - Your extension should degrade gracefully if a permission is not granted
|
||||
|
||||
5. **Don't store sensitive data** - Sandboxed extensions should not store sensitive user data
|
||||
|
||||
6. **Test in sandboxed mode** - Always test your extension in its intended sandbox environment
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Permission Denied Errors
|
||||
|
||||
If you see "Permission denied" errors, verify that:
|
||||
1. The permission is declared in your extension.json
|
||||
2. The permission was granted by the administrator
|
||||
3. You're calling the correct API method for that permission
|
||||
|
||||
### Timeout Errors (WASM)
|
||||
|
||||
If your WASM extension times out:
|
||||
1. Optimize your code for faster execution
|
||||
2. Request a higher `maxExecutionTime` limit
|
||||
3. Break large operations into smaller chunks
|
||||
|
||||
### CSP Violations (Iframe)
|
||||
|
||||
If resources fail to load due to CSP:
|
||||
1. Add the required domains to your CSP configuration
|
||||
2. Ensure you're using HTTPS for external resources
|
||||
3. Avoid inline scripts and styles where possible
|
||||
|
||||
### Core Trust Denied
|
||||
|
||||
If your extension is downgraded from `core` to another trust level:
|
||||
1. Check if the extension ID is in the administrator's `trusted_extensions` list
|
||||
2. If signature verification is required, ensure the extension is signed
|
||||
3. Verify the signing key is in the administrator's `trusted_signers`
|
||||
|
||||
See [Extension Signing](./signing) for how to sign your extension.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Security Overview](./security) - Extension security fundamentals
|
||||
- [Extension Signing](./signing) - How to sign extensions for core trust
|
||||
- [Administrator Configuration](./admin-configuration) - Trust configuration for admins
|
||||
@@ -26,44 +26,9 @@ under the License.
|
||||
|
||||
By default, extensions are disabled and must be explicitly enabled by setting the `ENABLE_EXTENSIONS` feature flag. Built-in extensions are included as part of the Superset codebase and are held to the same security standards and review processes as the rest of the application.
|
||||
|
||||
## Extension Sandboxing
|
||||
For external extensions, administrators are responsible for evaluating and verifying the security of any extensions they choose to install, just as they would when installing third-party NPM or PyPI packages. At this stage, all extensions run in the same context as the host application, without additional sandboxing. This means that external extensions can impact the security and performance of a Superset environment in the same way as any other installed dependency.
|
||||
|
||||
Superset provides a tiered sandbox architecture for running extensions with varying levels of trust and isolation. Extensions can declare their trust level and permissions in their manifest, and Superset will load them in the appropriate sandbox:
|
||||
|
||||
- **Core (Tier 1)**: Trusted extensions run in the main context with full access
|
||||
- **Iframe (Tier 2)**: Semi-trusted extensions run in browser-sandboxed iframes
|
||||
- **WASM (Tier 3)**: Untrusted logic runs in WebAssembly sandboxes
|
||||
|
||||
For detailed information about the sandbox system, see [Extension Sandboxing](./sandbox).
|
||||
|
||||
## Trust Model
|
||||
|
||||
Administrators are responsible for evaluating and verifying the security of any extensions they choose to install. Superset's sandbox system provides defense-in-depth:
|
||||
|
||||
1. **Core extensions** require explicit trust configuration and optionally signature verification
|
||||
2. **Iframe-sandboxed extensions** are isolated by the browser's same-origin policy
|
||||
3. **WASM-sandboxed extensions** have no access to browser APIs
|
||||
|
||||
A directory of community extensions is available in the [Community Extensions](./registry) page. Note that these extensions are not vetted by the Apache Superset project—administrators must evaluate each extension before installation.
|
||||
|
||||
## Extension Signing
|
||||
|
||||
Extensions can be cryptographically signed to verify their authenticity and integrity. This is required for extensions that need `core` trust level in production environments with signature verification enabled.
|
||||
|
||||
- **Developers**: See [Extension Signing](./signing) to learn how to sign your extensions
|
||||
- **Administrators**: See [Administrator Configuration](./admin-configuration) to configure trusted signers
|
||||
|
||||
## Administrator Configuration
|
||||
|
||||
Superset provides extensive configuration options for controlling extension trust levels, signature verification, and security policies. Key settings include:
|
||||
|
||||
- **Trusted extensions list**: Extensions allowed to run as `core`
|
||||
- **Signature verification**: Require valid signatures for core trust
|
||||
- **Default trust level**: Sandbox level for unlisted extensions
|
||||
|
||||
For complete configuration details, see [Administrator Configuration](./admin-configuration).
|
||||
|
||||
## Security Reporting
|
||||
We plan to introduce an optional sandboxed execution model for extensions in the future (as part of an additional SIP). Until then, administrators should exercise caution and follow best practices when selecting and deploying third-party extensions. A directory of community extensions is available in the [Community Extensions](./registry) page. Note that these extensions are not vetted by the Apache Superset project—administrators must evaluate each extension before installation.
|
||||
|
||||
**Any performance or security vulnerabilities introduced by external extensions should be reported directly to the extension author, not as Superset vulnerabilities.**
|
||||
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
---
|
||||
title: Extension Signing
|
||||
sidebar_position: 11
|
||||
---
|
||||
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
# Extension Signing
|
||||
|
||||
Signing your extension allows administrators to verify its authenticity and integrity. Signed extensions can run as `core` trust level in production environments where signature verification is required.
|
||||
|
||||
## Why Sign Extensions?
|
||||
|
||||
- **Trust**: Administrators can verify your extension comes from a known source
|
||||
- **Integrity**: Ensures the extension hasn't been modified since you signed it
|
||||
- **Core Access**: Required for extensions needing `core` trust level in secured deployments
|
||||
- **Distribution**: Makes your extension suitable for enterprise environments
|
||||
|
||||
## Generating Signing Keys
|
||||
|
||||
Generate a new Ed25519 keypair for signing your extensions:
|
||||
|
||||
```bash
|
||||
superset-extensions generate-keys --output my-signing-key.pem
|
||||
```
|
||||
|
||||
This creates two files:
|
||||
|
||||
| File | Purpose | Share? |
|
||||
|------|---------|--------|
|
||||
| `my-signing-key.pem` | Private key for signing | **Never share!** |
|
||||
| `my-signing-key.pub` | Public key for verification | Share with administrators |
|
||||
|
||||
**Output example:**
|
||||
|
||||
```
|
||||
✅ Private key: my-signing-key.pem
|
||||
✅ Public key: my-signing-key.pub
|
||||
Fingerprint: MCowBQYDK2Vw...
|
||||
|
||||
⚠️ Keep the private key secure! Only share the public key with administrators.
|
||||
|
||||
Usage:
|
||||
Sign an extension: superset-extensions bundle --sign my-signing-key.pem
|
||||
Share with admins: my-signing-key.pub
|
||||
```
|
||||
|
||||
## Signing an Extension
|
||||
|
||||
### During Bundle
|
||||
|
||||
The easiest way to sign is during the bundle step:
|
||||
|
||||
```bash
|
||||
superset-extensions bundle --sign my-signing-key.pem
|
||||
```
|
||||
|
||||
This builds, signs the manifest, and creates the `.supx` bundle in one command.
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
✅ Full build completed in dist/
|
||||
✅ Manifest signed
|
||||
✅ Bundle created (signed): my-extension-1.0.0.supx
|
||||
```
|
||||
|
||||
### Signing Existing Manifest
|
||||
|
||||
To sign an already-built manifest:
|
||||
|
||||
```bash
|
||||
superset-extensions sign --key my-signing-key.pem --manifest dist/manifest.json
|
||||
```
|
||||
|
||||
This creates `dist/manifest.sig` containing the signature.
|
||||
|
||||
## Bundle Structure
|
||||
|
||||
A signed extension bundle contains:
|
||||
|
||||
```
|
||||
my-extension-1.0.0.supx
|
||||
├── manifest.json # Extension manifest
|
||||
├── manifest.sig # Ed25519 signature (base64-encoded)
|
||||
├── frontend/dist/ # Frontend assets
|
||||
└── backend/src/ # Backend code (if applicable)
|
||||
```
|
||||
|
||||
The signature file (`manifest.sig`) contains a base64-encoded Ed25519 signature of the manifest content.
|
||||
|
||||
## Distributing Your Public Key
|
||||
|
||||
Share your public key (`.pub` file) with administrators who want to trust your extensions:
|
||||
|
||||
1. **Direct sharing**: Send the `.pub` file via secure channels
|
||||
2. **Documentation**: Include in your extension's README
|
||||
3. **Website**: Host on your organization's website with HTTPS
|
||||
|
||||
Administrators will add your public key to their `EXTENSIONS_TRUST_CONFIG.trusted_signers` configuration.
|
||||
|
||||
### Key Fingerprint
|
||||
|
||||
The fingerprint helps administrators verify they have the correct key. Include it in your documentation:
|
||||
|
||||
```
|
||||
Public Key Fingerprint: MCowBQYDK2Vw...
|
||||
```
|
||||
|
||||
Administrators should verify this fingerprint matches when adding your key.
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### Protect Your Private Key
|
||||
|
||||
- **Never commit** private keys to version control
|
||||
- **Use secure storage** like hardware security modules (HSM) for production keys
|
||||
- **Limit access** to the private key to authorized personnel only
|
||||
- **Back up securely** in case of key loss
|
||||
|
||||
### Key Rotation
|
||||
|
||||
Consider rotating keys periodically:
|
||||
|
||||
1. Generate a new keypair
|
||||
2. Notify administrators of the new public key
|
||||
3. Sign new releases with the new key
|
||||
4. Keep the old key available for verifying existing releases
|
||||
|
||||
### Multiple Keys
|
||||
|
||||
For organizations, consider separate keys for:
|
||||
|
||||
- Development/testing releases
|
||||
- Production releases
|
||||
- Different product teams
|
||||
|
||||
## Requesting Core Trust
|
||||
|
||||
If your extension needs `core` trust level:
|
||||
|
||||
1. **Sign your extension** using the process above
|
||||
2. **Document your public key** with fingerprint
|
||||
3. **Explain why core is needed** in your extension documentation
|
||||
4. **Provide your public key** to administrators
|
||||
|
||||
Administrators will then:
|
||||
1. Add your public key to `trusted_signers`
|
||||
2. Enable `require_core_signatures: True`
|
||||
3. Your signed extension can now run as `core`
|
||||
|
||||
## Verification Process
|
||||
|
||||
When Superset loads your extension:
|
||||
|
||||
1. Reads `manifest.json` and `manifest.sig` from the bundle
|
||||
2. Checks if the extension requests `core` trust level
|
||||
3. If `require_core_signatures` is enabled, verifies the signature
|
||||
4. Checks the signature against all keys in `trusted_signers`
|
||||
5. If verification passes, grants the requested trust level
|
||||
6. If verification fails, downgrades to `default_trust_level`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Signature verification failed"
|
||||
|
||||
- Ensure you're using the matching private key for the public key given to admins
|
||||
- Verify the manifest wasn't modified after signing
|
||||
- Check that the `.sig` file was included in the bundle
|
||||
|
||||
### "Private key must be Ed25519"
|
||||
|
||||
- The signing system only supports Ed25519 keys
|
||||
- Generate a new key using `superset-extensions generate-keys`
|
||||
|
||||
### Administrator Reports Invalid Signature
|
||||
|
||||
- Verify the public key file wasn't corrupted during transfer
|
||||
- Confirm the fingerprint matches between your key and theirs
|
||||
- Re-sign the extension and redistribute
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Signature Algorithm
|
||||
|
||||
Extensions use **Ed25519** signatures:
|
||||
|
||||
- Fast signature generation and verification
|
||||
- Small signature size (64 bytes)
|
||||
- Strong security guarantees
|
||||
- Deterministic signatures (same input always produces same output)
|
||||
|
||||
### Signature Format
|
||||
|
||||
The `manifest.sig` file contains:
|
||||
|
||||
```
|
||||
<base64-encoded Ed25519 signature>
|
||||
```
|
||||
|
||||
The signature is computed over the raw bytes of `manifest.json`.
|
||||
|
||||
### Key Format
|
||||
|
||||
Keys are stored in PEM format:
|
||||
|
||||
**Private key:**
|
||||
```
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEI...
|
||||
-----END PRIVATE KEY-----
|
||||
```
|
||||
|
||||
**Public key:**
|
||||
```
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MCowBQYDK2VwAyEA...
|
||||
-----END PUBLIC KEY-----
|
||||
```
|
||||
@@ -26,6 +26,9 @@ module.exports = {
|
||||
collapsed: true,
|
||||
items: [
|
||||
'contributing/overview',
|
||||
'guidelines/design-guidelines',
|
||||
'guidelines/frontend-style-guidelines',
|
||||
'guidelines/backend-style-guidelines',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -49,17 +52,7 @@ module.exports = {
|
||||
'extensions/development',
|
||||
'extensions/deployment',
|
||||
'extensions/mcp',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Security',
|
||||
collapsed: true,
|
||||
items: [
|
||||
'extensions/security',
|
||||
'extensions/sandbox',
|
||||
'extensions/signing',
|
||||
'extensions/admin-configuration',
|
||||
],
|
||||
},
|
||||
'extensions/security',
|
||||
'extensions/registry',
|
||||
],
|
||||
},
|
||||
@@ -71,5 +64,20 @@ module.exports = {
|
||||
'testing/overview',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'UI Components',
|
||||
collapsed: true,
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'components/index',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'autogenerated',
|
||||
dirName: 'components',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
114
docs/developer_portal/testing/storybook.md
Normal file
@@ -0,0 +1,114 @@
|
||||
---
|
||||
title: Storybook
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
# Storybook
|
||||
|
||||
Superset uses [Storybook](https://storybook.js.org/) for developing and testing UI components in isolation. Storybook provides a sandbox to build components independently, outside of the main application.
|
||||
|
||||
## Public Storybook
|
||||
|
||||
A public Storybook with components from the `master` branch is available at:
|
||||
|
||||
**[apache-superset.github.io/superset-ui](https://apache-superset.github.io/superset-ui/?path=/story/*)**
|
||||
|
||||
## Running Locally
|
||||
|
||||
### Main Superset Storybook
|
||||
|
||||
To run the main Superset Storybook locally:
|
||||
|
||||
```bash
|
||||
cd superset-frontend
|
||||
|
||||
# Start Storybook (opens at http://localhost:6006)
|
||||
npm run storybook
|
||||
|
||||
# Build static Storybook
|
||||
npm run build-storybook
|
||||
```
|
||||
|
||||
### @superset-ui Package Storybook
|
||||
|
||||
The `@superset-ui` packages have a separate Storybook for component library development:
|
||||
|
||||
```bash
|
||||
cd superset-frontend
|
||||
|
||||
# Install dependencies and bootstrap packages
|
||||
npm ci && npm run bootstrap
|
||||
|
||||
# Start the @superset-ui Storybook (opens at http://localhost:9001)
|
||||
cd packages/superset-ui-demo
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
## Adding Stories
|
||||
|
||||
### To an Existing Package
|
||||
|
||||
If stories already exist for the package, extend the `examples` array in the package's story file:
|
||||
|
||||
```
|
||||
storybook/stories/<package>/index.js
|
||||
```
|
||||
|
||||
### To a New Package
|
||||
|
||||
1. Add package dependencies:
|
||||
|
||||
```bash
|
||||
npm install <package>
|
||||
```
|
||||
|
||||
2. Create a story folder matching the package name:
|
||||
|
||||
```bash
|
||||
mkdir storybook/stories/superset-ui-<package>/
|
||||
```
|
||||
|
||||
3. Create an `index.js` file with the story configuration:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
examples: [
|
||||
{
|
||||
storyPath: '@superset-ui/package',
|
||||
storyName: 'My Story',
|
||||
renderStory: () => <MyComponent />,
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
Use the `|` separator for nested stories:
|
||||
```javascript
|
||||
storyPath: '@superset-ui/package|Category|Subcategory'
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Isolate components**: Stories should render components in isolation, without application context
|
||||
- **Show variations**: Create stories for different states, sizes, and configurations
|
||||
- **Document props**: Use Storybook's controls to expose configurable props
|
||||
- **Test edge cases**: Include stories for loading states, error states, and empty states
|
||||
@@ -1,40 +1,590 @@
|
||||
---
|
||||
title: API
|
||||
title: API Reference
|
||||
hide_title: true
|
||||
sidebar_position: 10
|
||||
---
|
||||
|
||||
import SwaggerUI from 'swagger-ui-react';
|
||||
import openapi from '/resources/openapi.json';
|
||||
import 'swagger-ui-react/swagger-ui.css';
|
||||
import { Alert } from 'antd';
|
||||
|
||||
## API
|
||||
## REST API Reference
|
||||
|
||||
Superset's public **REST API** follows the
|
||||
[OpenAPI specification](https://swagger.io/specification/), and is
|
||||
documented here. The docs below are generated using
|
||||
[Swagger React UI](https://www.npmjs.com/package/swagger-ui-react).
|
||||
|
||||
:::resources
|
||||
- [Blog: The Superset REST API](https://preset.io/blog/2020-10-01-superset-api/)
|
||||
- [Blog: Accessing APIs with Superset](https://preset.io/blog/accessing-apis-with-superset/)
|
||||
:::
|
||||
Superset exposes a comprehensive **REST API** that follows the [OpenAPI specification](https://swagger.io/specification/).
|
||||
You can use this API to programmatically interact with Superset for automation, integrations, and custom applications.
|
||||
|
||||
<Alert
|
||||
type="info"
|
||||
message={
|
||||
<div>
|
||||
<strong>NOTE! </strong>
|
||||
You can find an interactive version of this documentation on your local Superset
|
||||
instance at <strong>/swagger/v1</strong> (unless disabled)
|
||||
</div>
|
||||
showIcon
|
||||
message="Code Samples & Schema Documentation"
|
||||
description={
|
||||
<span>
|
||||
Each endpoint includes ready-to-use code samples in <strong>cURL</strong>, <strong>Python</strong>, and <strong>JavaScript</strong>.
|
||||
The sidebar includes <strong>Schema definitions</strong> for detailed data model documentation.
|
||||
</span>
|
||||
}
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
<hr />
|
||||
<div className="swagger-container">
|
||||
<SwaggerUI spec={openapi} />
|
||||
</div>
|
||||
---
|
||||
|
||||
### Authentication
|
||||
|
||||
Most API endpoints require authentication via JWT tokens.
|
||||
|
||||
#### Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Get a JWT token
|
||||
curl -X POST http://localhost:8088/api/v1/security/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username": "admin", "password": "admin", "provider": "db"}'
|
||||
|
||||
# 2. Use the access_token from the response
|
||||
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
http://localhost:8088/api/v1/dashboard/
|
||||
```
|
||||
|
||||
#### Security Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get the CSRF token](./api/get-the-csrf-token) | `/api/v1/security/csrf_token/` |
|
||||
| `POST` | [Get a guest token](./api/get-a-guest-token) | `/api/v1/security/guest_token/` |
|
||||
| `POST` | [Create security login](./api/create-security-login) | `/api/v1/security/login` |
|
||||
| `POST` | [Create security refresh](./api/create-security-refresh) | `/api/v1/security/refresh` |
|
||||
|
||||
---
|
||||
|
||||
### API Endpoints
|
||||
|
||||
#### Core Resources
|
||||
|
||||
<details>
|
||||
<summary><strong>Dashboards</strong> (26 endpoints) — Create, read, update, and delete dashboards.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `DELETE` | [Bulk delete dashboards](./api/bulk-delete-dashboards) | `/api/v1/dashboard/` |
|
||||
| `GET` | [Get a list of dashboards](./api/get-a-list-of-dashboards) | `/api/v1/dashboard/` |
|
||||
| `POST` | [Create a new dashboard](./api/create-a-new-dashboard) | `/api/v1/dashboard/` |
|
||||
| `GET` | [Get metadata information about this API resource (dashboard--info)](./api/get-metadata-information-about-this-api-resource-dashboard-info) | `/api/v1/dashboard/_info` |
|
||||
| `GET` | [Get a dashboard detail information](./api/get-a-dashboard-detail-information) | `/api/v1/dashboard/{id_or_slug}` |
|
||||
| `GET` | [Get a dashboard's chart definitions.](./api/get-a-dashboards-chart-definitions) | `/api/v1/dashboard/{id_or_slug}/charts` |
|
||||
| `POST` | [Create a copy of an existing dashboard](./api/create-a-copy-of-an-existing-dashboard) | `/api/v1/dashboard/{id_or_slug}/copy/` |
|
||||
| `GET` | [Get dashboard's datasets](./api/get-dashboards-datasets) | `/api/v1/dashboard/{id_or_slug}/datasets` |
|
||||
| `DELETE` | [Delete a dashboard's embedded configuration](./api/delete-a-dashboards-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` |
|
||||
| `GET` | [Get the dashboard's embedded configuration](./api/get-the-dashboards-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` |
|
||||
| `POST` | [Set a dashboard's embedded configuration](./api/set-a-dashboards-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` |
|
||||
| `PUT` | [Update dashboard by id_or_slug embedded](./api/update-dashboard-by-id-or-slug-embedded) | `/api/v1/dashboard/{id_or_slug}/embedded` |
|
||||
| `GET` | [Get dashboard's tabs](./api/get-dashboards-tabs) | `/api/v1/dashboard/{id_or_slug}/tabs` |
|
||||
| `DELETE` | [Delete a dashboard](./api/delete-a-dashboard) | `/api/v1/dashboard/{pk}` |
|
||||
| `PUT` | [Update a dashboard](./api/update-a-dashboard) | `/api/v1/dashboard/{pk}` |
|
||||
| `POST` | [Compute and cache a screenshot (dashboard-pk-cache-dashboard-screenshot)](./api/compute-and-cache-a-screenshot-dashboard-pk-cache-dashboard-screenshot) | `/api/v1/dashboard/{pk}/cache_dashboard_screenshot/` |
|
||||
| `PUT` | [Update colors configuration for a dashboard.](./api/update-colors-configuration-for-a-dashboard) | `/api/v1/dashboard/{pk}/colors` |
|
||||
| `DELETE` | [Remove the dashboard from the user favorite list](./api/remove-the-dashboard-from-the-user-favorite-list) | `/api/v1/dashboard/{pk}/favorites/` |
|
||||
| `POST` | [Mark the dashboard as favorite for the current user](./api/mark-the-dashboard-as-favorite-for-the-current-user) | `/api/v1/dashboard/{pk}/favorites/` |
|
||||
| `PUT` | [Update native filters configuration for a dashboard.](./api/update-native-filters-configuration-for-a-dashboard) | `/api/v1/dashboard/{pk}/filters` |
|
||||
| `GET` | [Get a computed screenshot from cache (dashboard-pk-screenshot-digest)](./api/get-a-computed-screenshot-from-cache-dashboard-pk-screenshot-digest) | `/api/v1/dashboard/{pk}/screenshot/{digest}/` |
|
||||
| `GET` | [Get dashboard's thumbnail](./api/get-dashboards-thumbnail) | `/api/v1/dashboard/{pk}/thumbnail/{digest}/` |
|
||||
| `GET` | [Download multiple dashboards as YAML files](./api/download-multiple-dashboards-as-yaml-files) | `/api/v1/dashboard/export/` |
|
||||
| `GET` | [Check favorited dashboards for current user](./api/check-favorited-dashboards-for-current-user) | `/api/v1/dashboard/favorite_status/` |
|
||||
| `POST` | [Import dashboard(s) with associated charts/datasets/databases](./api/import-dashboard-s-with-associated-charts-datasets-databases) | `/api/v1/dashboard/import/` |
|
||||
| `GET` | [Get related fields data (dashboard-related-column-name)](./api/get-related-fields-data-dashboard-related-column-name) | `/api/v1/dashboard/related/{column_name}` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Charts</strong> (20 endpoints) — Create, read, update, and delete charts (slices).</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `DELETE` | [Bulk delete charts](./api/bulk-delete-charts) | `/api/v1/chart/` |
|
||||
| `GET` | [Get a list of charts](./api/get-a-list-of-charts) | `/api/v1/chart/` |
|
||||
| `POST` | [Create a new chart](./api/create-a-new-chart) | `/api/v1/chart/` |
|
||||
| `GET` | [Get metadata information about this API resource (chart--info)](./api/get-metadata-information-about-this-api-resource-chart-info) | `/api/v1/chart/_info` |
|
||||
| `DELETE` | [Delete a chart](./api/delete-a-chart) | `/api/v1/chart/{pk}` |
|
||||
| `GET` | [Get a chart detail information](./api/get-a-chart-detail-information) | `/api/v1/chart/{pk}` |
|
||||
| `PUT` | [Update a chart](./api/update-a-chart) | `/api/v1/chart/{pk}` |
|
||||
| `GET` | [Compute and cache a screenshot (chart-pk-cache-screenshot)](./api/compute-and-cache-a-screenshot-chart-pk-cache-screenshot) | `/api/v1/chart/{pk}/cache_screenshot/` |
|
||||
| `GET` | [Return payload data response for a chart](./api/return-payload-data-response-for-a-chart) | `/api/v1/chart/{pk}/data/` |
|
||||
| `DELETE` | [Remove the chart from the user favorite list](./api/remove-the-chart-from-the-user-favorite-list) | `/api/v1/chart/{pk}/favorites/` |
|
||||
| `POST` | [Mark the chart as favorite for the current user](./api/mark-the-chart-as-favorite-for-the-current-user) | `/api/v1/chart/{pk}/favorites/` |
|
||||
| `GET` | [Get a computed screenshot from cache (chart-pk-screenshot-digest)](./api/get-a-computed-screenshot-from-cache-chart-pk-screenshot-digest) | `/api/v1/chart/{pk}/screenshot/{digest}/` |
|
||||
| `GET` | [Get chart thumbnail](./api/get-chart-thumbnail) | `/api/v1/chart/{pk}/thumbnail/{digest}/` |
|
||||
| `POST` | [Return payload data response for the given query (chart-data)](./api/return-payload-data-response-for-the-given-query-chart-data) | `/api/v1/chart/data` |
|
||||
| `GET` | [Return payload data response for the given query (chart-data-cache-key)](./api/return-payload-data-response-for-the-given-query-chart-data-cache-key) | `/api/v1/chart/data/{cache_key}` |
|
||||
| `GET` | [Download multiple charts as YAML files](./api/download-multiple-charts-as-yaml-files) | `/api/v1/chart/export/` |
|
||||
| `GET` | [Check favorited charts for current user](./api/check-favorited-charts-for-current-user) | `/api/v1/chart/favorite_status/` |
|
||||
| `POST` | [Import chart(s) with associated datasets and databases](./api/import-chart-s-with-associated-datasets-and-databases) | `/api/v1/chart/import/` |
|
||||
| `GET` | [Get related fields data (chart-related-column-name)](./api/get-related-fields-data-chart-related-column-name) | `/api/v1/chart/related/{column_name}` |
|
||||
| `PUT` | [Warm up the cache for the chart](./api/warm-up-the-cache-for-the-chart) | `/api/v1/chart/warm_up_cache` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Datasets</strong> (18 endpoints) — Manage datasets (tables) used for building charts.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `DELETE` | [Bulk delete datasets](./api/bulk-delete-datasets) | `/api/v1/dataset/` |
|
||||
| `GET` | [Get a list of datasets](./api/get-a-list-of-datasets) | `/api/v1/dataset/` |
|
||||
| `POST` | [Create a new dataset](./api/create-a-new-dataset) | `/api/v1/dataset/` |
|
||||
| `GET` | [Get metadata information about this API resource (dataset--info)](./api/get-metadata-information-about-this-api-resource-dataset-info) | `/api/v1/dataset/_info` |
|
||||
| `DELETE` | [Delete a dataset](./api/delete-a-dataset) | `/api/v1/dataset/{pk}` |
|
||||
| `GET` | [Get a dataset](./api/get-a-dataset) | `/api/v1/dataset/{pk}` |
|
||||
| `PUT` | [Update a dataset](./api/update-a-dataset) | `/api/v1/dataset/{pk}` |
|
||||
| `DELETE` | [Delete a dataset column](./api/delete-a-dataset-column) | `/api/v1/dataset/{pk}/column/{column_id}` |
|
||||
| `DELETE` | [Delete a dataset metric](./api/delete-a-dataset-metric) | `/api/v1/dataset/{pk}/metric/{metric_id}` |
|
||||
| `PUT` | [Refresh and update columns of a dataset](./api/refresh-and-update-columns-of-a-dataset) | `/api/v1/dataset/{pk}/refresh` |
|
||||
| `GET` | [Get charts and dashboards count associated to a dataset](./api/get-charts-and-dashboards-count-associated-to-a-dataset) | `/api/v1/dataset/{pk}/related_objects` |
|
||||
| `GET` | [Get distinct values from field data (dataset-distinct-column-name)](./api/get-distinct-values-from-field-data-dataset-distinct-column-name) | `/api/v1/dataset/distinct/{column_name}` |
|
||||
| `POST` | [Duplicate a dataset](./api/duplicate-a-dataset) | `/api/v1/dataset/duplicate` |
|
||||
| `GET` | [Download multiple datasets as YAML files](./api/download-multiple-datasets-as-yaml-files) | `/api/v1/dataset/export/` |
|
||||
| `POST` | [Retrieve a table by name, or create it if it does not exist](./api/retrieve-a-table-by-name-or-create-it-if-it-does-not-exist) | `/api/v1/dataset/get_or_create/` |
|
||||
| `POST` | [Import dataset(s) with associated databases](./api/import-dataset-s-with-associated-databases) | `/api/v1/dataset/import/` |
|
||||
| `GET` | [Get related fields data (dataset-related-column-name)](./api/get-related-fields-data-dataset-related-column-name) | `/api/v1/dataset/related/{column_name}` |
|
||||
| `PUT` | [Warm up the cache for each chart powered by the given table](./api/warm-up-the-cache-for-each-chart-powered-by-the-given-table) | `/api/v1/dataset/warm_up_cache` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Database</strong> (31 endpoints) — Manage database connections and metadata.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get a list of databases](./api/get-a-list-of-databases) | `/api/v1/database/` |
|
||||
| `POST` | [Create a new database](./api/create-a-new-database) | `/api/v1/database/` |
|
||||
| `GET` | [Get metadata information about this API resource (database--info)](./api/get-metadata-information-about-this-api-resource-database-info) | `/api/v1/database/_info` |
|
||||
| `DELETE` | [Delete a database](./api/delete-a-database) | `/api/v1/database/{pk}` |
|
||||
| `GET` | [Get a database](./api/get-a-database) | `/api/v1/database/{pk}` |
|
||||
| `PUT` | [Change a database](./api/change-a-database) | `/api/v1/database/{pk}` |
|
||||
| `GET` | [Get all catalogs from a database](./api/get-all-catalogs-from-a-database) | `/api/v1/database/{pk}/catalogs/` |
|
||||
| `GET` | [Get a database connection info](./api/get-a-database-connection-info) | `/api/v1/database/{pk}/connection` |
|
||||
| `GET` | [Get function names supported by a database](./api/get-function-names-supported-by-a-database) | `/api/v1/database/{pk}/function_names/` |
|
||||
| `GET` | [Get charts and dashboards count associated to a database](./api/get-charts-and-dashboards-count-associated-to-a-database) | `/api/v1/database/{pk}/related_objects/` |
|
||||
| `GET` | [The list of the database schemas where to upload information](./api/the-list-of-the-database-schemas-where-to-upload-information) | `/api/v1/database/{pk}/schemas_access_for_file_upload/` |
|
||||
| `GET` | [Get all schemas from a database](./api/get-all-schemas-from-a-database) | `/api/v1/database/{pk}/schemas/` |
|
||||
| `GET` | [Get database select star for table (database-pk-select-star-table-name)](./api/get-database-select-star-for-table-database-pk-select-star-table-name) | `/api/v1/database/{pk}/select_star/{table_name}/` |
|
||||
| `GET` | [Get database select star for table (database-pk-select-star-table-name-schema-name)](./api/get-database-select-star-for-table-database-pk-select-star-table-name-schema-name) | `/api/v1/database/{pk}/select_star/{table_name}/{schema_name}/` |
|
||||
| `DELETE` | [Delete a SSH tunnel](./api/delete-a-ssh-tunnel) | `/api/v1/database/{pk}/ssh_tunnel/` |
|
||||
| `POST` | [Re-sync all permissions for a database connection](./api/re-sync-all-permissions-for-a-database-connection) | `/api/v1/database/{pk}/sync_permissions/` |
|
||||
| `GET` | [Get table extra metadata (database-pk-table-extra-table-name-schema-name)](./api/get-table-extra-metadata-database-pk-table-extra-table-name-schema-name) | `/api/v1/database/{pk}/table_extra/{table_name}/{schema_name}/` |
|
||||
| `GET` | [Get table metadata](./api/get-table-metadata) | `/api/v1/database/{pk}/table_metadata/` |
|
||||
| `GET` | [Get table extra metadata (database-pk-table-metadata-extra)](./api/get-table-extra-metadata-database-pk-table-metadata-extra) | `/api/v1/database/{pk}/table_metadata/extra/` |
|
||||
| `GET` | [Get database table metadata](./api/get-database-table-metadata) | `/api/v1/database/{pk}/table/{table_name}/{schema_name}/` |
|
||||
| `GET` | [Get a list of tables for given database](./api/get-a-list-of-tables-for-given-database) | `/api/v1/database/{pk}/tables/` |
|
||||
| `POST` | [Upload a file to a database table](./api/upload-a-file-to-a-database-table) | `/api/v1/database/{pk}/upload/` |
|
||||
| `POST` | [Validate arbitrary SQL](./api/validate-arbitrary-sql) | `/api/v1/database/{pk}/validate_sql/` |
|
||||
| `GET` | [Get names of databases currently available](./api/get-names-of-databases-currently-available) | `/api/v1/database/available/` |
|
||||
| `GET` | [Download database(s) and associated dataset(s) as a zip file](./api/download-database-s-and-associated-dataset-s-as-a-zip-file) | `/api/v1/database/export/` |
|
||||
| `POST` | [Import database(s) with associated datasets](./api/import-database-s-with-associated-datasets) | `/api/v1/database/import/` |
|
||||
| `GET` | [Receive personal access tokens from OAuth2](./api/receive-personal-access-tokens-from-o-auth-2) | `/api/v1/database/oauth2/` |
|
||||
| `GET` | [Get related fields data (database-related-column-name)](./api/get-related-fields-data-database-related-column-name) | `/api/v1/database/related/{column_name}` |
|
||||
| `POST` | [Test a database connection](./api/test-a-database-connection) | `/api/v1/database/test_connection/` |
|
||||
| `POST` | [Upload a file and returns file metadata](./api/upload-a-file-and-returns-file-metadata) | `/api/v1/database/upload_metadata/` |
|
||||
| `POST` | [Validate database connection parameters](./api/validate-database-connection-parameters) | `/api/v1/database/validate_parameters/` |
|
||||
|
||||
</details>
|
||||
|
||||
#### Data Exploration
|
||||
|
||||
<details>
|
||||
<summary><strong>Explore</strong> (1 endpoints) — Chart exploration and data querying endpoints.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Assemble Explore related information in a single endpoint](./api/assemble-explore-related-information-in-a-single-endpoint) | `/api/v1/explore/` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>SQL Lab</strong> (6 endpoints) — Execute SQL queries and manage SQL Lab sessions.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get the bootstrap data for SqlLab page](./api/get-the-bootstrap-data-for-sql-lab-page) | `/api/v1/sqllab/` |
|
||||
| `POST` | [Estimate the SQL query execution cost](./api/estimate-the-sql-query-execution-cost) | `/api/v1/sqllab/estimate/` |
|
||||
| `POST` | [Execute a SQL query](./api/execute-a-sql-query) | `/api/v1/sqllab/execute/` |
|
||||
| `GET` | [Export the SQL query results to a CSV](./api/export-the-sql-query-results-to-a-csv) | `/api/v1/sqllab/export/{client_id}/` |
|
||||
| `POST` | [Format SQL code](./api/format-sql-code) | `/api/v1/sqllab/format_sql/` |
|
||||
| `GET` | [Get the result of a SQL query execution](./api/get-the-result-of-a-sql-query-execution) | `/api/v1/sqllab/results/` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Queries</strong> (17 endpoints) — View and manage SQL Lab query history.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get a list of queries](./api/get-a-list-of-queries) | `/api/v1/query/` |
|
||||
| `GET` | [Get query detail information](./api/get-query-detail-information) | `/api/v1/query/{pk}` |
|
||||
| `GET` | [Get distinct values from field data (query-distinct-column-name)](./api/get-distinct-values-from-field-data-query-distinct-column-name) | `/api/v1/query/distinct/{column_name}` |
|
||||
| `GET` | [Get related fields data (query-related-column-name)](./api/get-related-fields-data-query-related-column-name) | `/api/v1/query/related/{column_name}` |
|
||||
| `POST` | [Manually stop a query with client_id](./api/manually-stop-a-query-with-client-id) | `/api/v1/query/stop` |
|
||||
| `GET` | [Get a list of queries that changed after last_updated_ms](./api/get-a-list-of-queries-that-changed-after-last-updated-ms) | `/api/v1/query/updated_since` |
|
||||
| `DELETE` | [Bulk delete saved queries](./api/bulk-delete-saved-queries) | `/api/v1/saved_query/` |
|
||||
| `GET` | [Get a list of saved queries](./api/get-a-list-of-saved-queries) | `/api/v1/saved_query/` |
|
||||
| `POST` | [Create a saved query](./api/create-a-saved-query) | `/api/v1/saved_query/` |
|
||||
| `GET` | [Get metadata information about this API resource (saved-query--info)](./api/get-metadata-information-about-this-api-resource-saved-query-info) | `/api/v1/saved_query/_info` |
|
||||
| `DELETE` | [Delete a saved query](./api/delete-a-saved-query) | `/api/v1/saved_query/{pk}` |
|
||||
| `GET` | [Get a saved query](./api/get-a-saved-query) | `/api/v1/saved_query/{pk}` |
|
||||
| `PUT` | [Update a saved query](./api/update-a-saved-query) | `/api/v1/saved_query/{pk}` |
|
||||
| `GET` | [Get distinct values from field data (saved-query-distinct-column-name)](./api/get-distinct-values-from-field-data-saved-query-distinct-column-name) | `/api/v1/saved_query/distinct/{column_name}` |
|
||||
| `GET` | [Download multiple saved queries as YAML files](./api/download-multiple-saved-queries-as-yaml-files) | `/api/v1/saved_query/export/` |
|
||||
| `POST` | [Import saved queries with associated databases](./api/import-saved-queries-with-associated-databases) | `/api/v1/saved_query/import/` |
|
||||
| `GET` | [Get related fields data (saved-query-related-column-name)](./api/get-related-fields-data-saved-query-related-column-name) | `/api/v1/saved_query/related/{column_name}` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Datasources</strong> (1 endpoints) — Query datasource metadata and column values.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get possible values for a datasource column](./api/get-possible-values-for-a-datasource-column) | `/api/v1/datasource/{datasource_type}/{datasource_id}/column/{column_name}/values/` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Advanced Data Type</strong> (2 endpoints) — Endpoints for advanced data type operations and conversions.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Return an AdvancedDataTypeResponse](./api/return-an-advanced-data-type-response) | `/api/v1/advanced_data_type/convert` |
|
||||
| `GET` | [Return a list of available advanced data types](./api/return-a-list-of-available-advanced-data-types) | `/api/v1/advanced_data_type/types` |
|
||||
|
||||
</details>
|
||||
|
||||
#### Organization & Customization
|
||||
|
||||
<details>
|
||||
<summary><strong>Tags</strong> (15 endpoints) — Organize assets with tags.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `DELETE` | [Bulk delete tags](./api/bulk-delete-tags) | `/api/v1/tag/` |
|
||||
| `GET` | [Get a list of tags](./api/get-a-list-of-tags) | `/api/v1/tag/` |
|
||||
| `POST` | [Create a tag](./api/create-a-tag) | `/api/v1/tag/` |
|
||||
| `GET` | [Get metadata information about tag API endpoints](./api/get-metadata-information-about-tag-api-endpoints) | `/api/v1/tag/_info` |
|
||||
| `POST` | [Add tags to an object](./api/add-tags-to-an-object) | `/api/v1/tag/{object_type}/{object_id}/` |
|
||||
| `DELETE` | [Delete a tagged object](./api/delete-a-tagged-object) | `/api/v1/tag/{object_type}/{object_id}/{tag}/` |
|
||||
| `DELETE` | [Delete a tag](./api/delete-a-tag) | `/api/v1/tag/{pk}` |
|
||||
| `GET` | [Get a tag detail information](./api/get-a-tag-detail-information) | `/api/v1/tag/{pk}` |
|
||||
| `PUT` | [Update a tag](./api/update-a-tag) | `/api/v1/tag/{pk}` |
|
||||
| `DELETE` | [Delete tag by pk favorites](./api/delete-tag-by-pk-favorites) | `/api/v1/tag/{pk}/favorites/` |
|
||||
| `POST` | [Create tag by pk favorites](./api/create-tag-by-pk-favorites) | `/api/v1/tag/{pk}/favorites/` |
|
||||
| `POST` | [Bulk create tags and tagged objects](./api/bulk-create-tags-and-tagged-objects) | `/api/v1/tag/bulk_create` |
|
||||
| `GET` | [Get tag favorite status](./api/get-tag-favorite-status) | `/api/v1/tag/favorite_status/` |
|
||||
| `GET` | [Get all objects associated with a tag](./api/get-all-objects-associated-with-a-tag) | `/api/v1/tag/get_objects/` |
|
||||
| `GET` | [Get related fields data (tag-related-column-name)](./api/get-related-fields-data-tag-related-column-name) | `/api/v1/tag/related/{column_name}` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Annotation Layers</strong> (14 endpoints) — Manage annotation layers and annotations for charts.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `DELETE` | [Delete multiple annotation layers in a bulk operation](./api/delete-multiple-annotation-layers-in-a-bulk-operation) | `/api/v1/annotation_layer/` |
|
||||
| `GET` | [Get a list of annotation layers (annotation-layer)](./api/get-a-list-of-annotation-layers-annotation-layer) | `/api/v1/annotation_layer/` |
|
||||
| `POST` | [Create an annotation layer (annotation-layer)](./api/create-an-annotation-layer-annotation-layer) | `/api/v1/annotation_layer/` |
|
||||
| `GET` | [Get metadata information about this API resource (annotation-layer--info)](./api/get-metadata-information-about-this-api-resource-annotation-layer-info) | `/api/v1/annotation_layer/_info` |
|
||||
| `DELETE` | [Delete annotation layer (annotation-layer-pk)](./api/delete-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` |
|
||||
| `GET` | [Get an annotation layer (annotation-layer-pk)](./api/get-an-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` |
|
||||
| `PUT` | [Update an annotation layer (annotation-layer-pk)](./api/update-an-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` |
|
||||
| `DELETE` | [Bulk delete annotation layers](./api/bulk-delete-annotation-layers) | `/api/v1/annotation_layer/{pk}/annotation/` |
|
||||
| `GET` | [Get a list of annotation layers (annotation-layer-pk-annotation)](./api/get-a-list-of-annotation-layers-annotation-layer-pk-annotation) | `/api/v1/annotation_layer/{pk}/annotation/` |
|
||||
| `POST` | [Create an annotation layer (annotation-layer-pk-annotation)](./api/create-an-annotation-layer-annotation-layer-pk-annotation) | `/api/v1/annotation_layer/{pk}/annotation/` |
|
||||
| `DELETE` | [Delete annotation layer (annotation-layer-pk-annotation-annotation-id)](./api/delete-annotation-layer-annotation-layer-pk-annotation-annotation-id) | `/api/v1/annotation_layer/{pk}/annotation/{annotation_id}` |
|
||||
| `GET` | [Get an annotation layer (annotation-layer-pk-annotation-annotation-id)](./api/get-an-annotation-layer-annotation-layer-pk-annotation-annotation-id) | `/api/v1/annotation_layer/{pk}/annotation/{annotation_id}` |
|
||||
| `PUT` | [Update an annotation layer (annotation-layer-pk-annotation-annotation-id)](./api/update-an-annotation-layer-annotation-layer-pk-annotation-annotation-id) | `/api/v1/annotation_layer/{pk}/annotation/{annotation_id}` |
|
||||
| `GET` | [Get related fields data (annotation-layer-related-column-name)](./api/get-related-fields-data-annotation-layer-related-column-name) | `/api/v1/annotation_layer/related/{column_name}` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>CSS Templates</strong> (8 endpoints) — Manage CSS templates for custom dashboard styling.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `DELETE` | [Bulk delete CSS templates](./api/bulk-delete-css-templates) | `/api/v1/css_template/` |
|
||||
| `GET` | [Get a list of CSS templates](./api/get-a-list-of-css-templates) | `/api/v1/css_template/` |
|
||||
| `POST` | [Create a CSS template](./api/create-a-css-template) | `/api/v1/css_template/` |
|
||||
| `GET` | [Get metadata information about this API resource (css-template--info)](./api/get-metadata-information-about-this-api-resource-css-template-info) | `/api/v1/css_template/_info` |
|
||||
| `DELETE` | [Delete a CSS template](./api/delete-a-css-template) | `/api/v1/css_template/{pk}` |
|
||||
| `GET` | [Get a CSS template](./api/get-a-css-template) | `/api/v1/css_template/{pk}` |
|
||||
| `PUT` | [Update a CSS template](./api/update-a-css-template) | `/api/v1/css_template/{pk}` |
|
||||
| `GET` | [Get related fields data (css-template-related-column-name)](./api/get-related-fields-data-css-template-related-column-name) | `/api/v1/css_template/related/{column_name}` |
|
||||
|
||||
</details>
|
||||
|
||||
#### Sharing & Embedding
|
||||
|
||||
<details>
|
||||
<summary><strong>Dashboard Permanent Link</strong> (2 endpoints) — Create and retrieve permanent links to dashboard states.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | [Create a new dashboard's permanent link](./api/create-a-new-dashboards-permanent-link) | `/api/v1/dashboard/{pk}/permalink` |
|
||||
| `GET` | [Get dashboard's permanent link state](./api/get-dashboards-permanent-link-state) | `/api/v1/dashboard/permalink/{key}` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Explore Permanent Link</strong> (2 endpoints) — Create and retrieve permanent links to chart explore states.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | [Create a new permanent link (explore-permalink)](./api/create-a-new-permanent-link-explore-permalink) | `/api/v1/explore/permalink` |
|
||||
| `GET` | [Get chart's permanent link state](./api/get-charts-permanent-link-state) | `/api/v1/explore/permalink/{key}` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>SQL Lab Permanent Link</strong> (2 endpoints) — Create and retrieve permanent links to SQL Lab states.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | [Create a new permanent link (sqllab-permalink)](./api/create-a-new-permanent-link-sqllab-permalink) | `/api/v1/sqllab/permalink` |
|
||||
| `GET` | [Get permanent link state for SQLLab editor.](./api/get-permanent-link-state-for-sql-lab-editor) | `/api/v1/sqllab/permalink/{key}` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Embedded Dashboard</strong> (1 endpoints) — Configure embedded dashboard settings.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get a report schedule log (embedded-dashboard-uuid)](./api/get-a-report-schedule-log-embedded-dashboard-uuid) | `/api/v1/embedded_dashboard/{uuid}` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Dashboard Filter State</strong> (4 endpoints) — Manage temporary filter state for dashboards.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | [Create a dashboard's filter state](./api/create-a-dashboards-filter-state) | `/api/v1/dashboard/{pk}/filter_state` |
|
||||
| `DELETE` | [Delete a dashboard's filter state value](./api/delete-a-dashboards-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` |
|
||||
| `GET` | [Get a dashboard's filter state value](./api/get-a-dashboards-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` |
|
||||
| `PUT` | [Update a dashboard's filter state value](./api/update-a-dashboards-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Explore Form Data</strong> (4 endpoints) — Manage temporary form data for chart exploration.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | [Create a new form_data](./api/create-a-new-form-data) | `/api/v1/explore/form_data` |
|
||||
| `DELETE` | [Delete a form_data](./api/delete-a-form-data) | `/api/v1/explore/form_data/{key}` |
|
||||
| `GET` | [Get a form_data](./api/get-a-form-data) | `/api/v1/explore/form_data/{key}` |
|
||||
| `PUT` | [Update an existing form_data](./api/update-an-existing-form-data) | `/api/v1/explore/form_data/{key}` |
|
||||
|
||||
</details>
|
||||
|
||||
#### Scheduling & Alerts
|
||||
|
||||
<details>
|
||||
<summary><strong>Report Schedules</strong> (11 endpoints) — Configure scheduled reports and alerts.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `DELETE` | [Bulk delete report schedules](./api/bulk-delete-report-schedules) | `/api/v1/report/` |
|
||||
| `GET` | [Get a list of report schedules](./api/get-a-list-of-report-schedules) | `/api/v1/report/` |
|
||||
| `POST` | [Create a report schedule](./api/create-a-report-schedule) | `/api/v1/report/` |
|
||||
| `GET` | [Get metadata information about this API resource (report--info)](./api/get-metadata-information-about-this-api-resource-report-info) | `/api/v1/report/_info` |
|
||||
| `DELETE` | [Delete a report schedule](./api/delete-a-report-schedule) | `/api/v1/report/{pk}` |
|
||||
| `GET` | [Get a report schedule](./api/get-a-report-schedule) | `/api/v1/report/{pk}` |
|
||||
| `PUT` | [Update a report schedule](./api/update-a-report-schedule) | `/api/v1/report/{pk}` |
|
||||
| `GET` | [Get a list of report schedule logs](./api/get-a-list-of-report-schedule-logs) | `/api/v1/report/{pk}/log/` |
|
||||
| `GET` | [Get a report schedule log (report-pk-log-log-id)](./api/get-a-report-schedule-log-report-pk-log-log-id) | `/api/v1/report/{pk}/log/{log_id}` |
|
||||
| `GET` | [Get related fields data (report-related-column-name)](./api/get-related-fields-data-report-related-column-name) | `/api/v1/report/related/{column_name}` |
|
||||
| `GET` | [Get slack channels](./api/get-slack-channels) | `/api/v1/report/slack_channels/` |
|
||||
|
||||
</details>
|
||||
|
||||
#### Security & Access Control
|
||||
|
||||
<details>
|
||||
<summary><strong>Security Roles</strong> (10 endpoints) — Manage security roles and their permissions.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get security roles](./api/get-security-roles) | `/api/v1/security/roles/` |
|
||||
| `POST` | [Create security roles](./api/create-security-roles) | `/api/v1/security/roles/` |
|
||||
| `GET` | [Get security roles info](./api/get-security-roles-info) | `/api/v1/security/roles/_info` |
|
||||
| `DELETE` | [Delete security roles by pk](./api/delete-security-roles-by-pk) | `/api/v1/security/roles/{pk}` |
|
||||
| `GET` | [Get security roles by pk](./api/get-security-roles-by-pk) | `/api/v1/security/roles/{pk}` |
|
||||
| `PUT` | [Update security roles by pk](./api/update-security-roles-by-pk) | `/api/v1/security/roles/{pk}` |
|
||||
| `POST` | [Create security roles by role_id permissions](./api/create-security-roles-by-role-id-permissions) | `/api/v1/security/roles/{role_id}/permissions` |
|
||||
| `GET` | [Get security roles by role_id permissions](./api/get-security-roles-by-role-id-permissions) | `/api/v1/security/roles/{role_id}/permissions/` |
|
||||
| `PUT` | [Update security roles by role_id users](./api/update-security-roles-by-role-id-users) | `/api/v1/security/roles/{role_id}/users` |
|
||||
| `GET` | [List roles](./api/list-roles) | `/api/v1/security/roles/search/` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Security Users</strong> (6 endpoints) — Manage user accounts.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get security users](./api/get-security-users) | `/api/v1/security/users/` |
|
||||
| `POST` | [Create security users](./api/create-security-users) | `/api/v1/security/users/` |
|
||||
| `GET` | [Get security users info](./api/get-security-users-info) | `/api/v1/security/users/_info` |
|
||||
| `DELETE` | [Delete security users by pk](./api/delete-security-users-by-pk) | `/api/v1/security/users/{pk}` |
|
||||
| `GET` | [Get security users by pk](./api/get-security-users-by-pk) | `/api/v1/security/users/{pk}` |
|
||||
| `PUT` | [Update security users by pk](./api/update-security-users-by-pk) | `/api/v1/security/users/{pk}` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Security Permissions</strong> (3 endpoints) — View available permissions.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get security permissions](./api/get-security-permissions) | `/api/v1/security/permissions/` |
|
||||
| `GET` | [Get security permissions info](./api/get-security-permissions-info) | `/api/v1/security/permissions/_info` |
|
||||
| `GET` | [Get security permissions by pk](./api/get-security-permissions-by-pk) | `/api/v1/security/permissions/{pk}` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Security Resources (View Menus)</strong> (6 endpoints) — Manage security resources (view menus).</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get security resources](./api/get-security-resources) | `/api/v1/security/resources/` |
|
||||
| `POST` | [Create security resources](./api/create-security-resources) | `/api/v1/security/resources/` |
|
||||
| `GET` | [Get security resources info](./api/get-security-resources-info) | `/api/v1/security/resources/_info` |
|
||||
| `DELETE` | [Delete security resources by pk](./api/delete-security-resources-by-pk) | `/api/v1/security/resources/{pk}` |
|
||||
| `GET` | [Get security resources by pk](./api/get-security-resources-by-pk) | `/api/v1/security/resources/{pk}` |
|
||||
| `PUT` | [Update security resources by pk](./api/update-security-resources-by-pk) | `/api/v1/security/resources/{pk}` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Security Permissions on Resources (View Menus)</strong> (6 endpoints) — Manage permission-resource mappings.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get security permissions resources](./api/get-security-permissions-resources) | `/api/v1/security/permissions-resources/` |
|
||||
| `POST` | [Create security permissions resources](./api/create-security-permissions-resources) | `/api/v1/security/permissions-resources/` |
|
||||
| `GET` | [Get security permissions resources info](./api/get-security-permissions-resources-info) | `/api/v1/security/permissions-resources/_info` |
|
||||
| `DELETE` | [Delete security permissions resources by pk](./api/delete-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` |
|
||||
| `GET` | [Get security permissions resources by pk](./api/get-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` |
|
||||
| `PUT` | [Update security permissions resources by pk](./api/update-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Row Level Security</strong> (8 endpoints) — Manage row-level security rules for data access control.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `DELETE` | [Bulk delete RLS rules](./api/bulk-delete-rls-rules) | `/api/v1/rowlevelsecurity/` |
|
||||
| `GET` | [Get a list of RLS](./api/get-a-list-of-rls) | `/api/v1/rowlevelsecurity/` |
|
||||
| `POST` | [Create a new RLS rule](./api/create-a-new-rls-rule) | `/api/v1/rowlevelsecurity/` |
|
||||
| `GET` | [Get metadata information about this API resource (rowlevelsecurity--info)](./api/get-metadata-information-about-this-api-resource-rowlevelsecurity-info) | `/api/v1/rowlevelsecurity/_info` |
|
||||
| `DELETE` | [Delete an RLS](./api/delete-an-rls) | `/api/v1/rowlevelsecurity/{pk}` |
|
||||
| `GET` | [Get an RLS](./api/get-an-rls) | `/api/v1/rowlevelsecurity/{pk}` |
|
||||
| `PUT` | [Update an RLS rule](./api/update-an-rls-rule) | `/api/v1/rowlevelsecurity/{pk}` |
|
||||
| `GET` | [Get related fields data (rowlevelsecurity-related-column-name)](./api/get-related-fields-data-rowlevelsecurity-related-column-name) | `/api/v1/rowlevelsecurity/related/{column_name}` |
|
||||
|
||||
</details>
|
||||
|
||||
#### Import/Export & Administration
|
||||
|
||||
<details>
|
||||
<summary><strong>Import/export</strong> (2 endpoints) — Import and export Superset assets (dashboards, charts, databases).</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Export all assets](./api/export-all-assets) | `/api/v1/assets/export/` |
|
||||
| `POST` | [Import multiple assets](./api/import-multiple-assets) | `/api/v1/assets/import/` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>CacheRestApi</strong> (1 endpoints) — Cache management and invalidation operations.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | [Invalidate cache records and remove the database records](./api/invalidate-cache-records-and-remove-the-database-records) | `/api/v1/cachekey/invalidate` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>LogRestApi</strong> (4 endpoints) — Access audit logs and activity history.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get a list of logs](./api/get-a-list-of-logs) | `/api/v1/log/` |
|
||||
| `POST` | [Create log](./api/create-log) | `/api/v1/log/` |
|
||||
| `GET` | [Get a log detail information](./api/get-a-log-detail-information) | `/api/v1/log/{pk}` |
|
||||
| `GET` | [Get recent activity data for a user](./api/get-recent-activity-data-for-a-user) | `/api/v1/log/recent_activity/` |
|
||||
|
||||
</details>
|
||||
|
||||
#### User & System
|
||||
|
||||
<details>
|
||||
<summary><strong>Current User</strong> (2 endpoints) — Get information about the currently authenticated user.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get the user object](./api/get-the-user-object) | `/api/v1/me/` |
|
||||
| `GET` | [Get the user roles](./api/get-the-user-roles) | `/api/v1/me/roles/` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>User</strong> (1 endpoints) — User profile and preferences.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get the user avatar](./api/get-the-user-avatar) | `/api/v1/user/{user_id}/avatar.png` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Menu</strong> (1 endpoints) — Get the Superset menu structure.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get menu](./api/get-menu) | `/api/v1/menu/` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Available Domains</strong> (1 endpoints) — Get available domains for the Superset instance.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get all available domains](./api/get-all-available-domains) | `/api/v1/available_domains/` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>AsyncEventsRestApi</strong> (1 endpoints) — Real-time event streaming via Server-Sent Events (SSE).</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Read off of the Redis events stream](./api/read-off-of-the-redis-events-stream) | `/api/v1/async_event/` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>OpenApi</strong> (1 endpoints) — Access the OpenAPI specification.</summary>
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get api by version openapi](./api/get-api-by-version-openapi) | `/api/{version}/_openapi` |
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Additional Resources
|
||||
|
||||
- [Superset REST API Blog Post](https://preset.io/blog/2020-10-01-superset-api/)
|
||||
- [Accessing APIs with Superset](https://preset.io/blog/accessing-apis-with-superset/)
|
||||
|
||||
@@ -1,7 +1,63 @@
|
||||
---
|
||||
sidebar_position: 9
|
||||
title: Frequently Asked Questions
|
||||
description: Common questions about Apache Superset including performance, database support, visualizations, and configuration.
|
||||
keywords: [superset faq, superset questions, superset help, data visualization faq]
|
||||
---
|
||||
|
||||
import FAQSchema from '@site/src/components/FAQSchema';
|
||||
|
||||
<FAQSchema faqs={[
|
||||
{
|
||||
question: "How big of a dataset can Superset handle?",
|
||||
answer: "Superset can work with even gigantic databases. Superset acts as a thin layer above your underlying databases or data engines, which do all the processing. Superset simply visualizes the results of the query. The key to achieving acceptable performance is whether your database can execute queries and return results at acceptable speed."
|
||||
},
|
||||
{
|
||||
question: "What are the computing specifications required to run Superset?",
|
||||
answer: "The specs depend on how many users you have and their activity, not on the size of your data. Community members have reported 8GB RAM, 2vCPUs as adequate for a moderately-sized instance. Monitor your resource usage and adjust as needed."
|
||||
},
|
||||
{
|
||||
question: "Can I join or query multiple tables at one time?",
|
||||
answer: "Not in the Explore or Visualization UI directly. A Superset SQLAlchemy datasource can only be a single table or a view. You can create a view that joins tables, or use SQL Lab where you can write SQL queries to join multiple tables."
|
||||
},
|
||||
{
|
||||
question: "How do I create my own visualization?",
|
||||
answer: "Read the instructions in the Creating Visualization Plugins documentation to learn how to build custom visualizations for Superset."
|
||||
},
|
||||
{
|
||||
question: "Can I upload and visualize CSV data?",
|
||||
answer: "Yes! Superset supports CSV upload functionality. Read the Exploring Data documentation to learn how to enable and use CSV upload."
|
||||
},
|
||||
{
|
||||
question: "Why are my queries timing out?",
|
||||
answer: "There are many possible causes. For SQL Lab, Superset allows queries to run up to 6 hours by default (configurable via SQLLAB_ASYNC_TIME_LIMIT_SEC). For dashboard timeouts, check your gateway/proxy timeout settings and adjust SUPERSET_WEBSERVER_TIMEOUT in superset_config.py."
|
||||
},
|
||||
{
|
||||
question: "Why is the map not visible in the geospatial visualization?",
|
||||
answer: "You need to register a free account at Mapbox.com, obtain an API key, and add it to your .env file at the key MAPBOX_API_KEY."
|
||||
},
|
||||
{
|
||||
question: "What database engine can I use as a backend for Superset?",
|
||||
answer: "Superset is tested using MySQL, PostgreSQL, and SQLite backends for storing its internal metadata. While Superset supports many databases as data sources, only these are recommended for the metadata store in production."
|
||||
},
|
||||
{
|
||||
question: "Does Superset work with my database?",
|
||||
answer: "Superset supports any database with a Python SQLAlchemy dialect and DBAPI driver. Check the Connecting to Databases documentation for the full list of supported databases."
|
||||
},
|
||||
{
|
||||
question: "Does Superset offer a public API?",
|
||||
answer: "Yes, Superset has a public REST API documented using Swagger. Enable FAB_API_SWAGGER_UI in superset_config.py to access interactive API documentation at /swagger/v1."
|
||||
},
|
||||
{
|
||||
question: "Does Superset collect any telemetry data?",
|
||||
answer: "Superset uses Scarf by default to collect basic telemetry data to help maintainers understand version usage. Users can opt out by setting the SCARF_ANALYTICS environment variable to false."
|
||||
},
|
||||
{
|
||||
question: "Does Superset have a trash bin to recover deleted assets?",
|
||||
answer: "No, there is no built-in way to recover deleted dashboards, charts, or datasets. It is recommended to take periodic backups of the metadata database and use export functionality for recovery."
|
||||
}
|
||||
]} />
|
||||
|
||||
# FAQ
|
||||
|
||||
## How big of a dataset can Superset handle?
|
||||
|
||||
@@ -32,7 +32,7 @@ git clone https://github.com/apache/superset
|
||||
$ cd superset
|
||||
|
||||
# Set the repo to the state associated with the latest official version
|
||||
$ git checkout tags/5.0.0
|
||||
$ git checkout tags/6.0.0
|
||||
|
||||
# Fire up Superset using Docker Compose
|
||||
$ docker compose -f docker-compose-image-tag.yml up
|
||||
|
||||
@@ -19,9 +19,11 @@
|
||||
|
||||
import type { Config } from '@docusaurus/types';
|
||||
import type { Options, ThemeConfig } from '@docusaurus/preset-classic';
|
||||
import type * as OpenApiPlugin from 'docusaurus-plugin-openapi-docs';
|
||||
import { themes } from 'prism-react-renderer';
|
||||
import remarkImportPartial from 'remark-import-partial';
|
||||
import remarkLocalizeBadges from './plugins/remark-localize-badges.mjs';
|
||||
import remarkTechArticleSchema from './plugins/remark-tech-article-schema.mjs';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
@@ -45,7 +47,7 @@ if (!versionsConfig.components.disabled) {
|
||||
sidebarPath: require.resolve('./sidebarComponents.js'),
|
||||
editUrl:
|
||||
'https://github.com/apache/superset/edit/master/docs/components',
|
||||
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges],
|
||||
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges, remarkTechArticleSchema],
|
||||
admonitions: {
|
||||
keywords: ['note', 'tip', 'info', 'warning', 'danger', 'resources'],
|
||||
extendDefaults: true,
|
||||
@@ -73,7 +75,7 @@ if (!versionsConfig.developer_portal.disabled) {
|
||||
sidebarPath: require.resolve('./sidebarTutorials.js'),
|
||||
editUrl:
|
||||
'https://github.com/apache/superset/edit/master/docs/developer_portal',
|
||||
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges],
|
||||
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges, remarkTechArticleSchema],
|
||||
admonitions: {
|
||||
keywords: ['note', 'tip', 'info', 'warning', 'danger', 'resources'],
|
||||
extendDefaults: true,
|
||||
@@ -131,6 +133,12 @@ if (!versionsConfig.developer_portal.disabled && !versionsConfig.developer_porta
|
||||
docId: 'index',
|
||||
label: 'Overview',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
docsPluginId: 'developer_portal',
|
||||
docId: 'contributing/overview',
|
||||
label: 'Contributing',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
docsPluginId: 'developer_portal',
|
||||
@@ -146,14 +154,12 @@ if (!versionsConfig.developer_portal.disabled && !versionsConfig.developer_porta
|
||||
{
|
||||
type: 'doc',
|
||||
docsPluginId: 'developer_portal',
|
||||
docId: 'guidelines/design-guidelines',
|
||||
label: 'Guidelines',
|
||||
docId: 'components/index',
|
||||
label: 'UI Components',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
docsPluginId: 'developer_portal',
|
||||
docId: 'contributing/overview',
|
||||
label: 'Contributing',
|
||||
label: 'API Reference',
|
||||
href: '/docs/api',
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -166,29 +172,137 @@ const config: Config = {
|
||||
url: 'https://superset.apache.org',
|
||||
baseUrl: '/',
|
||||
onBrokenLinks: 'warn',
|
||||
onBrokenMarkdownLinks: 'throw',
|
||||
markdown: {
|
||||
mermaid: true,
|
||||
hooks: {
|
||||
onBrokenMarkdownLinks: 'throw',
|
||||
},
|
||||
},
|
||||
favicon: '/img/favicon.ico',
|
||||
organizationName: 'apache',
|
||||
projectName: 'superset',
|
||||
|
||||
// SEO: Structured data (Organization, Software, WebSite with SearchAction)
|
||||
headTags: [
|
||||
// SoftwareApplication schema
|
||||
{
|
||||
tagName: 'script',
|
||||
attributes: {
|
||||
type: 'application/ld+json',
|
||||
},
|
||||
innerHTML: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'Apache Superset',
|
||||
applicationCategory: 'BusinessApplication',
|
||||
operatingSystem: 'Cross-platform',
|
||||
description: 'Apache Superset is a modern, enterprise-ready business intelligence web application for data exploration and visualization.',
|
||||
url: 'https://superset.apache.org',
|
||||
license: 'https://www.apache.org/licenses/LICENSE-2.0',
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
name: 'Apache Software Foundation',
|
||||
url: 'https://www.apache.org/',
|
||||
logo: 'https://www.apache.org/foundation/press/kit/asf_logo.png',
|
||||
},
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '0',
|
||||
priceCurrency: 'USD',
|
||||
},
|
||||
featureList: [
|
||||
'Interactive dashboards',
|
||||
'SQL IDE',
|
||||
'40+ visualization types',
|
||||
'Semantic layer',
|
||||
'Role-based access control',
|
||||
'REST API',
|
||||
],
|
||||
}),
|
||||
},
|
||||
// WebSite schema with SearchAction (enables sitelinks search box in Google)
|
||||
{
|
||||
tagName: 'script',
|
||||
attributes: {
|
||||
type: 'application/ld+json',
|
||||
},
|
||||
innerHTML: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
name: 'Apache Superset',
|
||||
url: 'https://superset.apache.org',
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: {
|
||||
'@type': 'EntryPoint',
|
||||
urlTemplate: 'https://superset.apache.org/search?q={search_term_string}',
|
||||
},
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
}),
|
||||
},
|
||||
// Preconnect hints for faster external resource loading
|
||||
{
|
||||
tagName: 'link',
|
||||
attributes: {
|
||||
rel: 'preconnect',
|
||||
href: 'https://WR5FASX5ED-dsn.algolia.net',
|
||||
crossorigin: 'anonymous',
|
||||
},
|
||||
},
|
||||
{
|
||||
tagName: 'link',
|
||||
attributes: {
|
||||
rel: 'preconnect',
|
||||
href: 'https://analytics.apache.org',
|
||||
},
|
||||
},
|
||||
],
|
||||
themes: [
|
||||
'@saucelabs/theme-github-codeblock',
|
||||
'@docusaurus/theme-mermaid',
|
||||
'@docusaurus/theme-live-codeblock',
|
||||
'docusaurus-theme-openapi-docs',
|
||||
],
|
||||
plugins: [
|
||||
require.resolve('./src/webpack.extend.ts'),
|
||||
...dynamicPlugins,
|
||||
[
|
||||
'docusaurus-plugin-less',
|
||||
'docusaurus-plugin-openapi-docs',
|
||||
{
|
||||
lessOptions: {
|
||||
javascriptEnabled: true,
|
||||
id: 'api',
|
||||
docsPluginId: 'classic',
|
||||
config: {
|
||||
superset: {
|
||||
specPath: 'static/resources/openapi.json',
|
||||
outputDir: 'docs/api',
|
||||
sidebarOptions: {
|
||||
groupPathsBy: 'tag',
|
||||
categoryLinkSource: 'tag',
|
||||
sidebarCollapsible: true,
|
||||
sidebarCollapsed: true,
|
||||
},
|
||||
showSchemas: true,
|
||||
hideSendButton: true,
|
||||
showInfoPage: false,
|
||||
showExtensions: true,
|
||||
} satisfies OpenApiPlugin.Options,
|
||||
},
|
||||
},
|
||||
],
|
||||
...dynamicPlugins,
|
||||
// SEO: Generate robots.txt during build
|
||||
[
|
||||
require.resolve('./plugins/robots-txt-plugin.js'),
|
||||
{
|
||||
policies: [
|
||||
{
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
disallow: ['/api/v1/', '/_next/', '/static/js/*.map'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'@docusaurus/plugin-client-redirects',
|
||||
{
|
||||
@@ -350,7 +464,7 @@ const config: Config = {
|
||||
}
|
||||
return `https://github.com/apache/superset/edit/master/docs/${versionDocsDirPath}/${docPath}`;
|
||||
},
|
||||
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges],
|
||||
remarkPlugins: [remarkImportPartial, remarkLocalizeBadges, remarkTechArticleSchema],
|
||||
admonitions: {
|
||||
keywords: ['note', 'tip', 'info', 'warning', 'danger', 'resources'],
|
||||
extendDefaults: true,
|
||||
@@ -362,6 +476,7 @@ const config: Config = {
|
||||
disableVersioning: false,
|
||||
showLastUpdateAuthor: true,
|
||||
showLastUpdateTime: true,
|
||||
docItemComponent: '@theme/ApiItem', // Required for OpenAPI docs
|
||||
},
|
||||
blog: {
|
||||
showReadingTime: true,
|
||||
@@ -372,11 +487,57 @@ const config: Config = {
|
||||
theme: {
|
||||
customCss: require.resolve('./src/styles/custom.css'),
|
||||
},
|
||||
// SEO: Sitemap configuration with priorities
|
||||
sitemap: {
|
||||
lastmod: 'date',
|
||||
changefreq: 'weekly',
|
||||
priority: 0.5,
|
||||
ignorePatterns: ['/tags/**'],
|
||||
filename: 'sitemap.xml',
|
||||
createSitemapItems: async (params) => {
|
||||
const { defaultCreateSitemapItems, ...rest } = params;
|
||||
const items = await defaultCreateSitemapItems(rest);
|
||||
return items.map((item) => {
|
||||
// Boost priority for key pages
|
||||
if (item.url.includes('/docs/intro')) {
|
||||
return { ...item, priority: 1.0, changefreq: 'daily' };
|
||||
}
|
||||
if (item.url.includes('/docs/quickstart')) {
|
||||
return { ...item, priority: 0.9, changefreq: 'weekly' };
|
||||
}
|
||||
if (item.url.includes('/docs/installation/')) {
|
||||
return { ...item, priority: 0.8, changefreq: 'weekly' };
|
||||
}
|
||||
if (item.url.includes('/docs/databases')) {
|
||||
return { ...item, priority: 0.8, changefreq: 'weekly' };
|
||||
}
|
||||
if (item.url.includes('/docs/faq')) {
|
||||
return { ...item, priority: 0.7, changefreq: 'monthly' };
|
||||
}
|
||||
if (item.url === 'https://superset.apache.org/') {
|
||||
return { ...item, priority: 1.0, changefreq: 'daily' };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
},
|
||||
},
|
||||
} satisfies Options,
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
// SEO: OpenGraph and Twitter meta tags
|
||||
metadata: [
|
||||
{ name: 'keywords', content: 'data visualization, business intelligence, BI, dashboards, SQL, analytics, open source, Apache, charts, reporting' },
|
||||
{ property: 'og:type', content: 'website' },
|
||||
{ property: 'og:site_name', content: 'Apache Superset' },
|
||||
{ property: 'og:image', content: 'https://superset.apache.org/img/superset-og-image.png' },
|
||||
{ property: 'og:image:width', content: '1200' },
|
||||
{ property: 'og:image:height', content: '630' },
|
||||
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||
{ name: 'twitter:image', content: 'https://superset.apache.org/img/superset-og-image.png' },
|
||||
{ name: 'twitter:site', content: '@ApacheSuperset' },
|
||||
],
|
||||
colorMode: {
|
||||
defaultMode: 'dark',
|
||||
disableSwitch: false,
|
||||
@@ -473,8 +634,10 @@ const config: Config = {
|
||||
footer: {
|
||||
links: [],
|
||||
copyright: `
|
||||
<div class="footer__applitools">
|
||||
We use <a href="https://applitools.com/" target="_blank" rel="nofollow"><img src="/img/applitools.png" title="Applitools" /></a>
|
||||
<div class="footer__ci-services">
|
||||
<span>CI powered by</span>
|
||||
<a href="https://applitools.com/" target="_blank" rel="nofollow noopener noreferrer"><img src="/img/applitools.png" alt="Applitools" title="Applitools - Visual Testing" /></a>
|
||||
<a href="https://www.netlify.com/" target="_blank" rel="nofollow noopener noreferrer"><img src="/img/netlify.png" alt="Netlify" title="Netlify - Deploy Previews" /></a>
|
||||
</div>
|
||||
<p>Copyright © ${new Date().getFullYear()},
|
||||
The <a href="https://www.apache.org/" target="_blank" rel="noreferrer">Apache Software Foundation</a>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"copyright": {
|
||||
"message": "\n <div class=\"footer__applitools\">\n We use <a href=\"https://applitools.com/\" target=\"_blank\" rel=\"nofollow\"><img src=\"/img/applitools.png\" title=\"Applitools\" /></a>\n </div>\n <p>Copyright © 2024,\n The <a href=\"https://www.apache.org/\" target=\"_blank\" rel=\"noreferrer\">Apache Software Foundation</a>,\n Licensed under the Apache <a href=\"https://apache.org/licenses/LICENSE-2.0\" target=\"_blank\" rel=\"noreferrer\">License</a>.</p>\n <p><small>Apache Superset, Apache, Superset, the Superset logo, and the Apache feather logo are either registered trademarks or trademarks of The Apache Software Foundation. All other products or name brands are trademarks of their respective holders, including The Apache Software Foundation.\n <a href=\"https://www.apache.org/\" target=\"_blank\">Apache Software Foundation</a> resources</small></p>\n <img class=\"footer__divider\" src=\"/img/community/line.png\" alt=\"Divider\" />\n <p>\n <small>\n <a href=\"/docs/security/\" target=\"_blank\" rel=\"noreferrer\">Security</a> | \n <a href=\"https://www.apache.org/foundation/sponsorship.html\" target=\"_blank\" rel=\"noreferrer\">Donate</a> | \n <a href=\"https://www.apache.org/foundation/thanks.html\" target=\"_blank\" rel=\"noreferrer\">Thanks</a> | \n <a href=\"https://apache.org/events/current-event\" target=\"_blank\" rel=\"noreferrer\">Events</a> | \n <a href=\"https://apache.org/licenses/\" target=\"_blank\" rel=\"noreferrer\">License</a> | \n <a href=\"https://privacy.apache.org/policies/privacy-policy-public.html\" target=\"_blank\" rel=\"noreferrer\">Privacy</a>\n </small>\n </p>\n <!-- telemetry/analytics pixel: -->\n <img referrerPolicy=\"no-referrer-when-downgrade\" src=\"https://static.scarf.sh/a.png?x-pxid=39ae6855-95fc-4566-86e5-360d542b0a68\" />\n ",
|
||||
"message": "\n <div class=\"footer__ci-services\">\n <span>CI powered by</span>\n <a href=\"https://applitools.com/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><img src=\"/img/applitools.png\" alt=\"Applitools\" title=\"Applitools - Visual Testing\" /></a>\n <a href=\"https://www.netlify.com/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><img src=\"/img/netlify.png\" alt=\"Netlify\" title=\"Netlify - Deploy Previews\" /></a>\n </div>\n <p>Copyright © 2026,\n The <a href=\"https://www.apache.org/\" target=\"_blank\" rel=\"noreferrer\">Apache Software Foundation</a>,\n Licensed under the Apache <a href=\"https://apache.org/licenses/LICENSE-2.0\" target=\"_blank\" rel=\"noreferrer\">License</a>.</p>\n <p><small>Apache Superset, Apache, Superset, the Superset logo, and the Apache feather logo are either registered trademarks or trademarks of The Apache Software Foundation. All other products or name brands are trademarks of their respective holders, including The Apache Software Foundation.\n <a href=\"https://www.apache.org/\" target=\"_blank\">Apache Software Foundation</a> resources</small></p>\n <img class=\"footer__divider\" src=\"/img/community/line.png\" alt=\"Divider\" />\n <p>\n <small>\n <a href=\"/docs/security/\" target=\"_blank\" rel=\"noreferrer\">Security</a> | \n <a href=\"https://www.apache.org/foundation/sponsorship.html\" target=\"_blank\" rel=\"noreferrer\">Donate</a> | \n <a href=\"https://www.apache.org/foundation/thanks.html\" target=\"_blank\" rel=\"noreferrer\">Thanks</a> | \n <a href=\"https://apache.org/events/current-event\" target=\"_blank\" rel=\"noreferrer\">Events</a> | \n <a href=\"https://apache.org/licenses/\" target=\"_blank\" rel=\"noreferrer\">License</a> | \n <a href=\"https://privacy.apache.org/policies/privacy-policy-public.html\" target=\"_blank\" rel=\"noreferrer\">Privacy</a>\n </small>\n </p>\n <!-- telemetry/analytics pixel: -->\n <img referrerPolicy=\"no-referrer-when-downgrade\" src=\"https://static.scarf.sh/a.png?x-pxid=39ae6855-95fc-4566-86e5-360d542b0a68\" />\n ",
|
||||
"description": "The footer copyright"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
NODE_VERSION = "20"
|
||||
# Yarn version
|
||||
YARN_VERSION = "1.22.22"
|
||||
# Increase heap size for webpack bundling of Superset UI components
|
||||
NODE_OPTIONS = "--max-old-space-size=8192"
|
||||
|
||||
# Deploy preview settings
|
||||
[context.deploy-preview]
|
||||
|
||||
@@ -6,19 +6,24 @@
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"_init": "cat src/intro_header.txt ../README.md > docs/intro.md",
|
||||
"start": "yarn run _init && yarn run generate:extension-components && yarn run generate:database-docs && NODE_ENV=development docusaurus start",
|
||||
"start": "yarn run _init && yarn run generate:all && NODE_OPTIONS='--max-old-space-size=8192' NODE_ENV=development docusaurus start",
|
||||
"start:quick": "yarn run _init && NODE_OPTIONS='--max-old-space-size=8192' NODE_ENV=development docusaurus start",
|
||||
"stop": "pkill -f 'docusaurus start' || pkill -f 'docusaurus serve' || echo 'No docusaurus server running'",
|
||||
"build": "yarn run _init && yarn run generate:extension-components && yarn run generate:database-docs && DEBUG=docusaurus:* docusaurus build",
|
||||
"build": "yarn run _init && yarn run generate:all && NODE_OPTIONS='--max-old-space-size=8192' DEBUG=docusaurus:* docusaurus build",
|
||||
"generate:api-docs": "python3 scripts/fix-openapi-spec.py && docusaurus gen-api-docs superset && node scripts/convert-api-sidebar.mjs && node scripts/generate-api-index.mjs && node scripts/generate-api-tag-pages.mjs",
|
||||
"clean:api-docs": "docusaurus clean-api-docs superset",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "yarn run _init && docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"typecheck": "yarn run generate:extension-components && yarn run generate:database-docs && tsc",
|
||||
"typecheck": "yarn run generate:all && tsc",
|
||||
"generate:extension-components": "node scripts/generate-extension-components.mjs",
|
||||
"generate:superset-components": "node scripts/generate-superset-components.mjs",
|
||||
"generate:database-docs": "node scripts/generate-database-docs.mjs",
|
||||
"gen-db-docs": "node scripts/generate-database-docs.mjs",
|
||||
"generate:all": "yarn run generate:extension-components & yarn run generate:superset-components & yarn run generate:database-docs & wait && yarn run generate:api-docs",
|
||||
"lint:db-metadata": "python3 ../superset/db_engine_specs/lint_metadata.py",
|
||||
"lint:db-metadata:report": "python3 ../superset/db_engine_specs/lint_metadata.py --markdown -o ../superset/db_engine_specs/METADATA_STATUS.md",
|
||||
"update:readme-db-logos": "node scripts/generate-database-docs.mjs --update-readme",
|
||||
@@ -42,40 +47,45 @@
|
||||
"@emotion/core": "^11.0.0",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@fontsource/fira-code": "^5.2.7",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@saucelabs/theme-github-codeblock": "^0.3.0",
|
||||
"@storybook/addon-docs": "^8.6.15",
|
||||
"@storybook/blocks": "^8.6.11",
|
||||
"@storybook/channels": "^8.6.11",
|
||||
"@storybook/client-logger": "^8.6.11",
|
||||
"@storybook/components": "^8.6.11",
|
||||
"@storybook/core": "^8.6.11",
|
||||
"@storybook/core-events": "^8.6.11",
|
||||
"@storybook/blocks": "^8.6.15",
|
||||
"@storybook/channels": "^8.6.15",
|
||||
"@storybook/client-logger": "^8.6.15",
|
||||
"@storybook/components": "^8.6.15",
|
||||
"@storybook/core": "^8.6.15",
|
||||
"@storybook/core-events": "^8.6.15",
|
||||
"@storybook/csf": "^0.1.13",
|
||||
"@storybook/docs-tools": "^8.6.11",
|
||||
"@storybook/preview-api": "^8.6.11",
|
||||
"@storybook/theming": "^8.6.11",
|
||||
"@storybook/docs-tools": "^8.6.15",
|
||||
"@storybook/preview-api": "^8.6.15",
|
||||
"@storybook/theming": "^8.6.15",
|
||||
"@superset-ui/core": "^0.20.4",
|
||||
"antd": "^6.2.1",
|
||||
"caniuse-lite": "^1.0.30001765",
|
||||
"docusaurus-plugin-less": "^2.0.2",
|
||||
"@swc/core": "^1.15.11",
|
||||
"antd": "^6.2.3",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"caniuse-lite": "^1.0.30001769",
|
||||
"docusaurus-plugin-openapi-docs": "^4.6.0",
|
||||
"docusaurus-theme-openapi-docs": "^4.6.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"json-bigint": "^1.0.0",
|
||||
"less": "^4.5.1",
|
||||
"less-loader": "^12.3.0",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-resize-detector": "^9.1.1",
|
||||
"react-svg-pan-zoom": "^3.13.1",
|
||||
"react-table": "^7.8.0",
|
||||
"remark-import-partial": "^0.0.2",
|
||||
"reselect": "^5.1.1",
|
||||
"storybook": "^8.6.15",
|
||||
"swagger-ui-react": "^5.31.0",
|
||||
"swc-loader": "^0.2.7",
|
||||
"tinycolor2": "^1.4.2",
|
||||
"ts-loader": "^9.5.4",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
"unist-util-visit": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.9.1",
|
||||
@@ -89,11 +99,11 @@
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^17.0.0",
|
||||
"globals": "^17.3.0",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.53.1",
|
||||
"webpack": "^5.104.1"
|
||||
"typescript-eslint": "^8.54.0",
|
||||
"webpack": "^5.105.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -107,5 +117,10 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"resolutions": {
|
||||
"react-redux": "^9.2.0",
|
||||
"@reduxjs/toolkit": "^2.5.0",
|
||||
"baseline-browser-mapping": "^2.9.19"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
|
||||
}
|
||||
|
||||
153
docs/plugins/remark-tech-article-schema.mjs
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Note: visit from unist-util-visit is available if needed for tree traversal
|
||||
|
||||
/**
|
||||
* Remark plugin that automatically injects TechArticle schema import and component
|
||||
* into documentation MDX files based on frontmatter.
|
||||
*
|
||||
* This enables rich snippets for technical documentation in search results.
|
||||
*
|
||||
* Frontmatter options:
|
||||
* - title: (required) Article headline
|
||||
* - description: (required) Article description
|
||||
* - keywords: (optional) Array of keywords
|
||||
* - seo_proficiency: (optional) 'Beginner' or 'Expert', defaults to 'Beginner'
|
||||
* - seo_schema: (optional) Set to false to disable schema injection
|
||||
*/
|
||||
export default function remarkTechArticleSchema() {
|
||||
return (tree, file) => {
|
||||
const frontmatter = file.data.frontMatter || {};
|
||||
|
||||
// Skip if explicitly disabled or missing required fields
|
||||
if (frontmatter.seo_schema === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only add schema if we have title and description
|
||||
if (!frontmatter.title || !frontmatter.description) {
|
||||
return;
|
||||
}
|
||||
|
||||
const title = frontmatter.title;
|
||||
const description = frontmatter.description;
|
||||
const keywords = Array.isArray(frontmatter.keywords) ? frontmatter.keywords : [];
|
||||
const proficiencyLevel = frontmatter.seo_proficiency || 'Beginner';
|
||||
|
||||
// Create the import statement
|
||||
const importNode = {
|
||||
type: 'mdxjsEsm',
|
||||
value: `import TechArticleSchema from '@site/src/components/TechArticleSchema';`,
|
||||
data: {
|
||||
estree: {
|
||||
type: 'Program',
|
||||
sourceType: 'module',
|
||||
body: [
|
||||
{
|
||||
type: 'ImportDeclaration',
|
||||
specifiers: [
|
||||
{
|
||||
type: 'ImportDefaultSpecifier',
|
||||
local: { type: 'Identifier', name: 'TechArticleSchema' },
|
||||
},
|
||||
],
|
||||
source: {
|
||||
type: 'Literal',
|
||||
value: '@site/src/components/TechArticleSchema',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Create the component node for MDX
|
||||
const componentNode = {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'TechArticleSchema',
|
||||
attributes: [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'title',
|
||||
value: title,
|
||||
},
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'description',
|
||||
value: description,
|
||||
},
|
||||
...(keywords.length > 0
|
||||
? [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'keywords',
|
||||
value: {
|
||||
type: 'mdxJsxAttributeValueExpression',
|
||||
value: JSON.stringify(keywords),
|
||||
data: {
|
||||
estree: {
|
||||
type: 'Program',
|
||||
sourceType: 'module',
|
||||
body: [
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'ArrayExpression',
|
||||
elements: keywords.map((k) => ({
|
||||
type: 'Literal',
|
||||
value: k,
|
||||
})),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(proficiencyLevel !== 'Beginner'
|
||||
? [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'proficiencyLevel',
|
||||
value: proficiencyLevel,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
children: [],
|
||||
};
|
||||
|
||||
// Insert import at the beginning
|
||||
tree.children.unshift(importNode);
|
||||
|
||||
// Find the first heading and insert component after it
|
||||
let insertIndex = 1; // Default: after import
|
||||
for (let i = 1; i < tree.children.length; i++) {
|
||||
if (tree.children[i].type === 'heading') {
|
||||
insertIndex = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tree.children.splice(insertIndex, 0, componentNode);
|
||||
};
|
||||
}
|
||||
83
docs/plugins/robots-txt-plugin.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
/**
|
||||
* Docusaurus plugin to generate robots.txt during build
|
||||
* Configuration is passed via plugin options
|
||||
*/
|
||||
module.exports = function robotsTxtPlugin(context, options = {}) {
|
||||
const { siteConfig } = context;
|
||||
const {
|
||||
policies = [{ userAgent: '*', allow: '/' }],
|
||||
additionalSitemaps = [],
|
||||
} = options;
|
||||
|
||||
return {
|
||||
name: 'robots-txt-plugin',
|
||||
|
||||
async postBuild({ outDir }) {
|
||||
const sitemapUrl = `${siteConfig.url}/sitemap.xml`;
|
||||
|
||||
// Build robots.txt content
|
||||
const lines = [];
|
||||
|
||||
// Add policies
|
||||
for (const policy of policies) {
|
||||
lines.push(`User-agent: ${policy.userAgent}`);
|
||||
|
||||
if (policy.allow) {
|
||||
const allows = Array.isArray(policy.allow) ? policy.allow : [policy.allow];
|
||||
for (const allow of allows) {
|
||||
lines.push(`Allow: ${allow}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (policy.disallow) {
|
||||
const disallows = Array.isArray(policy.disallow) ? policy.disallow : [policy.disallow];
|
||||
for (const disallow of disallows) {
|
||||
lines.push(`Disallow: ${disallow}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (policy.crawlDelay) {
|
||||
lines.push(`Crawl-delay: ${policy.crawlDelay}`);
|
||||
}
|
||||
|
||||
lines.push(''); // Empty line between policies
|
||||
}
|
||||
|
||||
// Add sitemaps
|
||||
lines.push(`Sitemap: ${sitemapUrl}`);
|
||||
for (const sitemap of additionalSitemaps) {
|
||||
lines.push(`Sitemap: ${sitemap}`);
|
||||
}
|
||||
|
||||
// Write robots.txt
|
||||
const robotsPath = path.join(outDir, 'robots.txt');
|
||||
fs.writeFileSync(robotsPath, lines.join('\n'));
|
||||
|
||||
console.log('Generated robots.txt');
|
||||
},
|
||||
};
|
||||
};
|
||||
123
docs/scripts/convert-api-sidebar.mjs
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert the generated TypeScript API sidebar to CommonJS format.
|
||||
* This allows the sidebar to be imported by sidebars.js.
|
||||
* Also adds unique keys to duplicate labels to avoid translation conflicts.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const sidebarTsPath = path.join(__dirname, '..', 'docs', 'api', 'sidebar.ts');
|
||||
const sidebarJsPath = path.join(__dirname, '..', 'docs', 'api', 'sidebar.js');
|
||||
|
||||
if (!fs.existsSync(sidebarTsPath)) {
|
||||
console.log('No sidebar.ts found, skipping conversion');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(sidebarTsPath, 'utf8');
|
||||
|
||||
// Remove TypeScript import
|
||||
content = content.replace(/import type.*\n/g, '');
|
||||
|
||||
// Remove type annotation
|
||||
content = content.replace(/: SidebarsConfig/g, '');
|
||||
|
||||
// Change export default to module.exports
|
||||
content = content.replace(
|
||||
/export default sidebar\.apisidebar;/,
|
||||
'module.exports = sidebar.apisidebar;'
|
||||
);
|
||||
|
||||
// Parse the sidebar to add unique keys for duplicate labels
|
||||
// This avoids translation key conflicts when the same label appears multiple times
|
||||
try {
|
||||
// Extract the sidebar object
|
||||
const sidebarMatch = content.match(/const sidebar = (\{[\s\S]*\});/);
|
||||
if (sidebarMatch) {
|
||||
// Use Function constructor instead of eval for safer evaluation
|
||||
const sidebarObj = new Function(`return ${sidebarMatch[1]}`)();
|
||||
|
||||
// First pass: count labels
|
||||
const countLabels = (items) => {
|
||||
const counts = {};
|
||||
const count = (item) => {
|
||||
if (item.type === 'doc' && item.label) {
|
||||
counts[item.label] = (counts[item.label] || 0) + 1;
|
||||
}
|
||||
if (item.items) {
|
||||
item.items.forEach(count);
|
||||
}
|
||||
};
|
||||
items.forEach(count);
|
||||
return counts;
|
||||
};
|
||||
|
||||
const counts = countLabels(sidebarObj.apisidebar);
|
||||
|
||||
// Second pass: add keys to items with duplicate labels
|
||||
const addKeys = (items, prefix = 'api') => {
|
||||
for (const item of items) {
|
||||
if (item.type === 'doc' && item.label && counts[item.label] > 1) {
|
||||
item.key = item.id;
|
||||
}
|
||||
// Also add keys to categories to avoid conflicts with main sidebar categories
|
||||
if (item.type === 'category' && item.label) {
|
||||
item.key = `${prefix}-category-${item.label.toLowerCase().replace(/\s+/g, '-')}`;
|
||||
}
|
||||
if (item.items) {
|
||||
addKeys(item.items, prefix);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addKeys(sidebarObj.apisidebar);
|
||||
|
||||
// Regenerate the content with the updated sidebar
|
||||
content = `const sidebar = ${JSON.stringify(sidebarObj, null, 2)};
|
||||
|
||||
module.exports = sidebar.apisidebar;
|
||||
`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not add unique keys to sidebar:', e.message);
|
||||
// Fall back to simple conversion
|
||||
content = content.replace(
|
||||
/export default sidebar\.apisidebar;/,
|
||||
'module.exports = sidebar.apisidebar;'
|
||||
);
|
||||
}
|
||||
|
||||
// Add header with eslint-disable to allow @ts-nocheck
|
||||
const header = `/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Auto-generated CommonJS sidebar from sidebar.ts
|
||||
* Do not edit directly - run 'yarn generate:api-docs' to regenerate
|
||||
*/
|
||||
|
||||
`;
|
||||
|
||||
fs.writeFileSync(sidebarJsPath, header + content);
|
||||
console.log('Converted sidebar.ts to sidebar.js');
|
||||
296
docs/scripts/extract_custom_errors.py
Normal file
@@ -0,0 +1,296 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Extract custom_errors from database engine specs for documentation.
|
||||
|
||||
This script parses engine spec files to extract error handling information
|
||||
that can be displayed on database documentation pages.
|
||||
|
||||
Usage: python scripts/extract_custom_errors.py
|
||||
Output: JSON mapping of engine spec module names to their custom errors
|
||||
"""
|
||||
|
||||
import ast
|
||||
import json # noqa: TID251 - standalone docs script, not part of superset
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
# Map SupersetErrorType values to human-readable categories and issue codes
|
||||
ERROR_TYPE_INFO = {
|
||||
"CONNECTION_INVALID_USERNAME_ERROR": {
|
||||
"category": "Authentication",
|
||||
"description": "Invalid username",
|
||||
"issue_codes": [1012],
|
||||
},
|
||||
"CONNECTION_INVALID_PASSWORD_ERROR": {
|
||||
"category": "Authentication",
|
||||
"description": "Invalid password",
|
||||
"issue_codes": [1013],
|
||||
},
|
||||
"CONNECTION_ACCESS_DENIED_ERROR": {
|
||||
"category": "Authentication",
|
||||
"description": "Access denied",
|
||||
"issue_codes": [1014, 1015],
|
||||
},
|
||||
"CONNECTION_INVALID_HOSTNAME_ERROR": {
|
||||
"category": "Connection",
|
||||
"description": "Invalid hostname",
|
||||
"issue_codes": [1007],
|
||||
},
|
||||
"CONNECTION_PORT_CLOSED_ERROR": {
|
||||
"category": "Connection",
|
||||
"description": "Port closed or refused",
|
||||
"issue_codes": [1008],
|
||||
},
|
||||
"CONNECTION_HOST_DOWN_ERROR": {
|
||||
"category": "Connection",
|
||||
"description": "Host unreachable",
|
||||
"issue_codes": [1009],
|
||||
},
|
||||
"CONNECTION_UNKNOWN_DATABASE_ERROR": {
|
||||
"category": "Connection",
|
||||
"description": "Unknown database",
|
||||
"issue_codes": [1015],
|
||||
},
|
||||
"CONNECTION_DATABASE_PERMISSIONS_ERROR": {
|
||||
"category": "Permissions",
|
||||
"description": "Insufficient permissions",
|
||||
"issue_codes": [1017],
|
||||
},
|
||||
"CONNECTION_MISSING_PARAMETERS_ERROR": {
|
||||
"category": "Configuration",
|
||||
"description": "Missing parameters",
|
||||
"issue_codes": [1018],
|
||||
},
|
||||
"CONNECTION_DATABASE_TIMEOUT": {
|
||||
"category": "Connection",
|
||||
"description": "Connection timeout",
|
||||
"issue_codes": [1001, 1009],
|
||||
},
|
||||
"COLUMN_DOES_NOT_EXIST_ERROR": {
|
||||
"category": "Query",
|
||||
"description": "Column not found",
|
||||
"issue_codes": [1003, 1004],
|
||||
},
|
||||
"TABLE_DOES_NOT_EXIST_ERROR": {
|
||||
"category": "Query",
|
||||
"description": "Table not found",
|
||||
"issue_codes": [1003, 1005],
|
||||
},
|
||||
"SCHEMA_DOES_NOT_EXIST_ERROR": {
|
||||
"category": "Query",
|
||||
"description": "Schema not found",
|
||||
"issue_codes": [1003, 1016],
|
||||
},
|
||||
"SYNTAX_ERROR": {
|
||||
"category": "Query",
|
||||
"description": "SQL syntax error",
|
||||
"issue_codes": [1030],
|
||||
},
|
||||
"OBJECT_DOES_NOT_EXIST_ERROR": {
|
||||
"category": "Query",
|
||||
"description": "Object not found",
|
||||
"issue_codes": [1029],
|
||||
},
|
||||
"GENERIC_DB_ENGINE_ERROR": {
|
||||
"category": "General",
|
||||
"description": "Database engine error",
|
||||
"issue_codes": [1002],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def extract_string_from_call(node: ast.Call) -> str | None:
|
||||
"""Extract string from __() or _() translation calls."""
|
||||
if not node.args:
|
||||
return None
|
||||
arg = node.args[0]
|
||||
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
|
||||
return arg.value
|
||||
elif isinstance(arg, ast.JoinedStr):
|
||||
# f-string - try to reconstruct
|
||||
parts = []
|
||||
for value in arg.values:
|
||||
if isinstance(value, ast.Constant):
|
||||
parts.append(str(value.value))
|
||||
elif isinstance(value, ast.FormattedValue):
|
||||
# Just use a placeholder
|
||||
parts.append("{...}")
|
||||
return "".join(parts)
|
||||
return None
|
||||
|
||||
|
||||
def extract_custom_errors_from_file(filepath: Path) -> dict[str, list[dict[str, Any]]]:
|
||||
"""
|
||||
Extract custom_errors definitions from a Python engine spec file.
|
||||
|
||||
Returns a dict mapping class names to their custom errors list.
|
||||
"""
|
||||
results = {}
|
||||
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
source = f.read()
|
||||
|
||||
tree = ast.parse(source)
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ClassDef):
|
||||
class_name = node.name
|
||||
|
||||
for item in node.body:
|
||||
# Look for custom_errors = { ... }
|
||||
if (
|
||||
isinstance(item, ast.AnnAssign)
|
||||
and isinstance(item.target, ast.Name)
|
||||
and item.target.id == "custom_errors"
|
||||
and isinstance(item.value, ast.Dict)
|
||||
):
|
||||
errors = extract_errors_from_dict(item.value, source)
|
||||
if errors:
|
||||
results[class_name] = errors
|
||||
|
||||
# Also handle simple assignment: custom_errors = { ... }
|
||||
elif (
|
||||
isinstance(item, ast.Assign)
|
||||
and len(item.targets) == 1
|
||||
and isinstance(item.targets[0], ast.Name)
|
||||
and item.targets[0].id == "custom_errors"
|
||||
and isinstance(item.value, ast.Dict)
|
||||
):
|
||||
errors = extract_errors_from_dict(item.value, source)
|
||||
if errors:
|
||||
results[class_name] = errors
|
||||
|
||||
except (OSError, SyntaxError, ValueError) as e:
|
||||
print(f"Error parsing {filepath}: {e}", file=sys.stderr)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def extract_regex_info(key: ast.expr) -> dict[str, Any]:
|
||||
"""Extract regex pattern info from the dict key."""
|
||||
if isinstance(key, ast.Name):
|
||||
return {"regex_name": key.id}
|
||||
if isinstance(key, ast.Call):
|
||||
if (
|
||||
isinstance(key.func, ast.Attribute)
|
||||
and key.func.attr == "compile"
|
||||
and key.args
|
||||
and isinstance(key.args[0], ast.Constant)
|
||||
):
|
||||
return {"regex_pattern": key.args[0].value}
|
||||
return {}
|
||||
|
||||
|
||||
def extract_invalid_fields(extra_node: ast.Dict) -> list[str]:
|
||||
"""Extract invalid fields from the extra dict."""
|
||||
for k, v in zip(extra_node.keys, extra_node.values, strict=False):
|
||||
if (
|
||||
isinstance(k, ast.Constant)
|
||||
and k.value == "invalid"
|
||||
and isinstance(v, ast.List)
|
||||
):
|
||||
return [elem.value for elem in v.elts if isinstance(elem, ast.Constant)]
|
||||
return []
|
||||
|
||||
|
||||
def extract_error_tuple_info(value: ast.Tuple) -> dict[str, Any]:
|
||||
"""Extract error info from the (message, error_type, extra) tuple."""
|
||||
result: dict[str, Any] = {}
|
||||
|
||||
# First element: message template
|
||||
msg_node = value.elts[0]
|
||||
if isinstance(msg_node, ast.Call):
|
||||
message = extract_string_from_call(msg_node)
|
||||
if message:
|
||||
result["message_template"] = message
|
||||
elif isinstance(msg_node, ast.Constant):
|
||||
result["message_template"] = msg_node.value
|
||||
|
||||
# Second element: SupersetErrorType.SOMETHING
|
||||
type_node = value.elts[1]
|
||||
if isinstance(type_node, ast.Attribute):
|
||||
error_type = type_node.attr
|
||||
result["error_type"] = error_type
|
||||
if error_type in ERROR_TYPE_INFO:
|
||||
type_info = ERROR_TYPE_INFO[error_type]
|
||||
result["category"] = type_info["category"]
|
||||
result["description"] = type_info["description"]
|
||||
result["issue_codes"] = type_info["issue_codes"]
|
||||
|
||||
# Third element: extra dict with invalid fields
|
||||
if len(value.elts) >= 3 and isinstance(value.elts[2], ast.Dict):
|
||||
invalid_fields = extract_invalid_fields(value.elts[2])
|
||||
if invalid_fields:
|
||||
result["invalid_fields"] = invalid_fields
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def extract_errors_from_dict(dict_node: ast.Dict, source: str) -> list[dict[str, Any]]:
|
||||
"""Extract error information from a custom_errors dict AST node."""
|
||||
errors = []
|
||||
|
||||
for key, value in zip(dict_node.keys, dict_node.values, strict=False):
|
||||
if key is None or value is None:
|
||||
continue
|
||||
|
||||
error_info = extract_regex_info(key)
|
||||
|
||||
if isinstance(value, ast.Tuple) and len(value.elts) >= 2:
|
||||
error_info.update(extract_error_tuple_info(value))
|
||||
|
||||
if error_info.get("error_type") and error_info.get("message_template"):
|
||||
errors.append(error_info)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Main function to extract custom_errors from all engine specs."""
|
||||
# Find the superset root directory
|
||||
script_dir = Path(__file__).parent
|
||||
root_dir = script_dir.parent.parent
|
||||
specs_dir = root_dir / "superset" / "db_engine_specs"
|
||||
|
||||
if not specs_dir.exists():
|
||||
print(f"Error: Engine specs directory not found: {specs_dir}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
all_errors = {}
|
||||
|
||||
# Process each Python file in the specs directory
|
||||
for filepath in sorted(specs_dir.glob("*.py")):
|
||||
if filepath.name.startswith("_"):
|
||||
continue
|
||||
|
||||
module_name = filepath.stem
|
||||
class_errors = extract_custom_errors_from_file(filepath)
|
||||
|
||||
if class_errors:
|
||||
# Store errors by module and class
|
||||
all_errors[module_name] = class_errors
|
||||
|
||||
# Output as JSON
|
||||
print(json.dumps(all_errors, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
828
docs/scripts/fix-openapi-spec.py
Normal file
@@ -0,0 +1,828 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Fix missing schema references in the OpenAPI spec.
|
||||
|
||||
This script patches the openapi.json file to add any missing schemas
|
||||
that are referenced but not defined.
|
||||
"""
|
||||
|
||||
import json # noqa: TID251 - standalone docs script
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def add_missing_schemas(spec: dict[str, Any]) -> tuple[dict[str, Any], list[str]]:
|
||||
"""Add missing schema definitions to the OpenAPI spec."""
|
||||
schemas = spec.get("components", {}).get("schemas", {})
|
||||
fixed = []
|
||||
|
||||
# DashboardScreenshotPostSchema - based on superset/dashboards/schemas.py
|
||||
if "DashboardScreenshotPostSchema" not in schemas:
|
||||
schemas["DashboardScreenshotPostSchema"] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dataMask": {
|
||||
"type": "object",
|
||||
"description": "An object representing the data mask.",
|
||||
"additionalProperties": True,
|
||||
},
|
||||
"activeTabs": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "A list representing active tabs.",
|
||||
},
|
||||
"anchor": {
|
||||
"type": "string",
|
||||
"description": "A string representing the anchor.",
|
||||
},
|
||||
"urlParams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
},
|
||||
"description": "A list of tuples, each containing two strings.",
|
||||
},
|
||||
},
|
||||
}
|
||||
fixed.append("DashboardScreenshotPostSchema")
|
||||
|
||||
# DashboardNativeFiltersConfigUpdateSchema - based on superset/dashboards/schemas.py
|
||||
if "DashboardNativeFiltersConfigUpdateSchema" not in schemas:
|
||||
schemas["DashboardNativeFiltersConfigUpdateSchema"] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"deleted": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of deleted filter IDs.",
|
||||
},
|
||||
"modified": {
|
||||
"type": "array",
|
||||
"items": {"type": "object"},
|
||||
"description": "List of modified filter configurations.",
|
||||
},
|
||||
"reordered": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of filter IDs in new order.",
|
||||
},
|
||||
},
|
||||
}
|
||||
fixed.append("DashboardNativeFiltersConfigUpdateSchema")
|
||||
|
||||
# DashboardColorsConfigUpdateSchema - based on superset/dashboards/schemas.py
|
||||
if "DashboardColorsConfigUpdateSchema" not in schemas:
|
||||
schemas["DashboardColorsConfigUpdateSchema"] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color_namespace": {
|
||||
"type": "string",
|
||||
"nullable": True,
|
||||
"description": "The color namespace.",
|
||||
},
|
||||
"color_scheme": {
|
||||
"type": "string",
|
||||
"nullable": True,
|
||||
"description": "The color scheme name.",
|
||||
},
|
||||
"map_label_colors": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"},
|
||||
"description": "Mapping of labels to colors.",
|
||||
},
|
||||
"shared_label_colors": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"},
|
||||
"description": "Shared label colors across charts.",
|
||||
},
|
||||
"label_colors": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"},
|
||||
"description": "Label to color mapping.",
|
||||
},
|
||||
"color_scheme_domain": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Color scheme domain values.",
|
||||
},
|
||||
},
|
||||
}
|
||||
fixed.append("DashboardColorsConfigUpdateSchema")
|
||||
|
||||
# FormatQueryPayloadSchema - based on superset/sqllab/schemas.py
|
||||
if "FormatQueryPayloadSchema" not in schemas:
|
||||
schemas["FormatQueryPayloadSchema"] = {
|
||||
"type": "object",
|
||||
"required": ["sql"],
|
||||
"properties": {
|
||||
"sql": {
|
||||
"type": "string",
|
||||
"description": "The SQL query to format.",
|
||||
},
|
||||
"engine": {
|
||||
"type": "string",
|
||||
"nullable": True,
|
||||
"description": "The database engine.",
|
||||
},
|
||||
"database_id": {
|
||||
"type": "integer",
|
||||
"nullable": True,
|
||||
"description": "The database id.",
|
||||
},
|
||||
"template_params": {
|
||||
"type": "string",
|
||||
"nullable": True,
|
||||
"description": "The SQL query template params as JSON string.",
|
||||
},
|
||||
},
|
||||
}
|
||||
fixed.append("FormatQueryPayloadSchema")
|
||||
|
||||
# get_slack_channels_schema - based on superset/reports/schemas.py
|
||||
if "get_slack_channels_schema" not in schemas:
|
||||
schemas["get_slack_channels_schema"] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"search_string": {
|
||||
"type": "string",
|
||||
"description": "String to search for in channel names.",
|
||||
},
|
||||
"types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["public_channel", "private_channel"],
|
||||
},
|
||||
"description": "Types of channels to search.",
|
||||
},
|
||||
"exact_match": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to match channel names exactly.",
|
||||
},
|
||||
},
|
||||
}
|
||||
fixed.append("get_slack_channels_schema")
|
||||
|
||||
if "components" not in spec:
|
||||
spec["components"] = {}
|
||||
spec["components"]["schemas"] = schemas
|
||||
|
||||
return spec, fixed
|
||||
|
||||
|
||||
def path_to_operation_id(path: str, method: str) -> str:
|
||||
"""Convert a path and method to an operationId."""
|
||||
# Remove /api/v1/ prefix
|
||||
clean_path = path.replace("/api/v1/", "").strip("/")
|
||||
|
||||
# Replace path parameters
|
||||
clean_path = clean_path.replace("{", "by_").replace("}", "")
|
||||
|
||||
# Create operation name
|
||||
method_prefix = {
|
||||
"get": "get",
|
||||
"post": "create",
|
||||
"put": "update",
|
||||
"delete": "delete",
|
||||
"patch": "patch",
|
||||
}.get(method.lower(), method.lower())
|
||||
|
||||
return f"{method_prefix}_{clean_path}".replace("/", "_").replace("-", "_")
|
||||
|
||||
|
||||
def path_to_summary(path: str, method: str) -> str:
|
||||
"""Generate a human-readable summary from path and method."""
|
||||
# Remove /api/v1/ prefix
|
||||
clean_path = path.replace("/api/v1/", "").strip("/")
|
||||
|
||||
# Handle path parameters
|
||||
parts = []
|
||||
for part in clean_path.split("/"):
|
||||
if part.startswith("{") and part.endswith("}"):
|
||||
param = part[1:-1]
|
||||
parts.append(f"by {param}")
|
||||
else:
|
||||
parts.append(part.replace("_", " ").replace("-", " "))
|
||||
|
||||
resource = " ".join(parts)
|
||||
|
||||
method_verb = {
|
||||
"get": "Get",
|
||||
"post": "Create",
|
||||
"put": "Update",
|
||||
"delete": "Delete",
|
||||
"patch": "Update",
|
||||
}.get(method.lower(), method.capitalize())
|
||||
|
||||
return f"{method_verb} {resource}"
|
||||
|
||||
|
||||
def add_missing_operation_ids(spec: dict[str, Any]) -> int:
|
||||
"""Add operationId and summary to operations that are missing them."""
|
||||
fixed_count = 0
|
||||
|
||||
for path, methods in spec.get("paths", {}).items():
|
||||
for method, details in methods.items():
|
||||
if method not in ["get", "post", "put", "delete", "patch"]:
|
||||
continue
|
||||
|
||||
if not isinstance(details, dict):
|
||||
continue
|
||||
|
||||
summary = details.get("summary")
|
||||
operation_id = details.get("operationId")
|
||||
|
||||
if not summary and not operation_id:
|
||||
details["operationId"] = path_to_operation_id(path, method)
|
||||
details["summary"] = path_to_summary(path, method)
|
||||
fixed_count += 1
|
||||
|
||||
return fixed_count
|
||||
|
||||
|
||||
TAG_DESCRIPTIONS = {
|
||||
"Advanced Data Type": "Advanced data type operations and conversions.",
|
||||
"Annotation Layers": "Manage annotation layers and annotations for charts.",
|
||||
"AsyncEventsRestApi": "Real-time event streaming via Server-Sent Events (SSE).",
|
||||
"Available Domains": "Get available domains for the Superset instance.",
|
||||
"CSS Templates": "Manage CSS templates for custom dashboard styling.",
|
||||
"CacheRestApi": "Cache management and invalidation operations.",
|
||||
"Charts": "Create, read, update, and delete charts (slices).",
|
||||
"Current User": "Get information about the authenticated user.",
|
||||
"Dashboard Filter State": "Manage temporary filter state for dashboards.",
|
||||
"Dashboard Permanent Link": "Permanent links to dashboard states.",
|
||||
"Dashboards": "Create, read, update, and delete dashboards.",
|
||||
"Database": "Manage database connections and metadata.",
|
||||
"Datasets": "Manage datasets (tables) used for building charts.",
|
||||
"Datasources": "Query datasource metadata and column values.",
|
||||
"Embedded Dashboard": "Configure embedded dashboard settings.",
|
||||
"Explore": "Chart exploration and data querying endpoints.",
|
||||
"Explore Form Data": "Manage temporary form data for chart exploration.",
|
||||
"Explore Permanent Link": "Permanent links to chart explore states.",
|
||||
"Import/export": "Import and export Superset assets.",
|
||||
"LogRestApi": "Access audit logs and activity history.",
|
||||
"Menu": "Get the Superset menu structure.",
|
||||
"OpenApi": "Access the OpenAPI specification.",
|
||||
"Queries": "View and manage SQL Lab query history.",
|
||||
"Report Schedules": "Configure scheduled reports and alerts.",
|
||||
"Row Level Security": "Manage row-level security rules for data access.",
|
||||
"SQL Lab": "Execute SQL queries and manage SQL Lab sessions.",
|
||||
"SQL Lab Permanent Link": "Permanent links to SQL Lab states.",
|
||||
"Security": "Authentication and token management.",
|
||||
"Security Permissions": "View available permissions.",
|
||||
"Security Permissions on Resources (View Menus)": "Permission-resource mappings.",
|
||||
"Security Resources (View Menus)": "Manage security resources (view menus).",
|
||||
"Security Roles": "Manage security roles and their permissions.",
|
||||
"Security Users": "Manage user accounts.",
|
||||
"Tags": "Organize assets with tags.",
|
||||
"User": "User profile and preferences.",
|
||||
}
|
||||
|
||||
|
||||
def generate_code_sample(
|
||||
method: str, path: str, has_body: bool = False
|
||||
) -> list[dict[str, str]]:
|
||||
"""Generate code samples for an endpoint in multiple languages."""
|
||||
# Clean up path for display
|
||||
example_path = path.replace("{pk}", "1").replace("{id_or_slug}", "1")
|
||||
|
||||
samples = []
|
||||
|
||||
# cURL sample
|
||||
curl_cmd = f'curl -X {method.upper()} "http://localhost:8088{example_path}"'
|
||||
curl_cmd += ' \\\n -H "Authorization: Bearer $ACCESS_TOKEN"'
|
||||
if has_body:
|
||||
curl_cmd += ' \\\n -H "Content-Type: application/json"'
|
||||
curl_cmd += ' \\\n -d \'{"key": "value"}\''
|
||||
|
||||
samples.append(
|
||||
{
|
||||
"lang": "cURL",
|
||||
"label": "cURL",
|
||||
"source": curl_cmd,
|
||||
}
|
||||
)
|
||||
|
||||
# Python sample
|
||||
if method.lower() == "get":
|
||||
python_code = f"""import requests
|
||||
|
||||
response = requests.get(
|
||||
"http://localhost:8088{example_path}",
|
||||
headers={{"Authorization": "Bearer " + access_token}}
|
||||
)
|
||||
print(response.json())"""
|
||||
elif method.lower() == "post":
|
||||
python_code = f"""import requests
|
||||
|
||||
response = requests.post(
|
||||
"http://localhost:8088{example_path}",
|
||||
headers={{"Authorization": "Bearer " + access_token}},
|
||||
json={{"key": "value"}}
|
||||
)
|
||||
print(response.json())"""
|
||||
elif method.lower() == "put":
|
||||
python_code = f"""import requests
|
||||
|
||||
response = requests.put(
|
||||
"http://localhost:8088{example_path}",
|
||||
headers={{"Authorization": "Bearer " + access_token}},
|
||||
json={{"key": "value"}}
|
||||
)
|
||||
print(response.json())"""
|
||||
elif method.lower() == "delete":
|
||||
python_code = f"""import requests
|
||||
|
||||
response = requests.delete(
|
||||
"http://localhost:8088{example_path}",
|
||||
headers={{"Authorization": "Bearer " + access_token}}
|
||||
)
|
||||
print(response.status_code)"""
|
||||
else:
|
||||
python_code = f"""import requests
|
||||
|
||||
response = requests.{method.lower()}(
|
||||
"http://localhost:8088{example_path}",
|
||||
headers={{"Authorization": "Bearer " + access_token}}
|
||||
)
|
||||
print(response.json())"""
|
||||
|
||||
samples.append(
|
||||
{
|
||||
"lang": "Python",
|
||||
"label": "Python",
|
||||
"source": python_code,
|
||||
}
|
||||
)
|
||||
|
||||
# JavaScript sample
|
||||
if method.lower() == "get":
|
||||
js_code = f"""const response = await fetch(
|
||||
"http://localhost:8088{example_path}",
|
||||
{{
|
||||
headers: {{
|
||||
"Authorization": `Bearer ${{accessToken}}`
|
||||
}}
|
||||
}}
|
||||
);
|
||||
const data = await response.json();
|
||||
console.log(data);"""
|
||||
elif method.lower() in ["post", "put", "patch"]:
|
||||
js_code = f"""const response = await fetch(
|
||||
"http://localhost:8088{example_path}",
|
||||
{{
|
||||
method: "{method.upper()}",
|
||||
headers: {{
|
||||
"Authorization": `Bearer ${{accessToken}}`,
|
||||
"Content-Type": "application/json"
|
||||
}},
|
||||
body: JSON.stringify({{ key: "value" }})
|
||||
}}
|
||||
);
|
||||
const data = await response.json();
|
||||
console.log(data);"""
|
||||
else:
|
||||
js_code = f"""const response = await fetch(
|
||||
"http://localhost:8088{example_path}",
|
||||
{{
|
||||
method: "{method.upper()}",
|
||||
headers: {{
|
||||
"Authorization": `Bearer ${{accessToken}}`
|
||||
}}
|
||||
}}
|
||||
);
|
||||
console.log(response.status);"""
|
||||
|
||||
samples.append(
|
||||
{
|
||||
"lang": "JavaScript",
|
||||
"label": "JavaScript",
|
||||
"source": js_code,
|
||||
}
|
||||
)
|
||||
|
||||
return samples
|
||||
|
||||
|
||||
def add_code_samples(spec: dict[str, Any]) -> int:
|
||||
"""Add code samples to all endpoints."""
|
||||
count = 0
|
||||
|
||||
for path, methods in spec.get("paths", {}).items():
|
||||
for method, details in methods.items():
|
||||
if method not in ["get", "post", "put", "delete", "patch"]:
|
||||
continue
|
||||
if not isinstance(details, dict):
|
||||
continue
|
||||
|
||||
# Skip if already has code samples
|
||||
if "x-codeSamples" in details:
|
||||
continue
|
||||
|
||||
# Check if endpoint has a request body
|
||||
has_body = "requestBody" in details
|
||||
|
||||
details["x-codeSamples"] = generate_code_sample(method, path, has_body)
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
|
||||
def configure_servers(spec: dict[str, Any]) -> bool:
|
||||
"""Configure server URLs with variables for flexible API testing."""
|
||||
new_servers = [
|
||||
{
|
||||
"url": "http://localhost:8088",
|
||||
"description": "Local development server",
|
||||
},
|
||||
{
|
||||
"url": "{protocol}://{host}:{port}",
|
||||
"description": "Custom server",
|
||||
"variables": {
|
||||
"protocol": {
|
||||
"default": "http",
|
||||
"enum": ["http", "https"],
|
||||
"description": "HTTP protocol",
|
||||
},
|
||||
"host": {
|
||||
"default": "localhost",
|
||||
"description": "Server hostname or IP",
|
||||
},
|
||||
"port": {
|
||||
"default": "8088",
|
||||
"description": "Server port",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
# Check if already configured
|
||||
existing = spec.get("servers", [])
|
||||
if len(existing) >= 2 and any("variables" in s for s in existing):
|
||||
return False
|
||||
|
||||
spec["servers"] = new_servers
|
||||
return True
|
||||
|
||||
|
||||
def add_tag_definitions(spec: dict[str, Any]) -> int:
|
||||
"""Add tag definitions with descriptions to the OpenAPI spec."""
|
||||
# Collect all unique tags used in operations
|
||||
used_tags: set[str] = set()
|
||||
for _path, methods in spec.get("paths", {}).items():
|
||||
for method, details in methods.items():
|
||||
if method not in ["get", "post", "put", "delete", "patch"]:
|
||||
continue
|
||||
if not isinstance(details, dict):
|
||||
continue
|
||||
tags = details.get("tags", [])
|
||||
used_tags.update(tags)
|
||||
|
||||
# Create tag definitions
|
||||
tag_definitions = []
|
||||
for tag in sorted(used_tags):
|
||||
tag_def = {"name": tag}
|
||||
if tag in TAG_DESCRIPTIONS:
|
||||
tag_def["description"] = TAG_DESCRIPTIONS[tag]
|
||||
else:
|
||||
# Generate a generic description
|
||||
tag_def["description"] = f"Endpoints related to {tag}."
|
||||
tag_definitions.append(tag_def)
|
||||
|
||||
# Only update if we have new tags
|
||||
existing_tags = {t.get("name") for t in spec.get("tags", [])}
|
||||
new_tags = [t for t in tag_definitions if t["name"] not in existing_tags]
|
||||
|
||||
if new_tags or not spec.get("tags"):
|
||||
spec["tags"] = tag_definitions
|
||||
return len(tag_definitions)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def generate_example_from_schema( # noqa: C901
|
||||
schema: dict[str, Any],
|
||||
spec: dict[str, Any],
|
||||
depth: int = 0,
|
||||
max_depth: int = 5,
|
||||
) -> dict[str, Any] | list[Any] | str | int | float | bool | None:
|
||||
"""Generate an example value from an OpenAPI schema definition."""
|
||||
if depth > max_depth:
|
||||
return None
|
||||
|
||||
# Handle $ref
|
||||
if "$ref" in schema:
|
||||
ref_path = schema["$ref"]
|
||||
if ref_path.startswith("#/components/schemas/"):
|
||||
schema_name = ref_path.split("/")[-1]
|
||||
ref_schema = (
|
||||
spec.get("components", {}).get("schemas", {}).get(schema_name, {})
|
||||
)
|
||||
return generate_example_from_schema(ref_schema, spec, depth + 1, max_depth)
|
||||
return None
|
||||
|
||||
# If schema already has an example, use it
|
||||
if "example" in schema:
|
||||
return schema["example"]
|
||||
|
||||
schema_type = schema.get("type", "object")
|
||||
|
||||
if schema_type == "object":
|
||||
properties = schema.get("properties", {})
|
||||
if not properties:
|
||||
# Check for additionalProperties
|
||||
if schema.get("additionalProperties"):
|
||||
return {"key": "value"}
|
||||
return {}
|
||||
|
||||
result = {}
|
||||
for prop_name, prop_schema in properties.items():
|
||||
# Limit object depth and skip large nested objects
|
||||
if depth < max_depth:
|
||||
example_val = generate_example_from_schema(
|
||||
prop_schema, spec, depth + 1, max_depth
|
||||
)
|
||||
if example_val is not None:
|
||||
result[prop_name] = example_val
|
||||
return result
|
||||
|
||||
elif schema_type == "array":
|
||||
items_schema = schema.get("items", {})
|
||||
if items_schema:
|
||||
item_example = generate_example_from_schema(
|
||||
items_schema, spec, depth + 1, max_depth
|
||||
)
|
||||
if item_example is not None:
|
||||
return [item_example]
|
||||
return []
|
||||
|
||||
elif schema_type == "string":
|
||||
# Check for enum
|
||||
if "enum" in schema:
|
||||
return schema["enum"][0]
|
||||
# Check for format
|
||||
fmt = schema.get("format", "")
|
||||
if fmt == "date-time":
|
||||
return "2024-01-15T10:30:00Z"
|
||||
elif fmt == "date":
|
||||
return "2024-01-15"
|
||||
elif fmt == "email":
|
||||
return "user@example.com"
|
||||
elif fmt == "uri" or fmt == "url":
|
||||
return "https://example.com"
|
||||
elif fmt == "uuid":
|
||||
return "550e8400-e29b-41d4-a716-446655440000"
|
||||
# Use description hints or prop name
|
||||
return "string"
|
||||
|
||||
elif schema_type == "integer":
|
||||
if "minimum" in schema:
|
||||
return schema["minimum"]
|
||||
return 1
|
||||
|
||||
elif schema_type == "number":
|
||||
if "minimum" in schema:
|
||||
return schema["minimum"]
|
||||
return 1.0
|
||||
|
||||
elif schema_type == "boolean":
|
||||
return True
|
||||
|
||||
elif schema_type == "null":
|
||||
return None
|
||||
|
||||
# Handle oneOf, anyOf
|
||||
if "oneOf" in schema and schema["oneOf"]:
|
||||
return generate_example_from_schema(
|
||||
schema["oneOf"][0], spec, depth + 1, max_depth
|
||||
)
|
||||
if "anyOf" in schema and schema["anyOf"]:
|
||||
return generate_example_from_schema(
|
||||
schema["anyOf"][0], spec, depth + 1, max_depth
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def add_response_examples(spec: dict[str, Any]) -> int: # noqa: C901
|
||||
"""Add example values to API responses for better documentation."""
|
||||
count = 0
|
||||
|
||||
# First, add examples to standard error responses in components
|
||||
standard_errors = {
|
||||
"400": {"message": "Bad request: Invalid parameters provided"},
|
||||
"401": {"message": "Unauthorized: Authentication required"},
|
||||
"403": {
|
||||
"message": "Forbidden: You don't have permission to access this resource"
|
||||
},
|
||||
"404": {"message": "Not found: The requested resource does not exist"},
|
||||
"422": {"message": "Unprocessable entity: Validation error"},
|
||||
"500": {"message": "Internal server error: An unexpected error occurred"},
|
||||
}
|
||||
|
||||
responses = spec.get("components", {}).get("responses", {})
|
||||
for code, example_value in standard_errors.items():
|
||||
if code in responses:
|
||||
response = responses[code]
|
||||
content = response.get("content", {}).get("application/json", {})
|
||||
if content and "example" not in content:
|
||||
content["example"] = example_value
|
||||
count += 1
|
||||
|
||||
# Now add examples to inline response schemas in operations
|
||||
for _path, methods in spec.get("paths", {}).items():
|
||||
for method, details in methods.items():
|
||||
if method not in ["get", "post", "put", "delete", "patch"]:
|
||||
continue
|
||||
if not isinstance(details, dict):
|
||||
continue
|
||||
|
||||
responses_dict = details.get("responses", {})
|
||||
for _status_code, response in responses_dict.items():
|
||||
# Skip $ref responses (already handled above)
|
||||
if "$ref" in response:
|
||||
continue
|
||||
|
||||
content = response.get("content", {}).get("application/json", {})
|
||||
if not content:
|
||||
continue
|
||||
|
||||
# Skip if already has an example
|
||||
if "example" in content:
|
||||
continue
|
||||
|
||||
schema = content.get("schema", {})
|
||||
if schema:
|
||||
example = generate_example_from_schema(
|
||||
schema, spec, depth=0, max_depth=3
|
||||
)
|
||||
if example is not None and example != {}:
|
||||
content["example"] = example
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
|
||||
def add_request_body_examples(spec: dict[str, Any]) -> int:
|
||||
"""Add example values to API request bodies for better documentation."""
|
||||
count = 0
|
||||
|
||||
for _path, methods in spec.get("paths", {}).items():
|
||||
for method, details in methods.items():
|
||||
if method not in ["post", "put", "patch"]:
|
||||
continue
|
||||
if not isinstance(details, dict):
|
||||
continue
|
||||
|
||||
request_body = details.get("requestBody", {})
|
||||
if not request_body or "$ref" in request_body:
|
||||
continue
|
||||
|
||||
content = request_body.get("content", {}).get("application/json", {})
|
||||
if not content:
|
||||
continue
|
||||
|
||||
# Skip if already has an example
|
||||
if "example" in content:
|
||||
continue
|
||||
|
||||
schema = content.get("schema", {})
|
||||
if schema:
|
||||
example = generate_example_from_schema(
|
||||
schema, spec, depth=0, max_depth=4
|
||||
)
|
||||
if example is not None and example != {}:
|
||||
content["example"] = example
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
|
||||
def make_summaries_unique(spec: dict[str, Any]) -> int: # noqa: C901
|
||||
"""Make duplicate summaries unique by adding context from the path."""
|
||||
summary_info: dict[str, list[tuple[str, str]]] = {}
|
||||
fixed_count = 0
|
||||
|
||||
# First pass: collect all summaries and their paths (regardless of method)
|
||||
for path, methods in spec.get("paths", {}).items():
|
||||
for method, details in methods.items():
|
||||
if method not in ["get", "post", "put", "delete", "patch"]:
|
||||
continue
|
||||
if not isinstance(details, dict):
|
||||
continue
|
||||
summary = details.get("summary")
|
||||
if summary:
|
||||
if summary not in summary_info:
|
||||
summary_info[summary] = []
|
||||
summary_info[summary].append((path, method))
|
||||
|
||||
# Second pass: make duplicate summaries unique
|
||||
for path, methods in spec.get("paths", {}).items():
|
||||
for method, details in methods.items():
|
||||
if method not in ["get", "post", "put", "delete", "patch"]:
|
||||
continue
|
||||
if not isinstance(details, dict):
|
||||
continue
|
||||
summary = details.get("summary")
|
||||
if summary and len(summary_info.get(summary, [])) > 1:
|
||||
# Create a unique suffix from the full path
|
||||
# e.g., /api/v1/chart/{pk}/cache_screenshot/ -> "chart-cache-screenshot"
|
||||
clean_path = path.replace("/api/v1/", "").strip("/")
|
||||
# Remove parameter placeholders and convert to slug
|
||||
clean_path = clean_path.replace("{", "").replace("}", "")
|
||||
path_slug = clean_path.replace("/", "-").replace("_", "-")
|
||||
|
||||
# Check if this suffix is already in the summary
|
||||
if path_slug not in summary.lower():
|
||||
new_summary = f"{summary} ({path_slug})"
|
||||
details["summary"] = new_summary
|
||||
fixed_count += 1
|
||||
|
||||
return fixed_count
|
||||
|
||||
|
||||
def main() -> None: # noqa: C901
|
||||
"""Main function to fix the OpenAPI spec."""
|
||||
script_dir = Path(__file__).parent
|
||||
spec_path = script_dir.parent / "static" / "resources" / "openapi.json"
|
||||
|
||||
if not spec_path.exists():
|
||||
print(f"Error: OpenAPI spec not found at {spec_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Reading OpenAPI spec from {spec_path}")
|
||||
|
||||
with open(spec_path, encoding="utf-8") as f:
|
||||
spec = json.load(f)
|
||||
|
||||
spec, fixed_schemas = add_missing_schemas(spec)
|
||||
fixed_ops = add_missing_operation_ids(spec)
|
||||
fixed_tags = add_tag_definitions(spec)
|
||||
fixed_servers = configure_servers(spec)
|
||||
|
||||
changes_made = False
|
||||
|
||||
if fixed_servers:
|
||||
print("Configured server URLs with variables for flexible API testing")
|
||||
changes_made = True
|
||||
|
||||
if fixed_samples := add_code_samples(spec):
|
||||
print(f"Added code samples to {fixed_samples} endpoints")
|
||||
changes_made = True
|
||||
|
||||
if fixed_examples := add_response_examples(spec):
|
||||
print(f"Added example JSON responses to {fixed_examples} response schemas")
|
||||
changes_made = True
|
||||
|
||||
if fixed_request_examples := add_request_body_examples(spec):
|
||||
print(f"Added example JSON to {fixed_request_examples} request bodies")
|
||||
changes_made = True
|
||||
|
||||
if fixed_schemas:
|
||||
print(f"Added missing schemas: {', '.join(fixed_schemas)}")
|
||||
changes_made = True
|
||||
|
||||
if fixed_ops:
|
||||
print(f"Added operationId/summary to {fixed_ops} operations")
|
||||
changes_made = True
|
||||
|
||||
if fixed_tags:
|
||||
print(f"Added {fixed_tags} tag definitions with descriptions")
|
||||
changes_made = True
|
||||
|
||||
if fixed_summaries := make_summaries_unique(spec):
|
||||
print(f"Made {fixed_summaries} duplicate summaries unique")
|
||||
changes_made = True
|
||||
|
||||
if changes_made:
|
||||
with open(spec_path, "w", encoding="utf-8") as f:
|
||||
json.dump(spec, f, indent=2)
|
||||
f.write("\n") # Ensure trailing newline for pre-commit
|
||||
|
||||
print(f"Updated {spec_path}")
|
||||
else:
|
||||
print("No fixes needed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
277
docs/scripts/generate-api-index.mjs
Normal file
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generates a comprehensive API index MDX file from the OpenAPI spec.
|
||||
* This creates the api.mdx landing page with all endpoints organized by category.
|
||||
*
|
||||
* Uses the generated sidebar to get correct endpoint slugs (the plugin's
|
||||
* slug algorithm differs from a simple slugify, e.g. handling apostrophes
|
||||
* and camelCase differently).
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { createRequire } from 'module';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const SPEC_PATH = path.join(__dirname, '..', 'static', 'resources', 'openapi.json');
|
||||
const SIDEBAR_PATH = path.join(__dirname, '..', 'docs', 'api', 'sidebar.js');
|
||||
const OUTPUT_PATH = path.join(__dirname, '..', 'docs', 'api.mdx');
|
||||
|
||||
// Category groupings for better organization
|
||||
const CATEGORY_GROUPS = {
|
||||
'Authentication': ['Security'],
|
||||
'Core Resources': ['Dashboards', 'Charts', 'Datasets', 'Database'],
|
||||
'Data Exploration': ['Explore', 'SQL Lab', 'Queries', 'Datasources', 'Advanced Data Type'],
|
||||
'Organization & Customization': ['Tags', 'Annotation Layers', 'CSS Templates'],
|
||||
'Sharing & Embedding': [
|
||||
'Dashboard Permanent Link', 'Explore Permanent Link', 'SQL Lab Permanent Link',
|
||||
'Embedded Dashboard', 'Dashboard Filter State', 'Explore Form Data'
|
||||
],
|
||||
'Scheduling & Alerts': ['Report Schedules'],
|
||||
'Security & Access Control': [
|
||||
'Security Roles', 'Security Users', 'Security Permissions',
|
||||
'Security Resources (View Menus)', 'Security Permissions on Resources (View Menus)',
|
||||
'Row Level Security'
|
||||
],
|
||||
'Import/Export & Administration': ['Import/export', 'CacheRestApi', 'LogRestApi'],
|
||||
'User & System': ['Current User', 'User', 'Menu', 'Available Domains', 'AsyncEventsRestApi', 'OpenApi'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a map from sidebar label → doc slug by reading the generated sidebar.
|
||||
* This ensures we use the exact same slugs that docusaurus-openapi-docs generated.
|
||||
*/
|
||||
function buildSlugMap() {
|
||||
const labelToSlug = {};
|
||||
|
||||
try {
|
||||
const sidebar = require(SIDEBAR_PATH);
|
||||
|
||||
const extractDocs = (items) => {
|
||||
for (const item of items) {
|
||||
if (item.type === 'doc' && item.label && item.id) {
|
||||
// id is like "api/create-security-login" → slug "create-security-login"
|
||||
const slug = item.id.replace(/^api\//, '');
|
||||
labelToSlug[item.label] = slug;
|
||||
}
|
||||
if (item.items) extractDocs(item.items);
|
||||
}
|
||||
};
|
||||
|
||||
extractDocs(sidebar);
|
||||
console.log(`Loaded ${Object.keys(labelToSlug).length} slug mappings from sidebar`);
|
||||
} catch {
|
||||
console.warn('Could not read sidebar, will use computed slugs');
|
||||
}
|
||||
|
||||
return labelToSlug;
|
||||
}
|
||||
|
||||
function slugify(text) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
}
|
||||
|
||||
function main() {
|
||||
console.log(`Reading OpenAPI spec from ${SPEC_PATH}`);
|
||||
const spec = JSON.parse(fs.readFileSync(SPEC_PATH, 'utf-8'));
|
||||
|
||||
// Build slug map from the generated sidebar
|
||||
const labelToSlug = buildSlugMap();
|
||||
|
||||
// Build a map of tag -> endpoints
|
||||
const tagEndpoints = {};
|
||||
const tagDescriptions = {};
|
||||
|
||||
// Get tag descriptions
|
||||
for (const tag of spec.tags || []) {
|
||||
tagDescriptions[tag.name] = tag.description || '';
|
||||
}
|
||||
|
||||
// Collect endpoints by tag
|
||||
for (const [pathUrl, methods] of Object.entries(spec.paths || {})) {
|
||||
for (const [method, details] of Object.entries(methods)) {
|
||||
if (!['get', 'post', 'put', 'delete', 'patch'].includes(method)) continue;
|
||||
|
||||
const tags = details.tags || ['Untagged'];
|
||||
const summary = details.summary || `${method.toUpperCase()} ${pathUrl}`;
|
||||
|
||||
// Use sidebar slug if available, fall back to computed slug
|
||||
const slug = labelToSlug[summary] || slugify(summary);
|
||||
|
||||
for (const tag of tags) {
|
||||
if (!tagEndpoints[tag]) {
|
||||
tagEndpoints[tag] = [];
|
||||
}
|
||||
tagEndpoints[tag].push({
|
||||
method: method.toUpperCase(),
|
||||
path: pathUrl,
|
||||
summary,
|
||||
slug,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort endpoints within each tag by path
|
||||
for (const tag of Object.keys(tagEndpoints)) {
|
||||
tagEndpoints[tag].sort((a, b) => a.path.localeCompare(b.path));
|
||||
}
|
||||
|
||||
// Generate MDX content
|
||||
let mdx = `---
|
||||
title: API Reference
|
||||
hide_title: true
|
||||
sidebar_position: 10
|
||||
---
|
||||
|
||||
import { Alert } from 'antd';
|
||||
|
||||
## REST API Reference
|
||||
|
||||
Superset exposes a comprehensive **REST API** that follows the [OpenAPI specification](https://swagger.io/specification/).
|
||||
You can use this API to programmatically interact with Superset for automation, integrations, and custom applications.
|
||||
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="Code Samples & Schema Documentation"
|
||||
description={
|
||||
<span>
|
||||
Each endpoint includes ready-to-use code samples in <strong>cURL</strong>, <strong>Python</strong>, and <strong>JavaScript</strong>.
|
||||
The sidebar includes <strong>Schema definitions</strong> for detailed data model documentation.
|
||||
</span>
|
||||
}
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
`;
|
||||
|
||||
// Track which tags we've rendered
|
||||
const renderedTags = new Set();
|
||||
|
||||
// Render Authentication first (it's critical for using the API)
|
||||
mdx += `### Authentication
|
||||
|
||||
Most API endpoints require authentication via JWT tokens.
|
||||
|
||||
#### Quick Start
|
||||
|
||||
\`\`\`bash
|
||||
# 1. Get a JWT token
|
||||
curl -X POST http://localhost:8088/api/v1/security/login \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"username": "admin", "password": "admin", "provider": "db"}'
|
||||
|
||||
# 2. Use the access_token from the response
|
||||
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \\
|
||||
http://localhost:8088/api/v1/dashboard/
|
||||
\`\`\`
|
||||
|
||||
#### Security Endpoints
|
||||
|
||||
`;
|
||||
|
||||
// Render Security tag endpoints
|
||||
if (tagEndpoints['Security']) {
|
||||
mdx += `| Method | Endpoint | Description |\n`;
|
||||
mdx += `|--------|----------|-------------|\n`;
|
||||
for (const ep of tagEndpoints['Security']) {
|
||||
mdx += `| \`${ep.method}\` | [${ep.summary}](./api/${ep.slug}) | \`${ep.path}\` |\n`;
|
||||
}
|
||||
mdx += '\n';
|
||||
renderedTags.add('Security');
|
||||
}
|
||||
|
||||
mdx += `---\n\n### API Endpoints\n\n`;
|
||||
|
||||
// Render each category group
|
||||
for (const [groupName, groupTags] of Object.entries(CATEGORY_GROUPS)) {
|
||||
if (groupName === 'Authentication') continue; // Already rendered
|
||||
|
||||
const tagsInGroup = groupTags.filter(tag => tagEndpoints[tag] && !renderedTags.has(tag));
|
||||
if (tagsInGroup.length === 0) continue;
|
||||
|
||||
mdx += `#### ${groupName}\n\n`;
|
||||
|
||||
for (const tag of tagsInGroup) {
|
||||
const description = tagDescriptions[tag] || '';
|
||||
const endpoints = tagEndpoints[tag];
|
||||
|
||||
mdx += `<details>\n`;
|
||||
mdx += `<summary><strong>${tag}</strong> (${endpoints.length} endpoints) — ${description}</summary>\n\n`;
|
||||
mdx += `| Method | Endpoint | Description |\n`;
|
||||
mdx += `|--------|----------|-------------|\n`;
|
||||
|
||||
for (const ep of endpoints) {
|
||||
mdx += `| \`${ep.method}\` | [${ep.summary}](./api/${ep.slug}) | \`${ep.path}\` |\n`;
|
||||
}
|
||||
|
||||
mdx += `\n</details>\n\n`;
|
||||
renderedTags.add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
// Render any remaining tags not in a group
|
||||
const remainingTags = Object.keys(tagEndpoints).filter(tag => !renderedTags.has(tag));
|
||||
if (remainingTags.length > 0) {
|
||||
mdx += `#### Other\n\n`;
|
||||
|
||||
for (const tag of remainingTags.sort()) {
|
||||
const description = tagDescriptions[tag] || '';
|
||||
const endpoints = tagEndpoints[tag];
|
||||
|
||||
mdx += `<details>\n`;
|
||||
mdx += `<summary><strong>${tag}</strong> (${endpoints.length} endpoints) — ${description}</summary>\n\n`;
|
||||
mdx += `| Method | Endpoint | Description |\n`;
|
||||
mdx += `|--------|----------|-------------|\n`;
|
||||
|
||||
for (const ep of endpoints) {
|
||||
mdx += `| \`${ep.method}\` | [${ep.summary}](./api/${ep.slug}) | \`${ep.path}\` |\n`;
|
||||
}
|
||||
|
||||
mdx += `\n</details>\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
mdx += `---
|
||||
|
||||
### Additional Resources
|
||||
|
||||
- [Superset REST API Blog Post](https://preset.io/blog/2020-10-01-superset-api/)
|
||||
- [Accessing APIs with Superset](https://preset.io/blog/accessing-apis-with-superset/)
|
||||
`;
|
||||
|
||||
// Write output
|
||||
fs.writeFileSync(OUTPUT_PATH, mdx);
|
||||
console.log(`Generated API index at ${OUTPUT_PATH}`);
|
||||
console.log(`Total tags: ${Object.keys(tagEndpoints).length}`);
|
||||
console.log(`Total endpoints: ${Object.values(tagEndpoints).flat().length}`);
|
||||
}
|
||||
|
||||
main();
|
||||
176
docs/scripts/generate-api-tag-pages.mjs
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Replaces auto-generated tag pages (DocCardList cards) with endpoint tables
|
||||
* showing HTTP method, endpoint name, and URI path for each endpoint in the tag.
|
||||
*
|
||||
* Runs after `docusaurus gen-api-docs` and `convert-api-sidebar.mjs`.
|
||||
* Uses the generated sidebar to get correct endpoint slugs.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { createRequire } from 'module';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const SPEC_PATH = path.join(__dirname, '..', 'static', 'resources', 'openapi.json');
|
||||
const API_DOCS_DIR = path.join(__dirname, '..', 'docs', 'api');
|
||||
const SIDEBAR_PATH = path.join(API_DOCS_DIR, 'sidebar.js');
|
||||
|
||||
function slugify(text) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a map from sidebar label → doc slug by reading the generated sidebar.
|
||||
*/
|
||||
function buildSlugMap() {
|
||||
const labelToSlug = {};
|
||||
|
||||
try {
|
||||
const sidebar = require(SIDEBAR_PATH);
|
||||
|
||||
const extractDocs = (items) => {
|
||||
for (const item of items) {
|
||||
if (item.type === 'doc' && item.label && item.id) {
|
||||
const slug = item.id.replace(/^api\//, '');
|
||||
labelToSlug[item.label] = slug;
|
||||
}
|
||||
if (item.items) extractDocs(item.items);
|
||||
}
|
||||
};
|
||||
|
||||
extractDocs(sidebar);
|
||||
} catch {
|
||||
console.warn('Could not read sidebar, will use computed slugs');
|
||||
}
|
||||
|
||||
return labelToSlug;
|
||||
}
|
||||
|
||||
function main() {
|
||||
console.log('Generating API tag pages with endpoint tables...');
|
||||
|
||||
const spec = JSON.parse(fs.readFileSync(SPEC_PATH, 'utf-8'));
|
||||
const labelToSlug = buildSlugMap();
|
||||
|
||||
// Build tag descriptions from the spec
|
||||
const tagDescriptions = {};
|
||||
for (const tag of spec.tags || []) {
|
||||
tagDescriptions[tag.name] = tag.description || '';
|
||||
}
|
||||
|
||||
// Build tag → endpoints map
|
||||
const tagEndpoints = {};
|
||||
for (const [pathUrl, methods] of Object.entries(spec.paths || {})) {
|
||||
for (const [method, details] of Object.entries(methods)) {
|
||||
if (!['get', 'post', 'put', 'delete', 'patch'].includes(method)) continue;
|
||||
|
||||
const tags = details.tags || ['Untagged'];
|
||||
const summary = details.summary || `${method.toUpperCase()} ${pathUrl}`;
|
||||
const slug = labelToSlug[summary] || slugify(summary);
|
||||
|
||||
for (const tag of tags) {
|
||||
if (!tagEndpoints[tag]) {
|
||||
tagEndpoints[tag] = [];
|
||||
}
|
||||
tagEndpoints[tag].push({
|
||||
method: method.toUpperCase(),
|
||||
path: pathUrl,
|
||||
summary,
|
||||
slug,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort endpoints within each tag by path then method
|
||||
for (const tag of Object.keys(tagEndpoints)) {
|
||||
tagEndpoints[tag].sort((a, b) =>
|
||||
a.path.localeCompare(b.path) || a.method.localeCompare(b.method)
|
||||
);
|
||||
}
|
||||
|
||||
// Scan existing .tag.mdx files and match by frontmatter title
|
||||
const tagFiles = fs.readdirSync(API_DOCS_DIR)
|
||||
.filter(f => f.endsWith('.tag.mdx'));
|
||||
|
||||
let updated = 0;
|
||||
for (const tagFile of tagFiles) {
|
||||
const tagFilePath = path.join(API_DOCS_DIR, tagFile);
|
||||
const existing = fs.readFileSync(tagFilePath, 'utf-8');
|
||||
|
||||
// Extract frontmatter
|
||||
const frontmatterMatch = existing.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (!frontmatterMatch) {
|
||||
console.warn(` No frontmatter in ${tagFile}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const frontmatter = frontmatterMatch[1];
|
||||
|
||||
// Extract the title from frontmatter (this matches the spec tag name)
|
||||
const titleMatch = frontmatter.match(/title:\s*"([^"]+)"/);
|
||||
if (!titleMatch) {
|
||||
console.warn(` No title in ${tagFile}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const tagName = titleMatch[1];
|
||||
const endpoints = tagEndpoints[tagName];
|
||||
|
||||
if (!endpoints || endpoints.length === 0) {
|
||||
console.warn(` No endpoints found for tag "${tagName}" (${tagFile})`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const description = tagDescriptions[tagName] || '';
|
||||
|
||||
// Build the endpoint table
|
||||
let table = '| Method | Endpoint | Path |\n';
|
||||
table += '|--------|----------|------|\n';
|
||||
for (const ep of endpoints) {
|
||||
table += `| \`${ep.method}\` | [${ep.summary}](./${ep.slug}) | \`${ep.path}\` |\n`;
|
||||
}
|
||||
|
||||
// Generate the new MDX content
|
||||
const mdx = `---
|
||||
${frontmatter}
|
||||
---
|
||||
|
||||
${description}
|
||||
|
||||
${table}
|
||||
`;
|
||||
|
||||
fs.writeFileSync(tagFilePath, mdx);
|
||||
updated++;
|
||||
}
|
||||
|
||||
console.log(`Updated ${updated} tag pages with endpoint tables`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -30,9 +30,12 @@
|
||||
|
||||
import { spawnSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import { createRequire } from 'module';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const ROOT_DIR = path.resolve(__dirname, '../..');
|
||||
@@ -41,6 +44,7 @@ const DATA_OUTPUT_DIR = path.join(DOCS_DIR, 'src/data');
|
||||
const DATA_OUTPUT_FILE = path.join(DATA_OUTPUT_DIR, 'databases.json');
|
||||
const MDX_OUTPUT_DIR = path.join(DOCS_DIR, 'docs/databases');
|
||||
const MDX_SUPPORTED_DIR = path.join(MDX_OUTPUT_DIR, 'supported');
|
||||
const IMAGES_DIR = path.join(DOCS_DIR, 'static/img/databases');
|
||||
|
||||
/**
|
||||
* Try to run the full lib.py script with Flask context
|
||||
@@ -607,30 +611,109 @@ const README_PATH = path.join(ROOT_DIR, 'README.md');
|
||||
const README_START_MARKER = '<!-- SUPPORTED_DATABASES_START -->';
|
||||
const README_END_MARKER = '<!-- SUPPORTED_DATABASES_END -->';
|
||||
|
||||
/**
|
||||
* Read image dimensions, with fallback SVG viewBox parsing for cases where
|
||||
* image-size can't handle SVG width/height attributes (e.g., scientific notation).
|
||||
*/
|
||||
function getImageDimensions(imgPath) {
|
||||
const sizeOf = require('image-size');
|
||||
try {
|
||||
const dims = sizeOf(imgPath);
|
||||
// image-size may misparse SVG attributes (e.g. width="1e3" → 1).
|
||||
// Fall back to viewBox parsing if a dimension looks wrong.
|
||||
if (dims.type === 'svg' && (dims.width < 2 || dims.height < 2)) {
|
||||
const content = fs.readFileSync(imgPath, 'utf-8');
|
||||
const vbMatch = content.match(/viewBox=["']([^"']+)["']/);
|
||||
if (vbMatch) {
|
||||
const parts = vbMatch[1].trim().split(/[\s,]+/).map(Number);
|
||||
if (parts.length >= 4 && parts[2] > 0 && parts[3] > 0) {
|
||||
return { width: parts[2], height: parts[3] };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dims.width > 0 && dims.height > 0) {
|
||||
return { width: dims.width, height: dims.height };
|
||||
}
|
||||
} catch { /* fall through */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute display dimensions that fit within a bounding box while preserving
|
||||
* the image's aspect ratio. Enforces a minimum height so very wide logos
|
||||
* remain legible.
|
||||
*/
|
||||
function fitToBoundingBox(imgWidth, imgHeight, maxWidth, maxHeight, minHeight) {
|
||||
const ratio = imgWidth / imgHeight;
|
||||
// Start at max height, compute width
|
||||
let h = maxHeight;
|
||||
let w = h * ratio;
|
||||
// If too wide, cap width and reduce height
|
||||
if (w > maxWidth) {
|
||||
w = maxWidth;
|
||||
h = w / ratio;
|
||||
}
|
||||
// If height fell below minimum, enforce minimum (allow width to exceed max)
|
||||
if (h < minHeight) {
|
||||
h = minHeight;
|
||||
w = h * ratio;
|
||||
}
|
||||
return { width: Math.round(w), height: Math.round(h) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the database logos HTML for README.md
|
||||
* Only includes databases that have logos defined
|
||||
* Only includes databases that have logos and homepage URLs.
|
||||
* Deduplicates by logo filename to match the docs homepage behavior.
|
||||
* Reads actual image dimensions to preserve aspect ratios.
|
||||
*/
|
||||
function generateReadmeLogos(databases) {
|
||||
// Get databases with logos, sorted alphabetically
|
||||
// Get databases with logos and homepage URLs, sorted alphabetically,
|
||||
// deduplicated by logo filename (matches docs homepage logic in index.tsx)
|
||||
const seenLogos = new Set();
|
||||
const dbsWithLogos = Object.entries(databases)
|
||||
.filter(([, db]) => db.documentation?.logo)
|
||||
.sort(([a], [b]) => a.localeCompare(b));
|
||||
.filter(([, db]) => db.documentation?.logo && db.documentation?.homepage_url)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.filter(([, db]) => {
|
||||
const logo = db.documentation.logo;
|
||||
if (seenLogos.has(logo)) return false;
|
||||
seenLogos.add(logo);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (dbsWithLogos.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Generate HTML img tags
|
||||
const MAX_WIDTH = 150;
|
||||
const MAX_HEIGHT = 40;
|
||||
const MIN_HEIGHT = 24;
|
||||
|
||||
const DOCS_BASE = 'https://superset.apache.org/docs/databases/supported';
|
||||
|
||||
// Generate linked logo tags with aspect-ratio-preserving dimensions
|
||||
const logoTags = dbsWithLogos.map(([name, db]) => {
|
||||
const logo = db.documentation.logo;
|
||||
const alt = name.toLowerCase().replace(/\s+/g, '-');
|
||||
// Use docs site URL for logos
|
||||
return ` <img src="https://superset.apache.org/img/databases/${logo}" alt="${alt}" border="0" width="80" height="40" class="database-logo" />`;
|
||||
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
const imgPath = path.join(IMAGES_DIR, logo);
|
||||
|
||||
const dims = getImageDimensions(imgPath);
|
||||
let sizeAttrs;
|
||||
if (dims) {
|
||||
const { width, height } = fitToBoundingBox(dims.width, dims.height, MAX_WIDTH, MAX_HEIGHT, MIN_HEIGHT);
|
||||
sizeAttrs = `width="${width}" height="${height}"`;
|
||||
} else {
|
||||
console.warn(` Could not read dimensions for ${logo}, using height-only fallback`);
|
||||
sizeAttrs = `height="${MAX_HEIGHT}"`;
|
||||
}
|
||||
|
||||
const img = `<img src="docs/static/img/databases/${logo}" alt="${name}" ${sizeAttrs} />`;
|
||||
return ` <a href="${DOCS_BASE}/${slug}" title="${name}">${img}</a>`;
|
||||
});
|
||||
|
||||
// Use between logos for spacing (GitHub strips style/class attributes)
|
||||
return `<p align="center">
|
||||
${logoTags.join('\n')}
|
||||
${logoTags.join(' \n')}
|
||||
</p>`;
|
||||
}
|
||||
|
||||
@@ -675,6 +758,78 @@ function updateReadme(databases) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract custom_errors from engine specs for troubleshooting documentation
|
||||
* Returns a map of module names to their custom errors
|
||||
*/
|
||||
function extractCustomErrors() {
|
||||
console.log('Extracting custom_errors from engine specs...');
|
||||
|
||||
try {
|
||||
const scriptPath = path.join(__dirname, 'extract_custom_errors.py');
|
||||
const result = spawnSync('python3', [scriptPath], {
|
||||
cwd: ROOT_DIR,
|
||||
encoding: 'utf-8',
|
||||
timeout: 30000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
throw new Error(result.stderr || 'Python script failed');
|
||||
}
|
||||
|
||||
const customErrors = JSON.parse(result.stdout);
|
||||
const moduleCount = Object.keys(customErrors).length;
|
||||
const errorCount = Object.values(customErrors).reduce((sum, classes) =>
|
||||
sum + Object.values(classes).reduce((s, errs) => s + errs.length, 0), 0);
|
||||
console.log(` Found ${errorCount} custom errors across ${moduleCount} modules`);
|
||||
return customErrors;
|
||||
} catch (err) {
|
||||
console.log(' Could not extract custom_errors:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge custom_errors into database documentation
|
||||
* Maps by module name since that's how both datasets are keyed
|
||||
*/
|
||||
function mergeCustomErrors(databases, customErrors) {
|
||||
if (!customErrors) return;
|
||||
|
||||
let mergedCount = 0;
|
||||
|
||||
for (const [, db] of Object.entries(databases)) {
|
||||
if (!db.module) continue;
|
||||
// Normalize module name: Flask mode uses full path (superset.db_engine_specs.postgres),
|
||||
// but customErrors is keyed by file stem (postgres)
|
||||
const moduleName = db.module.split('.').pop();
|
||||
if (!customErrors[moduleName]) continue;
|
||||
|
||||
// Get all errors from all classes in this module
|
||||
const moduleErrors = customErrors[moduleName];
|
||||
const allErrors = [];
|
||||
|
||||
for (const classErrors of Object.values(moduleErrors)) {
|
||||
allErrors.push(...classErrors);
|
||||
}
|
||||
|
||||
if (allErrors.length > 0) {
|
||||
// Add to documentation
|
||||
db.documentation = db.documentation || {};
|
||||
db.documentation.custom_errors = allErrors;
|
||||
mergedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (mergedCount > 0) {
|
||||
console.log(` Merged custom_errors into ${mergedCount} database docs`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load existing database data if available
|
||||
*/
|
||||
@@ -768,6 +923,10 @@ async function main() {
|
||||
databases = mergeWithExistingDiagnostics(databases, existingData);
|
||||
}
|
||||
|
||||
// Extract and merge custom_errors for troubleshooting documentation
|
||||
const customErrors = extractCustomErrors();
|
||||
mergeCustomErrors(databases, customErrors);
|
||||
|
||||
// Build statistics
|
||||
const statistics = buildStatistics(databases);
|
||||
|
||||
|
||||
1415
docs/scripts/generate-superset-components.mjs
Normal file
@@ -110,9 +110,26 @@ const sidebars = {
|
||||
'testing/frontend-testing',
|
||||
'testing/backend-testing',
|
||||
'testing/e2e-testing',
|
||||
'testing/storybook',
|
||||
'testing/ci-cd',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'UI Components',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
type: 'autogenerated',
|
||||
dirName: 'components',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
label: 'API Reference',
|
||||
href: '/docs/api',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -107,9 +107,21 @@ const sidebars = {
|
||||
id: 'faq',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'API',
|
||||
id: 'api',
|
||||
type: 'category',
|
||||
label: 'API Reference',
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'api',
|
||||
},
|
||||
items: (() => {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
return require('./docs/api/sidebar.js');
|
||||
} catch {
|
||||
// Generated by `yarn generate:api-docs`; empty until then
|
||||
return [];
|
||||
}
|
||||
})(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -39,11 +39,12 @@ const StyledBlurredSection = styled('section')`
|
||||
|
||||
interface BlurredSectionProps {
|
||||
children: ReactNode;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const BlurredSection = ({ children }: BlurredSectionProps) => {
|
||||
const BlurredSection = ({ children, id }: BlurredSectionProps) => {
|
||||
return (
|
||||
<StyledBlurredSection>
|
||||
<StyledBlurredSection id={id}>
|
||||
{children}
|
||||
<img className="blur" src="/img/community/blur.png" alt="Blur" />
|
||||
</StyledBlurredSection>
|
||||
|
||||
66
docs/src/components/FAQSchema.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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 { JSX } from 'react';
|
||||
import Head from '@docusaurus/Head';
|
||||
|
||||
interface FAQItem {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
interface FAQSchemaProps {
|
||||
faqs: FAQItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that injects FAQPage JSON-LD structured data
|
||||
* Use this on FAQ pages to enable rich snippets in search results
|
||||
*
|
||||
* @example
|
||||
* <FAQSchema faqs={[
|
||||
* { question: "What is Superset?", answer: "Apache Superset is..." },
|
||||
* { question: "How do I install it?", answer: "You can install via..." }
|
||||
* ]} />
|
||||
*/
|
||||
export default function FAQSchema({ faqs }: FAQSchemaProps): JSX.Element | null {
|
||||
// FAQPage schema requires a non-empty mainEntity array per schema.org specs
|
||||
if (!faqs || faqs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqs.map((faq) => ({
|
||||
'@type': 'Question',
|
||||
name: faq.question,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: faq.answer,
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<script type="application/ld+json">{JSON.stringify(schema)}</script>
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
@@ -18,33 +18,245 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
// A simple component to display a story example
|
||||
export function StoryExample({ component: Component, props = {} }) {
|
||||
// Lazy-loaded component registry - populated on first use in browser
|
||||
let componentRegistry = null;
|
||||
let SupersetProviders = null;
|
||||
|
||||
function getComponentRegistry() {
|
||||
if (typeof window === 'undefined') {
|
||||
return {}; // SSR - return empty
|
||||
}
|
||||
|
||||
if (componentRegistry !== null) {
|
||||
return componentRegistry; // Already loaded
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const antd = require('antd');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const SupersetComponents = require('@superset/components');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const CoreUI = require('@apache-superset/core/ui');
|
||||
|
||||
// Build component registry with antd as base fallback layer.
|
||||
// Some Superset components (e.g., Typography) use styled-components that may
|
||||
// fail to initialize in the docs build. Antd originals serve as fallbacks.
|
||||
componentRegistry = { ...antd, ...SupersetComponents, ...CoreUI };
|
||||
|
||||
return componentRegistry;
|
||||
} catch (error) {
|
||||
console.error('[StorybookWrapper] Failed to load components:', error);
|
||||
componentRegistry = {};
|
||||
return componentRegistry;
|
||||
}
|
||||
}
|
||||
|
||||
function getProviders() {
|
||||
if (typeof window === 'undefined') {
|
||||
return ({ children }) => children; // SSR
|
||||
}
|
||||
|
||||
if (SupersetProviders !== null) {
|
||||
return SupersetProviders;
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { themeObject } = require('@apache-superset/core/ui');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { App, ConfigProvider } = require('antd');
|
||||
|
||||
// Configure Ant Design to render portals (tooltips, dropdowns, etc.)
|
||||
// inside the closest .storybook-example container instead of document.body
|
||||
// This fixes positioning issues in the docs pages
|
||||
const getPopupContainer = (triggerNode) => {
|
||||
// Find the closest .storybook-example container
|
||||
const container = triggerNode?.closest?.('.storybook-example');
|
||||
return container || document.body;
|
||||
};
|
||||
|
||||
SupersetProviders = ({ children }) => (
|
||||
<themeObject.SupersetThemeProvider>
|
||||
<ConfigProvider
|
||||
getPopupContainer={getPopupContainer}
|
||||
getTargetContainer={() => document.body}
|
||||
>
|
||||
<App>{children}</App>
|
||||
</ConfigProvider>
|
||||
</themeObject.SupersetThemeProvider>
|
||||
);
|
||||
return SupersetProviders;
|
||||
} catch (error) {
|
||||
console.error('[StorybookWrapper] Failed to load providers:', error);
|
||||
return ({ children }) => children;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a value is a valid React component (function, forwardRef, memo, etc.)
|
||||
function isReactComponent(value) {
|
||||
if (!value) return false;
|
||||
// Function/class components
|
||||
if (typeof value === 'function') return true;
|
||||
// forwardRef, memo, lazy — React wraps these as objects with $$typeof
|
||||
if (typeof value === 'object' && value.$$typeof) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve component from string name or React component
|
||||
// Supports dot notation for nested components (e.g., 'Icons.InfoCircleOutlined')
|
||||
function resolveComponent(component) {
|
||||
if (!component) return null;
|
||||
// If already a component (function/class/forwardRef), return as-is
|
||||
if (isReactComponent(component)) return component;
|
||||
// If string, look up in registry
|
||||
if (typeof component === 'string') {
|
||||
const registry = getComponentRegistry();
|
||||
// Handle dot notation (e.g., 'Icons.InfoCircleOutlined')
|
||||
if (component.includes('.')) {
|
||||
const parts = component.split('.');
|
||||
let current = registry[parts[0]];
|
||||
for (let i = 1; i < parts.length && current; i++) {
|
||||
current = current[parts[i]];
|
||||
}
|
||||
return isReactComponent(current) ? current : null;
|
||||
}
|
||||
return registry[component] || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Loading placeholder for SSR
|
||||
function LoadingPlaceholder() {
|
||||
return (
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<div
|
||||
className="storybook-example"
|
||||
style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '4px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
}}
|
||||
>
|
||||
{Component && <Component {...props} />}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '4px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
minHeight: '100px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#999',
|
||||
}}
|
||||
>
|
||||
Loading component...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// A simple component to display a story with controls
|
||||
export function StoryWithControls({
|
||||
component: Component,
|
||||
props = {},
|
||||
controls = [],
|
||||
}) {
|
||||
// A simple component to display a story example
|
||||
export function StoryExample({ component, props = {} }) {
|
||||
return (
|
||||
<BrowserOnly fallback={<LoadingPlaceholder />}>
|
||||
{() => {
|
||||
const Component = resolveComponent(component);
|
||||
const Providers = getProviders();
|
||||
const { children, restProps } = extractChildren(props);
|
||||
return (
|
||||
<Providers>
|
||||
<div
|
||||
className="storybook-example"
|
||||
style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '4px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
position: 'relative', // Required for portal positioning
|
||||
}}
|
||||
>
|
||||
{Component ? (
|
||||
<Component {...restProps}>{children}</Component>
|
||||
) : (
|
||||
<div style={{ color: '#999' }}>
|
||||
Component "{String(component)}" not found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Providers>
|
||||
);
|
||||
}}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
|
||||
// Props that should be rendered as children rather than passed as props
|
||||
const CHILDREN_PROP_NAMES = ['label', 'children', 'text', 'content'];
|
||||
|
||||
// Extract children from props based on common conventions
|
||||
function extractChildren(props) {
|
||||
for (const propName of CHILDREN_PROP_NAMES) {
|
||||
if (props[propName] !== undefined && props[propName] !== null && props[propName] !== '') {
|
||||
const { [propName]: childContent, ...restProps } = props;
|
||||
return { children: childContent, restProps };
|
||||
}
|
||||
}
|
||||
return { children: null, restProps: props };
|
||||
}
|
||||
|
||||
// Generate sample children for layout components
|
||||
// Supports:
|
||||
// - Array of strings: ['Item 1', 'Item 2'] - renders as styled divs
|
||||
// - Array of component descriptors: [{ component: 'Button', props: { children: 'Click' } }]
|
||||
// - Number: 3 - generates that many sample items
|
||||
// - String: 'content' - renders as literal content
|
||||
function generateSampleChildren(sampleChildren, sampleChildrenStyle) {
|
||||
if (!sampleChildren) return null;
|
||||
|
||||
// Default style if none provided (minimal, just enough to see items)
|
||||
const itemStyle = sampleChildrenStyle || {};
|
||||
|
||||
// If it's an array, check if items are component descriptors or strings
|
||||
if (Array.isArray(sampleChildren)) {
|
||||
return sampleChildren.map((item, i) => {
|
||||
// Component descriptor: { component: 'Button', props: { ... } }
|
||||
if (item && typeof item === 'object' && item.component) {
|
||||
const ChildComponent = resolveComponent(item.component);
|
||||
if (ChildComponent) {
|
||||
return <ChildComponent key={i} {...item.props} />;
|
||||
}
|
||||
// Fallback if component not found
|
||||
return <div key={i}>{item.props?.children || `Unknown: ${item.component}`}</div>;
|
||||
}
|
||||
// Simple string
|
||||
return (
|
||||
<div key={i} style={itemStyle}>
|
||||
{item}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// If it's a number, generate that many sample items
|
||||
if (typeof sampleChildren === 'number') {
|
||||
return new Array(sampleChildren).fill(null).map((_, i) => (
|
||||
<div key={i} style={itemStyle}>
|
||||
Item {i + 1}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
// If it's a string, treat as literal content
|
||||
if (typeof sampleChildren === 'string') {
|
||||
return sampleChildren;
|
||||
}
|
||||
|
||||
return sampleChildren;
|
||||
}
|
||||
|
||||
// Inner component for StoryWithControls (browser-only)
|
||||
// renderComponent allows overriding which component to actually render (useful when the named
|
||||
// component is a namespace object like Icons, not a React component)
|
||||
// triggerProp: for components like Modal that need a trigger, specify the boolean prop that controls visibility
|
||||
function StoryWithControlsInner({ component, renderComponent, props, controls, sampleChildren, sampleChildrenStyle, triggerProp, onHideProp }) {
|
||||
// Use renderComponent if provided, otherwise use the main component name
|
||||
const componentToRender = renderComponent || component;
|
||||
const Component = resolveComponent(componentToRender);
|
||||
const Providers = getProviders();
|
||||
const [stateProps, setStateProps] = React.useState(props);
|
||||
|
||||
const updateProp = (key, value) => {
|
||||
@@ -54,8 +266,77 @@ export function StoryWithControls({
|
||||
}));
|
||||
};
|
||||
|
||||
// Extract children from props (label, children, text, content)
|
||||
// When sampleChildren is explicitly provided, skip extraction so all props
|
||||
// (like 'content') stay as component props rather than becoming children
|
||||
const { children: propsChildren, restProps } = sampleChildren
|
||||
? { children: null, restProps: stateProps }
|
||||
: extractChildren(stateProps);
|
||||
// Filter out undefined values so they don't override component defaults
|
||||
const filteredProps = Object.fromEntries(
|
||||
Object.entries(restProps).filter(([, v]) => v !== undefined)
|
||||
);
|
||||
|
||||
// Resolve any prop values that are component descriptors
|
||||
// e.g., { component: 'Button', props: { children: 'Click' } }
|
||||
// Also resolves descriptors nested inside array items:
|
||||
// e.g., items: [{ id: 'x', element: { component: 'div', props: { children: 'text' } } }]
|
||||
Object.keys(filteredProps).forEach(key => {
|
||||
const value = filteredProps[key];
|
||||
if (value && typeof value === 'object' && !Array.isArray(value) && value.component) {
|
||||
const PropComponent = resolveComponent(value.component);
|
||||
if (PropComponent) {
|
||||
filteredProps[key] = <PropComponent {...value.props} />;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
filteredProps[key] = value.map((item, idx) => {
|
||||
if (item && typeof item === 'object') {
|
||||
const resolved = { ...item };
|
||||
Object.keys(resolved).forEach(field => {
|
||||
const fieldValue = resolved[field];
|
||||
if (fieldValue && typeof fieldValue === 'object' && !Array.isArray(fieldValue) && fieldValue.component) {
|
||||
const FieldComponent = resolveComponent(fieldValue.component);
|
||||
if (FieldComponent) {
|
||||
resolved[field] = React.createElement(FieldComponent, { key: `${key}-${idx}`, ...fieldValue.props });
|
||||
}
|
||||
}
|
||||
});
|
||||
return resolved;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// For List-like components with dataSource but no renderItem, provide a default
|
||||
if (filteredProps.dataSource && !filteredProps.renderItem) {
|
||||
const ListItem = resolveComponent('List')?.Item;
|
||||
filteredProps.renderItem = (item) =>
|
||||
ListItem
|
||||
? React.createElement(ListItem, null, String(item))
|
||||
: React.createElement('div', null, String(item));
|
||||
}
|
||||
|
||||
// Use sample children if provided, otherwise use props children
|
||||
const children = generateSampleChildren(sampleChildren, sampleChildrenStyle) || propsChildren;
|
||||
|
||||
// For components with a trigger (like Modal with show/onHide), add handlers.
|
||||
// onHideProp supports comma-separated names for components with multiple close
|
||||
// callbacks (e.g., "onHide,handleSave,onConfirmNavigation").
|
||||
const triggerProps = {};
|
||||
if (triggerProp && onHideProp) {
|
||||
const closeHandler = () => updateProp(triggerProp, false);
|
||||
onHideProp.split(',').forEach(prop => {
|
||||
triggerProps[prop.trim()] = closeHandler;
|
||||
});
|
||||
}
|
||||
|
||||
// Get the Button component for trigger buttons
|
||||
const ButtonComponent = resolveComponent('Button');
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<Providers>
|
||||
<div className="storybook-with-controls">
|
||||
<div
|
||||
className="storybook-example"
|
||||
@@ -64,9 +345,24 @@ export function StoryWithControls({
|
||||
borderRadius: '4px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
position: 'relative', // Required for portal positioning
|
||||
}}
|
||||
>
|
||||
{Component && <Component {...stateProps} />}
|
||||
{Component ? (
|
||||
<>
|
||||
{/* Show a trigger button for components like Modal */}
|
||||
{triggerProp && ButtonComponent && (
|
||||
<ButtonComponent onClick={() => updateProp(triggerProp, true)}>
|
||||
Open {component}
|
||||
</ButtonComponent>
|
||||
)}
|
||||
<Component {...filteredProps} {...triggerProps}>{children}</Component>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ color: '#999' }}>
|
||||
Component "{String(componentToRender)}" not found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{controls.length > 0 && (
|
||||
@@ -87,26 +383,64 @@ export function StoryWithControls({
|
||||
</label>
|
||||
{control.type === 'select' ? (
|
||||
<select
|
||||
value={stateProps[control.name]}
|
||||
onChange={e => updateProp(control.name, e.target.value)}
|
||||
value={stateProps[control.name] ?? ''}
|
||||
onChange={e => updateProp(control.name, e.target.value || undefined)}
|
||||
style={{ width: '100%', padding: '5px' }}
|
||||
>
|
||||
{control.options.map(option => (
|
||||
<option value="">— None —</option>
|
||||
{control.options?.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : control.type === 'inline-radio' || control.type === 'radio' ? (
|
||||
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
|
||||
{control.options?.map(option => (
|
||||
<label
|
||||
key={option}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: '4px' }}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={control.name}
|
||||
value={option}
|
||||
checked={stateProps[control.name] === option}
|
||||
onChange={e => updateProp(control.name, e.target.value)}
|
||||
/>
|
||||
{option}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : control.type === 'boolean' ? (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={stateProps[control.name]}
|
||||
onChange={e => updateProp(control.name, e.target.checked)}
|
||||
/>
|
||||
) : control.type === 'number' ? (
|
||||
<input
|
||||
type="number"
|
||||
value={stateProps[control.name]}
|
||||
onChange={e => updateProp(control.name, Number(e.target.value))}
|
||||
style={{ width: '100%', padding: '5px' }}
|
||||
/>
|
||||
) : control.type === 'color' ? (
|
||||
<input
|
||||
type="color"
|
||||
value={stateProps[control.name] || '#000000'}
|
||||
onChange={e => updateProp(control.name, e.target.value)}
|
||||
style={{
|
||||
width: '50px',
|
||||
height: '30px',
|
||||
padding: '2px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={stateProps[control.name]}
|
||||
value={stateProps[control.name] ?? ''}
|
||||
onChange={e => updateProp(control.name, e.target.value)}
|
||||
style={{ width: '100%', padding: '5px' }}
|
||||
/>
|
||||
@@ -116,6 +450,81 @@ export function StoryWithControls({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</Providers>
|
||||
);
|
||||
}
|
||||
|
||||
// A simple component to display a story with controls
|
||||
// renderComponent: optional override for which component to render (e.g., 'Icons.InfoCircleOutlined' when component='Icons')
|
||||
// triggerProp/onHideProp: for components like Modal that need a button to open (e.g., triggerProp="show", onHideProp="onHide")
|
||||
export function StoryWithControls({ component: Component, renderComponent, props = {}, controls = [], sampleChildren, sampleChildrenStyle, triggerProp, onHideProp }) {
|
||||
return (
|
||||
<BrowserOnly fallback={<LoadingPlaceholder />}>
|
||||
{() => (
|
||||
<StoryWithControlsInner
|
||||
component={Component}
|
||||
renderComponent={renderComponent}
|
||||
props={props}
|
||||
controls={controls}
|
||||
sampleChildren={sampleChildren}
|
||||
sampleChildrenStyle={sampleChildrenStyle}
|
||||
triggerProp={triggerProp}
|
||||
onHideProp={onHideProp}
|
||||
/>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
|
||||
// Inner component for ComponentGallery (browser-only)
|
||||
function ComponentGalleryInner({ component, sizes, styles, sizeProp, styleProp }) {
|
||||
const Component = resolveComponent(component);
|
||||
const Providers = getProviders();
|
||||
|
||||
if (!Component) {
|
||||
return (
|
||||
<div style={{ color: '#999' }}>
|
||||
Component "{String(component)}" not found
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Providers>
|
||||
<div className="component-gallery">
|
||||
{sizes.map(size => (
|
||||
<div key={size} style={{ marginBottom: 40 }}>
|
||||
<h4 style={{ marginBottom: 16, color: '#666' }}>{size}</h4>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px', alignItems: 'center' }}>
|
||||
{styles.map(style => (
|
||||
<Component
|
||||
key={`${style}_${size}`}
|
||||
{...{ [sizeProp]: size, [styleProp]: style }}
|
||||
>
|
||||
{style}
|
||||
</Component>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Providers>
|
||||
);
|
||||
}
|
||||
|
||||
// A component to display a gallery of all variants (sizes x styles)
|
||||
export function ComponentGallery({ component, sizes = [], styles = [], sizeProp = 'size', styleProp = 'variant' }) {
|
||||
return (
|
||||
<BrowserOnly fallback={<LoadingPlaceholder />}>
|
||||
{() => (
|
||||
<ComponentGalleryInner
|
||||
component={component}
|
||||
sizes={sizes}
|
||||
styles={styles}
|
||||
sizeProp={sizeProp}
|
||||
styleProp={styleProp}
|
||||
/>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
|
||||
91
docs/src/components/TechArticleSchema.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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 { JSX } from 'react';
|
||||
import Head from '@docusaurus/Head';
|
||||
import { useLocation } from '@docusaurus/router';
|
||||
|
||||
interface TechArticleSchemaProps {
|
||||
title: string;
|
||||
description: string;
|
||||
datePublished?: string;
|
||||
dateModified?: string;
|
||||
keywords?: string[];
|
||||
proficiencyLevel?: 'Beginner' | 'Expert';
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that injects TechArticle JSON-LD structured data for documentation pages.
|
||||
* This helps search engines understand technical documentation content.
|
||||
*
|
||||
* @example
|
||||
* <TechArticleSchema
|
||||
* title="Installing Superset with Docker"
|
||||
* description="Learn how to install Apache Superset using Docker Compose"
|
||||
* keywords={['docker', 'installation', 'superset']}
|
||||
* proficiencyLevel="Beginner"
|
||||
* />
|
||||
*/
|
||||
export default function TechArticleSchema({
|
||||
title,
|
||||
description,
|
||||
datePublished,
|
||||
dateModified,
|
||||
keywords = [],
|
||||
proficiencyLevel = 'Beginner',
|
||||
}: TechArticleSchemaProps): JSX.Element {
|
||||
const location = useLocation();
|
||||
const url = `https://superset.apache.org${location.pathname}`;
|
||||
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'TechArticle',
|
||||
headline: title,
|
||||
description,
|
||||
url,
|
||||
proficiencyLevel,
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
name: 'Apache Superset Contributors',
|
||||
url: 'https://github.com/apache/superset/graphs/contributors',
|
||||
},
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: 'Apache Software Foundation',
|
||||
url: 'https://www.apache.org/',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: 'https://www.apache.org/foundation/press/kit/asf_logo.png',
|
||||
},
|
||||
},
|
||||
mainEntityOfPage: {
|
||||
'@type': 'WebPage',
|
||||
'@id': url,
|
||||
},
|
||||
...(datePublished && { datePublished }),
|
||||
...(dateModified && { dateModified }),
|
||||
...(keywords.length > 0 && { keywords: keywords.join(', ') }),
|
||||
};
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<script type="application/ld+json">{JSON.stringify(schema)}</script>
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
KeyOutlined,
|
||||
SearchOutlined,
|
||||
LinkOutlined,
|
||||
BugOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { DatabaseData, DatabaseInfo, TimeGrains } from './types';
|
||||
|
||||
@@ -44,6 +45,8 @@ interface TableEntry {
|
||||
hasDrivers: boolean;
|
||||
hasAuthMethods: boolean;
|
||||
hasConnectionString: boolean;
|
||||
hasCustomErrors: boolean;
|
||||
customErrorCount: number;
|
||||
joins?: boolean;
|
||||
subqueries?: boolean;
|
||||
supports_dynamic_schema?: boolean;
|
||||
@@ -223,6 +226,8 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
db.documentation?.connection_string ||
|
||||
(db.documentation?.drivers?.length ?? 0) > 0
|
||||
),
|
||||
hasCustomErrors: (db.documentation?.custom_errors?.length ?? 0) > 0,
|
||||
customErrorCount: db.documentation?.custom_errors?.length ?? 0,
|
||||
isCompatible: false,
|
||||
});
|
||||
|
||||
@@ -246,6 +251,8 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
hasDrivers: false,
|
||||
hasAuthMethods: false,
|
||||
hasConnectionString: Boolean(compat.connection_string),
|
||||
hasCustomErrors: false,
|
||||
customErrorCount: 0,
|
||||
joins: db.joins,
|
||||
subqueries: db.subqueries,
|
||||
supports_dynamic_schema: db.supports_dynamic_schema,
|
||||
@@ -457,7 +464,7 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
{
|
||||
title: 'Documentation',
|
||||
key: 'docs',
|
||||
width: 150,
|
||||
width: 180,
|
||||
render: (_: unknown, record: TableEntry) => (
|
||||
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
|
||||
{record.hasConnectionString && (
|
||||
@@ -475,6 +482,13 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
Auth
|
||||
</Tag>
|
||||
)}
|
||||
{record.hasCustomErrors && (
|
||||
<Tooltip title={`${record.customErrorCount} troubleshooting tips`}>
|
||||
<Tag icon={<BugOutlined />} color="volcano">
|
||||
Errors
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
BookOutlined,
|
||||
EditOutlined,
|
||||
GithubOutlined,
|
||||
BugOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { DatabaseInfo } from './types';
|
||||
|
||||
@@ -59,8 +60,6 @@ const CodeBlock: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
);
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
const { Panel } = Collapse;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
interface DatabasePageProps {
|
||||
database: DatabaseInfo;
|
||||
@@ -111,21 +110,20 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
|
||||
|
||||
return (
|
||||
<Card title="Drivers" style={{ marginBottom: 16 }}>
|
||||
<Tabs>
|
||||
{docs.drivers.map((driver, idx) => (
|
||||
<TabPane
|
||||
tab={
|
||||
<span>
|
||||
{driver.name}
|
||||
{driver.is_recommended && (
|
||||
<Tag color="green" style={{ marginLeft: 8 }}>
|
||||
Recommended
|
||||
</Tag>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
key={idx}
|
||||
>
|
||||
<Tabs
|
||||
items={docs.drivers.map((driver, idx) => ({
|
||||
key: String(idx),
|
||||
label: (
|
||||
<span>
|
||||
{driver.name}
|
||||
{driver.is_recommended && (
|
||||
<Tag color="green" style={{ marginLeft: 8 }}>
|
||||
Recommended
|
||||
</Tag>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{driver.pypi_package && (
|
||||
<div>
|
||||
@@ -144,9 +142,9 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
|
||||
</a>
|
||||
)}
|
||||
</Space>
|
||||
</TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -164,46 +162,51 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
|
||||
}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Collapse accordion>
|
||||
{docs.authentication_methods.map((auth, idx) => (
|
||||
<Panel header={auth.name} key={idx}>
|
||||
{auth.description && <Paragraph>{auth.description}</Paragraph>}
|
||||
{auth.requirements && (
|
||||
<Alert
|
||||
message="Requirements"
|
||||
description={auth.requirements}
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
{auth.connection_string &&
|
||||
renderConnectionString(
|
||||
auth.connection_string,
|
||||
'Connection String'
|
||||
<Collapse
|
||||
accordion
|
||||
items={docs.authentication_methods.map((auth, idx) => ({
|
||||
key: String(idx),
|
||||
label: auth.name,
|
||||
children: (
|
||||
<>
|
||||
{auth.description && <Paragraph>{auth.description}</Paragraph>}
|
||||
{auth.requirements && (
|
||||
<Alert
|
||||
message="Requirements"
|
||||
description={auth.requirements}
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
{auth.secure_extra && (
|
||||
<div>
|
||||
<Text strong>Secure Extra Configuration:</Text>
|
||||
<CodeBlock>
|
||||
{JSON.stringify(auth.secure_extra, null, 2)}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
)}
|
||||
{auth.engine_parameters && (
|
||||
<div>
|
||||
<Text strong>Engine Parameters:</Text>
|
||||
<CodeBlock>
|
||||
{JSON.stringify(auth.engine_parameters, null, 2)}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
)}
|
||||
{auth.notes && (
|
||||
<Alert message={auth.notes} type="info" showIcon />
|
||||
)}
|
||||
</Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
{auth.connection_string &&
|
||||
renderConnectionString(
|
||||
auth.connection_string,
|
||||
'Connection String',
|
||||
)}
|
||||
{auth.secure_extra && (
|
||||
<div>
|
||||
<Text strong>Secure Extra Configuration:</Text>
|
||||
<CodeBlock>
|
||||
{JSON.stringify(auth.secure_extra, null, 2)}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
)}
|
||||
{auth.engine_parameters && (
|
||||
<div>
|
||||
<Text strong>Engine Parameters:</Text>
|
||||
<CodeBlock>
|
||||
{JSON.stringify(auth.engine_parameters, null, 2)}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
)}
|
||||
{auth.notes && (
|
||||
<Alert message={auth.notes} type="info" showIcon />
|
||||
)}
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -221,23 +224,27 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
|
||||
}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Collapse>
|
||||
{docs.engine_parameters.map((param, idx) => (
|
||||
<Panel header={param.name} key={idx}>
|
||||
{param.description && <Paragraph>{param.description}</Paragraph>}
|
||||
{param.json && (
|
||||
<CodeBlock>
|
||||
{JSON.stringify(param.json, null, 2)}
|
||||
</CodeBlock>
|
||||
)}
|
||||
{param.docs_url && (
|
||||
<a href={param.docs_url} target="_blank" rel="noreferrer">
|
||||
<LinkOutlined /> Learn more
|
||||
</a>
|
||||
)}
|
||||
</Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
<Collapse
|
||||
items={docs.engine_parameters.map((param, idx) => ({
|
||||
key: String(idx),
|
||||
label: param.name,
|
||||
children: (
|
||||
<>
|
||||
{param.description && (
|
||||
<Paragraph>{param.description}</Paragraph>
|
||||
)}
|
||||
{param.json && (
|
||||
<CodeBlock>{JSON.stringify(param.json, null, 2)}</CodeBlock>
|
||||
)}
|
||||
{param.docs_url && (
|
||||
<a href={param.docs_url} target="_blank" rel="noreferrer">
|
||||
<LinkOutlined /> Learn more
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -246,75 +253,81 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
|
||||
const renderCompatibleDatabases = () => {
|
||||
if (!docs?.compatible_databases?.length) return null;
|
||||
|
||||
// Create array of all panel keys to expand by default
|
||||
const allPanelKeys = docs.compatible_databases.map((_, idx) => idx);
|
||||
// Create array of all item keys to expand by default
|
||||
const allItemKeys = docs.compatible_databases.map((_, idx) => String(idx));
|
||||
|
||||
return (
|
||||
<Card title="Compatible Databases" style={{ marginBottom: 16 }}>
|
||||
<Paragraph>
|
||||
The following databases are compatible with the {name} driver:
|
||||
</Paragraph>
|
||||
<Collapse defaultActiveKey={allPanelKeys}>
|
||||
{docs.compatible_databases.map((compat, idx) => (
|
||||
<Panel
|
||||
header={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
{compat.logo && (
|
||||
<img
|
||||
src={`/img/databases/${compat.logo}`}
|
||||
alt={compat.name}
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span>{compat.name}</span>
|
||||
</div>
|
||||
}
|
||||
key={idx}
|
||||
>
|
||||
{compat.description && (
|
||||
<Paragraph>{compat.description}</Paragraph>
|
||||
)}
|
||||
{compat.connection_string &&
|
||||
renderConnectionString(compat.connection_string)}
|
||||
{compat.parameters && (
|
||||
<div>
|
||||
<Text strong>Parameters:</Text>
|
||||
<Table
|
||||
dataSource={Object.entries(compat.parameters).map(
|
||||
([key, value]) => ({
|
||||
key,
|
||||
parameter: key,
|
||||
description: value,
|
||||
})
|
||||
)}
|
||||
columns={[
|
||||
{ title: 'Parameter', dataIndex: 'parameter', key: 'p' },
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'description',
|
||||
key: 'd',
|
||||
},
|
||||
]}
|
||||
pagination={false}
|
||||
size="small"
|
||||
<Collapse
|
||||
defaultActiveKey={allItemKeys}
|
||||
items={docs.compatible_databases.map((compat, idx) => ({
|
||||
key: String(idx),
|
||||
label: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
{compat.logo && (
|
||||
<img
|
||||
src={`/img/databases/${compat.logo}`}
|
||||
alt={compat.name}
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{compat.notes && (
|
||||
<Alert
|
||||
message={compat.notes}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
)}
|
||||
</Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
)}
|
||||
<span>{compat.name}</span>
|
||||
</div>
|
||||
),
|
||||
children: (
|
||||
<>
|
||||
{compat.description && (
|
||||
<Paragraph>{compat.description}</Paragraph>
|
||||
)}
|
||||
{compat.connection_string &&
|
||||
renderConnectionString(compat.connection_string)}
|
||||
{compat.parameters && (
|
||||
<div>
|
||||
<Text strong>Parameters:</Text>
|
||||
<Table
|
||||
dataSource={Object.entries(compat.parameters).map(
|
||||
([key, value]) => ({
|
||||
key,
|
||||
parameter: key,
|
||||
description: value,
|
||||
}),
|
||||
)}
|
||||
columns={[
|
||||
{
|
||||
title: 'Parameter',
|
||||
dataIndex: 'parameter',
|
||||
key: 'p',
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'description',
|
||||
key: 'd',
|
||||
},
|
||||
]}
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{compat.notes && (
|
||||
<Alert
|
||||
message={compat.notes}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -375,7 +388,7 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
|
||||
'YEAR',
|
||||
];
|
||||
const extendedGrains = Object.keys(database.time_grains).filter(
|
||||
(g) => !commonGrains.includes(g)
|
||||
g => !commonGrains.includes(g),
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -383,12 +396,14 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text strong>Common Time Grains:</Text>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
{commonGrains.map((grain) => (
|
||||
{commonGrains.map(grain => (
|
||||
<TimeGrainBadge
|
||||
key={grain}
|
||||
grain={grain}
|
||||
supported={Boolean(
|
||||
database.time_grains[grain as keyof typeof database.time_grains]
|
||||
database.time_grains[
|
||||
grain as keyof typeof database.time_grains
|
||||
],
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
@@ -398,12 +413,14 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
|
||||
<div>
|
||||
<Text strong>Extended Time Grains:</Text>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
{extendedGrains.map((grain) => (
|
||||
{extendedGrains.map(grain => (
|
||||
<TimeGrainBadge
|
||||
key={grain}
|
||||
grain={grain}
|
||||
supported={Boolean(
|
||||
database.time_grains[grain as keyof typeof database.time_grains]
|
||||
database.time_grains[
|
||||
grain as keyof typeof database.time_grains
|
||||
],
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
@@ -414,11 +431,139 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// Render troubleshooting / custom errors section
|
||||
const renderTroubleshooting = () => {
|
||||
if (!docs?.custom_errors?.length) return null;
|
||||
|
||||
// Group errors by category
|
||||
const errorsByCategory: Record<string, typeof docs.custom_errors> = {};
|
||||
for (const error of docs.custom_errors) {
|
||||
const category = error.category || 'General';
|
||||
if (!errorsByCategory[category]) {
|
||||
errorsByCategory[category] = [];
|
||||
}
|
||||
errorsByCategory[category].push(error);
|
||||
}
|
||||
|
||||
// Define category order for consistent display
|
||||
const categoryOrder = [
|
||||
'Authentication',
|
||||
'Connection',
|
||||
'Permissions',
|
||||
'Query',
|
||||
'Configuration',
|
||||
'General',
|
||||
];
|
||||
|
||||
const sortedCategories = Object.keys(errorsByCategory).sort((a, b) => {
|
||||
const aIdx = categoryOrder.indexOf(a);
|
||||
const bIdx = categoryOrder.indexOf(b);
|
||||
if (aIdx === -1 && bIdx === -1) return a.localeCompare(b);
|
||||
if (aIdx === -1) return 1;
|
||||
if (bIdx === -1) return -1;
|
||||
return aIdx - bIdx;
|
||||
});
|
||||
|
||||
// Category colors
|
||||
const categoryColors: Record<string, string> = {
|
||||
Authentication: 'orange',
|
||||
Connection: 'red',
|
||||
Permissions: 'purple',
|
||||
Query: 'blue',
|
||||
Configuration: 'cyan',
|
||||
General: 'default',
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<>
|
||||
<BugOutlined /> Troubleshooting
|
||||
</>
|
||||
}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Paragraph type="secondary">
|
||||
Common error messages you may encounter when connecting to or querying{' '}
|
||||
{name}, along with their causes and solutions.
|
||||
</Paragraph>
|
||||
<Collapse
|
||||
accordion
|
||||
items={sortedCategories.map(category => ({
|
||||
key: category,
|
||||
label: (
|
||||
<span>
|
||||
<Tag color={categoryColors[category] || 'default'}>
|
||||
{category}
|
||||
</Tag>
|
||||
{errorsByCategory[category].length} error
|
||||
{errorsByCategory[category].length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<>
|
||||
{errorsByCategory[category].map((error, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
style={{
|
||||
marginBottom:
|
||||
idx < errorsByCategory[category].length - 1 ? 16 : 0,
|
||||
paddingBottom:
|
||||
idx < errorsByCategory[category].length - 1 ? 16 : 0,
|
||||
borderBottom:
|
||||
idx < errorsByCategory[category].length - 1
|
||||
? '1px solid var(--ifm-color-emphasis-200)'
|
||||
: 'none',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Text strong>
|
||||
{error.description || error.error_type}
|
||||
</Text>
|
||||
</div>
|
||||
<Alert
|
||||
message={error.message_template}
|
||||
type="error"
|
||||
style={{ marginBottom: 8 }}
|
||||
/>
|
||||
{error.invalid_fields &&
|
||||
error.invalid_fields.length > 0 && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Text type="secondary">Check these fields: </Text>
|
||||
{error.invalid_fields.map(field => (
|
||||
<Tag key={field} color="warning">
|
||||
{field}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{error.issue_codes && error.issue_codes.length > 0 && (
|
||||
<div>
|
||||
<Text type="secondary">Related issue codes: </Text>
|
||||
{error.issue_codes.map(code => (
|
||||
<Tag key={code}>
|
||||
<a
|
||||
href={`/docs/using-superset/issue-codes#issue-${code}`}
|
||||
style={{ color: 'inherit' }}
|
||||
>
|
||||
Issue {code}
|
||||
</a>
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="database-page"
|
||||
id={name.toLowerCase().replace(/\s+/g, '-')}
|
||||
>
|
||||
<div className="database-page" id={name.toLowerCase().replace(/\s+/g, '-')}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
{docs?.logo && (
|
||||
<img
|
||||
@@ -431,7 +576,9 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Title level={1} style={{ margin: 0 }}>{name}</Title>
|
||||
<Title level={1} style={{ margin: 0 }}>
|
||||
{name}
|
||||
</Title>
|
||||
{docs?.homepage_url && (
|
||||
<a
|
||||
href={docs.homepage_url}
|
||||
@@ -479,7 +626,7 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
|
||||
{docs.pypi_packages?.length > 0 && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text strong>Required packages: </Text>
|
||||
{docs.pypi_packages.map((pkg) => (
|
||||
{docs.pypi_packages.map(pkg => (
|
||||
<Tag key={pkg} color="blue">
|
||||
{pkg}
|
||||
</Tag>
|
||||
@@ -511,7 +658,7 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
|
||||
key,
|
||||
parameter: key,
|
||||
description: value,
|
||||
})
|
||||
}),
|
||||
)}
|
||||
columns={[
|
||||
{ title: 'Parameter', dataIndex: 'parameter', key: 'p' },
|
||||
@@ -537,7 +684,7 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
|
||||
<div key={idx}>
|
||||
{renderConnectionString(
|
||||
example.connection_string,
|
||||
example.description
|
||||
example.description,
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
@@ -556,6 +703,9 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
|
||||
{/* Time Grains */}
|
||||
{renderTimeGrains()}
|
||||
|
||||
{/* Troubleshooting / Custom Errors */}
|
||||
{renderTroubleshooting()}
|
||||
|
||||
{/* Compatible Databases */}
|
||||
{renderCompatibleDatabases()}
|
||||
|
||||
@@ -587,7 +737,11 @@ const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
|
||||
</a>
|
||||
)}
|
||||
{docs.sqlalchemy_docs_url && (
|
||||
<a href={docs.sqlalchemy_docs_url} target="_blank" rel="noreferrer">
|
||||
<a
|
||||
href={docs.sqlalchemy_docs_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<LinkOutlined /> SQLAlchemy Dialect Documentation
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -86,6 +86,17 @@ export interface CompatibleDatabase {
|
||||
docs_url?: string;
|
||||
}
|
||||
|
||||
export interface CustomError {
|
||||
error_type: string; // e.g., "CONNECTION_INVALID_USERNAME_ERROR"
|
||||
message_template: string; // e.g., 'The username "%(username)s" does not exist.'
|
||||
regex_pattern?: string; // The regex pattern that matches this error (optional, for reference)
|
||||
regex_name?: string; // The name of the regex constant (e.g., "CONNECTION_INVALID_USERNAME_REGEX")
|
||||
invalid_fields?: string[]; // Fields that are invalid, e.g., ["username", "password"]
|
||||
issue_codes?: number[]; // Related issue codes from ISSUE_CODES mapping
|
||||
category?: string; // Error category: "Authentication", "Connection", "Query", etc.
|
||||
description?: string; // Human-readable short description of the error type
|
||||
}
|
||||
|
||||
export interface DatabaseDocumentation {
|
||||
description?: string;
|
||||
logo?: string;
|
||||
@@ -111,6 +122,7 @@ export interface DatabaseDocumentation {
|
||||
sqlalchemy_docs_url?: string;
|
||||
advanced_features?: Record<string, string>;
|
||||
compatible_databases?: CompatibleDatabase[];
|
||||
custom_errors?: CustomError[]; // Database-specific error messages and troubleshooting info
|
||||
}
|
||||
|
||||
export interface TimeGrains {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { List } from 'antd';
|
||||
import Layout from '@theme/Layout';
|
||||
import { mq } from '../utils';
|
||||
import SectionHeader from '../components/SectionHeader';
|
||||
@@ -92,8 +91,12 @@ const StyledJoinCommunity = styled('section')`
|
||||
max-width: 540px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px 20px 35px;
|
||||
list-style: none;
|
||||
}
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
@@ -189,39 +192,33 @@ const Community = () => {
|
||||
/>
|
||||
</BlurredSection>
|
||||
<StyledJoinCommunity>
|
||||
<List
|
||||
className="list"
|
||||
itemLayout="horizontal"
|
||||
dataSource={communityLinks}
|
||||
renderItem={({ url, title, description, image, ariaLabel }) => (
|
||||
<List.Item className="item">
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
<a
|
||||
className="title"
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<img className="icon" src={`/img/community/${image}`} />
|
||||
</a>
|
||||
}
|
||||
title={
|
||||
<ul className="list">
|
||||
{communityLinks.map(
|
||||
({ url, title, description, image, ariaLabel }) => (
|
||||
<li className="item" key={title}>
|
||||
<a
|
||||
className="avatar"
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<img className="icon" src={`/img/community/${image}`} />
|
||||
</a>
|
||||
<div>
|
||||
<a href={url} target="_blank" rel="noreferrer">
|
||||
<p className="title" style={{ marginBottom: 0 }}>
|
||||
{title}
|
||||
</p>
|
||||
</a>
|
||||
}
|
||||
description={<p className="description">{description}</p>}
|
||||
aria-label="Community link"
|
||||
/>
|
||||
</List.Item>
|
||||
<p className="description">{description}</p>
|
||||
</div>
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</ul>
|
||||
</StyledJoinCommunity>
|
||||
<BlurredSection>
|
||||
<BlurredSection id="superset-community-calendar">
|
||||
<SectionHeader
|
||||
level="h2"
|
||||
title="Superset Community Calendar"
|
||||
|
||||
@@ -28,7 +28,7 @@ import databaseData from '../data/databases.json';
|
||||
import BlurredSection from '../components/BlurredSection';
|
||||
import DataSet from '../../../RESOURCES/INTHEWILD.yaml';
|
||||
import type { DatabaseData } from '../components/databases/types';
|
||||
import '../styles/main.less';
|
||||
import '../styles/main.css';
|
||||
|
||||
// Build database list from databases.json (databases with logos)
|
||||
// Deduplicate by logo filename to avoid showing the same logo twice
|
||||
@@ -109,13 +109,10 @@ const StyledMain = styled('main')`
|
||||
|
||||
const StyledTitleContainer = styled('div')`
|
||||
position: relative;
|
||||
padding: 130px 20px 0;
|
||||
margin-bottom: 160px;
|
||||
padding: 130px 20px 20px;
|
||||
margin-bottom: 0;
|
||||
background-image: url('/img/grid-background.jpg');
|
||||
background-size: cover;
|
||||
${mq[1]} {
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
.info-container {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
@@ -798,7 +795,7 @@ export default function Home(): JSX.Element {
|
||||
</StyledIntegrations>
|
||||
</BlurredSection>
|
||||
{/* Only show carousel when we have enough logos (>10) for a good display */}
|
||||
{companiesWithLogos.length > 10 && (
|
||||
{companiesWithLogos.length > 7 && (
|
||||
<BlurredSection>
|
||||
<div style={{ padding: '0 20px' }}>
|
||||
<SectionHeader
|
||||
|
||||
118
docs/src/shims/null-module.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Null module shim for packages not available in the docs build.
|
||||
// These are transitive dependencies of superset-frontend components that exist
|
||||
// in the barrel file but are never rendered on the docs site.
|
||||
// webpack needs these to resolve at build time even though the code paths
|
||||
// that use them are never executed at runtime.
|
||||
//
|
||||
// This shim uses a recursive Proxy to handle nested property access chains:
|
||||
// import ace from 'ace-builds'; ace.config.set(...) → works (returns proxy)
|
||||
// import { useResizeDetector } from 'react-resize-detector' → returns noop hook
|
||||
// import ReactAce from 'react-ace' → returns NullComponent
|
||||
|
||||
const NullComponent = () => null;
|
||||
|
||||
// For hooks that return objects/arrays
|
||||
const useNoop = () => ({});
|
||||
|
||||
// Mock for useResizeDetector - returns { ref, width, height } where ref.current exists
|
||||
const useResizeDetectorMock = () => ({
|
||||
ref: { current: null },
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a recursive proxy that handles any depth of property access.
|
||||
* This allows patterns like ace.config.set() or ace.config.setModuleUrl() to work.
|
||||
*
|
||||
* The proxy is both callable (returns undefined) and accessible (returns another proxy).
|
||||
*/
|
||||
function createDeepProxy() {
|
||||
const handler = {
|
||||
// Handle property access - return another proxy for chaining
|
||||
get(target, prop) {
|
||||
// Standard module properties
|
||||
if (prop === 'default') return createDeepProxy();
|
||||
if (prop === '__esModule') return true;
|
||||
|
||||
// Symbol properties (used by JS internals)
|
||||
if (typeof prop === 'symbol') {
|
||||
if (prop === Symbol.toPrimitive) return () => '';
|
||||
if (prop === Symbol.toStringTag) return 'NullModule';
|
||||
if (prop === Symbol.iterator) return undefined;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// React-specific properties
|
||||
if (prop === '$$typeof') return undefined;
|
||||
if (prop === 'propTypes') return undefined;
|
||||
if (prop === 'displayName') return 'NullComponent';
|
||||
|
||||
// Specific hook mocks for known hooks that need proper return values
|
||||
if (prop === 'useResizeDetector') {
|
||||
return useResizeDetectorMock;
|
||||
}
|
||||
|
||||
// Common hook names return useNoop for better compatibility
|
||||
if (typeof prop === 'string' && prop.startsWith('use')) {
|
||||
return useNoop;
|
||||
}
|
||||
|
||||
// Return another proxy to allow further chaining (ace.config.set)
|
||||
return createDeepProxy();
|
||||
},
|
||||
|
||||
// Handle function calls - return undefined (safe default)
|
||||
apply() {
|
||||
return undefined;
|
||||
},
|
||||
|
||||
// Handle new ClassName() - return an empty object
|
||||
construct() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
// Create a proxy over a function so it's both callable and has properties
|
||||
return new Proxy(function NullModule() {}, handler);
|
||||
}
|
||||
|
||||
// Create the main module export as a deep proxy
|
||||
const nullModule = createDeepProxy();
|
||||
|
||||
// Support both CommonJS and ES module patterns
|
||||
module.exports = nullModule;
|
||||
module.exports.default = createDeepProxy();
|
||||
module.exports.__esModule = true;
|
||||
|
||||
// Named exports for common patterns (webpack may inline these)
|
||||
module.exports.useResizeDetector = useResizeDetectorMock;
|
||||
module.exports.withResizeDetector = createDeepProxy();
|
||||
module.exports.Resizable = NullComponent;
|
||||
module.exports.ResizableBox = NullComponent;
|
||||
module.exports.FixedSizeList = NullComponent;
|
||||
module.exports.VariableSizeList = NullComponent;
|
||||
|
||||
// ace-builds specific exports that CodeEditor uses
|
||||
module.exports.config = createDeepProxy();
|
||||
module.exports.require = createDeepProxy();
|
||||
module.exports.edit = createDeepProxy();
|
||||
54
docs/src/shims/react-table.js
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Shim for react-table to handle CommonJS to ES module interop
|
||||
// react-table v7 is CommonJS, but Superset components import it with ES module syntax
|
||||
// Use relative path to avoid circular dependency since webpack aliases 'react-table' to this file
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- CJS interop shim for react-table v7
|
||||
const reactTable = require('../../node_modules/react-table');
|
||||
|
||||
// Re-export all named exports
|
||||
export const {
|
||||
useTable,
|
||||
useFilters,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useGlobalFilter,
|
||||
useRowSelect,
|
||||
useRowState,
|
||||
useColumnOrder,
|
||||
useExpanded,
|
||||
useGroupBy,
|
||||
useResizeColumns,
|
||||
useBlockLayout,
|
||||
useAbsoluteLayout,
|
||||
useFlexLayout,
|
||||
actions,
|
||||
defaultColumn,
|
||||
makePropGetter,
|
||||
reduceHooks,
|
||||
loopHooks,
|
||||
ensurePluginOrder,
|
||||
functionalUpdate,
|
||||
useGetLatest,
|
||||
safeUseLayoutEffect,
|
||||
} = reactTable;
|
||||
|
||||
// Default export
|
||||
export default reactTable;
|
||||
@@ -187,3 +187,270 @@ ul.dropdown__menu svg {
|
||||
[data-theme='dark'] .ant-collapse-header {
|
||||
color: var(--ifm-font-base-color);
|
||||
}
|
||||
|
||||
/* Hide the non-functional "Send API Request" button and Response block in API docs */
|
||||
/* The interactive API testing doesn't work due to CORS restrictions */
|
||||
.openapi-explorer__request-btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.openapi-explorer__response-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* API Method Badge Colors - Swagger-style color coding */
|
||||
/* These override Infima badge classes used by docusaurus-openapi-docs */
|
||||
|
||||
/* GET - Blue (badge--primary) */
|
||||
.openapi__method-endpoint .badge--primary {
|
||||
background-color: #61affe !important;
|
||||
border-color: #61affe !important;
|
||||
}
|
||||
|
||||
/* POST - Green (badge--success) */
|
||||
.openapi__method-endpoint .badge--success {
|
||||
background-color: #49cc90 !important;
|
||||
border-color: #49cc90 !important;
|
||||
}
|
||||
|
||||
/* PUT - Info/Cyan -> Orange (badge--info) */
|
||||
.openapi__method-endpoint .badge--info {
|
||||
background-color: #fca130 !important;
|
||||
border-color: #fca130 !important;
|
||||
}
|
||||
|
||||
/* PATCH - Warning/Yellow -> Teal (badge--warning) */
|
||||
.openapi__method-endpoint .badge--warning {
|
||||
background-color: #50e3c2 !important;
|
||||
border-color: #50e3c2 !important;
|
||||
color: #1b1b1d !important;
|
||||
}
|
||||
|
||||
/* DELETE - Red (badge--danger) */
|
||||
.openapi__method-endpoint .badge--danger {
|
||||
background-color: #f93e3e !important;
|
||||
border-color: #f93e3e !important;
|
||||
}
|
||||
|
||||
/* Sidebar method badges - colored dots before endpoint names */
|
||||
/* The method classes (get, post, etc.) are on the <li> (menu__list-item),
|
||||
so we target the <a> (menu__link) inside using descendant selector */
|
||||
.menu__list-item.api-method > .menu__link::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.menu__list-item.get.api-method > .menu__link::before {
|
||||
background-color: #61affe;
|
||||
}
|
||||
|
||||
.menu__list-item.post.api-method > .menu__link::before {
|
||||
background-color: #49cc90;
|
||||
}
|
||||
|
||||
.menu__list-item.put.api-method > .menu__link::before {
|
||||
background-color: #fca130;
|
||||
}
|
||||
|
||||
.menu__list-item.patch.api-method > .menu__link::before {
|
||||
background-color: #50e3c2;
|
||||
}
|
||||
|
||||
.menu__list-item.delete.api-method > .menu__link::before {
|
||||
background-color: #f93e3e;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Component Example Isolation
|
||||
Prevents Docusaurus/Infima styles from bleeding into Superset components
|
||||
============================================ */
|
||||
|
||||
/* Reset link styles inside component examples */
|
||||
.storybook-example a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
vertical-align: inherit;
|
||||
}
|
||||
|
||||
.storybook-example a:hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Reset list styles */
|
||||
.storybook-example ul,
|
||||
.storybook-example ol {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* Override Infima's .markdown li + li margin */
|
||||
.storybook-example li + li,
|
||||
.markdown .storybook-example li + li {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Reset heading styles */
|
||||
.storybook-example h1,
|
||||
.storybook-example h2,
|
||||
.storybook-example h3,
|
||||
.storybook-example h4,
|
||||
.storybook-example h5,
|
||||
.storybook-example h6 {
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/* Reset paragraph margins */
|
||||
.storybook-example p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Reset table margins - Infima applies margin-bottom via --ifm-spacing-vertical */
|
||||
.storybook-example table {
|
||||
margin: 0;
|
||||
display: table;
|
||||
}
|
||||
|
||||
/* Ensure Ant Design components render correctly */
|
||||
.storybook-example .ant-breadcrumb {
|
||||
line-height: 1.5715;
|
||||
}
|
||||
|
||||
.storybook-example .ant-breadcrumb a {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.storybook-example .ant-breadcrumb a:hover {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Ant Design Popup/Portal Isolation
|
||||
These components render outside .storybook-example via portals
|
||||
============================================ */
|
||||
|
||||
/* DatePicker, TimePicker dropdown panels - reset Infima table styles
|
||||
Using doubled selectors for higher specificity than Infima's defaults */
|
||||
.ant-picker-dropdown.ant-picker-dropdown table,
|
||||
.ant-picker-dropdown.ant-picker-dropdown thead,
|
||||
.ant-picker-dropdown.ant-picker-dropdown tbody,
|
||||
.ant-picker-dropdown.ant-picker-dropdown tr,
|
||||
.ant-picker-dropdown.ant-picker-dropdown th,
|
||||
.ant-picker-dropdown.ant-picker-dropdown td {
|
||||
border: none;
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-picker-dropdown.ant-picker-dropdown table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
display: table;
|
||||
}
|
||||
|
||||
/* Override Infima's zebra striping with higher specificity */
|
||||
.ant-picker-dropdown.ant-picker-dropdown tr:nth-child(2n),
|
||||
.ant-picker-dropdown.ant-picker-dropdown tbody tr:nth-child(2n) {
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-picker-dropdown.ant-picker-dropdown th,
|
||||
.ant-picker-dropdown.ant-picker-dropdown td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Select, Dropdown, Popover portals */
|
||||
.ant-select-dropdown.ant-select-dropdown table,
|
||||
.ant-select-dropdown.ant-select-dropdown thead,
|
||||
.ant-select-dropdown.ant-select-dropdown tbody,
|
||||
.ant-select-dropdown.ant-select-dropdown tr,
|
||||
.ant-select-dropdown.ant-select-dropdown th,
|
||||
.ant-select-dropdown.ant-select-dropdown td,
|
||||
.ant-dropdown.ant-dropdown table,
|
||||
.ant-dropdown.ant-dropdown thead,
|
||||
.ant-dropdown.ant-dropdown tbody,
|
||||
.ant-dropdown.ant-dropdown tr,
|
||||
.ant-dropdown.ant-dropdown th,
|
||||
.ant-dropdown.ant-dropdown td,
|
||||
.ant-popover.ant-popover table,
|
||||
.ant-popover.ant-popover thead,
|
||||
.ant-popover.ant-popover tbody,
|
||||
.ant-popover.ant-popover tr,
|
||||
.ant-popover.ant-popover th,
|
||||
.ant-popover.ant-popover td {
|
||||
border: none;
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-select-dropdown.ant-select-dropdown tr:nth-child(2n),
|
||||
.ant-dropdown.ant-dropdown tr:nth-child(2n),
|
||||
.ant-popover.ant-popover tr:nth-child(2n) {
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Modal portals */
|
||||
.ant-modal.ant-modal table,
|
||||
.ant-modal.ant-modal thead,
|
||||
.ant-modal.ant-modal tbody,
|
||||
.ant-modal.ant-modal tr,
|
||||
.ant-modal.ant-modal th,
|
||||
.ant-modal.ant-modal td {
|
||||
border: none;
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-modal.ant-modal tr:nth-child(2n) {
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Live Code Editor Height Limits
|
||||
Prevents tall code blocks from dominating the page
|
||||
============================================ */
|
||||
|
||||
/* Limit the code editor height and make it scrollable */
|
||||
/* Target multiple possible class names used by Docusaurus/react-live */
|
||||
.playgroundEditor,
|
||||
[class*="playgroundEditor"],
|
||||
.live-editor,
|
||||
[class*="liveEditor"] {
|
||||
max-height: 350px !important;
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
/* The actual textarea/code area inside the editor */
|
||||
.playgroundEditor textarea,
|
||||
.playgroundEditor pre,
|
||||
[class*="playgroundEditor"] textarea,
|
||||
[class*="playgroundEditor"] pre {
|
||||
max-height: 350px !important;
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
/* Also limit the preview area for consistency */
|
||||
.playgroundPreview,
|
||||
[class*="playgroundPreview"] {
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Hide sidebar items with sidebar_class_name: hidden in frontmatter */
|
||||
.menu__list-item.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
@import 'antd-theme.less';
|
||||
|
||||
body {
|
||||
font-family: var(--ifm-font-family-base);
|
||||
@@ -57,13 +56,7 @@ html[data-theme='dark'] .docusaurus-highlight-code-line {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Hacks to disable Swagger UI's "try it out" interactive mode */
|
||||
.try-out,
|
||||
.auth-wrapper,
|
||||
.information-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Legacy Swagger UI styles (for versioned docs that still use swagger-ui-react) */
|
||||
.swagger-ui table td,
|
||||
.swagger-ui table th,
|
||||
.swagger-ui table tr {
|
||||
@@ -87,26 +80,29 @@ a > span > svg {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
&::before {
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(180deg, #11b0d8 0%, #116f86 100%);
|
||||
content: '';
|
||||
display: block;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
&::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.default-button-theme::before {
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(180deg, #11b0d8 0%, #116f86 100%);
|
||||
content: '';
|
||||
display: block;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.default-button-theme:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.default-button-theme:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Navbar */
|
||||
@@ -115,32 +111,32 @@ a > span > svg {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.get-started-button {
|
||||
border-radius: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
width: 142px;
|
||||
padding: 7px 0;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.navbar .get-started-button {
|
||||
border-radius: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
width: 142px;
|
||||
padding: 7px 0;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.github-button {
|
||||
background-image: url('/img/github.png');
|
||||
background-size: contain;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.navbar .github-button {
|
||||
background-image: url('/img/github.png');
|
||||
background-size: contain;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.navbar--dark {
|
||||
background-color: transparent;
|
||||
border-bottom: 1px solid rgba(24, 115, 132, 0.4);
|
||||
}
|
||||
|
||||
.github-button {
|
||||
background-image: url('/img/github-dark.png');
|
||||
}
|
||||
.navbar--dark .github-button {
|
||||
background-image: url('/img/github-dark.png');
|
||||
}
|
||||
|
||||
.navbar__logo {
|
||||
@@ -159,11 +155,11 @@ a > span > svg {
|
||||
.navbar {
|
||||
padding-right: 8px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.get-started-button,
|
||||
.github-button {
|
||||
display: none;
|
||||
}
|
||||
.navbar .get-started-button,
|
||||
.navbar .github-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar__items {
|
||||
@@ -192,20 +188,20 @@ a > span > svg {
|
||||
--docsearch-searchbox-background: var(--ifm-navbar-background-color);
|
||||
border: 1px solid #187384;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&.DocSearch-Button {
|
||||
width: 225px;
|
||||
}
|
||||
.navbar .DocSearch.DocSearch-Button {
|
||||
width: 225px;
|
||||
}
|
||||
|
||||
.DocSearch-Search-Icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.navbar .DocSearch .DocSearch-Search-Icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.DocSearch-Button-Key,
|
||||
.DocSearch-Button-Placeholder {
|
||||
display: none;
|
||||
}
|
||||
.navbar .DocSearch .DocSearch-Button-Key,
|
||||
.navbar .DocSearch .DocSearch-Button-Placeholder {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar--dark .DocSearch {
|
||||
@@ -226,18 +222,37 @@ a > span > svg {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.footer__applitools {
|
||||
.footer__ci-services {
|
||||
background-color: #0d3e49;
|
||||
color: #e1e1e1;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 16px 0;
|
||||
padding: 12px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 34px;
|
||||
}
|
||||
.footer__ci-services span {
|
||||
font-size: 13px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.footer__ci-services a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.footer__ci-services a:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.footer__ci-services img {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.footer__divider {
|
||||
@@ -252,7 +267,16 @@ a > span > svg {
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 996px) {
|
||||
.footer__applitools img {
|
||||
height: 28px;
|
||||
.footer__ci-services {
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
.footer__ci-services span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.footer__ci-services img {
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
10
docs/src/theme.d.ts
vendored
@@ -30,3 +30,13 @@ declare module '@theme/Layout' {
|
||||
|
||||
export default function Layout(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/Playground/Header' {
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface Props {
|
||||
readonly children?: ReactNode;
|
||||
}
|
||||
|
||||
export default function PlaygroundHeader(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
107
docs/src/theme/Playground/Preview/index.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 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 React, { type ReactNode } from 'react';
|
||||
import { LiveError, LivePreview } from 'react-live';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
import { ErrorBoundaryErrorMessageFallback } from '@docusaurus/theme-common';
|
||||
import ErrorBoundary from '@docusaurus/ErrorBoundary';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import PlaygroundHeader from '@theme/Playground/Header';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
// Get the theme wrapper for Superset components
|
||||
function getThemeWrapper() {
|
||||
if (typeof window === 'undefined') {
|
||||
return ({ children }: { children: React.ReactNode }) => <>{children}</>;
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { themeObject } = require('@apache-superset/core/ui');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { App } = require('antd');
|
||||
|
||||
if (!themeObject?.SupersetThemeProvider) {
|
||||
return ({ children }: { children: React.ReactNode }) => <>{children}</>;
|
||||
}
|
||||
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<themeObject.SupersetThemeProvider>
|
||||
<App>{children}</App>
|
||||
</themeObject.SupersetThemeProvider>
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('[PlaygroundPreview] Failed to load theme provider:', e);
|
||||
return ({ children }: { children: React.ReactNode }) => <>{children}</>;
|
||||
}
|
||||
}
|
||||
|
||||
function Loader() {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
function ThemedLivePreview(): ReactNode {
|
||||
const ThemeWrapper = getThemeWrapper();
|
||||
return (
|
||||
<ThemeWrapper>
|
||||
<LivePreview />
|
||||
</ThemeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function PlaygroundLivePreview(): ReactNode {
|
||||
// No SSR for the live preview
|
||||
// See https://github.com/facebook/docusaurus/issues/5747
|
||||
return (
|
||||
<BrowserOnly fallback={<Loader />}>
|
||||
{() => (
|
||||
<>
|
||||
<ErrorBoundary
|
||||
fallback={(params) => (
|
||||
<ErrorBoundaryErrorMessageFallback {...params} />
|
||||
)}
|
||||
>
|
||||
<ThemedLivePreview />
|
||||
</ErrorBoundary>
|
||||
<LiveError />
|
||||
</>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PlaygroundPreview(): ReactNode {
|
||||
return (
|
||||
<>
|
||||
<PlaygroundHeader>
|
||||
<Translate
|
||||
id="theme.Playground.result"
|
||||
description="The result label of the live codeblocks"
|
||||
>
|
||||
Result
|
||||
</Translate>
|
||||
</PlaygroundHeader>
|
||||
<div className={styles.playgroundPreview}>
|
||||
<PlaygroundLivePreview />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -16,12 +16,8 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
@primary-color: #20a7c9;
|
||||
@info-color: #66bcfe;
|
||||
@success-color: #59c189;
|
||||
@processing-color: #66bcfe;
|
||||
@error-color: #e04355;
|
||||
@highlight-color: #e04355;
|
||||
@normal-color: #d9d9d9;
|
||||
@white: #FFF;
|
||||
@black: #000;
|
||||
|
||||
.playgroundPreview {
|
||||
padding: 1rem;
|
||||
background-color: var(--ifm-pre-background);
|
||||
}
|
||||
@@ -18,36 +18,49 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Card, Input, Space, Tag, Tooltip } from 'antd';
|
||||
|
||||
// Import extension components from @apache-superset/core/ui
|
||||
// This matches the established pattern used throughout the Superset codebase
|
||||
// Resolved via webpack alias to superset-frontend/packages/superset-core/src/ui/components
|
||||
import { Alert } from '@apache-superset/core/ui';
|
||||
// Browser-only check for SSR safety
|
||||
const isBrowser = typeof window !== 'undefined';
|
||||
|
||||
/**
|
||||
* ReactLiveScope provides the scope for live code blocks.
|
||||
* Any component added here will be available in ```tsx live blocks.
|
||||
*
|
||||
* To add more components:
|
||||
* 1. Import the component from @apache-superset/core above
|
||||
* 2. Add it to the scope object below
|
||||
* Components are conditionally loaded only in the browser to avoid
|
||||
* SSG issues with Emotion CSS-in-JS jsx runtime.
|
||||
*
|
||||
* Components are available by name, e.g.:
|
||||
* <Button>Click me</Button>
|
||||
* <Avatar size="large" />
|
||||
* <Badge count={5} />
|
||||
*/
|
||||
const ReactLiveScope = {
|
||||
|
||||
// Base scope with React (always available)
|
||||
const ReactLiveScope: Record<string, unknown> = {
|
||||
// React core
|
||||
React,
|
||||
...React,
|
||||
|
||||
// Extension components from @apache-superset/core
|
||||
Alert,
|
||||
|
||||
// Common Ant Design components (for demos)
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
Space,
|
||||
Tag,
|
||||
Tooltip,
|
||||
};
|
||||
|
||||
// Only load Superset components in browser context
|
||||
// This prevents SSG errors from Emotion CSS-in-JS
|
||||
if (isBrowser) {
|
||||
try {
|
||||
// Dynamic require for browser-only execution
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const SupersetComponents = require('@superset/components');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { Alert } = require('@apache-superset/core/ui');
|
||||
|
||||
console.log('[ReactLiveScope] SupersetComponents keys:', Object.keys(SupersetComponents || {}).slice(0, 10));
|
||||
console.log('[ReactLiveScope] Has Button?', 'Button' in (SupersetComponents || {}));
|
||||
|
||||
Object.assign(ReactLiveScope, SupersetComponents, { Alert });
|
||||
|
||||
console.log('[ReactLiveScope] Final scope keys:', Object.keys(ReactLiveScope).slice(0, 20));
|
||||
} catch (e) {
|
||||
console.error('[ReactLiveScope] Failed to load Superset components:', e);
|
||||
}
|
||||
}
|
||||
|
||||
export default ReactLiveScope;
|
||||
|
||||
@@ -74,6 +74,14 @@ export default function Root({ children }) {
|
||||
window._paq.push(['trackSiteSearch', keyword, category, resultsCount]);
|
||||
};
|
||||
|
||||
// Helper to track page views
|
||||
const trackPageView = (url, title) => {
|
||||
if (devMode) {
|
||||
console.log('Matomo trackPageView:', { url, title });
|
||||
}
|
||||
window._paq.push(['trackPageView']);
|
||||
};
|
||||
|
||||
|
||||
// Track external link clicks using domain as category (vendor-agnostic)
|
||||
const handleLinkClick = (event) => {
|
||||
@@ -221,7 +229,6 @@ export default function Root({ children }) {
|
||||
trackDocsVersion();
|
||||
|
||||
if (devMode) {
|
||||
console.log('Tracking page view:', currentPath, currentTitle);
|
||||
window._paq.push(['setDomains', ['superset.apache.org']]);
|
||||
window._paq.push([
|
||||
'setCustomUrl',
|
||||
@@ -233,7 +240,7 @@ export default function Root({ children }) {
|
||||
|
||||
window._paq.push(['setReferrerUrl', window.location.href]);
|
||||
window._paq.push(['setDocumentTitle', currentTitle]);
|
||||
window._paq.push(['trackPageView']);
|
||||
trackPageView(currentPath, currentTitle);
|
||||
|
||||
// Check for 404 after page renders
|
||||
setTimeout(track404, 500);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import webpack from 'webpack';
|
||||
import type { Plugin } from '@docusaurus/types';
|
||||
|
||||
export default function webpackExtendPlugin(): Plugin<void> {
|
||||
@@ -26,14 +27,86 @@ export default function webpackExtendPlugin(): Plugin<void> {
|
||||
configureWebpack(config) {
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
// Use NormalModuleReplacementPlugin to forcefully replace react-table
|
||||
// This is necessary because regular aliases don't work for modules in nested node_modules
|
||||
const reactTableShim = path.resolve(__dirname, './shims/react-table.js');
|
||||
config.plugins?.push(
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/^react-table$/,
|
||||
reactTableShim,
|
||||
),
|
||||
);
|
||||
|
||||
// Stub out heavy third-party packages that are transitive dependencies of
|
||||
// superset-frontend components. The barrel file (components/index.ts)
|
||||
// re-exports all components, so webpack must resolve their imports even
|
||||
// though these components are never rendered on the docs site.
|
||||
const nullModuleShim = path.resolve(__dirname, './shims/null-module.js');
|
||||
const heavyDepsPatterns = [
|
||||
/^brace(\/|$)/, // ACE editor modes/themes
|
||||
/^react-ace(\/|$)/,
|
||||
/^ace-builds(\/|$)/,
|
||||
/^react-js-cron(\/|$)/, // Cron picker + CSS
|
||||
// react-resize-detector: NOT shimmed — DropdownContainer needs it at runtime
|
||||
// for overflow detection. Resolves from superset-frontend/node_modules.
|
||||
/^react-window(\/|$)/,
|
||||
/^re-resizable(\/|$)/,
|
||||
/^react-draggable(\/|$)/,
|
||||
/^ag-grid-react(\/|$)/,
|
||||
/^ag-grid-community(\/|$)/,
|
||||
];
|
||||
heavyDepsPatterns.forEach(pattern => {
|
||||
config.plugins?.push(
|
||||
new webpack.NormalModuleReplacementPlugin(pattern, nullModuleShim),
|
||||
);
|
||||
});
|
||||
|
||||
// Add YAML loader rule directly to existing rules
|
||||
config.module?.rules?.push({
|
||||
test: /\.ya?ml$/,
|
||||
use: 'js-yaml-loader',
|
||||
});
|
||||
|
||||
// Add swc-loader rule for superset-frontend files
|
||||
// SWC is a Rust-based transpiler that's significantly faster than babel
|
||||
const supersetFrontendPath = path.resolve(
|
||||
__dirname,
|
||||
'../../superset-frontend',
|
||||
);
|
||||
config.module?.rules?.push({
|
||||
test: /\.(tsx?|jsx?)$/,
|
||||
include: supersetFrontendPath,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'swc-loader',
|
||||
options: {
|
||||
// Ignore superset-frontend/.swcrc which references plugins not
|
||||
// installed in the docs workspace (e.g. @swc/plugin-emotion)
|
||||
swcrc: false,
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
tsx: true,
|
||||
},
|
||||
transform: {
|
||||
react: {
|
||||
runtime: 'automatic',
|
||||
importSource: '@emotion/react',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
devtool: isDev ? 'eval-source-map' : config.devtool,
|
||||
devtool: isDev ? false : config.devtool,
|
||||
cache: {
|
||||
type: 'filesystem',
|
||||
buildDependencies: {
|
||||
config: [__filename],
|
||||
},
|
||||
},
|
||||
...(isDev && {
|
||||
optimization: {
|
||||
...config.optimization,
|
||||
@@ -44,8 +117,16 @@ export default function webpackExtendPlugin(): Plugin<void> {
|
||||
},
|
||||
}),
|
||||
resolve: {
|
||||
// Add superset-frontend node_modules to module resolution
|
||||
modules: [
|
||||
...(config.resolve?.modules || []),
|
||||
path.resolve(__dirname, '../../superset-frontend/node_modules'),
|
||||
],
|
||||
alias: {
|
||||
...config.resolve.alias,
|
||||
// Ensure single React instance across all modules (critical for hooks to work)
|
||||
react: path.resolve(__dirname, '../node_modules/react'),
|
||||
'react-dom': path.resolve(__dirname, '../node_modules/react-dom'),
|
||||
// Allow importing from superset-frontend
|
||||
src: path.resolve(__dirname, '../../superset-frontend/src'),
|
||||
// '@superset-ui/core': path.resolve(
|
||||
@@ -58,14 +139,29 @@ export default function webpackExtendPlugin(): Plugin<void> {
|
||||
__dirname,
|
||||
'../../superset-frontend/packages/superset-ui-core/src/components',
|
||||
),
|
||||
// Extension API package - allows docs to import from @apache-superset/core/ui
|
||||
// This matches the established pattern used throughout the Superset codebase
|
||||
// Point directly to components to avoid importing theme (which has font dependencies)
|
||||
// Note: TypeScript types come from docs/src/types/apache-superset-core (see tsconfig.json)
|
||||
// This split is intentional: webpack resolves actual source, tsconfig provides simplified types
|
||||
// Also alias the full package path for internal imports within components
|
||||
'@superset-ui/core/components': path.resolve(
|
||||
__dirname,
|
||||
'../../superset-frontend/packages/superset-ui-core/src/components',
|
||||
),
|
||||
// Use a shim for react-table to handle CommonJS to ES module interop
|
||||
// react-table v7 is CommonJS, but Superset components import it with ES module syntax
|
||||
'react-table': path.resolve(__dirname, './shims/react-table.js'),
|
||||
// Extension API package - resolve @apache-superset/core and its sub-paths
|
||||
// to source so the docs build doesn't depend on pre-built lib/ artifacts.
|
||||
// More specific sub-path aliases must come first; webpack matches the
|
||||
// longest prefix.
|
||||
'@apache-superset/core/ui': path.resolve(
|
||||
__dirname,
|
||||
'../../superset-frontend/packages/superset-core/src/ui/components',
|
||||
'../../superset-frontend/packages/superset-core/src/ui',
|
||||
),
|
||||
'@apache-superset/core/api/core': path.resolve(
|
||||
__dirname,
|
||||
'../../superset-frontend/packages/superset-core/src/api/core',
|
||||
),
|
||||
'@apache-superset/core': path.resolve(
|
||||
__dirname,
|
||||
'../../superset-frontend/packages/superset-core/src',
|
||||
),
|
||||
// Add proper Storybook aliases
|
||||
'@storybook/blocks': path.resolve(
|
||||
@@ -123,8 +219,6 @@ export default function webpackExtendPlugin(): Plugin<void> {
|
||||
),
|
||||
},
|
||||
},
|
||||
// We're removing the ts-loader rule that was processing superset-frontend files
|
||||
// This will prevent TypeScript errors from files outside the docs directory
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
13
docs/static/feature-flags.json
vendored
@@ -114,6 +114,12 @@
|
||||
"lifecycle": "testing",
|
||||
"description": "Allow users to export full CSV of table viz type. Warning: Could cause server memory/compute issues with large datasets."
|
||||
},
|
||||
{
|
||||
"name": "AWS_DATABASE_IAM_AUTH",
|
||||
"default": false,
|
||||
"lifecycle": "testing",
|
||||
"description": "Enable AWS IAM authentication for database connections (Aurora, Redshift). Allows cross-account role assumption via STS AssumeRole. Security note: When enabled, ensure Superset's IAM role has restricted sts:AssumeRole permissions to prevent unauthorized access."
|
||||
},
|
||||
{
|
||||
"name": "CACHE_IMPERSONATION",
|
||||
"default": false,
|
||||
@@ -241,6 +247,13 @@
|
||||
"description": "Enables dashboard virtualization for improved performance",
|
||||
"category": "path_to_deprecation"
|
||||
},
|
||||
{
|
||||
"name": "DASHBOARD_VIRTUALIZATION_DEFER_DATA",
|
||||
"default": false,
|
||||
"lifecycle": "stable",
|
||||
"description": "Supports simultaneous data and dashboard virtualization for backend performance",
|
||||
"category": "runtime_config"
|
||||
},
|
||||
{
|
||||
"name": "DATAPANEL_CLOSED_BY_DEFAULT",
|
||||
"default": false,
|
||||
|
||||
BIN
docs/static/img/atomic-design.png
vendored
Normal file
|
After Width: | Height: | Size: 159 KiB |
21
docs/static/img/community/bluesky-symbol.svg
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 568 501" width="40" height="40" fill="#1185FE">
|
||||
<path d="M123.121 33.664C188.241 82.553 258.281 181.68 284 234.873c25.719-53.192 95.759-152.32 160.879-201.21C491.866-1.611 568-28.906 568 57.947c0 17.346-9.945 145.713-15.778 166.555-20.275 72.453-94.155 90.933-159.875 79.748C507.222 323.8 536.444 388.56 473.333 453.32c-119.86 122.992-172.272-30.859-185.702-70.281-2.462-7.227-3.614-10.608-3.631-7.733-.017-2.875-1.169.506-3.631 7.733-13.43 39.422-65.842 193.273-185.702 70.281-63.111-64.76-33.889-129.52 80.986-149.071-65.72 11.185-139.6-7.295-159.875-79.748C9.945 203.659 0 75.291 0 57.946 0-28.906 76.135-1.612 123.121 33.664z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
23
docs/static/img/community/globe-symbol.svg
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="#484848" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
21
docs/static/img/community/linkedin-symbol.svg
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="40" height="40" fill="#0A66C2">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
21
docs/static/img/community/x-symbol.svg
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="40" height="40" fill="#484848">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
BIN
docs/static/img/databases/alloydb.png
vendored
Normal file
|
After Width: | Height: | Size: 86 KiB |
57
docs/static/img/databases/apache-iotdb.svg
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1"
|
||||
id="Õ_xBA__x2264__x201E__1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 107.7 107.7"
|
||||
style="enable-background:new 0 0 107.7 107.7;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#9E2878;}
|
||||
</style>
|
||||
<g>
|
||||
<g id="g1133" transform="translate(-244.51235,-228.78793)">
|
||||
<path id="path1119" class="st0" d="M340.8,253.8c2.6-1,5.5,0.4,6.2,3c0.7,2.6-1.1,5.7-3.7,6.4c-3.2,0.6-6.2-1.4-9.3,0.2
|
||||
c-3.1,1.7-4,5.2-4.7,8.3c-1.4,5-8.5,7.3-12.1,4.2c-3.3-2.4-3.4-7.8-0.2-11c2.2-2.5,5.9-3.3,8.8-2c2.7,1.2,6,1.7,8.6-0.4
|
||||
C337.5,260.1,336.7,255.1,340.8,253.8L340.8,253.8z"/>
|
||||
<path id="path1121" class="st0" d="M280.5,244.7c4.2-2.2,9.5,1.5,8.2,6.1c-1.4,5.4-0.7,11.5,2.9,15.5c3.4,4,9.8,4.8,14.6,1.9
|
||||
c3.7-2.1,6-5.8,7.4-9.6c1-3.1,0.6-6.2,1.1-9.3c1-3.8,5.8-6,9.1-4.2c3.2,1.4,4,5.9,1.7,8.8c-1.4,2.2-4.2,2.7-6.3,3.8
|
||||
c-3.6,1.9-6.5,4.9-8.4,8.6c-1.2,2.1-1.1,4.5-1.7,6.7c-0.9,1.9-3,2.7-4.8,2.9c-4.8,0.6-9.5,3-13,6.7c-1.7,1.7-3.1,4.2-5.6,4.2
|
||||
c-2.7-0.2-4.7-2.6-7.4-2.7c-4.9-0.7-10.2,0.7-14.4,4c-3.7,3.1-9.4,0.8-9.6-3.7c-0.9-5.1,6.2-9.5,10.1-6.2c4.3,4,10.8,5.6,16.9,3.7
|
||||
c5.6-1.9,9.7-8.3,8.6-14c-0.9-4.9-4.2-9.3-8.6-11.4c-1.7-0.8-3.6-1.6-4.2-3.5C275.6,250.2,277.3,246.2,280.5,244.7L280.5,244.7z"
|
||||
/>
|
||||
<path id="path1123" class="st0" d="M277.8,235.9c2.2-0.8,4.2,1.3,3.3,3.4c-0.6,2.1-3.7,2.7-4.8,1.1
|
||||
C275,238.9,276,236.4,277.8,235.9z"/>
|
||||
<path id="path1125" class="st0" d="M246.9,278.8c2.2-0.8,4.2,1.2,3.3,3.4c-0.6,2.1-3.7,2.7-4.8,1C244,281.9,245,279.3,246.9,278.8
|
||||
L246.9,278.8z"/>
|
||||
<path id="path1127" class="st0" d="M328.2,236.2c2.2-0.7,4.2,1.3,3.3,3.5c-0.6,2-3.7,2.7-4.8,1
|
||||
C325.4,239.2,326.3,236.8,328.2,236.2z"/>
|
||||
<path id="path1129" class="st0" d="M253.6,257.7c0.4-3.7,5.5-5.9,8.1-3.6c1.9,1.1,1.6,3.6,2.2,5.4c0.4,2.4,2.7,4.3,5.2,4.3
|
||||
c3.2,0.3,6.4-2.3,9.5-1.2c4.8,1.2,6.5,7.5,3.2,11.5c-2.9,4.1-9.3,4.5-12,0.7c-2.4-2.8-0.5-7.1-2.7-10.1c-1.7-2.9-5.4-2.7-8.3-1.9
|
||||
C255.8,263.8,253,260.7,253.6,257.7L253.6,257.7z"/>
|
||||
<path id="path1131" class="st0" d="M300.8,230c3.3-1.9,7.5,0.9,6.7,4.6c0,2.3-2.2,3.6-3.5,5.2c-1.9,1.9-2.3,4.9-1.2,7
|
||||
c1.3,2.9,4.9,4,5.5,7.2c1.2,4.8-3.3,10.1-8.2,9.8c-4.8,0.1-8.3-5-6.3-9.5c1.2-3.8,5.8-4.9,7.2-8.5c1.7-3.2-0.4-6.1-2.4-8.1
|
||||
C296.7,235.6,297.9,231.4,300.8,230z"/>
|
||||
</g>
|
||||
<g id="g1149" transform="translate(-244.51235,-228.78793)">
|
||||
<path id="path1135" class="st0" d="M256,311.5c-2.6,1-5.5-0.4-6.2-3c-0.7-2.6,1.1-5.7,3.7-6.4c3.2-0.6,6.2,1.4,9.3-0.2
|
||||
c3.1-1.6,4-5.1,4.7-8.2c1.4-5,8.5-7.3,12.1-4.2c3.3,2.4,3.4,7.8,0.2,10.9c-2.2,2.5-5.9,3.3-8.8,2c-2.7-1.2-6-1.7-8.6,0.4
|
||||
C259.1,305,259.9,310.2,256,311.5L256,311.5z"/>
|
||||
<path id="path1137" class="st0" d="M316.1,320.5c-4.2,2.2-9.5-1.5-8.2-6c1.4-5.4,0.7-11.6-2.9-15.6c-3.4-4-9.8-4.7-14.6-1.9
|
||||
c-3.7,2-6,5.7-7.4,9.5c-1,3.1-0.6,6.2-1.1,9.3c-1,3.8-5.8,6-9.1,4.3c-3.2-1.4-4-5.9-1.7-8.9c1.4-2.2,4.2-2.7,6.3-3.8
|
||||
c3.6-1.9,6.5-4.9,8.4-8.6c1.2-2,1.1-4.5,1.7-6.6c0.9-1.9,3-2.7,4.8-2.9c4.8-0.6,9.5-3,13-6.7c1.7-1.6,3.1-4.2,5.6-4.1
|
||||
c2.7,0.1,4.7,2.5,7.4,2.7c4.9,0.6,10.2-0.8,14.4-4c3.7-3.2,9.4-0.9,9.6,3.6c0.9,5.1-6.2,9.5-10.1,6.2c-4.3-4-10.8-5.6-16.9-3.7
|
||||
c-5.6,1.9-9.7,8.3-8.6,14c0.9,4.8,4.2,9.3,8.6,11.3c1.7,0.8,3.6,1.6,4.2,3.5C321.2,314.9,319.4,319.1,316.1,320.5L316.1,320.5z"/>
|
||||
<path id="path1139" class="st0" d="M318.9,329.4c-2.2,0.8-4.2-1.3-3.3-3.4c0.6-2.1,3.7-2.7,4.8-1.1
|
||||
C321.7,326.3,320.7,328.8,318.9,329.4z"/>
|
||||
<path id="path1141" class="st0" d="M349.9,286.5c-2.2,0.7-4.2-1.3-3.3-3.5c0.6-2.1,3.7-2.7,4.8-1
|
||||
C352.8,283.5,351.8,285.9,349.9,286.5z"/>
|
||||
<path id="path1143" class="st0" d="M268.5,329c-2.2,0.7-4.2-1.3-3.3-3.5c0.6-2,3.7-2.7,4.8-1C271.3,325.9,270.3,328.5,268.5,329z"
|
||||
/>
|
||||
<path id="path1145" class="st0" d="M343.1,307.4c-0.4,3.7-5.5,5.9-8.1,3.6c-1.9-1.1-1.6-3.6-2.2-5.4c-0.4-2.4-2.7-4.3-5.2-4.3
|
||||
c-3.2-0.3-6.4,2.3-9.5,1.2c-4.8-1.2-6.5-7.5-3.2-11.5c2.9-4.1,9.3-4.5,12-0.7c2.4,2.8,0.5,7,2.7,10c1.7,2.9,5.4,2.7,8.3,1.9
|
||||
C341,301.4,343.8,304.4,343.1,307.4L343.1,307.4z"/>
|
||||
<path id="path1147" class="st0" d="M295.8,335.2c-3.3,2-7.5-0.9-6.7-4.6c0-2.3,2.2-3.6,3.5-5.2c1.9-1.9,2.3-4.9,1.2-7
|
||||
c-1.3-2.9-4.9-4-5.5-7.2c-1.2-4.8,3.3-10.1,8.2-9.8c4.8-0.1,8.3,5,6.3,9.6c-1.2,3.7-5.8,4.8-7.2,8.4c-1.7,3.2,0.4,6.1,2.4,8.2
|
||||
C300,329.6,298.8,333.8,295.8,335.2L295.8,335.2z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
BIN
docs/static/img/databases/apache-phoenix.png
vendored
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/static/img/databases/cratedb.png
vendored
|
Before Width: | Height: | Size: 1.5 KiB |
1
docs/static/img/databases/cratedb.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 436 58" width="436" height="58"><style>.a{fill:#009dc7}</style><title>CrateDB</title><path class="a" d="m3.6 0h14.5v14.5h-14.5z"/><path class="a" d="m25.4 0h14.5v14.5h-14.5z"/><path class="a" d="m47.2 0h14.5v36.2h-14.5z"/><path class="a" d="m3.6 21.7h36.3v14.5h-36.3z"/><path class="a" d="m3.6 43.4h58.1v14.5h-58.1z"/><path d="m249.5 16.9h12.1v8.4h-12.1v19.3h12.1v8.4h-10.4c-1.5 0.1-2.9-0.2-4.3-0.7-1.3-0.6-2.6-1.3-3.6-2.3-1.1-1-1.9-2.2-2.5-3.6-0.6-1.3-1-2.7-1-4.2v-16.9h-9.7v-8.4h9.7v-12.1h9.7z"/><path d="m159.8 22.9q0-1.2 0.5-2.3 0.4-1.1 1.3-2 0.9-0.8 2-1.3 1.1-0.4 2.3-0.4h12.1v8.4h-13.3q-1 0-1.9 0.4-0.8 0.4-1.5 1-0.7 0.7-1.1 1.6-0.3 0.9-0.3 1.9v22.9h-9.7v-36.2h7.2l1.2 6h1.2z"/><path d="m90.8 16.9c0-1.6 0.3-3.2 0.9-4.6 0.6-1.5 1.5-2.8 2.7-3.9 1.1-1.2 2.4-2 3.9-2.7 1.5-0.6 3-0.9 4.6-0.9h36.4v9.7h-38.8v28.9h38.8v9.7h-36.4c-1.6 0-3.1-0.3-4.6-0.9-1.5-0.6-2.8-1.5-3.9-2.6-1.2-1.2-2.1-2.5-2.7-4-0.6-1.4-0.9-3-0.9-4.6v-24.1z"/><path fill-rule="evenodd" d="m358.9 6.1c2.1 0.9 4 2.1 5.5 3.7 1.6 1.5 2.9 3.4 3.7 5.5 0.9 2 1.3 4.2 1.3 6.4v14.5c0 2.2-0.4 4.4-1.3 6.5-0.8 2-2.1 3.9-3.7 5.4-1.5 1.6-3.4 2.8-5.5 3.7-2 0.8-4.2 1.3-6.4 1.3h-34v-48.3h34c2.2 0 4.4 0.5 6.4 1.3zm0.3 12.8q-0.6-1.3-1.6-2.3-1-1-2.4-1.6-1.3-0.5-2.7-0.5h-24.3v28.9h24.3q1.4 0 2.7-0.5 1.4-0.6 2.4-1.6 1-1 1.6-2.3 0.5-1.4 0.5-2.8v-14.5q0-1.4-0.5-2.8z"/><path fill-rule="evenodd" d="m212.7 16.9c1.3 0 2.5 0.2 3.7 0.7 1.2 0.5 2.3 1.2 3.2 2.1 0.9 0.9 1.7 1.9 2.2 3.1 0.4 1.2 0.7 2.5 0.7 3.7v26.6h-7.3l-1.2-4.8h-1.2q0 0.9-0.4 1.8-0.3 0.9-1 1.6-0.7 0.7-1.6 1-0.9 0.4-1.8 0.4h-14c-1.5 0.1-2.9-0.2-4.3-0.7-1.4-0.5-2.6-1.3-3.7-2.3-1.1-1-1.9-2.2-2.5-3.5-0.6-1.3-1-2.8-1-4.2 0-1.4 0.3-2.8 0.9-4.1 0.6-1.2 1.4-2.4 2.4-3.4 1-0.9 2.2-1.7 3.5-2.2 1.3-0.5 2.6-0.7 4-0.7h19.4v-7.3h-25.4v-7.8zm-3.6 28.9q0.7 0 1.3-0.2 0.7-0.3 1.2-0.8 0.5-0.5 0.8-1.2 0.3-0.7 0.3-1.4v-3.6h-17.6c-0.9 0-1.8 0.4-2.5 1.1-0.7 0.6-1.1 1.6-1.1 2.5 0 1 0.4 1.9 1.1 2.6 0.6 0.7 1.6 1 2.5 1.1h14z"/><path fill-rule="evenodd" d="m296.7 16.9c1.6 0 3.2 0.3 4.7 0.9 1.4 0.6 2.8 1.5 3.9 2.6 1.1 1.1 2 2.5 2.6 3.9 0.6 1.5 0.9 3.1 0.9 4.7v9.6h-30.2v6.6h27.8v7.9h-25.4c-1.6 0-3.2-0.3-4.6-0.9-1.5-0.6-2.8-1.5-4-2.6-1.1-1.2-2-2.5-2.6-4-0.6-1.4-0.9-3-0.9-4.6v-12c0-1.6 0.3-3.2 0.9-4.7 0.6-1.4 1.5-2.8 2.6-3.9 1.2-1.1 2.5-2 4-2.6 1.4-0.6 3-0.9 4.6-0.9h15.7zm3.7 15.1v-3.7q0-0.7-0.3-1.4-0.3-0.6-0.8-1.1-0.5-0.5-1.2-0.8-0.6-0.3-1.3-0.3h-14.6q-0.7 0-1.4 0.3-0.6 0.3-1.1 0.8-0.6 0.5-0.8 1.2-0.3 0.6-0.3 1.4v3.6z"/><path fill-rule="evenodd" d="m427.3 27.7q0 0 0 0 0 0 0 0.1zm0 0c2.3 1.9 3.9 4.4 4.6 7.3 0.8 2.9 0.6 5.9-0.4 8.7-1.1 2.7-2.9 5.1-5.3 6.8-2.5 1.7-5.4 2.6-8.3 2.6h-38.8v-48.2h36.3c2.7 0 5.3 0.7 7.6 2.1 2.3 1.3 4.1 3.3 5.3 5.7 1.3 2.3 1.8 5 1.6 7.6-0.2 2.6-1.1 5.2-2.6 7.4zm-38.5-3.7h26.6c1.3 0 2.5-0.5 3.4-1.4 0.9-0.9 1.4-2.1 1.4-3.4 0-1.2-0.5-2.5-1.4-3.4-0.9-0.9-2.1-1.4-3.4-1.4h-26.6zm32.5 17.5c0.9-1 1.3-2.3 1.3-3.6 0-1.4-0.4-2.6-1.3-3.6-0.9-1-2.1-1.7-3.4-1.9h-29.1v10.9h29.1c1.3-0.2 2.5-0.8 3.4-1.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
BIN
docs/static/img/databases/mongodb.png
vendored
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/static/img/databases/neon.png
vendored
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/static/img/databases/risingwave.png
vendored
|
Before Width: | Height: | Size: 1.5 KiB |
182
docs/static/img/databases/risingwave.svg
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="1100"
|
||||
height="300"
|
||||
viewBox="0 0 291.04166 79.375002"
|
||||
version="1.1"
|
||||
id="svg93"
|
||||
inkscape:version="1.1.2 (b8e25be8, 2022-02-05)"
|
||||
sodipodi:docname="rwreadme.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview95"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:zoom="0.39692378"
|
||||
inkscape:cx="578.19665"
|
||||
inkscape:cy="137.30596"
|
||||
inkscape:window-width="1584"
|
||||
inkscape:window-height="1027"
|
||||
inkscape:window-x="336"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="g19" />
|
||||
<defs
|
||||
id="defs90">
|
||||
<style
|
||||
id="style97">.cls-1{fill:#3a7ceb;}.cls-2{fill:#e23e2b;}.cls-3{fill:#f0b400;}.cls-4{fill:#4acfd2;}</style>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath23">
|
||||
<path
|
||||
d="M 0,1080 H 1920 V 0 H 0 Z"
|
||||
id="path21" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath91">
|
||||
<path
|
||||
d="M 0,1080 H 1920 V 0 H 0 Z"
|
||||
id="path89" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
id="g19"
|
||||
clip-path="url(#clipPath23)"
|
||||
transform="matrix(0.35277777,0,0,-0.35277777,-280.32591,259.26734)">
|
||||
<g
|
||||
id="g355"
|
||||
transform="matrix(1.7,0,0,1.7,-756.33217,-454.30518)">
|
||||
<g
|
||||
id="g25"
|
||||
transform="translate(1074.2552,637.33841)">
|
||||
<path
|
||||
d="m 0,0 h 1.913 c 1.998,0 3.532,0.414 4.601,1.241 1.068,0.827 1.602,2.016 1.602,3.567 0,1.55 -0.534,2.739 -1.602,3.567 -1.069,0.827 -2.603,1.24 -4.601,1.24 H 0 Z M 22.28,-21.557 H 9.667 L 0,-6.565 v -8.311 c 0,-3.689 -2.991,-6.681 -6.681,-6.681 h -3.451 V 17.421 H 5.635 c 2.171,0 4.066,-0.319 5.686,-0.956 1.62,-0.638 2.955,-1.508 4.007,-2.611 1.05,-1.103 1.843,-2.378 2.378,-3.825 0.533,-1.448 0.801,-2.999 0.801,-4.653 0,-2.964 -0.716,-5.368 -2.146,-7.211 -1.43,-1.844 -3.541,-3.094 -6.332,-3.748 z"
|
||||
style="fill:#3a7ceb;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path27" />
|
||||
</g>
|
||||
<g
|
||||
id="g29"
|
||||
transform="translate(1108.2186,638.84481)">
|
||||
<path
|
||||
d="m 0,0 v -17.487 c 0,-3.079 -2.497,-5.576 -5.576,-5.576 h -3.78 V 3.456 h 5.9 C -1.547,3.456 0,1.909 0,0 m -9.977,13.64 c 0,0.724 0.138,1.404 0.414,2.042 0.275,0.637 0.654,1.197 1.137,1.68 0.482,0.482 1.042,0.861 1.68,1.138 0.638,0.275 1.318,0.413 2.042,0.413 0.724,0 1.404,-0.138 2.042,-0.413 0.637,-0.277 1.197,-0.656 1.68,-1.138 0.482,-0.483 0.861,-1.043 1.137,-1.68 0.276,-0.638 0.414,-1.318 0.414,-2.042 0,-0.723 -0.138,-1.405 -0.414,-2.042 C -0.121,10.96 -0.5,10.4 -0.982,9.918 -1.465,9.435 -2.025,9.056 -2.662,8.781 -3.3,8.504 -3.98,8.367 -4.704,8.367 c -0.724,0 -1.404,0.137 -2.042,0.414 -0.638,0.275 -1.198,0.654 -1.68,1.137 -0.483,0.482 -0.862,1.042 -1.137,1.68 -0.276,0.637 -0.414,1.319 -0.414,2.042"
|
||||
style="fill:#3a7ceb;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path31" />
|
||||
</g>
|
||||
<g
|
||||
id="g33"
|
||||
transform="translate(1131.1708,635.47761)">
|
||||
<path
|
||||
d="m 0,0 c -1.724,0.896 -3.412,1.344 -5.066,1.344 -0.862,0 -1.56,-0.173 -2.094,-0.517 -0.534,-0.345 -0.801,-0.81 -0.801,-1.396 0,-0.31 0.043,-0.56 0.129,-0.749 0.086,-0.19 0.276,-0.371 0.569,-0.543 0.292,-0.173 0.723,-0.328 1.292,-0.466 0.569,-0.137 1.319,-0.31 2.249,-0.516 2.585,-0.517 4.514,-1.44 5.79,-2.766 1.274,-1.327 1.912,-3.024 1.912,-5.092 0,-1.551 -0.31,-2.946 -0.93,-4.187 -0.62,-1.241 -1.482,-2.284 -2.585,-3.128 -1.103,-0.845 -2.429,-1.499 -3.98,-1.964 -1.551,-0.465 -3.257,-0.698 -5.118,-0.698 -3.585,0 -7.117,1.016 -10.597,3.05 l 3.412,6.617 c 2.618,-1.793 5.099,-2.688 7.443,-2.688 0.861,0 1.568,0.189 2.12,0.569 0.551,0.378 0.827,0.86 0.827,1.447 0,0.344 -0.044,0.628 -0.129,0.853 -0.087,0.224 -0.268,0.43 -0.543,0.62 -0.276,0.189 -0.681,0.362 -1.215,0.517 -0.535,0.155 -1.232,0.318 -2.094,0.491 -2.894,0.586 -4.919,1.456 -6.073,2.611 -1.156,1.154 -1.732,2.748 -1.732,4.781 0,1.482 0.275,2.818 0.827,4.007 0.551,1.189 1.344,2.197 2.378,3.024 1.033,0.827 2.282,1.465 3.748,1.913 1.464,0.447 3.093,0.672 4.885,0.672 2.929,0 5.772,-0.569 8.529,-1.706 z"
|
||||
style="fill:#3a7ceb;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path35" />
|
||||
</g>
|
||||
<g
|
||||
id="g37"
|
||||
transform="translate(1148.2289,638.84481)">
|
||||
<path
|
||||
d="m 0,0 v -17.487 c 0,-3.079 -2.497,-5.576 -5.576,-5.576 h -3.78 V 3.456 h 5.9 C -1.547,3.456 0,1.909 0,0 m -9.977,13.64 c 0,0.724 0.138,1.404 0.414,2.042 0.275,0.637 0.654,1.197 1.137,1.68 0.482,0.482 1.042,0.861 1.68,1.138 0.638,0.275 1.318,0.413 2.042,0.413 0.724,0 1.404,-0.138 2.042,-0.413 0.637,-0.277 1.197,-0.656 1.68,-1.138 0.482,-0.483 0.861,-1.043 1.137,-1.68 0.276,-0.638 0.414,-1.318 0.414,-2.042 0,-0.723 -0.138,-1.405 -0.414,-2.042 C -0.121,10.96 -0.5,10.4 -0.982,9.918 -1.465,9.435 -2.025,9.056 -2.662,8.781 -3.3,8.504 -3.98,8.367 -4.704,8.367 c -0.724,0 -1.404,0.137 -2.042,0.414 -0.638,0.275 -1.198,0.654 -1.68,1.137 -0.483,0.482 -0.862,1.042 -1.137,1.68 -0.276,0.637 -0.414,1.319 -0.414,2.042"
|
||||
style="fill:#3a7ceb;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path39" />
|
||||
</g>
|
||||
<g
|
||||
id="g41"
|
||||
transform="translate(1153.6049,642.30081)">
|
||||
<path
|
||||
d="m 0,0 h 9.356 v -3.36 c 1.275,1.551 2.568,2.593 3.878,3.128 1.309,0.534 2.843,0.801 4.6,0.801 1.862,0 3.455,-0.302 4.782,-0.905 1.326,-0.603 2.456,-1.456 3.386,-2.559 0.758,-0.896 1.275,-1.896 1.551,-2.998 0.276,-1.103 0.414,-2.361 0.414,-3.774 v -11.276 c 0,-3.079 -2.497,-5.576 -5.576,-5.576 H 18.61 v 13.389 c 0,1.309 -0.095,2.369 -0.284,3.179 -0.19,0.809 -0.526,1.455 -1.008,1.939 -0.414,0.413 -0.879,0.705 -1.396,0.878 -0.517,0.173 -1.069,0.259 -1.654,0.259 -1.586,0 -2.801,-0.474 -3.645,-1.422 -0.845,-0.948 -1.267,-2.3 -1.267,-4.058 v -8.588 c 0,-3.079 -2.496,-5.576 -5.575,-5.576 H 0 Z"
|
||||
style="fill:#3a7ceb;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path43" />
|
||||
</g>
|
||||
<g
|
||||
id="g45"
|
||||
transform="translate(1195.58,629.17041)">
|
||||
<path
|
||||
d="m 0,0 c 0,-0.862 0.155,-1.663 0.465,-2.404 0.31,-0.741 0.732,-1.388 1.267,-1.938 0.534,-0.552 1.171,-0.982 1.913,-1.292 0.74,-0.311 1.542,-0.466 2.403,-0.466 0.827,0 1.611,0.155 2.352,0.466 0.741,0.31 1.378,0.74 1.913,1.292 0.534,0.55 0.964,1.197 1.292,1.938 0.328,0.741 0.492,1.526 0.492,2.353 0,0.861 -0.164,1.654 -0.492,2.378 C 11.277,3.05 10.838,3.688 10.287,4.239 9.736,4.79 9.089,5.221 8.349,5.532 7.607,5.842 6.841,5.997 6.048,5.997 5.221,5.997 4.446,5.833 3.722,5.506 2.999,5.178 2.36,4.747 1.81,4.213 1.258,3.679 0.818,3.05 0.491,2.327 0.163,1.603 0,0.827 0,0 m 16.926,13.13 c 2.243,0 4.062,-1.818 4.062,-4.062 V -11.58 c 0,-1.482 -0.086,-2.808 -0.259,-3.98 -0.172,-1.171 -0.413,-2.205 -0.723,-3.102 -0.414,-1.137 -1.06,-2.196 -1.939,-3.179 -0.879,-0.982 -1.938,-1.835 -3.179,-2.558 -1.241,-0.725 -2.645,-1.293 -4.213,-1.706 -1.569,-0.414 -3.266,-0.621 -5.092,-0.621 -2.068,0 -3.963,0.259 -5.687,0.776 -1.723,0.516 -3.23,1.232 -4.522,2.145 -1.293,0.913 -2.353,1.982 -3.18,3.205 -0.827,1.224 -1.379,2.559 -1.654,4.006 H 1.085 c 0.655,-1.758 2.137,-2.636 4.446,-2.636 1.965,0 3.472,0.551 4.524,1.654 1.05,1.103 1.576,2.637 1.576,4.601 v 2.533 c -0.69,-0.655 -1.353,-1.198 -1.99,-1.628 -0.638,-0.432 -1.284,-0.785 -1.938,-1.06 -0.656,-0.276 -1.345,-0.474 -2.068,-0.595 -0.724,-0.12 -1.517,-0.181 -2.378,-0.181 -1.896,0 -3.636,0.328 -5.221,0.983 -1.586,0.654 -2.956,1.568 -4.11,2.739 -1.155,1.172 -2.051,2.576 -2.688,4.213 -0.638,1.637 -0.956,3.455 -0.956,5.454 0,2.033 0.343,3.92 1.033,5.661 0.689,1.74 1.629,3.256 2.818,4.549 1.189,1.292 2.601,2.308 4.239,3.05 1.636,0.74 3.42,1.111 5.35,1.111 3.136,0 5.772,-1.172 7.909,-3.515 v 2.791 z"
|
||||
style="fill:#3a7ceb;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path47" />
|
||||
</g>
|
||||
<g
|
||||
id="g49"
|
||||
transform="translate(1230.2025,651.31941)">
|
||||
<path
|
||||
d="M 0,0 5.803,-21.735 14.074,3.44 h 8.064 L 30.409,-21.735 36.211,0 c 0.541,2.029 2.379,3.44 4.479,3.44 h 7.037 L 36.096,-35.538 H 25.757 l -7.651,22.591 -7.651,-22.591 H 0.116 L -11.515,3.44 h 7.036 C -2.379,3.44 -0.542,2.029 0,0"
|
||||
style="fill:#3a7ceb;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path51" />
|
||||
</g>
|
||||
<g
|
||||
id="g53"
|
||||
transform="translate(1287.0272,629.11921)">
|
||||
<path
|
||||
d="m 0,0 c 0,-0.862 0.155,-1.664 0.465,-2.404 0.31,-0.742 0.732,-1.388 1.267,-1.939 0.534,-0.551 1.171,-0.982 1.913,-1.292 0.74,-0.31 1.542,-0.465 2.403,-0.465 0.827,0 1.611,0.155 2.352,0.465 0.741,0.31 1.378,0.741 1.913,1.292 0.534,0.551 0.964,1.189 1.292,1.913 0.328,0.724 0.492,1.499 0.492,2.326 0,0.828 -0.164,1.611 -0.492,2.353 -0.328,0.74 -0.758,1.386 -1.292,1.938 C 9.778,4.738 9.141,5.169 8.4,5.479 7.659,5.79 6.875,5.945 6.048,5.945 5.187,5.945 4.385,5.79 3.645,5.479 2.903,5.169 2.266,4.738 1.732,4.187 1.197,3.635 0.775,3.006 0.465,2.3 0.155,1.593 0,0.827 0,0 m 11.787,13.182 h 6.423 c 1.648,0 2.985,-1.336 2.985,-2.984 v -23.535 h -9.408 v 2.946 c -2,-2.516 -4.705,-3.774 -8.117,-3.774 -1.93,0 -3.705,0.354 -5.324,1.06 -1.62,0.706 -3.033,1.697 -4.239,2.973 -1.207,1.274 -2.146,2.774 -2.817,4.497 -0.672,1.723 -1.008,3.601 -1.008,5.635 0,1.895 0.327,3.696 0.982,5.402 0.654,1.706 1.568,3.196 2.739,4.472 1.172,1.274 2.567,2.282 4.188,3.023 1.619,0.741 3.412,1.112 5.376,1.112 3.308,0 6.048,-1.155 8.22,-3.464 z"
|
||||
style="fill:#3a7ceb;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path55" />
|
||||
</g>
|
||||
<g
|
||||
id="g57"
|
||||
transform="translate(1322.8539,639.17391)">
|
||||
<path
|
||||
d="M 0,0 4.856,-10.468 9.806,0.03 c 0.892,1.891 2.794,3.097 4.884,3.097 h 7.018 L 7.906,-23.392 H 1.651 L -11.996,3.127 h 7.099 C -2.795,3.127 -0.884,1.907 0,0"
|
||||
style="fill:#3a7ceb;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path59" />
|
||||
</g>
|
||||
<g
|
||||
id="g61"
|
||||
transform="translate(1364.8783,632.89261)">
|
||||
<path
|
||||
d="M 0,0 C -0.31,1.31 -0.948,2.36 -1.913,3.153 -2.878,3.946 -4.05,4.342 -5.428,4.342 -6.875,4.342 -8.056,3.963 -8.969,3.205 -9.882,2.447 -10.46,1.378 -10.701,0 Z m -10.959,-5.428 c 0,-4.032 1.895,-6.048 5.687,-6.048 2.032,0 3.566,0.827 4.6,2.481 h 9.047 c -1.828,-6.066 -6.394,-9.098 -13.699,-9.098 -2.241,0 -4.291,0.336 -6.152,1.008 -1.861,0.672 -3.456,1.628 -4.782,2.869 -1.327,1.241 -2.352,2.722 -3.075,4.446 -0.724,1.723 -1.086,3.652 -1.086,5.79 0,2.205 0.344,4.195 1.034,5.97 0.689,1.775 1.671,3.283 2.947,4.524 1.274,1.24 2.817,2.196 4.626,2.869 1.809,0.672 3.851,1.008 6.126,1.008 2.24,0 4.256,-0.336 6.048,-1.008 C 2.153,8.71 3.67,7.736 4.911,6.462 6.152,5.187 7.099,3.627 7.754,1.784 8.409,-0.061 8.736,-2.137 8.736,-4.446 v -0.982 z"
|
||||
style="fill:#3a7ceb;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path63" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g65"
|
||||
transform="translate(834.46932,615.63096)"
|
||||
style="fill:#3a7ceb;fill-opacity:1">
|
||||
<path
|
||||
d="m 0,0 c 2.631,-2.663 5.385,-5.128 8.245,-7.398 7.412,-5.887 15.531,-10.457 24.042,-13.707 7.92,-3.025 16.177,-4.907 24.518,-5.645 25.774,-2.28 52.344,6.361 72.192,25.97 18.539,18.316 27.468,42.562 26.977,66.71 -1.092,5.997 -3.509,5.222 -3.427,8.318 0.575,-0.466 3.148,-2.951 5.036,-5.649 23.639,-35.608 19.594,-84.095 -11.954,-115.265 -28.918,-28.57 -72.105,-33.857 -106.307,-16.006 -8.184,4.272 -15.854,9.866 -22.69,16.786 -3.119,3.157 -5.96,6.485 -8.524,9.952 C 1.179,-26.566 -3.725,-16.178 -6.608,-5.365 -8.059,0.073 -8.999,5.618 -9.426,11.195 -6.631,7.278 -3.49,3.532 0,0"
|
||||
style="fill:#3a7ceb;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path67" />
|
||||
</g>
|
||||
<g
|
||||
id="g69"
|
||||
transform="translate(893.24962,688.94836)">
|
||||
<path
|
||||
d="m 0,0 c 1.724,-1.745 2.919,-3.811 3.592,-6.004 0.215,-1.013 0.411,-2.032 0.592,-3.05 3.02,-17.13 1.122,-34.95 -5.679,-51.195 -2.925,13.93 -9.133,27.311 -18.638,38.906 -0.867,1.063 -1.768,2.11 -2.691,3.141 -4.817,5.221 -2.949,13.376 2.064,18.329 C -14.993,5.825 -5.698,5.767 0,0"
|
||||
style="fill:#e23e2b;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path71" />
|
||||
</g>
|
||||
<g
|
||||
id="g73"
|
||||
transform="translate(903.34482,697.22136)">
|
||||
<path
|
||||
d="m 0,0 c 7.721,7.628 20.162,7.553 27.791,-0.168 4.886,-4.947 7.056,-11 5.637,-17.315 l -0.199,-0.886 C 29.492,-34.683 21.223,-50.161 8.436,-62.794 c -5.719,-5.651 -11.999,-10.39 -18.655,-14.22 -2.539,-1.463 -5.133,-2.791 -7.773,-3.99 2.476,4.025 4.608,8.199 6.402,12.483 6.8,16.244 8.698,34.064 5.678,51.194 -0.022,0.255 -0.235,1.283 -0.235,1.283 C -6.915,-10.163 -4.545,-4.491 0,0"
|
||||
style="fill:#f0b400;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path75" />
|
||||
</g>
|
||||
<g
|
||||
id="g77"
|
||||
transform="translate(885.35312,616.21746)">
|
||||
<path
|
||||
d="m 0,0 c 2.64,1.199 5.234,2.527 7.773,3.99 6.656,3.83 12.935,8.569 18.655,14.22 12.786,12.633 21.056,28.111 24.792,44.425 0.2,0.873 0.296,1.518 0.473,2.409 1.024,5.123 3.642,9.917 7.64,13.867 10.654,10.527 27.456,11.077 38.355,-0.233 2.733,-2.837 7.101,-6.227 7.53,-15.069 0.007,-3.738 -0.219,-7.479 -0.674,-11.198 C 102.142,32.716 93.324,13.662 78.114,-1.365 58.267,-20.975 31.695,-29.616 5.922,-27.337 c -8.342,0.739 -16.599,2.622 -24.518,5.645 -8.512,3.249 -16.631,7.821 -24.044,13.707 C -28.138,-8.771 -13.481,-6.109 0,0"
|
||||
style="fill:#4acfd2;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path79" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
1
docs/static/img/databases/supabase.svg
vendored
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
docs/static/img/extensions/editors-bundle.png
vendored
Normal file
|
After Width: | Height: | Size: 388 KiB |
220
docs/static/img/logos/club25deagosto.svg
vendored
Normal file
@@ -0,0 +1,220 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="419.92645mm"
|
||||
height="664.284mm"
|
||||
viewBox="0 0 419.92645 664.28402"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="Logo 25 full transparente.svg"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="0.28180672"
|
||||
inkscape:cx="-35.485314"
|
||||
inkscape:cy="942.13509"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1008"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer2"
|
||||
showgrid="false"
|
||||
showguides="true"><inkscape:grid
|
||||
id="grid792"
|
||||
units="mm"
|
||||
originx="127.13166"
|
||||
originy="294.77868"
|
||||
spacingx="1"
|
||||
spacingy="1"
|
||||
empcolor="#0099e5"
|
||||
empopacity="0.30196078"
|
||||
color="#0099e5"
|
||||
opacity="0.14901961"
|
||||
empspacing="5"
|
||||
enabled="true"
|
||||
visible="false" /></sodipodi:namedview><defs
|
||||
id="defs1" /><g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Capa 2"
|
||||
transform="matrix(16.785907,0,0,16.785907,-1379.0426,-1310.3372)"
|
||||
style="display:inline"><path
|
||||
style="display:none;fill:#ffffff;stroke:#ffffff;stroke-width:33.2612"
|
||||
d="M 969.02922,898.66558 968.74908,63.873566 h 49.67942 49.6795 v 12.749576 c 0,20.163418 13.2134,79.715458 25.2704,113.891858 41.2493,116.92445 126.0825,213.33066 235.6456,267.79281 33.7227,16.76301 37.3546,19.48775 35.5517,26.67136 -2.4596,9.79977 -383.39445,1214.40233 -390.32118,1234.28433 -4.25551,12.2146 -4.98422,-102.2238 -5.2253,-820.59792 z"
|
||||
id="path4"
|
||||
transform="matrix(0.01576223,0,0,0.01576223,82.154786,78.061745)"
|
||||
inkscape:label="Relleno blanco derecha" /><path
|
||||
style="display:none;fill:#ffffff;stroke:#ffffff;stroke-width:33.2612"
|
||||
d="M 418.43781,1108.0289 221.50466,484.37454 255.77157,467.94466 C 342.51452,426.35428 414.22899,360.20279 462.70973,277.05901 493.97419,223.4409 518.51143,148.29568 523.79549,89.984045 l 2.36607,-26.110479 h 45.64145 45.64146 V 897.77845 c 0,458.64765 -0.46653,833.90485 -1.03674,833.90485 -0.57022,0 -89.65669,-280.6444 -197.96992,-623.6544 z"
|
||||
id="path5"
|
||||
transform="matrix(0.01576223,0,0,0.01576223,82.154786,78.061745)"
|
||||
inkscape:label="Relleno blanco izquieda" /><path
|
||||
style="display:inline;fill:#1d0695;fill-opacity:1;stroke:#1d0695;stroke-width:1.00005;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path788-6"
|
||||
d="m -99.481003,78.564575 a 7,7 0 0 1 -4.836877,6.657396 l -2.16312,-6.657396 z"
|
||||
sodipodi:type="arc"
|
||||
sodipodi:arc-type="slice"
|
||||
sodipodi:start="0"
|
||||
sodipodi:end="1.2566371"
|
||||
sodipodi:ry="7"
|
||||
sodipodi:rx="7"
|
||||
sodipodi:cy="78.564575"
|
||||
sodipodi:cx="-106.481"
|
||||
inkscape:label="Esquina izquierda"
|
||||
transform="scale(-1,1)" /><path
|
||||
style="fill:#1d0695;fill-opacity:1;stroke:#1d0695;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path788"
|
||||
d="m 89.933891,78.65564 a 7,7 0 0 1 -4.856213,6.663646 L 82.933891,78.65564 Z"
|
||||
sodipodi:type="arc"
|
||||
sodipodi:arc-type="slice"
|
||||
sodipodi:start="0"
|
||||
sodipodi:end="1.2595395"
|
||||
sodipodi:ry="7"
|
||||
sodipodi:rx="7"
|
||||
sodipodi:cy="78.65564"
|
||||
sodipodi:cx="82.933891"
|
||||
inkscape:label="Esquina izquierda" /><path
|
||||
style="display:inline;fill:#ff0000;stroke:#ff0000;stroke-width:0.882544;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 92.343042,79.504907 4.598918,0.0049 0.0017,26.274323 -4.588022,-9.9e-4 z"
|
||||
id="path792"
|
||||
inkscape:label="Rectángulo"
|
||||
sodipodi:nodetypes="ccccc" /><path
|
||||
style="display:inline;opacity:1;fill:#000000;fill-opacity:0;stroke:#1d0695;stroke-width:1.00005;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 82.837015,78.563312 23.651695,-0.0015 -11.838335,37.414748 z"
|
||||
id="path786"
|
||||
sodipodi:nodetypes="cccc"
|
||||
inkscape:label="Triángulo"
|
||||
transform="translate(-1.2708333e-6)" /><path
|
||||
style="fill:#1d0695;fill-opacity:1;stroke:#1d0695;stroke-width:1.03941;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 105.21073,301.76961 c -6.53995,-20.71706 -11.875109,-37.6831 -11.855911,-37.70229 0.0192,-0.0192 10.763101,-0.0223 23.875341,-0.007 l 23.84043,0.028 -11.92048,37.67543 c -6.55626,20.72148 -11.94929,37.67492 -11.98452,37.67432 -0.0352,-6.1e-4 -5.41492,-16.95144 -11.95486,-37.6685 z"
|
||||
id="path54"
|
||||
transform="matrix(0.10669253,0,0,0.10669253,82.149909,78.059739)"
|
||||
inkscape:label="Relleno azul abajo" /></g><g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Texto"
|
||||
transform="matrix(6.768873,0,0,6.768873,-47.398718,-0.20588049)"><text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:11.2451px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#ffffff;stroke:#ffffff;stroke-width:1.5938"
|
||||
x="17.628099"
|
||||
y="15.110926"
|
||||
id="text16"
|
||||
transform="scale(1.8040993,0.55429321)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan16"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:11.2451px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.5938"
|
||||
x="17.628099"
|
||||
y="15.110926">C</tspan></text><text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:10.8582px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#ffffff;stroke:#ffffff;stroke-width:1.53895"
|
||||
x="19.495819"
|
||||
y="25.445749"
|
||||
id="text17"
|
||||
transform="scale(1.6638658,0.60101)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan17"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:10.8582px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.53895"
|
||||
x="19.495819"
|
||||
y="25.445749">A</tspan></text><text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:7.95775px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#ffffff;stroke:#ffffff;stroke-width:1.12787"
|
||||
x="27.980883"
|
||||
y="25.586826"
|
||||
id="text18"
|
||||
transform="scale(1.1569098,0.86437164)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan18"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:7.95775px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.12787"
|
||||
x="27.980883"
|
||||
y="25.586826">25</tspan></text><text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:7.99489px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#ffffff;stroke:#ffffff;stroke-width:1.13313"
|
||||
x="27.62228"
|
||||
y="33.788048"
|
||||
id="text19"
|
||||
transform="scale(1.1646165,0.85865179)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan19"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:7.99489px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.13313"
|
||||
x="27.62228"
|
||||
y="33.788048">DE</tspan></text><text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:10.8582px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#ffffff;stroke:#ffffff;stroke-width:1.53895"
|
||||
x="19.49581"
|
||||
y="59.22831"
|
||||
id="text20"
|
||||
transform="scale(1.6638665,0.60100975)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan20"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:10.8582px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.53895"
|
||||
x="19.49581"
|
||||
y="59.22831">A</tspan></text><text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:11.2553px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#ffffff;stroke:#ffffff;stroke-width:1.59524"
|
||||
x="17.650023"
|
||||
y="76.522369"
|
||||
id="text21"
|
||||
transform="scale(1.8063001,0.55361786)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan21"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:11.2553px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.59524"
|
||||
x="17.650023"
|
||||
y="76.522369">G</tspan></text><text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:11.0582px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#ffffff;stroke:#ffffff;stroke-width:1.5673"
|
||||
x="18.147587"
|
||||
y="86.019577"
|
||||
id="text22"
|
||||
transform="scale(1.7637218,0.56698285)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan22"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:11.0582px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.5673"
|
||||
x="18.147587"
|
||||
y="86.019577">O</tspan></text><text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:11.0296px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#ffffff;stroke:#ffffff;stroke-width:1.56325"
|
||||
x="18.269663"
|
||||
y="97.356186"
|
||||
id="text23"
|
||||
transform="scale(1.7575891,0.56896121)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan23"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:11.0296px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.56325"
|
||||
x="18.269663"
|
||||
y="97.356186">S</tspan></text><text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:10.6716px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#ffffff;stroke:#ffffff;stroke-width:1.51252"
|
||||
x="20.083975"
|
||||
y="100.755"
|
||||
id="text24"
|
||||
transform="scale(1.6258383,0.61506731)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan24"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:10.6716px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.51252"
|
||||
x="20.083975"
|
||||
y="100.755">T</tspan></text><text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:11.0582px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#ffffff;stroke:#ffffff;stroke-width:1.5673"
|
||||
x="18.147587"
|
||||
y="119.65324"
|
||||
id="text25"
|
||||
transform="scale(1.7637218,0.56698285)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan25"
|
||||
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:11.0582px;font-family:'IBM Plex Mono';-inkscape-font-specification:'IBM Plex Mono, Thin';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.5673"
|
||||
x="18.147587"
|
||||
y="119.65324">O</tspan></text></g></svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
BIN
docs/static/img/logos/hpe.png
vendored
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
docs/static/img/logos/xnet.png
vendored
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
docs/static/img/netlify.png
vendored
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
docs/static/img/screenshots/dashboard.jpg
vendored
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
docs/static/img/screenshots/explore.jpg
vendored
|
Before Width: | Height: | Size: 636 KiB After Width: | Height: | Size: 104 KiB |