mirror of
https://github.com/apache/superset.git
synced 2026-06-09 17:49:26 +00:00
Compare commits
90 Commits
fix/embedd
...
test/chatb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d42da24365 | ||
|
|
92224ae270 | ||
|
|
87956741ff | ||
|
|
8efaf38a2f | ||
|
|
979e01e7fb | ||
|
|
e2a971ef69 | ||
|
|
23f6133983 | ||
|
|
be364bb093 | ||
|
|
40dcace5d0 | ||
|
|
74f9b72f64 | ||
|
|
579c8d8377 | ||
|
|
369e4adc76 | ||
|
|
76466e3845 | ||
|
|
0249b8c1b3 | ||
|
|
a583f1859c | ||
|
|
6d174fa71f | ||
|
|
e2ed989639 | ||
|
|
2abbb64e6b | ||
|
|
c6faa50338 | ||
|
|
817a35f445 | ||
|
|
a6d2c95480 | ||
|
|
c29591b3b1 | ||
|
|
365914f1c7 | ||
|
|
41da35e9db | ||
|
|
861e668f74 | ||
|
|
41059c68bb | ||
|
|
94092d2f72 | ||
|
|
986148d924 | ||
|
|
f04221a06c | ||
|
|
70aa96458a | ||
|
|
8beea84952 | ||
|
|
3f0fbbaac9 | ||
|
|
ce602fc5a8 | ||
|
|
8731974e5c | ||
|
|
a06eb8fc78 | ||
|
|
aa8b474c58 | ||
|
|
efdfefeea2 | ||
|
|
f77fa3ae39 | ||
|
|
bffc3fc58f | ||
|
|
2b8e31bf68 | ||
|
|
74d1c83ec5 | ||
|
|
15ad31effd | ||
|
|
1523d797ca | ||
|
|
97be689b5c | ||
|
|
dab628c13a | ||
|
|
e1bc8e7ae8 | ||
|
|
5312d0adf8 | ||
|
|
041ecbc248 | ||
|
|
a33fcb0edd | ||
|
|
316dd3db22 | ||
|
|
d52e28c564 | ||
|
|
a2eda11a81 | ||
|
|
63249c8c97 | ||
|
|
32c42076dd | ||
|
|
acc80242f0 | ||
|
|
a621520387 | ||
|
|
9a79588d35 | ||
|
|
7e8b8e25a5 | ||
|
|
ab5ea1f7d3 | ||
|
|
a340293fef | ||
|
|
469979714f | ||
|
|
585745695c | ||
|
|
c7bbfff475 | ||
|
|
8e47eb1cc1 | ||
|
|
afc4f3c9b3 | ||
|
|
35125d8521 | ||
|
|
ab66f0066a | ||
|
|
dfa4dbb96c | ||
|
|
f10a296ed0 | ||
|
|
f17e4de9cd | ||
|
|
7f6f805ffa | ||
|
|
df6e0095dc | ||
|
|
c4b9f7b6e5 | ||
|
|
23d4574caf | ||
|
|
da4d09a006 | ||
|
|
1645a9652f | ||
|
|
22d9332794 | ||
|
|
504826bb24 | ||
|
|
4e8145f14b | ||
|
|
a74684b062 | ||
|
|
96f2fb3659 | ||
|
|
e3efa0ae71 | ||
|
|
97ba65b0cd | ||
|
|
d7592913ea | ||
|
|
8f03c5a1ea | ||
|
|
8b52fff57b | ||
|
|
197e14de1b | ||
|
|
7c9efd529b | ||
|
|
f8e7ee9dc2 | ||
|
|
c77c9b29f9 |
75
.github/SECURITY.md
vendored
75
.github/SECURITY.md
vendored
@@ -1,75 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
This is a project of the [Apache Software Foundation](https://apache.org) and follows the
|
||||
ASF [vulnerability handling process](https://apache.org/security/#vulnerability-handling).
|
||||
|
||||
## Reporting Vulnerabilities
|
||||
|
||||
**⚠️ Please do not file GitHub issues for security vulnerabilities as they are public! ⚠️**
|
||||
|
||||
|
||||
Apache Software Foundation takes a rigorous standpoint in annihilating the security issues
|
||||
in its software projects. Apache Superset is highly sensitive and forthcoming to issues
|
||||
pertaining to its features and functionality.
|
||||
If you have any concern or believe you have found a vulnerability in Apache Superset,
|
||||
please get in touch with the Apache Superset Security Team privately at
|
||||
e-mail address [security@superset.apache.org](mailto:security@superset.apache.org).
|
||||
|
||||
More details can be found on the ASF website at
|
||||
[ASF vulnerability reporting process](https://apache.org/security/#reporting-a-vulnerability)
|
||||
|
||||
**Submission Standards & AI Policy**
|
||||
|
||||
To ensure engineering focus remains on verified risks and to manage high reporting volumes, all reports must meet the following criteria:
|
||||
- Plain Text Format: In accordance with Apache guidelines, please provide all details in plain text within the email body. Avoid sending PDFs, Word documents, or password-protected archives.
|
||||
- Mandatory AI Disclosure: If you utilized Large Language Models (LLMs) or AI tools to identify a flaw or assist in writing a report, you must disclose this in your submission so our triage team can contextualize the findings.
|
||||
- Human-Verified PoC: All submissions must include a manual, step-by-step Proof of Concept (PoC) performed on a supported release. Raw AI outputs, hypothetical chat transcripts, or unverified scanner logs will be closed as Invalid.
|
||||
|
||||
We kindly ask you to include the following information in your report to assist our developers in triaging and remediating issues efficiently:
|
||||
- Version/Commit: The specific version of Apache Superset or the Git commit hash you are using.
|
||||
- Configuration: A sanitized copy of your `superset_config.py` file or any config overrides.
|
||||
- Environment: Your deployment method (e.g., Docker Compose, Helm, or source) and relevant OS/Browser details.
|
||||
- Impacted Component: Identification of the affected area (e.g., Python backend, React frontend, or a specific database connector).
|
||||
- Expected vs. Actual Behavior: A clear description of the intended system behavior versus the observed vulnerability.
|
||||
- Detailed Reproduction Steps: Clear, manual steps to reproduce the vulnerability.
|
||||
|
||||
**Vulnerability Definition**
|
||||
|
||||
Apache Superset considers a security vulnerability to be a demonstrable issue that has meaningful impact on confidentiality, integrity, or availability beyond the intended security model. Low-impact boundary variations or technical edge cases in existing access controls may be classified as hardening improvements rather than vulnerabilities, even if exploitable.
|
||||
|
||||
**Out of Scope Vulnerabilities**
|
||||
|
||||
To prioritize engineering efforts on genuine architectural risks, the following scenarios are explicitly out of scope and will not be issued a CVE:
|
||||
- **Attacks requiring Admin privileges**: (e.g., CSS injection, template manipulation, dashboard ownership overrides, or modifying global system settings). Per the CVE vulnerability definition in CNA Operational Rules 4.1, a qualifying vulnerability must allow violation of a security policy. The Admin role is a fully trusted operational boundary defined by Apache Superset's security policy; actions within this boundary do not violate that policy and are therefore considered intended capabilities 'by design,' not vulnerabilities.
|
||||
- **Brute Force and Rate Limiting**: Reports targeting a lack of resource exhaustion protections, generic rate-limiting, or volumetric Denial of Service (DoS) attempts.
|
||||
- **Theoretical attack vectors**: Issues without a demonstrable, reproducible exploit path.
|
||||
- **Non-Exploitable Findings**: Missing security headers, generic banner disclosures, or descriptive error messages that do not lead to a direct, documented exploit.
|
||||
- **User enumeration**: API responses, timing differences, or error messages that reveal whether user accounts, IDs, dashboards, or datasets exist.
|
||||
- **Information disclosure (low impact)**: Software version disclosure, generic error messages, stack traces without sensitive data exposure, or system configuration details that don't enable further exploitation.
|
||||
- **Resource exhaustion requiring authentication**: Denial of Service attacks that require valid user credentials and don't bypass rate limiting or resource controls.
|
||||
- **Missing security headers**: Without demonstration of a concrete exploit scenario that leverages the missing header.
|
||||
|
||||
**Outcome of Reports**
|
||||
|
||||
Reports that are deemed out-of-scope for a CVE but represent valid security best practices or hardening opportunities may be converted into public GitHub issues. This allows the community to contribute to the general hardening of the platform even when a specific vulnerability threshold is not met.
|
||||
|
||||
Note that Apache Superset is not responsible for any third-party dependencies that may
|
||||
have security issues. Any vulnerabilities found in third-party dependencies should be
|
||||
reported to the maintainers of those projects. Results from security scans of Apache
|
||||
Superset dependencies found on its official Docker image can be remediated at release time
|
||||
by extending the image itself.
|
||||
|
||||
**Vulnerability Aggregation & CVE Attribution**
|
||||
|
||||
In accordance with MITRE CNA Operational Rules (4.1.10, 4.1.11, and 4.2.13), Apache Superset issues CVEs based on the underlying architectural root cause rather than the number of affected endpoints or exploit payloads.
|
||||
- Aggregation: If multiple exploit vectors stem from the same programmatic failure or shared vulnerable code, they must be aggregated into a single, comprehensive report.
|
||||
- Independent Fixes: Separate CVEs will only be assigned if the vulnerabilities reside in decoupled architectural modules and can be fixed independently of one another.
|
||||
Reports that fail to aggregate related findings will be merged during triage to ensure an accurate and defensible CVE record.
|
||||
|
||||
**Your responsible disclosure and collaboration are invaluable.**
|
||||
|
||||
## Extra Information
|
||||
|
||||
- [Apache Superset documentation](https://superset.apache.org/docs/security)
|
||||
- [Common Vulnerabilities and Exposures by release](https://superset.apache.org/docs/security/cves)
|
||||
- [How Security Vulnerabilities are Reported & Handled in Apache Superset (Blog)](https://preset.io/blog/how-security-vulnerabilities-are-reported-and-handled-in-apache-superset/)
|
||||
33
.github/actions/setup-backend/action.yml
vendored
33
.github/actions/setup-backend/action.yml
vendored
@@ -24,32 +24,41 @@ runs:
|
||||
- name: Interpret Python Version
|
||||
id: set-python-version
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_PYTHON_VERSION: ${{ inputs.python-version }}
|
||||
run: |
|
||||
if [ "${{ inputs.python-version }}" = "current" ]; then
|
||||
echo "PYTHON_VERSION=3.11" >> $GITHUB_ENV
|
||||
elif [ "${{ inputs.python-version }}" = "next" ]; then
|
||||
if [ "$INPUT_PYTHON_VERSION" = "current" ]; then
|
||||
RESOLVED_VERSION="3.11"
|
||||
elif [ "$INPUT_PYTHON_VERSION" = "next" ]; then
|
||||
# currently disabled in GHA matrixes because of library compatibility issues
|
||||
echo "PYTHON_VERSION=3.12" >> $GITHUB_ENV
|
||||
elif [ "${{ inputs.python-version }}" = "previous" ]; then
|
||||
echo "PYTHON_VERSION=3.10" >> $GITHUB_ENV
|
||||
RESOLVED_VERSION="3.12"
|
||||
elif [ "$INPUT_PYTHON_VERSION" = "previous" ]; then
|
||||
RESOLVED_VERSION="3.10"
|
||||
elif printf '%s' "$INPUT_PYTHON_VERSION" | grep -Eq '^[0-9]+\.[0-9]+(\.[0-9]+)?$'; then
|
||||
RESOLVED_VERSION="$INPUT_PYTHON_VERSION"
|
||||
else
|
||||
echo "PYTHON_VERSION=${{ inputs.python-version }}" >> $GITHUB_ENV
|
||||
echo "Invalid python-version: '$INPUT_PYTHON_VERSION'" >&2
|
||||
exit 1
|
||||
fi
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
echo "python-version=$RESOLVED_VERSION" >> "$GITHUB_OUTPUT"
|
||||
- name: Set up Python ${{ steps.set-python-version.outputs.python-version }}
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
python-version: ${{ steps.set-python-version.outputs.python-version }}
|
||||
cache: ${{ inputs.cache }}
|
||||
- name: Install dependencies
|
||||
env:
|
||||
INPUT_INSTALL_SUPERSET: ${{ inputs.install-superset }}
|
||||
INPUT_REQUIREMENTS_TYPE: ${{ inputs.requirements-type }}
|
||||
run: |
|
||||
if [ "${{ inputs.install-superset }}" = "true" ]; then
|
||||
if [ "$INPUT_INSTALL_SUPERSET" = "true" ]; then
|
||||
sudo apt-get update && sudo apt-get -y install libldap2-dev libsasl2-dev
|
||||
|
||||
pip install --upgrade pip setuptools wheel uv
|
||||
|
||||
if [ "${{ inputs.requirements-type }}" = "dev" ]; then
|
||||
if [ "$INPUT_REQUIREMENTS_TYPE" = "dev" ]; then
|
||||
uv pip install --system -r requirements/development.txt
|
||||
elif [ "${{ inputs.requirements-type }}" = "base" ]; then
|
||||
elif [ "$INPUT_REQUIREMENTS_TYPE" = "base" ]; then
|
||||
uv pip install --system -r requirements/base.txt
|
||||
fi
|
||||
|
||||
|
||||
16
.github/workflows/bashlib.sh
vendored
16
.github/workflows/bashlib.sh
vendored
@@ -59,6 +59,15 @@ build-assets() {
|
||||
say "::endgroup::"
|
||||
}
|
||||
|
||||
build-embedded-sdk() {
|
||||
cd "$GITHUB_WORKSPACE/superset-embedded-sdk"
|
||||
|
||||
say "::group::Build embedded SDK bundle for E2E tests"
|
||||
npm ci
|
||||
npm run build
|
||||
say "::endgroup::"
|
||||
}
|
||||
|
||||
build-instrumented-assets() {
|
||||
cd "$GITHUB_WORKSPACE/superset-frontend"
|
||||
|
||||
@@ -276,7 +285,12 @@ playwright-run() {
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
local serverlog="${HOME}/superset-playwright.log"
|
||||
local port=8081
|
||||
PLAYWRIGHT_BASE_URL="http://localhost:${port}"
|
||||
# Use 127.0.0.1 explicitly: `flask run` binds IPv4 only, and Node's DNS
|
||||
# resolution for `localhost` can return `::1` first (IPv6), which then
|
||||
# refuses against the IPv4 listener and surfaces as
|
||||
# `connect ECONNREFUSED ::1:<port>` in API helpers driven from Node
|
||||
# (e.g., the embedded test app's exposed token fetcher).
|
||||
PLAYWRIGHT_BASE_URL="http://127.0.0.1:${port}"
|
||||
if [ -n "$APP_ROOT" ]; then
|
||||
export SUPERSET_APP_ROOT=$APP_ROOT
|
||||
PLAYWRIGHT_BASE_URL=${PLAYWRIGHT_BASE_URL}${APP_ROOT}/
|
||||
|
||||
11
.github/workflows/docker.yml
vendored
11
.github/workflows/docker.yml
vendored
@@ -73,20 +73,21 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_PRESET: ${{ matrix.build_preset }}
|
||||
run: |
|
||||
# Single platform builds in pull_request context to speed things up
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
if [ "$GITHUB_EVENT_NAME" = "push" ]; then
|
||||
PLATFORM_ARG="--platform linux/arm64 --platform linux/amd64"
|
||||
# can only --load images in single-platform builds
|
||||
PUSH_OR_LOAD="--push"
|
||||
elif [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
elif [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
|
||||
PLATFORM_ARG="--platform linux/amd64"
|
||||
PUSH_OR_LOAD="--load"
|
||||
fi
|
||||
|
||||
supersetbot docker \
|
||||
$PUSH_OR_LOAD \
|
||||
--preset ${{ matrix.build_preset }} \
|
||||
--preset "$BUILD_PRESET" \
|
||||
--context "$EVENT" \
|
||||
--context-ref "$RELEASE" $FORCE_LATEST \
|
||||
--extra-flags "--build-arg INCLUDE_CHROMIUM=false --tag $IMAGE_TAG" \
|
||||
@@ -112,8 +113,10 @@ jobs:
|
||||
- name: docker-compose sanity check
|
||||
if: (steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker) && matrix.build_preset == 'dev'
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_PRESET: ${{ matrix.build_preset }}
|
||||
run: |
|
||||
export SUPERSET_BUILD_TARGET=${{ matrix.build_preset }}
|
||||
export SUPERSET_BUILD_TARGET=$BUILD_PRESET
|
||||
# This should reuse the CACHED image built in the previous steps
|
||||
docker compose build superset-init --build-arg DEV_MODE=false --build-arg INCLUDE_CHROMIUM=false
|
||||
docker compose up superset-init --exit-code-from superset-init
|
||||
|
||||
6
.github/workflows/latest-release-tag.yml
vendored
6
.github/workflows/latest-release-tag.yml
vendored
@@ -19,10 +19,10 @@ jobs:
|
||||
|
||||
- name: Check for latest tag
|
||||
id: latest-tag
|
||||
run: |
|
||||
source ./scripts/tag_latest_release.sh $(echo ${GITHUB_EVENT_RELEASE_TAG_NAME}) --dry-run
|
||||
env:
|
||||
GITHUB_EVENT_RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
source ./scripts/tag_latest_release.sh "$RELEASE_TAG_NAME" --dry-run
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
|
||||
4
.github/workflows/pre-commit.yml
vendored
4
.github/workflows/pre-commit.yml
vendored
@@ -71,10 +71,12 @@ jobs:
|
||||
output: ' '
|
||||
|
||||
- name: pre-commit
|
||||
env:
|
||||
CHANGED_FILES: ${{ steps.changed_files.outputs.files }}
|
||||
run: |
|
||||
set +e # Don't exit immediately on failure
|
||||
export SKIP=type-checking-frontend
|
||||
pre-commit run --files ${{ steps.changed_files.outputs.files }}
|
||||
pre-commit run --files $CHANGED_FILES
|
||||
PRE_COMMIT_EXIT_CODE=$?
|
||||
git diff --quiet --exit-code
|
||||
GIT_DIFF_EXIT_CODE=$?
|
||||
|
||||
9
.github/workflows/showtime-trigger.yml
vendored
9
.github/workflows/showtime-trigger.yml
vendored
@@ -2,6 +2,7 @@ name: 🎪 Superset Showtime
|
||||
|
||||
# Ultra-simple: just sync on any PR state change
|
||||
on:
|
||||
# zizmor: ignore[dangerous-triggers] - required to react to PR label changes; this workflow does not check out or execute PR-provided code
|
||||
pull_request_target:
|
||||
types: [labeled, unlabeled, synchronize, closed]
|
||||
|
||||
@@ -102,7 +103,7 @@ jobs:
|
||||
- name: Install Superset Showtime
|
||||
if: steps.auth.outputs.authorized == 'true'
|
||||
run: |
|
||||
echo "::notice::Maintainer ${GITHUB_ACTOR} triggered deploy for PR ${PULL_REQUEST_NUMBER}"
|
||||
echo "::notice::Maintainer $GITHUB_ACTOR triggered deploy for PR ${PULL_REQUEST_NUMBER}"
|
||||
pip install --upgrade superset-showtime
|
||||
showtime version
|
||||
|
||||
@@ -173,9 +174,11 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
CHECK_PR_NUMBER: ${{ steps.check.outputs.pr_number }}
|
||||
CHECK_TARGET_SHA: ${{ steps.check.outputs.target_sha }}
|
||||
run: |
|
||||
PR_NUM="${{ steps.check.outputs.pr_number }}"
|
||||
TARGET_SHA="${{ steps.check.outputs.target_sha }}"
|
||||
PR_NUM="$CHECK_PR_NUMBER"
|
||||
TARGET_SHA="$CHECK_TARGET_SHA"
|
||||
if [[ -n "$TARGET_SHA" ]]; then
|
||||
python -m showtime sync $PR_NUM --sha "$TARGET_SHA"
|
||||
else
|
||||
|
||||
1
.github/workflows/superset-docs-deploy.yml
vendored
1
.github/workflows/superset-docs-deploy.yml
vendored
@@ -2,6 +2,7 @@ name: Docs Deployment
|
||||
|
||||
on:
|
||||
# Deploy after integration tests complete on master
|
||||
# zizmor: ignore[dangerous-triggers] - runs in base-branch context after a trusted upstream workflow; scoped to master
|
||||
workflow_run:
|
||||
workflows: ["Python-Integration"]
|
||||
types: [completed]
|
||||
|
||||
1
.github/workflows/superset-docs-verify.yml
vendored
1
.github/workflows/superset-docs-verify.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
- "superset/db_engine_specs/**"
|
||||
- ".github/workflows/superset-docs-verify.yml"
|
||||
types: [synchronize, opened, reopened, ready_for_review]
|
||||
# zizmor: ignore[dangerous-triggers] - runs in base-branch context and only consumes artifacts from the trusted upstream workflow
|
||||
workflow_run:
|
||||
workflows: ["Python-Integration"]
|
||||
types: [completed]
|
||||
|
||||
11
.github/workflows/superset-e2e.yml
vendored
11
.github/workflows/superset-e2e.yml
vendored
@@ -141,8 +141,9 @@ jobs:
|
||||
- name: Set safe app root
|
||||
if: failure()
|
||||
id: set-safe-app-root
|
||||
env:
|
||||
APP_ROOT: ${{ matrix.app_root }}
|
||||
run: |
|
||||
APP_ROOT="${{ matrix.app_root }}"
|
||||
SAFE_APP_ROOT=${APP_ROOT//\//_}
|
||||
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
|
||||
- name: Upload Artifacts
|
||||
@@ -239,6 +240,11 @@ jobs:
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: build-instrumented-assets
|
||||
- name: Build embedded SDK
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: build-embedded-sdk
|
||||
- name: Install Playwright
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
@@ -254,8 +260,9 @@ jobs:
|
||||
- name: Set safe app root
|
||||
if: failure()
|
||||
id: set-safe-app-root
|
||||
env:
|
||||
APP_ROOT: ${{ matrix.app_root }}
|
||||
run: |
|
||||
APP_ROOT="${{ matrix.app_root }}"
|
||||
SAFE_APP_ROOT=${APP_ROOT//\//_}
|
||||
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
|
||||
- name: Upload Playwright Artifacts
|
||||
|
||||
8
.github/workflows/superset-helm-release.yml
vendored
8
.github/workflows/superset-helm-release.yml
vendored
@@ -62,6 +62,8 @@ jobs:
|
||||
run: echo "branch_name=helm-publish-${GITHUB_SHA:0:7}" >> $GITHUB_ENV
|
||||
|
||||
- name: Force recreate branch from gh-pages
|
||||
env:
|
||||
BRANCH_NAME: ${{ env.branch_name }}
|
||||
run: |
|
||||
# Ensure a clean working directory
|
||||
git reset --hard
|
||||
@@ -73,13 +75,13 @@ jobs:
|
||||
git fetch origin gh-pages
|
||||
|
||||
# Check out and reset the target branch based on gh-pages
|
||||
git checkout -B ${{ env.branch_name }} origin/gh-pages
|
||||
git checkout -B "$BRANCH_NAME" origin/gh-pages
|
||||
|
||||
# Remove submodules from the branch
|
||||
git submodule deinit -f --all
|
||||
|
||||
# Force push to the remote branch
|
||||
git push origin ${{ env.branch_name }} --force
|
||||
git push origin "$BRANCH_NAME" --force
|
||||
|
||||
# Return to the original branch
|
||||
git checkout local_gha_temp
|
||||
@@ -104,7 +106,7 @@ jobs:
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const branchName = '${{ env.branch_name }}';
|
||||
const branchName = process.env.BRANCH_NAME;
|
||||
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
|
||||
|
||||
if (!branchName) {
|
||||
|
||||
20
.github/workflows/superset-playwright.yml
vendored
20
.github/workflows/superset-playwright.yml
vendored
@@ -113,6 +113,11 @@ jobs:
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: build-instrumented-assets
|
||||
- name: Build embedded SDK
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: build-embedded-sdk
|
||||
- name: Install Playwright
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
@@ -125,6 +130,21 @@ jobs:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
with:
|
||||
run: playwright-run "${{ matrix.app_root }}" experimental/
|
||||
- name: Run Playwright (Embedded Tests)
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
# Scope embedded-only env vars to this step. Setting them at the job
|
||||
# level enabled the EMBEDDED_SUPERSET feature flag inside Flask for
|
||||
# the preceding "Required Tests" and "Experimental Tests" steps too,
|
||||
# which loads extra handlers and destabilizes the werkzeug dev
|
||||
# server under the 2-worker Playwright load. Required Tests should
|
||||
# match master's Flask configuration.
|
||||
SUPERSET_FEATURE_EMBEDDED_SUPERSET: "true"
|
||||
INCLUDE_EMBEDDED: "true"
|
||||
with:
|
||||
run: playwright-run "${{ matrix.app_root }}" embedded
|
||||
- name: Set safe app root
|
||||
if: failure()
|
||||
id: set-safe-app-root
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: Translation Regression Comment
|
||||
|
||||
on:
|
||||
# zizmor: ignore[dangerous-triggers] - runs in base-branch context and only consumes the uploaded artifact; never checks out PR code (see note below)
|
||||
workflow_run:
|
||||
workflows: ["Translations"]
|
||||
types: [completed]
|
||||
|
||||
16
.github/workflows/superset-translations.yml
vendored
16
.github/workflows/superset-translations.yml
vendored
@@ -84,13 +84,15 @@ jobs:
|
||||
# drift on the base branch.
|
||||
- name: Fetch base ref and create comparison worktree
|
||||
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
|
||||
env:
|
||||
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||
run: |
|
||||
# For PRs use the base branch; for direct pushes compare against the previous commit.
|
||||
BASE_REF="${{ github.event.pull_request.base.ref }}"
|
||||
BASE_REF="$PR_BASE_REF"
|
||||
if [ -n "$BASE_REF" ]; then
|
||||
git fetch --depth=1 origin "$BASE_REF"
|
||||
else
|
||||
git fetch --depth=2 origin "${{ github.ref }}"
|
||||
git fetch --depth=2 origin "$GITHUB_REF"
|
||||
fi
|
||||
git worktree add /tmp/base-worktree FETCH_HEAD
|
||||
|
||||
@@ -111,13 +113,9 @@ jobs:
|
||||
--translations-dir /tmp/base-worktree/superset/translations \
|
||||
> /tmp/before.json
|
||||
|
||||
# Reset the PR worktree's translations to the pristine BASE state so
|
||||
# both babel_update runs start from the same .po files. The only
|
||||
# difference between the runs is the source code.
|
||||
- name: Reset PR worktree translations to pristine BASE
|
||||
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
|
||||
run: git checkout FETCH_HEAD -- superset/translations/
|
||||
|
||||
# Run babel_update against the PR source and PR translations. This keeps
|
||||
# committed .po fixes in play while the base babel_update above still
|
||||
# cancels out translation drift already present on the base branch.
|
||||
- name: Run babel_update against PR source
|
||||
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
|
||||
run: ./scripts/translations/babel_update.sh
|
||||
|
||||
4
.github/workflows/tag-release.yml
vendored
4
.github/workflows/tag-release.yml
vendored
@@ -68,9 +68,11 @@ jobs:
|
||||
build: "true"
|
||||
|
||||
- name: Use Node.js 20
|
||||
# zizmor: ignore[cache-poisoning] - node only runs the supersetbot CLI; no dependency cache is enabled
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: 20
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Setup supersetbot
|
||||
uses: ./.github/actions/setup-supersetbot/
|
||||
@@ -125,9 +127,11 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20
|
||||
# zizmor: ignore[cache-poisoning] - node only runs the supersetbot CLI; no dependency cache is enabled
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: 20
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Setup supersetbot
|
||||
uses: ./.github/actions/setup-supersetbot/
|
||||
|
||||
2
.github/workflows/welcome-new-users.yml
vendored
2
.github/workflows/welcome-new-users.yml
vendored
@@ -1,12 +1,14 @@
|
||||
name: Welcome New Contributor
|
||||
|
||||
on:
|
||||
# zizmor: ignore[dangerous-triggers] - posts a welcome comment only; does not check out or execute PR-provided code
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
welcome:
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
|
||||
38
AGENTS.md
38
AGENTS.md
@@ -52,6 +52,43 @@ Common pre-commit failures:
|
||||
- **External API exposure** - Use UUIDs in public APIs instead of internal integer IDs
|
||||
- **Existing models** - Add UUID fields alongside integer IDs for gradual migration
|
||||
|
||||
## Security and Threat Model
|
||||
|
||||
Before evaluating any code path for security issues, read [`SECURITY.md`](SECURITY.md). It is the canonical, authoritative source for Apache Superset's security model and is referenced by both human reporters and automated scanners.
|
||||
|
||||
In short, the test for whether a finding is in scope is one question:
|
||||
|
||||
> *Does it let a principal perform an action the role and capability matrix in `SECURITY.md` does not entitle them to?*
|
||||
|
||||
If yes, it is in scope. If no, it is not.
|
||||
|
||||
The three trust boundaries are:
|
||||
|
||||
1. **The Admin role** is a fully trusted operational principal. Anything an Admin can do through documented configuration, API, or UI is an intended capability, not a vulnerability.
|
||||
2. **The operator** owns deployment-time decisions (secrets, network exposure, feature-flag selection, connector and codec choices, notification destinations, third-party plugins). Misconfiguration at this layer is a deployment defect, not a Superset vulnerability.
|
||||
3. **The codebase** is responsible for enforcing the role and capability matrix wherever it exposes functionality to a principal: API routes, command and DAO layers, UI handlers, background jobs, and any other entry point. A missing or incorrect enforcement check is in scope no matter where it lives.
|
||||
|
||||
The security model assumes that operator-controlled infrastructure, including the metadata database, cache backends, message brokers, secret stores, and deployment environment, remains within the operator's trust boundary. Vulnerabilities must demonstrate a security boundary violation by an attacker who does not already control those systems.
|
||||
|
||||
Route-level authorization in this codebase uses one of three Flask-AppBuilder decorators depending on the route type:
|
||||
|
||||
- `@protect()` for REST API routes (`ModelRestApi` / `BaseApi`)
|
||||
- `@has_access_api` for legacy view routes
|
||||
- `@has_access` for legacy HTML view routes
|
||||
|
||||
Object-level authorization via `security_manager.raise_for_access(...)` applies to data-bearing resources: dashboards, charts, datasets and datasources, queries, database and table access, and query contexts. Other resources (annotations, tags, CSS templates, reports, RLS rules, and similar) rely on the route-level decorator plus DAO `base_filters` for ownership scoping; the absence of `raise_for_access` on these resources is by design, not a finding. Code that omits the per-object gate on a route that returns or mutates a specific data-bearing object is in scope; code that follows the correct pattern for its resource class can still contain injection, SSRF, XSS, or other classes of finding unrelated to authorization, which are evaluated separately.
|
||||
|
||||
The full role and capability matrix, in-scope and out-of-scope class lists, and CVE aggregation rules are in [`SECURITY.md`](SECURITY.md). Defer to that document for any specifics.
|
||||
|
||||
**Requirements for findings filed by automated tooling**
|
||||
|
||||
Automated scanners (LLM-based code scanners, static analyzers, dependency tools) that file findings against this codebase must, in each finding, name:
|
||||
|
||||
1. The specific role and capability matrix row in [`SECURITY.md`](SECURITY.md) the finding believes is violated.
|
||||
2. The principal the finding assumes the attacker holds (Public, Gamma, sql_lab, Alpha, Admin, Embedded guest token, or a custom role with explicit capability grants).
|
||||
|
||||
Findings that cannot identify both should be filed as questions, not vulnerabilities. This requirement exists to ensure every reported issue is testable against the published security model and to keep speculative or pattern-match-only reports out of the triage queue.
|
||||
|
||||
## Key Directories
|
||||
|
||||
```
|
||||
@@ -128,6 +165,7 @@ The Developer Portal auto-generates MDX documentation from Storybook stories. **
|
||||
## Architecture Patterns
|
||||
|
||||
### Security & Features
|
||||
- **Security model**: see the top-level [Security and Threat Model](#security-and-threat-model) section and [`SECURITY.md`](SECURITY.md)
|
||||
- **RBAC**: Role-based access via Flask-AppBuilder
|
||||
- **Feature flags**: Control feature rollouts
|
||||
- **Row-level security**: SQL-based data access control
|
||||
|
||||
895
CHATBOT_SIP.md
Normal file
895
CHATBOT_SIP.md
Normal file
@@ -0,0 +1,895 @@
|
||||
Chatbot extensions
|
||||
Author: Enzo Martellucci
|
||||
Team: Preset
|
||||
Status: Draft | Under Review | Completed
|
||||
Day: May, 2026
|
||||
|
||||
1. Introduction
|
||||
This SIP proposes a new extension point that enables third-party chatbot integrations to be embedded directly into the Superset user interface through the existing extension framework.
|
||||
The goal is to provide a stable, supported mechanism for chatbot providers to integrate with Superset without requiring direct access to internal application state, Redux stores, or implementation-specific frontend modules. Chatbot extensions should interact with Superset through the same extension-oriented principles already established for other extension surfaces, such as SQL Lab.
|
||||
The proposal focuses on three core concerns:
|
||||
• Defining how chatbot extensions are registered and rendered.
|
||||
• Defining how chatbot extensions receive contextual information about the currently active application surface.
|
||||
• Defining how administrators manage chatbot availability and select the active chatbot when multiple chatbot extensions are installed.
|
||||
This SIP intentionally does not prescribe any specific chatbot implementation, user experience, LLM provider, or backend architecture.
|
||||
1.1 Motivation
|
||||
AI-powered assistants are becoming a common way for users to interact with analytical applications. Superset should provide a standardized extension mechanism that allows community-built chatbot integrations to participate in the platform without depending on internal frontend implementation details.
|
||||
Today, chatbot integrations must either be embedded through custom application modifications or rely on unsupported access to internal application state. Both approaches create maintenance challenges and make integrations fragile when frontend architecture evolves.
|
||||
|
||||
This SIP introduces a stable extension contract that:
|
||||
• Enables chatbot integrations to be distributed as standard Superset extensions.
|
||||
• Preserves separation between host and extension responsibilities.
|
||||
• Allows chatbot implementations to access contextual information about the current page and entity being viewed.
|
||||
• Keeps authorization and permission enforcement aligned with existing Superset APIs.
|
||||
• Remains compatible with future frontend architecture changes.
|
||||
1.2 Goals
|
||||
The goals of this SIP are:
|
||||
Introduce a dedicated chatbot extension point within the Superset application shell.
|
||||
Provide chatbot extensions with host-managed, permission-aligned page context.
|
||||
Establish stable extension-facing APIs for dashboard, explore, dataset, and navigation context.
|
||||
Support deployment-wide administration of chatbot availability and selection.
|
||||
Maintain isolation between chatbot implementations and Superset internals.
|
||||
Preserve compatibility with future extension capabilities and AI-related initiatives.
|
||||
1.3 Out of Scope
|
||||
The following capabilities are explicitly out of scope for this SIP.
|
||||
Client Actions and Agentic UI Manipulation
|
||||
This SIP defines how chatbot extensions are mounted and how they receive context from the host application.
|
||||
It does not define how a chatbot performs actions within the user interface, such as:
|
||||
• Modifying chart configuration.
|
||||
• Updating dashboard layouts.
|
||||
• Editing SQL queries.
|
||||
• Triggering frontend workflows.
|
||||
These capabilities are deferred to the proposed Client Actions SIP.
|
||||
Chatbot User Experience
|
||||
The chatbot user interface remains entirely owned by the extension.
|
||||
This SIP does not prescribe:
|
||||
• Visual design.
|
||||
• Conversation experience.
|
||||
• Streaming behavior.
|
||||
• Message persistence.
|
||||
• Prompting strategy.
|
||||
• Accessibility implementation details.
|
||||
• Branding or styling.
|
||||
LLM and Backend Infrastructure
|
||||
The following concerns remain extension-specific:
|
||||
• Model providers.
|
||||
• MCP implementations.
|
||||
• Agent frameworks.
|
||||
• Tool execution systems.
|
||||
• Prompt orchestration.
|
||||
• Backend services.
|
||||
Superset acts only as the host application and context provider.
|
||||
|
||||
2. Requirements
|
||||
|
||||
2.1 Functional Requirements
|
||||
Registration and Rendering
|
||||
The platform must allow extensions to register chatbot providers through the standard extension system.
|
||||
The host must:
|
||||
• Support registration of chatbot extensions.
|
||||
• Render a chatbot UI contributed by an extension.
|
||||
• Maintain a single active chatbot instance at any given time.
|
||||
• Make the chatbot available across supported application surfaces.
|
||||
• Support fully custom chatbot user interfaces.
|
||||
|
||||
Context Sharing
|
||||
The platform must provide chatbot extensions with contextual information about the user's current application state.
|
||||
At minimum, the host must expose:
|
||||
• Current page type (`home`, `dashboard`, `explore`, `sqllab`, `dataset`, `other`).
|
||||
• Dashboard context.
|
||||
• Explore/chart context.
|
||||
• Dataset identity context.
|
||||
• SQL Lab context.
|
||||
• Navigation events.
|
||||
The chatbot must be notified of relevant context changes without polling.
|
||||
Examples include:
|
||||
• Route changes.
|
||||
• Dashboard changes.
|
||||
• Chart changes.
|
||||
• Dataset changes.
|
||||
• Title changes.
|
||||
• Filter changes.
|
||||
Host-Owned Context
|
||||
Context exposed to extensions must be computed by the host application.
|
||||
Extensions must not be required to:
|
||||
• Read Redux state.
|
||||
• Access internal application modules.
|
||||
• Depend on component-level implementation details.
|
||||
• Reconstruct semantic context from frontend internals.
|
||||
Instead, extensions consume stable namespace APIs provided by the host.
|
||||
Conversation State
|
||||
The conversation state remains entirely owned by the chatbot extension.
|
||||
This includes:
|
||||
• Message history.
|
||||
• Tool execution state.
|
||||
• Streaming buffers.
|
||||
• Conversation persistence.
|
||||
• Session management.
|
||||
The host is responsible only for exposing contextual information.
|
||||
2.2 Non-Functional Requirements
|
||||
Security and Authorization
|
||||
Context shared with chatbot extensions must remain aligned with Superset's existing authorization model.
|
||||
The host must not expose:
|
||||
• Entities the current user cannot access.
|
||||
• Metadata outside the user's permission scope.
|
||||
• Datasource-derived information unavailable through existing APIs.
|
||||
Authorization remains enforced by backend APIs. The extension-facing APIs defined by this SIP operate on data that has already been scoped to the current user.
|
||||
Stable Extension Contracts
|
||||
Extension-facing APIs must remain independent of frontend implementation details.
|
||||
Extensions should rely on documented namespace contracts rather than:
|
||||
• Redux slices.
|
||||
• Internal selectors.
|
||||
• Component state.
|
||||
• Routing implementation details.
|
||||
This allows frontend architecture to evolve without breaking extensions.
|
||||
Performance
|
||||
The architecture must minimize impact on existing application performance.
|
||||
In particular:
|
||||
• Context APIs must avoid unnecessary application re-renders.
|
||||
• Context change notifications must not rely on polling.
|
||||
• Chatbot integrations should not introduce additional work for unrelated surfaces.
|
||||
Fault Isolation
|
||||
Failures within chatbot extensions must not affect the stability of the host application.
|
||||
Errors originating from third-party chatbot implementations should be isolated to the chatbot mount boundary.
|
||||
Extensibility
|
||||
The architecture should support future:
|
||||
• Application surfaces.
|
||||
• AI-related capabilities.
|
||||
• Extension APIs.
|
||||
• Context providers.
|
||||
without requiring redesign of the chatbot extension model.
|
||||
Vendor Neutrality
|
||||
The architecture must remain independent of any specific:
|
||||
• LLM provider.
|
||||
• AI platform.
|
||||
• Agent framework.
|
||||
• Backend implementation.
|
||||
|
||||
3. Administration
|
||||
3.1 Overview
|
||||
Administrators can manage chatbot availability and select the active chatbot when multiple chatbot extensions are installed.
|
||||
Administration is exposed through the existing Extensions management interface.
|
||||
For chatbot extensions, administrators can:
|
||||
• Enable or disable individual chatbot extensions.
|
||||
• Select the default chatbot when multiple chatbot providers are available.
|
||||
Only one chatbot may be active at a time.
|
||||
3.2 Default Chatbot Selection
|
||||
Extensions that contribute a chatbot view participate in a deployment-wide chatbot selection process.
|
||||
The host discovers available chatbot candidates from the chatbot contribution location and allows administrators to designate a single active chatbot.
|
||||
|
||||
When multiple chatbot extensions are installed:
|
||||
Administrators select the preferred chatbot.
|
||||
The host resolves the active chatbot using the configured selection.
|
||||
Only the selected chatbot is rendered.
|
||||
Changes are applied dynamically without requiring a page reload.
|
||||
3.3 Scope of Administration
|
||||
The administration model introduced by this SIP is deployment-wide.
|
||||
Administrative settings answer the question:
|
||||
"Which chatbot integrations are available within this Superset deployment?"
|
||||
They do not answer:
|
||||
"Which chatbot integrations does a specific user prefer to use?"
|
||||
This distinction is intentional.
|
||||
Deployment administrators determine which integrations are available across the environment, while user-specific preferences remain a separate concern.
|
||||
3.4 Future User Preferences
|
||||
Per-user chatbot preferences are considered an important future capability but are intentionally out of scope for this SIP.
|
||||
This proposal does not introduce user-scoped extension availability.
|
||||
Instead, future user preferences should be layered on top of deployment availability using the following model:
|
||||
Effective Availability = Deployment Availability AND User Preference
|
||||
The recommended persistence layer for future user preferences is the Extension Storage API, which provides user-scoped extension storage and aligns with the architecture established by SIP-127 (User Preferences).
|
||||
This separation preserves a clear distinction between:
|
||||
• Deployment configuration.
|
||||
• User customization.
|
||||
and avoids introducing multiple ownership models for extension availability.
|
||||
Consequently, this SIP focuses exclusively on deployment-wide administration and active chatbot selection. 4. Proposed Extension Point
|
||||
4.1 Overview
|
||||
This SIP introduces a single extension point that allows chatbot providers to integrate directly into the Superset application shell.
|
||||
Extension Point
|
||||
Contribution Location
|
||||
Registration API
|
||||
Cardinality
|
||||
Chatbot Bubble
|
||||
superset.chatbot
|
||||
views.registerView()
|
||||
Singleton
|
||||
|
||||
The chatbot contribution point is application-wide and persists across supported Superset surfaces, including dashboards, Explore, SQL Lab, and dataset-related pages.
|
||||
Unlike most contribution locations, which allow multiple contributions to be rendered simultaneously, the chatbot location is intentionally exclusive and renders a single active provider.
|
||||
|
||||
4.2 Chatbot Contribution Location
|
||||
Contribution Area
|
||||
The contribution location introduced by this SIP is:
|
||||
superset.chatbot
|
||||
The host provides a fixed mount point within the application shell and renders the active chatbot provider at that location.
|
||||
The mount point persists across route changes, allowing chatbot conversations and UI state to remain available while users navigate between application surfaces.
|
||||
The chatbot extension contributes a single React component representing the entire chatbot experience.
|
||||
Manifest Support
|
||||
The current contribution manifest schema is focused on SQL Lab contribution locations and does not provide an application-shell-level contribution scope.
|
||||
To support chatbot integrations, the manifest schema must be extended with an application-level contribution scope capable of declaring:
|
||||
{
|
||||
"views": {
|
||||
"app": [
|
||||
{
|
||||
"location": "superset.chatbot"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
This is a schema-level change and requires updates to both:
|
||||
• Manifest validation.
|
||||
• Runtime registration infrastructure.
|
||||
The runtime registration API alone is not sufficient because chatbot contributions must also be discoverable through extension manifests.
|
||||
4.3 Singleton Rendering Model
|
||||
The chatbot location is intentionally exclusive.
|
||||
Only one chatbot may be active at a time.
|
||||
This differs from other contribution locations that allow multiple views to be rendered simultaneously.
|
||||
Motivation
|
||||
Chatbot interactions are inherently conversational and user-focused.
|
||||
Rendering multiple chatbot providers simultaneously would:
|
||||
• Create competing user experiences.
|
||||
• Introduce ambiguity regarding which chatbot should respond.
|
||||
• Increase UI complexity.
|
||||
• Reduce discoverability.
|
||||
For these reasons, chatbot rendering is treated as a deployment-level selection rather than a multi-provider composition model.
|
||||
Resolution Rules
|
||||
The host applies the following behavior:
|
||||
Installed Chatbots
|
||||
Behavior
|
||||
None
|
||||
No chatbot is rendered
|
||||
One
|
||||
The chatbot is rendered automatically
|
||||
Multiple
|
||||
The administrator-selected chatbot is rendered
|
||||
|
||||
The singleton policy is implemented entirely by the host.
|
||||
Extensions continue to register normally through the existing view registry.
|
||||
4.4 Provider Isolation
|
||||
A key architectural principle of this SIP is that extensions may discover registrations but may not invoke another extension's rendering logic.
|
||||
Public View Discovery
|
||||
The existing registry exposes:
|
||||
getViews(location);
|
||||
This API returns metadata describing registered views:
|
||||
interface View {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
}
|
||||
The returned descriptors are intentionally passive metadata.
|
||||
They allow extensions and host components to:
|
||||
• Discover available contributions.
|
||||
• Display contribution information.
|
||||
• Populate administration interfaces.
|
||||
They do not allow rendering.
|
||||
Why Providers Are Not Exposed
|
||||
The view provider is executable rendering logic.
|
||||
If providers were exposed through the public registry:
|
||||
• Extensions could render another extension's UI.
|
||||
• Extensions could bypass host lifecycle management.
|
||||
• Extensions could circumvent fault-isolation boundaries.
|
||||
• Rendering ownership would become ambiguous.
|
||||
This would violate the separation between extension discovery and extension execution.
|
||||
For this reason:
|
||||
"Extensions may discover registered views, but only the host may render registered views."
|
||||
Host-Managed Resolution
|
||||
The host uses internal APIs to resolve the active chatbot provider.
|
||||
These APIs are not exposed through the public extension surface.
|
||||
Conceptually:
|
||||
const provider = getViewProvider("superset.chatbot", selectedId);
|
||||
The active chatbot is determined through a host-managed resolution policy:
|
||||
const chatbot = getActiveChatbot(adminSelectedId, enabledMap);
|
||||
This policy considers:
|
||||
• Enabled state.
|
||||
• Administrative selection.
|
||||
• Runtime settings.
|
||||
• Registration state.
|
||||
|
||||
before rendering any provider.
|
||||
As a result, chatbot selection is implemented as a host-side rendering policy rather than a new registration primitive.
|
||||
4.5 Chatbot Lifecycle
|
||||
Host Responsibilities
|
||||
The host is responsible for:
|
||||
• Providing the chatbot mount point.
|
||||
• Resolving the active chatbot provider.
|
||||
• Loading chatbot extensions.
|
||||
• Managing chatbot lifecycle integration.
|
||||
• Handling activation and deactivation.
|
||||
• Maintaining fault isolation boundaries.
|
||||
• Preserving chatbot availability across route changes.
|
||||
• Providing context APIs defined by this SIP.
|
||||
The host also provides fixed positioning and layering behavior to ensure chatbot visibility remains consistent throughout the application.
|
||||
Fault Isolation
|
||||
Chatbot providers execute within a host-managed boundary.
|
||||
Failures originating from a chatbot extension must not affect the rest of the application.
|
||||
Examples include:
|
||||
• Module Federation loading failures.
|
||||
• Runtime exceptions.
|
||||
• Provider initialization errors.
|
||||
If a chatbot fails to load, the host logs the failure, surfaces an appropriate notification, and continues operating normally.
|
||||
The application shell remains functional even when the chatbot provider is unavailable.
|
||||
|
||||
4.6 Extension Responsibilities
|
||||
The registered chatbot component owns the complete chatbot experience.
|
||||
The extension is responsible for:
|
||||
User Interface
|
||||
• Collapsed bubble UI.
|
||||
• Expanded panel UI.
|
||||
• Branding.
|
||||
• Icons and badges.
|
||||
• Layout.
|
||||
• Responsiveness.
|
||||
Interaction Model
|
||||
• Open and close behavior.
|
||||
• Keyboard shortcuts.
|
||||
• Focus management.
|
||||
• Accessibility behavior.
|
||||
• Conversation navigation.
|
||||
Conversation Runtime
|
||||
• Message history.
|
||||
• Streaming state.
|
||||
• Tool execution.
|
||||
• Persistence.
|
||||
• Session management.
|
||||
Backend Integration
|
||||
• LLM communication.
|
||||
• MCP integration.
|
||||
• Agent orchestration.
|
||||
• Tool invocation.
|
||||
The host does not manage any chatbot-specific runtime state.
|
||||
|
||||
4.7 Registration Example
|
||||
Chatbot extensions register a single provider through the existing view registration API.
|
||||
import { views, type ExtensionContext } from '@apache-superset/core';
|
||||
import { ChatbotApp } from './ChatbotApp';
|
||||
|
||||
export function activate(context: ExtensionContext) {
|
||||
const disposable = views.registerView(
|
||||
{
|
||||
id: 'acme.chatbot',
|
||||
name: 'Acme Chatbot',
|
||||
icon: 'Bubble',
|
||||
},
|
||||
'superset.chatbot',
|
||||
() => <ChatbotApp />,
|
||||
);
|
||||
context.subscriptions.push(disposable);
|
||||
}
|
||||
The registration process remains consistent with existing extension contribution patterns.
|
||||
The only difference is that the host applies singleton resolution before selecting the provider to render.
|
||||
|
||||
4.8 Chatbot Descriptor Metadata
|
||||
Chatbot registrations may include an optional icon descriptor.
|
||||
{
|
||||
id: 'acme.chatbot',
|
||||
name: 'Acme Chatbot',
|
||||
icon: 'Bubble',
|
||||
}
|
||||
This metadata is used by:
|
||||
• Extension administration interfaces.
|
||||
• Chatbot selection interfaces.
|
||||
• Extension discovery surfaces.
|
||||
Design Decision
|
||||
The icon descriptor is treated as static registration metadata.
|
||||
Runtime UI state such as:
|
||||
• Notification indicators.
|
||||
• Unread counts.
|
||||
• Loading states.
|
||||
• Thinking indicators.
|
||||
belongs to the chatbot component itself rather than the registration descriptor.
|
||||
This keeps the registry simple while allowing chatbot implementations complete control over their user experience.
|
||||
If future requirements emerge for host-visible dynamic icon updates, that capability can be introduced independently without expanding the registration model defined by this SIP. 5. Context and Namespace Model
|
||||
5.1 Overview
|
||||
Chatbot extensions require access to contextual information about the user's current activity within Superset. This SIP introduces a namespace-based context model that allows extensions to consume stable, host-managed APIs rather than depending on internal frontend implementation details.
|
||||
The host exposes context through a set of surface-specific namespaces. Each namespace owns the context for a particular application surface and provides:
|
||||
• Synchronous state getters.
|
||||
• Event-based change notifications.
|
||||
• Stable extension-facing contracts.
|
||||
• Context aligned with the current user's authorized application view.
|
||||
Extensions consume these namespaces and compose them into higher-level context models tailored to their own use cases.
|
||||
5.2 Design Principles
|
||||
The namespace model is guided by the following principles.
|
||||
Stable Extension Contracts
|
||||
Extensions must depend on documented APIs rather than frontend implementation details.
|
||||
In particular, extensions must not depend on:
|
||||
• Redux slices.
|
||||
• Store shape.
|
||||
• Selectors.
|
||||
• Component-local state.
|
||||
• Routing implementation details.
|
||||
This allows Superset to evolve its frontend architecture without breaking extension integrations.
|
||||
Host-Owned Context Normalization
|
||||
The host is responsible for transforming application state into semantic extension-facing contracts.
|
||||
Extensions consume normalized context rather than deriving it from raw frontend state.
|
||||
Backend-Authorized Context
|
||||
Authorization remains a backend responsibility.
|
||||
Namespaces expose context that has already been scoped by backend APIs according to the current user's permissions.
|
||||
Namespaces do not implement authorization logic themselves and should not be considered security boundaries.
|
||||
Event-Driven Updates
|
||||
Context changes are propagated through events rather than polling.
|
||||
Extensions can subscribe to context updates and react immediately when relevant application state changes.
|
||||
5.3 Available Namespaces
|
||||
The following namespaces are available to chatbot extensions.
|
||||
Namespace
|
||||
Status
|
||||
Purpose
|
||||
sqlLab
|
||||
Existing
|
||||
SQL Lab context and events
|
||||
authentication
|
||||
Existing
|
||||
Current user and session context
|
||||
commands
|
||||
Existing
|
||||
Host actions and commands
|
||||
dashboard
|
||||
New
|
||||
Dashboard context
|
||||
explore
|
||||
New
|
||||
Explore/chart context
|
||||
dataset
|
||||
New
|
||||
Dataset identity context
|
||||
navigation
|
||||
New
|
||||
Routing and page context
|
||||
|
||||
The new namespaces introduced by this SIP follow the same high-level contract pattern established by the existing sqlLab namespace.
|
||||
5.4 Namespace API Shape
|
||||
Each namespace follows a common structure:
|
||||
const current = namespace.getCurrent();
|
||||
const disposable = namespace.onDidChange((next) => {
|
||||
// react to updates
|
||||
});
|
||||
The exact contracts differ by surface, but every namespace provides:
|
||||
• One or more synchronous getters.
|
||||
• Event-based change notifications.
|
||||
• Stable semantic contracts.
|
||||
This pattern allows extensions to remain synchronized with application state without polling.
|
||||
|
||||
5.5 Dashboard Namespace
|
||||
The dashboard namespace provides contextual information about the currently active dashboard.
|
||||
API
|
||||
dashboard.getCurrentDashboard();
|
||||
Contract
|
||||
interface DashboardContext {
|
||||
dashboardId: number;
|
||||
title: string;
|
||||
filters: FilterValue[];
|
||||
charts: ChartSummary[];
|
||||
}
|
||||
|
||||
interface ChartSummary {
|
||||
chartId: number;
|
||||
chartName: string;
|
||||
vizType: string;
|
||||
datasourceId: number | null;
|
||||
datasourceName: string | null;
|
||||
isVisible: boolean;}
|
||||
The context includes:
|
||||
• Dashboard identity.
|
||||
• Active filter state.
|
||||
• Dashboard charts.
|
||||
• Per-chart visibility information.
|
||||
Returning all charts while exposing visibility allows chatbot implementations to answer both:
|
||||
• "Which charts are currently visible?"
|
||||
• "Find the chart named Revenue by Region."
|
||||
without requiring additional lookups.
|
||||
Normalization Requirements
|
||||
The namespace must expose semantic dashboard context rather than raw application state.
|
||||
For example:
|
||||
dashboard.getCurrentDashboard();
|
||||
returns a normalized contract rather than Redux slices or internal entities.
|
||||
This abstraction layer preserves compatibility as frontend implementation details evolve.
|
||||
Page-Type Guarding
|
||||
The getter returns undefined when the current page is not a dashboard.
|
||||
Conceptually:
|
||||
if (navigation.getPageType() !== "dashboard") {
|
||||
return undefined;
|
||||
}
|
||||
This prevents stale dashboard state from leaking across application surfaces.
|
||||
5.6 Explore Namespace
|
||||
The explore namespace provides context for the currently active Explore session.
|
||||
API
|
||||
explore.getCurrentChart();
|
||||
Contract
|
||||
interface ChartContext {
|
||||
chartId: number | null;
|
||||
chartName: string | null;
|
||||
datasourceId: number | null;
|
||||
datasourceName: string | null;
|
||||
vizType: string;
|
||||
}
|
||||
|
||||
The namespace exposes:
|
||||
• Chart identity. `chartId` and `chartName` are null for a new, unsaved chart that has not yet been persisted.
|
||||
• Saved chart metadata (name, datasource, viz type)
|
||||
• Current Explore context: `vizType` reflects the type currently selected in the editor, so the value tracks the live session rather than only the last saved state.
|
||||
The contract is intentionally focused on chart-specific information relevant to chatbot integrations.
|
||||
Reflecting the live editing session — rather than reconstructing chart state from
|
||||
the route alone — is the primary reason this SIP exposes frontend context
|
||||
directly (see §6.2, Option C).
|
||||
|
||||
Page-Type Guarding
|
||||
The getter returns undefined when the current page is not an Explore surface.
|
||||
Conceptually:
|
||||
if (navigation.getPageType() !== "explore") {
|
||||
return undefined;
|
||||
}
|
||||
This ensures the namespace reflects only active Explore context.
|
||||
5.7 Dataset Namespace
|
||||
The dataset namespace exposes the dataset currently being viewed or edited.
|
||||
API
|
||||
dataset.getCurrentDataset();
|
||||
Contract
|
||||
interface DatasetContext {
|
||||
datasetId: number;
|
||||
datasetName: string;
|
||||
schema: string | null;
|
||||
catalog: string | null;
|
||||
databaseName: string | null;
|
||||
isVirtual: boolean;}
|
||||
This contract is intentionally identity-focused.
|
||||
It answers:
|
||||
• Which dataset is currently in focus?
|
||||
• Is the dataset virtual or physical?
|
||||
• Which database and schema does it belong to?
|
||||
It does not expose:
|
||||
• Column definitions.
|
||||
• Lineage information.
|
||||
• Dataset dependencies.
|
||||
Those concerns are expected to be resolved by backend services using the dataset identifier.
|
||||
Producer-Backed Context
|
||||
Unlike dashboard and explore namespaces, dataset pages do not currently expose a shared source of truth suitable for namespace consumption.
|
||||
For this reason, dataset context is published by dataset pages through a host-managed producer mechanism.
|
||||
Dataset pages publish the active dataset as it loads, and:
|
||||
dataset.getCurrentDataset();
|
||||
returns the most recently published value.
|
||||
Until dataset information has been published, the getter returns:
|
||||
undefined;
|
||||
This design keeps the public contract stable without requiring the introduction of a dedicated Redux slice.
|
||||
|
||||
Example Use Cases
|
||||
The dataset namespace enables chatbot workflows such as:
|
||||
• Explain this dataset.
|
||||
• Summarize this dataset's purpose.
|
||||
• Show lineage for this dataset.
|
||||
• Which charts depend on this dataset?
|
||||
The namespace provides the identity required to perform those lookups while avoiding duplication of backend metadata.
|
||||
5.8 Navigation Namespace
|
||||
The navigation namespace provides routing-related context.
|
||||
API
|
||||
navigation.getPageType();
|
||||
Events
|
||||
navigation.onDidChangePage(...)
|
||||
|
||||
Contract
|
||||
type PageType =
|
||||
| "home"
|
||||
| "dashboard"
|
||||
| "explore"
|
||||
| "sqllab"
|
||||
| "dataset"
|
||||
| "other";
|
||||
The namespace answers a single question:
|
||||
"Which application surface is currently active?"
|
||||
It intentionally does not expose entity-specific information.
|
||||
Entity context remains owned by the corresponding surface namespace.
|
||||
Examples:
|
||||
dashboard.getCurrentDashboard();
|
||||
explore.getCurrentChart();
|
||||
dataset.getCurrentDataset();
|
||||
This separation preserves clear ownership boundaries and prevents duplication across namespaces.
|
||||
|
||||
5.9 Context Composition
|
||||
This SIP intentionally does not introduce a host-owned aggregate context object.
|
||||
Instead, extensions compose the context they require from individual namespaces.
|
||||
For example:
|
||||
const pageContext = {
|
||||
pageType: navigation.getPageType(),
|
||||
dashboard: dashboard.getCurrentDashboard(),
|
||||
chart: explore.getCurrentChart(),
|
||||
dataset: dataset.getCurrentDataset(),
|
||||
sqlLab: sqlLab.getCurrentTab(),
|
||||
};
|
||||
The extension assembles a higher-level context tailored to its own requirements.
|
||||
The host remains responsible for:
|
||||
• Context ownership.
|
||||
• Context normalization.
|
||||
• Authorization alignment.
|
||||
The extension remains responsible for:
|
||||
• Context composition.
|
||||
• Prompt construction.
|
||||
• Application-specific interpretation.
|
||||
This separation avoids introducing a centralized context abstraction while allowing new surfaces to be added incrementally over time.
|
||||
|
||||
5.10 Compatibility and Evolution
|
||||
Namespace contracts are part of the public Superset extension API surface.
|
||||
Breaking changes require standard compatibility and deprecation processes.
|
||||
Extensions should depend only on documented namespace contracts and must not rely on implementation details behind those contracts.
|
||||
As new application surfaces become extension-aware, additional namespaces may be introduced without affecting existing integrations.
|
||||
This additive model allows the extension ecosystem to evolve while preserving backward compatibility.
|
||||
|
||||
6. Design Decisions
|
||||
This section consolidates the key architectural decisions made by this SIP and summarizes the alternatives that were evaluated.
|
||||
The goal is to capture the rationale behind the extension model so that future contributors can understand not only what was selected, but why alternative approaches were rejected.
|
||||
6.1 Decision Summary
|
||||
Decision
|
||||
Topic
|
||||
Selected Approach
|
||||
D1
|
||||
Page Context Model
|
||||
Extension-composed context from host-provided namespaces
|
||||
D2
|
||||
Chatbot Resolution
|
||||
Host-managed singleton resolution
|
||||
D3
|
||||
Descriptor Metadata
|
||||
Static icon metadata
|
||||
D4
|
||||
Administration Scope
|
||||
Deployment-wide administration
|
||||
D5
|
||||
Per-Page Visibility
|
||||
Deferred - open question, see §8
|
||||
D6
|
||||
Generalized Floating Slots
|
||||
Deferred - open question, see §8
|
||||
|
||||
6.2 D1 — Page Context Model
|
||||
A central design question is how chatbot extensions obtain contextual information about the currently active application surface.
|
||||
Three approaches were considered.
|
||||
Option A — Host-Owned Aggregate Context
|
||||
The host exposes a single API:
|
||||
context.getPageContext();
|
||||
which returns a fully assembled context object containing dashboard, chart, dataset, navigation, and SQL Lab information.
|
||||
Rejected Because
|
||||
• The host becomes responsible for understanding every application surface.
|
||||
• The aggregate contract grows whenever a new surface is introduced.
|
||||
• Changes in any surface can trigger unnecessary recomputation.
|
||||
• The host becomes coupled to a single canonical context model.
|
||||
• Ownership boundaries become unclear over time.
|
||||
Option B — Surface Namespaces Composed by Extensions (Selected)
|
||||
The host exposes independent namespaces:
|
||||
• dashboard
|
||||
• explore
|
||||
• dataset
|
||||
• navigation
|
||||
• sqlLab
|
||||
Extensions compose these primitives into their own application-specific context.
|
||||
Advantages
|
||||
• Clear ownership boundaries.
|
||||
• Independent evolution of namespaces.
|
||||
• Additive extensibility.
|
||||
• Reduced coupling between surfaces.
|
||||
• Extensions subscribe only to the context they require.
|
||||
Option C — Route-Only Context
|
||||
The host exposes only routing information.
|
||||
Chatbot providers independently reconstruct context through APIs or backend services.
|
||||
Rejected Because
|
||||
This approach cannot reliably represent transient frontend state.
|
||||
Examples include:
|
||||
• Unsaved chart edits.
|
||||
• Temporary dashboard filters.
|
||||
• Active dashboard tabs.
|
||||
• SQL editor state.
|
||||
• Draft configuration changes.
|
||||
As a result, chatbot context would frequently drift from what the user is actually viewing.
|
||||
Decision
|
||||
Option B is selected.
|
||||
The host owns context normalization while extensions own context composition.
|
||||
This preserves separation of concerns, minimizes coupling, and provides a stable foundation for future extension capabilities.
|
||||
6.3 D2 — Singleton Chatbot Resolution
|
||||
When multiple chatbot extensions are installed, the host must determine which chatbot is rendered.
|
||||
This decision shapes both the rendering model and the extension isolation model.
|
||||
Option A — Expose Providers Through getViews()
|
||||
Allow:
|
||||
getViews(location);
|
||||
to return both metadata and rendering providers.
|
||||
Rejected Because
|
||||
Rendering providers are executable logic.
|
||||
Exposing providers would allow one extension to:
|
||||
• Render another extension.
|
||||
• Bypass host lifecycle management.
|
||||
• Circumvent fault isolation.
|
||||
• Assume ownership of another extension's UI.
|
||||
This violates a deliberate separation between extension discovery and extension execution.
|
||||
Option B — Host-Managed Provider Resolution (Selected)
|
||||
The host exposes only metadata publicly while retaining provider resolution internally.
|
||||
Conceptually:
|
||||
const provider = getViewProvider("superset.chatbot", selectedId);
|
||||
Chatbot selection is handled through a host-managed policy:
|
||||
const chatbot = getActiveChatbot(adminSelectedId, enabledMap);
|
||||
Advantages
|
||||
• Preserves extension isolation.
|
||||
• Preserves host ownership of rendering.
|
||||
• Supports administrative selection.
|
||||
• Supports enablement checks.
|
||||
• Supports future policy evolution.
|
||||
Option C — Reuse resolveView()
|
||||
Use the existing rendering helper:
|
||||
resolveView(id);
|
||||
to render chatbot providers.
|
||||
Rejected Because
|
||||
resolveView() assumes the caller already knows which view should be rendered.
|
||||
It does not account for:
|
||||
• Administrative selection.
|
||||
• Enablement state.
|
||||
• Settings synchronization.
|
||||
• Chatbot-specific resolution policy.
|
||||
Decision
|
||||
Option B is selected.
|
||||
The host owns chatbot selection and rendering.
|
||||
The registry remains a discovery mechanism rather than a rendering mechanism.
|
||||
Architectural Principle
|
||||
A core principle established by this SIP is:
|
||||
"Extensions may discover registered views, but only the host may render registered views."
|
||||
This preserves extension isolation and prevents cross-extension rendering dependencies.
|
||||
6.4 D3 — Descriptor Metadata Ownership
|
||||
Chatbot registrations may include metadata used by administrative and discovery interfaces.
|
||||
A key question is whether descriptor metadata should be static or runtime-updatable.
|
||||
|
||||
Option A — Static Descriptor Metadata (Selected)
|
||||
Metadata is defined at registration time and remains unchanged for the lifetime of the registration.
|
||||
Example:
|
||||
{
|
||||
id: 'acme.chatbot',
|
||||
name: 'Acme Chatbot',
|
||||
icon: 'Bubble',
|
||||
}
|
||||
Advantages
|
||||
• Simpler registry implementation.
|
||||
• Clear ownership model.
|
||||
• Consistent administration UI.
|
||||
• No registry update lifecycle.
|
||||
Option B — Runtime-Updatable Metadata
|
||||
Extensions can update descriptor metadata after registration.
|
||||
Examples:
|
||||
• Notification badges.
|
||||
• Thinking indicators.
|
||||
• Dynamic branding.
|
||||
Rejected Because
|
||||
These states belong to the chatbot user interface rather than the registration descriptor.
|
||||
Supporting dynamic metadata would:
|
||||
• Increase registry complexity.
|
||||
• Introduce update synchronization concerns.
|
||||
• Provide limited benefit for current consumers.
|
||||
Decision
|
||||
Option A is selected.
|
||||
Descriptor metadata remains static.
|
||||
Dynamic UI state remains the responsibility of the chatbot component.
|
||||
Future requirements for dynamic metadata can be addressed independently if needed.
|
||||
6.5 D4 — Administration Scope
|
||||
This SIP introduces deployment-wide chatbot administration.
|
||||
A key question is whether availability should be deployment-scoped or user-scoped.
|
||||
Option A — Deployment-Wide Administration (Selected)
|
||||
Administrators manage:
|
||||
• Extension availability.
|
||||
• Default chatbot selection.
|
||||
These settings apply to the entire deployment.
|
||||
Advantages
|
||||
• Clear administrative ownership.
|
||||
• Simple operational model.
|
||||
• Consistent with existing extension administration patterns.
|
||||
• Avoids introducing multiple configuration layers.
|
||||
Option B — User-Scoped Availability
|
||||
Availability and chatbot selection become user-specific settings.
|
||||
Rejected Because
|
||||
Administrative availability and user preference represent different concerns.
|
||||
Administrators answer:
|
||||
"Which integrations are available in this deployment?"
|
||||
Users answer:
|
||||
"Which available integrations do I prefer?"
|
||||
Combining these concerns into a single model creates unclear ownership and duplicated configuration responsibilities.
|
||||
|
||||
Decision
|
||||
Option A is selected.
|
||||
This SIP introduces only deployment-wide administration.
|
||||
Future user preferences should be layered on top using the following model:
|
||||
Effective Availability = Deployment Availability AND User Preference
|
||||
The recommended persistence mechanism for user-specific preferences is the Extension Storage API.
|
||||
This approach aligns with SIP-127 and preserves a clear separation between administrative configuration and user customization.
|
||||
|
||||
7. Risks and Future Considerations
|
||||
The selected architecture introduces several tradeoffs.
|
||||
Namespace Maintenance
|
||||
As additional application surfaces become extension-aware, new namespaces may be required.
|
||||
This increases the maintenance burden of the extension API surface.
|
||||
Contract Evolution
|
||||
Namespace contracts are intended to be stable.
|
||||
Over time, extensions may require additional context that is not initially exposed.
|
||||
Future additions must preserve compatibility and avoid leaking implementation details.
|
||||
Context Growth
|
||||
Dashboard and chart context may become increasingly rich over time.
|
||||
Care must be taken to ensure context APIs remain focused and do not evolve into large aggregate objects.
|
||||
Extension Expectations
|
||||
Chatbot vendors may request direct access to internal application state for convenience.
|
||||
This SIP intentionally rejects that approach in favor of stable semantic contracts.
|
||||
Maintaining that boundary may require additional namespace evolution over time. 8. Open Questions
|
||||
D5 — Per-Page Visibility
|
||||
Should chatbot extensions be able to declare page visibility constraints?
|
||||
Two approaches remain possible.
|
||||
Extension-Controlled Visibility
|
||||
Extensions observe:
|
||||
navigation.onDidChangePage(...)
|
||||
and decide whether to render themselves.
|
||||
Host-Enforced Visibility
|
||||
Extensions declare supported page types through manifest metadata and the host enforces visibility.
|
||||
Recommendation
|
||||
Defer this decision.
|
||||
The current architecture already supports extension-controlled visibility without requiring additional platform capabilities.
|
||||
|
||||
D6 — Generalized Floating Contribution Areas
|
||||
The current proposal introduces a chatbot-specific contribution location:
|
||||
superset.chatbot
|
||||
A future question is whether this should evolve into a more generic floating-widget framework.
|
||||
Examples might include:
|
||||
• Chatbots.
|
||||
• Guided tours.
|
||||
• Notification centers.
|
||||
• Productivity assistants.
|
||||
Recommendation
|
||||
Keep the contribution area chatbot-specific.
|
||||
If broader floating-widget requirements emerge, introduce a dedicated abstraction rather than expanding the scope of this SIP.
|
||||
|
||||
9. Related Documents
|
||||
Contribution types
|
||||
Client actions
|
||||
|
||||
The following proposals are related to this SIP.
|
||||
Extension Storage API
|
||||
Add storage API for extensions (#39171)
|
||||
Introduces namespace-isolated storage for extensions with support for:
|
||||
• Local storage.
|
||||
• Session storage.
|
||||
• Ephemeral server storage.
|
||||
• Persistent database-backed storage.
|
||||
This proposal is complementary to the administration model defined by this SIP and is the recommended foundation for future user-specific extension preferences.
|
||||
|
||||
SIP-127 — User Preferences
|
||||
[SIP-127] User Preferences (#28047)
|
||||
Establishes the per-user preference model used by Superset core.
|
||||
The Extension Storage API serves as the extension-scoped equivalent of this pattern and provides the recommended approach for future user-specific chatbot preferences. 10. Migration Plan
|
||||
Base branch enxdev/chat-prototype
|
||||
Branch for testing test/chatbot-local
|
||||
The following capabilities are required to fully realize this SIP.
|
||||
Core Platform Changes
|
||||
Implemented
|
||||
• superset.chatbot contribution location.
|
||||
• Host-side chatbot resolution.
|
||||
• Administration UI for chatbot selection.
|
||||
• Dashboard namespace.
|
||||
• Explore namespace.
|
||||
• Navigation namespace.
|
||||
• Runtime settings synchronization.
|
||||
Pending
|
||||
• Dataset namespace implementation.
|
||||
• Dashboard chart visibility context.
|
||||
• Permission-scoped dashboard context endpoint.
|
||||
• Manifest support for application-level contribution scopes.
|
||||
• Optional descriptor icon support.
|
||||
|
||||
11. Implementation Phases
|
||||
Phase 1 — Chatbot Mount Point
|
||||
• Chatbot contribution location.
|
||||
• Host-side rendering.
|
||||
• Lifecycle management.
|
||||
• Fault isolation.
|
||||
Status: Complete
|
||||
Phase 2 — Administration
|
||||
• Enable/disable support.
|
||||
• Default chatbot selection.
|
||||
• Runtime synchronization.
|
||||
Status: Complete
|
||||
Phase 3 — Context APIs
|
||||
• Dashboard namespace.
|
||||
• Explore namespace.
|
||||
• Navigation namespace.
|
||||
• Dataset namespace.
|
||||
Status: Partially Complete
|
||||
Remaining work:
|
||||
• Dataset namespace.
|
||||
• Dashboard chart visibility context.
|
||||
• Dashboard context endpoint.
|
||||
Phase 4 — Client Actions
|
||||
Client actions and agentic UI interactions remain outside the scope of this SIP and are expected to be addressed through a separate proposal.
|
||||
145
SECURITY.md
Normal file
145
SECURITY.md
Normal file
@@ -0,0 +1,145 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# Security Policy
|
||||
|
||||
This is a project of the [Apache Software Foundation](https://apache.org) and follows the
|
||||
ASF [vulnerability handling process](https://apache.org/security/#vulnerability-handling).
|
||||
|
||||
## Reporting Vulnerabilities
|
||||
|
||||
**⚠️ Please do not file GitHub issues for security vulnerabilities as they are public! ⚠️**
|
||||
|
||||
|
||||
Apache Software Foundation takes a rigorous standpoint in resolving the security issues
|
||||
in its software projects. Apache Superset is highly sensitive and forthcoming to issues
|
||||
pertaining to its features and functionality.
|
||||
If you have any concern or believe you have found a vulnerability in Apache Superset,
|
||||
please get in touch with the Apache Superset Security Team privately at
|
||||
e-mail address [security@superset.apache.org](mailto:security@superset.apache.org).
|
||||
|
||||
More details can be found on the ASF website at
|
||||
[ASF vulnerability reporting process](https://apache.org/security/#reporting-a-vulnerability)
|
||||
|
||||
**Submission Standards & AI Policy**
|
||||
|
||||
To ensure engineering focus remains on verified risks and to manage high reporting volumes, all reports must meet the following criteria:
|
||||
- Plain Text Format: In accordance with Apache guidelines, please provide all details in plain text within the email body. Avoid sending PDFs, Word documents, or password-protected archives.
|
||||
- Mandatory AI Disclosure: If you utilized Large Language Models (LLMs) or AI tools to identify a flaw or assist in writing a report, you must disclose this in your submission so our triage team can contextualize the findings.
|
||||
- Human-Verified PoC: All submissions must include a manual, step-by-step Proof of Concept (PoC) performed on a supported release. Raw AI outputs, hypothetical chat transcripts, or unverified scanner logs will be closed as Invalid.
|
||||
|
||||
We kindly ask you to include the following information in your report to assist our developers in triaging and remediating issues efficiently:
|
||||
- Version/Commit: The specific version of Apache Superset or the Git commit hash you are using.
|
||||
- Configuration: A sanitized copy of your `superset_config.py` file or any config overrides.
|
||||
- Environment: Your deployment method (e.g., Docker Compose, Helm, or source) and relevant OS/Browser details.
|
||||
- Impacted Component: Identification of the affected area (e.g., Python backend, React frontend, or a specific database connector).
|
||||
- Expected vs. Actual Behavior: A clear description of the intended system behavior versus the observed vulnerability.
|
||||
- Detailed Reproduction Steps: Clear, manual steps to reproduce the vulnerability.
|
||||
|
||||
## Security Model
|
||||
|
||||
This section defines what Apache Superset considers a security issue and what it does not. It is the canonical reference for reporters, the Apache Superset Security Team, and any automated tool (LLM-based scanner, static analyzer, dependency tool) that needs to constrain its hypotheses to behaviors that genuinely violate the project's security policy.
|
||||
|
||||
The model is intentionally written in terms of principals, trust boundaries, and capability surface rather than in terms of specific files, functions, or libraries. New code paths inherit the model automatically.
|
||||
|
||||
### Trust Boundaries
|
||||
|
||||
Apache Superset's threat model assumes three trust boundaries.
|
||||
|
||||
1. *The Admin role* is a fully trusted operational principal. Anything an Admin can do through the documented user interface, REST API, or configuration system is an intended capability, not a vulnerability, even if individually powerful or destructive. The Admin role is, by policy, equivalent to operating-system-level trust over the Apache Superset application. This is unavoidable rather than aspirational: an Admin can, for example, register new database connections of arbitrary type, execute arbitrary SQL through SQL Lab, render Jinja templates that resolve to SQL or rendered HTML, and override application configuration. Granting Admin is functionally equivalent to granting shell access on the host, which is the reasoning behind treating it as a trust boundary in the sense of MITRE CNA Operational Rules 4.1.
|
||||
|
||||
2. *The operator* is whoever deploys, configures, and runs Apache Superset. Behaviors that depend on deployment-time decisions are the operator's responsibility, not Apache Superset's. This includes the values of secrets, the network reachability of the application and its data sources, the choice of database connectors and cache backends, the selection of feature flags, the destinations of notifications, and the trust placed in third-party plugins. Defaults that fail closed are the responsibility of the Apache Superset codebase. Defaults that fail open must be accompanied by a documented hardening requirement; applying that hardening is the operator's responsibility, while shipping an undocumented or unflagged fail-open default is a codebase issue.
|
||||
|
||||
3. *The Apache Superset codebase* is responsible for enforcing the role and capability matrix below across its product surface. A failure to enforce, anywhere in that surface, is in scope. The codebase's commitments are limited to the role and capability matrix and to controls Apache Superset's own documentation (this file and the linked Security documentation) explicitly positions as security boundaries; configurable hardening that operators can layer on top is treated separately under *Vulnerability Scope* below.
|
||||
|
||||
### Roles and Capabilities
|
||||
|
||||
Apache Superset ships with the following first-class principals. Detailed permission definitions live in the [Security documentation](https://superset.apache.org/docs/security).
|
||||
|
||||
| Principal | Read data | Write objects | Execute SQL | Manage databases | Manage users, roles, RLS |
|
||||
|---|---|---|---|---|---|
|
||||
| Public (anonymous) | none by default | no | no | no | no |
|
||||
| Gamma | only granted datasets | own charts and dashboards on granted datasets | no by default (requires the `sql_lab` role) | no | no |
|
||||
| Alpha | all data sources | own charts, dashboards, and datasets | no by default (requires the `sql_lab` role) | data upload to existing databases only | no |
|
||||
| Admin | all | all | yes | yes | yes |
|
||||
| Embedded guest token | data sources reachable through the embedded dashboards the token authorizes | no | no | no | no |
|
||||
|
||||
The `sql_lab` role is *additive*: it grants the SQL Lab permission set on top of the base role above, and is the only path by which Gamma or Alpha gain SQL execution capability. Database access is still scoped per the base role's grants. Admin includes SQL Lab access by default.
|
||||
|
||||
Deployments may grant or revoke individual view-menu permissions, which shifts the boundary for that deployment but does not redefine the model. Any custom role created by an operator inherits the same principle: its capabilities are whatever the operator has explicitly granted it. The Public principal follows the same rule: operators may grant the Public role read access to specific datasets or dashboards (typically for anonymous reporting use cases), which shifts the boundary for that deployment without redefining the model.
|
||||
|
||||
### Vulnerability Scope
|
||||
|
||||
The test for whether a finding is in scope is a single question:
|
||||
|
||||
> *Does this finding let a principal perform an action the role and capability matrix above does not entitle them to?*
|
||||
|
||||
If yes, it is in scope. If no, it is out of scope. The lists below apply that test to the classes Apache Superset most commonly receives reports about; they are illustrative, not exhaustive.
|
||||
|
||||
*In Scope*
|
||||
|
||||
- A user, embedded guest, or anonymous visitor reads, modifies, or deletes data outside their granted set. Includes object-level access bypass on charts, dashboards, datasets, saved queries, tags, annotations, and similar per-object endpoints, and row-level-security rule bypass.
|
||||
- A user supplies input that the codebase should sanitize or parameterize but does not, causing arbitrary SQL, template code, or scripts to execute. Includes injection through Jinja templates, SQL-construction paths, and any field the codebase passes to a query engine or template engine.
|
||||
- A user bypasses authentication, fixates or reuses another user's session, or reaches an authenticated endpoint without logging in.
|
||||
- An embedded guest token authorizes actions outside the dashboard it was issued for, or can be forged, replayed, or escalated to a higher principal.
|
||||
- Apache Superset, acting on behalf of an unprivileged user, fetches an outbound URL the user controls in a feature where Apache Superset itself, not the operator, controls the outbound destination set (server-side request forgery).
|
||||
- An Apache Superset default fails open without an accompanying documented hardening requirement. The codebase is responsible for shipping fail-closed defaults or for documenting the hardening required when a default fails open; failures of that responsibility are in scope (see *Trust Boundaries*).
|
||||
- A user bypasses a control Apache Superset documents specifically as a security boundary. This includes row-level security, the access checks tied to the role and capability matrix above, and any feature whose documentation positions it as security-relevant. The codebase commits to enforcing those controls; bypasses are in scope regardless of which principal triggers them.
|
||||
- A user causes a script to execute in another user's browser through a field the codebase renders to that other user (cross-site scripting), or causes cross-origin leakage of authenticated session state or data.
|
||||
- A user reaches a route, page, or API endpoint that requires a role they do not have.
|
||||
|
||||
*Out of Scope*
|
||||
|
||||
- Any action an Admin role can perform through documented configuration, API, or UI. The Admin role is a trusted operational principal by policy. Per MITRE CNA Operational Rules 4.1, a qualifying vulnerability must violate a security policy; behavior within a documented trust boundary does not.
|
||||
- Deployment or operator decisions: the values of secrets and tokens, whether internal networks are reachable from the server, which database connectors or cache backends are enabled, which feature flags are set, where notifications are delivered, and which third-party plugins are loaded.
|
||||
- Compromise, modification, or malicious control of trusted backend infrastructure. Apache Superset assumes the integrity of its metastore, cache backends (for example Redis or Memcached), message brokers, secret stores, and other operator-managed infrastructure. Findings that require an attacker to read from, write to, or otherwise tamper with these systems, including injecting malicious state, serialized objects, cache entries, task metadata, configuration, or database records, are post-compromise scenarios and do not constitute vulnerabilities in Apache Superset itself. A finding remains in scope only if an unprivileged user can cause such modification through a vulnerability in Apache Superset.
|
||||
- Code paths whose intended purpose is example data, demos, fixtures, local development, or documentation, rather than the production runtime.
|
||||
- How a downstream application (spreadsheet program, email client, browser handling user-downloaded files) interprets output Apache Superset produced for it.
|
||||
- Findings without a reproducible proof of concept against a supported release. The burden of demonstrating exploitability rests with the reporter; findings closed for lack of a proof of concept may be refiled if one is later produced.
|
||||
- Brute force, rate limiting, denial of service, or resource exhaustion that does not bypass a documented control.
|
||||
- Missing security headers, banner or version disclosure, user or object enumeration through error messages or timing, and similar low-impact information disclosure that does not enable a further concrete exploit.
|
||||
- Bypasses of configurable defense-in-depth hardening that Apache Superset does not document as a security boundary. Apache Superset is not a SQL or database firewall: operator-deployable filters such as SQL function or table denylists, URI restrictions on already-authorized database connectors, and similar belt-and-braces controls are provided to let operators layer hardening on top of the role and capability matrix, not as firewall-grade guarantees the codebase commits to. Findings against such hardening are improvements, not vulnerabilities, unless the documentation positions the specific control as security-relevant.
|
||||
- Hardening suggestions that improve defense in depth but do not violate the security model.
|
||||
|
||||
Findings in third-party dependencies fall into two cases. A finding in a transitive dependency, or in an operator-selected dependency that Apache Superset does not ship, is out of scope and should be reported to the dependency's maintainers. A finding caused by Apache Superset pinning a known-vulnerable version of a direct dependency it ships, or using a dependency in a way that creates a vulnerability the dependency itself does not have, remains in scope. Dependency findings in the official Apache Superset Docker image that fall into the first case can be remediated by extending the image at release time.
|
||||
|
||||
When uncertain whether a finding falls in scope, please file it through the reporting process above. The triage team will classify it and explain the reasoning if it is closed as out of scope.
|
||||
|
||||
**Outcome of Reports**
|
||||
|
||||
Reports that are deemed out of scope for a CVE but represent valid security best practices or hardening opportunities are typically converted into public GitHub issues, where the community can contribute fixes alongside the maintainers. The triage decision and reasoning are communicated back to the reporter in either case.
|
||||
|
||||
**Vulnerability Aggregation & CVE Attribution**
|
||||
|
||||
In accordance with MITRE CNA Operational Rules (4.1.10, 4.1.11, and 4.2.13), Apache Superset issues CVEs based on the underlying architectural root cause rather than the number of affected endpoints or exploit payloads.
|
||||
- Aggregation: If multiple exploit vectors stem from the same programmatic failure or shared vulnerable code, they must be aggregated into a single, comprehensive report.
|
||||
- Independent Fixes: Separate CVEs will only be assigned if the vulnerabilities reside in decoupled architectural modules and can be fixed independently of one another.
|
||||
Reports that fail to aggregate related findings will be merged during triage to ensure an accurate and defensible CVE record.
|
||||
|
||||
**Your responsible disclosure and collaboration are invaluable.**
|
||||
|
||||
## Extra Information
|
||||
|
||||
- [Apache Superset documentation](https://superset.apache.org/docs/security)
|
||||
- [Common Vulnerabilities and Exposures by release](https://superset.apache.org/docs/security/cves)
|
||||
- [How Security Vulnerabilities are Reported & Handled in Apache Superset (Blog)](https://preset.io/blog/how-security-vulnerabilities-are-reported-and-handled-in-apache-superset/)
|
||||
|
||||
**For LLM agents and automated scanners**
|
||||
|
||||
This file is the canonical source of truth for what Apache Superset considers a security vulnerability. Automated tooling (LLM-based code scanners, static analyzers, dependency tools) should treat the Security Model section as authoritative when classifying findings. The repository's [AGENTS.md](AGENTS.md) file contains a short pointer to this document for agents that read AGENTS.md as their entry point.
|
||||
322
TICKETS.md
Normal file
322
TICKETS.md
Normal file
@@ -0,0 +1,322 @@
|
||||
# Chatbot Extensions — Tickets
|
||||
|
||||
Lightweight, pre-implementation tickets. Each says what to build and where the
|
||||
boundaries are; it does not prescribe the final code. Stack order (bottom → top):
|
||||
contribution point → frontend API mount → eager loading → admin UI → backend
|
||||
settings/permissions → context sharing → import/delete.
|
||||
|
||||
---
|
||||
|
||||
## 1. Define the contribution point
|
||||
|
||||
**Goal:** Introduce the `superset.chatbot` contribution area and the host plumbing
|
||||
needed to mount a single chatbot at the application-shell level, persistent across
|
||||
routes. This is the keystone everything else builds on.
|
||||
|
||||
**Build:**
|
||||
|
||||
- Register `superset.chatbot` as a recognized contribution location in the view
|
||||
registry.
|
||||
- Add an app-shell / app-root contribution scope to the extension manifest schema
|
||||
so the location can be declared in `extension.json` (the current schema is
|
||||
SQL-Lab-only). Teach both manifest validation and runtime registration about it.
|
||||
- Provide an exclusive-location resolver that selects exactly one renderable
|
||||
chatbot for the slot, with a deterministic first-to-register fallback and a seam
|
||||
for an externally supplied "active chatbot id" (so admin/runtime policy can plug
|
||||
in later without touching the resolver).
|
||||
- Host-managed mount layout: fixed bottom-right, 24px margin, z-index above content
|
||||
and toasts, below modals.
|
||||
|
||||
**Out of scope:** fault isolation, admin selection UI, the lifecycle/teardown
|
||||
contract, eager loading, streaming, context namespaces, authoring docs.
|
||||
|
||||
**Depends on:** nothing — this unblocks the rest.
|
||||
|
||||
**Done when:** an extension can register at `superset.chatbot` and render at the
|
||||
app shell across routes; the resolver returns one provider (admin-id seam +
|
||||
first-to-register fallback); unregistering removes the mount cleanly with no
|
||||
duplicate bubbles.
|
||||
|
||||
Base branch: `enxdev/chat-prototype`
|
||||
|
||||
**External Links:** https://github.com/apache/superset/pull/40439
|
||||
|
||||
---
|
||||
|
||||
## 2. Host resolution & mount (frontend API entry point)
|
||||
|
||||
**Goal:** Turn a registered `superset.chatbot` view into a rendered, fault-isolated
|
||||
bubble — the host-internal provider accessor, the selection policy, and the
|
||||
fixed-position mount.
|
||||
|
||||
**Build:**
|
||||
|
||||
- Host-internal accessors on the views registry: `getViewProvider(location, id)`
|
||||
and `getRegisteredViewIds(location)`. Keep the public `getViews` descriptor-only —
|
||||
do not expose providers on the public surface.
|
||||
- A registry change subscription so a mount can re-resolve without polling (fired
|
||||
on register/unregister).
|
||||
- The `getActiveChatbot(adminSelectedId?, enabledMap?)` resolver implementing the
|
||||
selection policy: empty → none; drop disabled ids; admin-selected-and-enabled
|
||||
wins; else first enabled in registration order.
|
||||
- A `ChatbotMount` component at the app shell that renders the active provider
|
||||
inside the host `ErrorBoundary`, re-resolves on registry change, and renders
|
||||
nothing when no chatbot is active.
|
||||
|
||||
**Out of scope:** the contribution location itself (#40439); eager-loading the
|
||||
bundle (#40441); the settings endpoint (#40443, consumed here with silent
|
||||
fallback); admin UI; the lifecycle/teardown contract.
|
||||
|
||||
**Depends on:** #40439 (imports `CHATBOT_LOCATION`). The settings endpoint is a soft
|
||||
forward-dependency — the mount falls back to first-registered-enabled if it 404s.
|
||||
|
||||
**Done when:** the provider accessor and resolver behave per the policy; the mount
|
||||
renders/clears correctly and survives a throwing provider via `ErrorBoundary`;
|
||||
`getViews` stays descriptor-only.
|
||||
|
||||
Base branch: `enxdev/feat/chatbot-contribution-point` (on #40439)
|
||||
|
||||
**External Links:** https://github.com/apache/superset/pull/40440
|
||||
|
||||
---
|
||||
|
||||
## 3. Eager loading & extension lifecycle/teardown
|
||||
|
||||
> Merged: this ticket also covers the **lifecycle & teardown contract** — both are
|
||||
> implemented in the same PR (#40441), so they are tracked together.
|
||||
|
||||
**Goal:** Boot extension bundles at app-shell startup so contributions register
|
||||
before the first route, and define the host contract for tearing those
|
||||
contributions down on uninstall.
|
||||
|
||||
**Build — eager loading:**
|
||||
|
||||
- An `ExtensionsStartup` component that, once the session is confirmed and behind
|
||||
`FeatureFlag.EnableExtensions`, kicks off `initializeExtensions()` in the
|
||||
background. The host renders immediately; the mount re-resolves reactively when
|
||||
registrations land.
|
||||
- Wire `window.superset` so Module-Federation remotes can consume host namespaces.
|
||||
- Mount `<ChatbotMount />` as a sibling of the route switch, inside
|
||||
`ExtensionsStartup`.
|
||||
- On bundle-load failure: a danger toast, host stays interactive, corner stays
|
||||
empty. Add a global `unhandledrejection` logger (log only; do not suppress the
|
||||
browser default).
|
||||
|
||||
**Build — lifecycle/teardown contract (Model A1, per-contribution dispose):**
|
||||
|
||||
- During the `./index` factory call, intercept the public registrars and collect
|
||||
the returned `Disposable`s keyed by extension id.
|
||||
- `deactivateExtension(id)` is the single teardown entrypoint: it fires every
|
||||
collected `Disposable` and removes the extension from the index. A throwing
|
||||
`Disposable` must not block the others (catch per-disposable). Idempotent;
|
||||
unknown id is a no-op.
|
||||
- Trigger semantics to document: **uninstall** → `deactivateExtension(id)`;
|
||||
**disable** → mount filters by `enabledMap`, does NOT fire disposables, re-enable
|
||||
needs no reload; **replace** (singleton) → resolver re-selects, the losing
|
||||
extension is not deactivated. Disposal order is best-effort (registration order),
|
||||
not a contract — consumers must be order-independent.
|
||||
|
||||
**Out of scope:** selective per-type eager loading (not feasible without running the
|
||||
factory); the mount-boundary `ErrorBoundary` (#40440); the settings endpoint and
|
||||
its subscription primitive (#40443); context namespaces (#40444); an async-aware
|
||||
`deactivate(): Promise<void>` — file separately only if a graceful-flush
|
||||
requirement appears.
|
||||
|
||||
**Depends on:** #40440 (`ChatbotMount`, resolver, registry-subscription hook). Soft
|
||||
build-time deps on #40443 (settings subscription) and #40444 (namespaces) — land
|
||||
those first or stub the imports.
|
||||
|
||||
**Done when:** enabled extensions init once at startup behind the flag without
|
||||
gating initial render; the bubble appears reactively on registration;
|
||||
`deactivateExtension(id)` disposes all of an extension's contributions (per-disposable
|
||||
catch, idempotent); load failure toasts without throwing; teardown is verified
|
||||
end-to-end on the reference chatbot (including an abort-registry controller that must
|
||||
still abort on deactivate).
|
||||
|
||||
Base branch: `enxdev/feat/chatbot-frontend-api` (on #40440)
|
||||
|
||||
**External Links:** https://github.com/apache/superset/pull/40441
|
||||
|
||||
---
|
||||
|
||||
## 4. Admin configuration UI
|
||||
|
||||
**Goal:** Let an admin enable/disable the chatbot and, when more than one chatbot is
|
||||
installed, choose which is active.
|
||||
|
||||
**Resolve before building:**
|
||||
|
||||
- Is "disable the chatbot" the existing generic extension-disable, or a
|
||||
chatbot-specific toggle? (Determines the ticket's size — prefer reusing the
|
||||
existing flag.)
|
||||
- Where does the UI live? Default: the existing extensions management surface, not a
|
||||
new page.
|
||||
- How does the "default chatbot" selection persist? Reuse existing extension-state
|
||||
storage or a config value — do not invent a table.
|
||||
- Which permission gates it? Default: the existing Extensions-API write permission.
|
||||
|
||||
**Build:**
|
||||
|
||||
- Enable/disable control that empties the `superset.chatbot` slot when off (no broken
|
||||
placeholder).
|
||||
- (Gated on the singleton-policy decision) A selection control listing candidates via
|
||||
`getViews('superset.chatbot')`, activating the choice through the resolver, falling
|
||||
back to first-to-register when unset.
|
||||
- Switching the active chatbot or disabling it at runtime must dispose the previously
|
||||
active chatbot via its `Disposable` and release its in-flight stream readers (via
|
||||
`AbortController`) — no two bubbles, no leaked readers.
|
||||
|
||||
**Out of scope:** the singleton-policy decision itself; per-page visibility; the
|
||||
resolver implementation (consumed here).
|
||||
|
||||
**Depends on:** #40440 (resolver) for selection; enable/disable does not wait on the
|
||||
policy decision.
|
||||
|
||||
**Done when:** admin can enable/disable (gated by the chosen permission) and the slot
|
||||
empties when off; selection picks the active chatbot with first-to-register fallback;
|
||||
the persistence-mechanism and permission decisions are recorded in the ticket.
|
||||
|
||||
Base branch: `enxdev/chat-prototype`
|
||||
|
||||
**External Links:** https://github.com/apache/superset/pull/40442
|
||||
|
||||
---
|
||||
|
||||
## 5. Permissions
|
||||
|
||||
**Goal:** Guarantee the new page-context surface cannot expose anything the current
|
||||
user can't already access through Superset's standard security model. Chatbot
|
||||
extensions fetch data as any other frontend surface and inherit only the current
|
||||
user's privileges; this ticket covers only the new host → extension context-sharing
|
||||
path.
|
||||
|
||||
**Build:**
|
||||
|
||||
- The page-context namespaces (#40444) must derive entity metadata from the same
|
||||
permission checks that gate the underlying page — not a raw Redux pass-through.
|
||||
- Canonical threat (SIP §2.1): a dashboard the user can view that contains a chart
|
||||
whose dataset they cannot query — that chart's metadata (id, name, datasource,
|
||||
viz type, form_data) must be dropped from the context payload.
|
||||
- Context carries only lightweight semantic data + identifiers that resolve through
|
||||
already-protected APIs; never inline dataset rows or query results.
|
||||
- Filtering applies equally to the initial read and every change-notification
|
||||
payload. A chatbot an admin has disabled receives no context at all.
|
||||
|
||||
**Out of scope:** REST API authorization, RBAC, RLS (already enforced by Superset);
|
||||
LLM/backend auth; the singleton selection policy. The chatbot authenticates via the
|
||||
user's existing session (cookie + CSRF) — no separate credential is issued.
|
||||
|
||||
**Depends on:** the Spike sizing the new namespaces (the per-getter filtering lands
|
||||
with those getters); the Context-sharing ticket consumes the filtered getters this
|
||||
one specifies.
|
||||
|
||||
**Done when:** context never exposes entities/ids/metadata the user can't access
|
||||
(even via a manually-entered URL); the dashboard payload omits charts whose dataset
|
||||
the user can't query; no inline privileged payloads; filtering covers change events
|
||||
as well as the initial read; a disabled chatbot gets nothing.
|
||||
|
||||
Base branch: `enxdev/chat-prototype`
|
||||
|
||||
**External Links:** https://github.com/apache/superset/pull/40443
|
||||
|
||||
---
|
||||
|
||||
## 6. Context sharing
|
||||
|
||||
**Goal:** Let the chatbot read semantic page context and subscribe to changes through
|
||||
public per-surface core namespaces only — never the host Redux store.
|
||||
|
||||
**Approach:** Deliver context through per-surface namespaces — the existing `sqlLab`
|
||||
namespace plus new `dashboard` / `explore` / `navigation` namespaces that mirror its
|
||||
shape (a state getter + an `Event<T>` change subscription). No new aggregate context
|
||||
API. The new namespaces copy `sqlLab`'s shape but must filter the Redux state they
|
||||
read (the permission filtering itself is specified by #40443).
|
||||
|
||||
**Build:**
|
||||
|
||||
- Route all chatbot page-context reads through one narrow adapter module with a fixed
|
||||
interface — the adapter is the deliverable, not scattered call sites — so swapping
|
||||
to core namespaces is a one-line change.
|
||||
- Back the adapter with `sqlLab` immediately; back the dashboard/explore/navigation
|
||||
portions and wire change notifications through `navigation`'s page-change event once
|
||||
those namespaces ship.
|
||||
|
||||
**Out of scope:** the permission-filtering logic (#40443 + upstream namespace work);
|
||||
designing the namespace API surface (upstream OSS work, sized by the Spike).
|
||||
|
||||
**Depends on:** a Spike to size the new namespaces (state getters + events + the
|
||||
per-getter permission filtering). The namespace _shape_ is settled by the `sqlLab`
|
||||
precedent; the filtering is real design work.
|
||||
|
||||
**Done when:** all context reads go through the single adapter (zero direct Redux
|
||||
imports, greppable); SQL Lab context works today; dashboard/explore context is either
|
||||
delivered or explicitly tracked as OSS-blocked (not faked); change notifications need
|
||||
no polling; no extra host re-renders.
|
||||
|
||||
Base branch: `enxdev/chat-prototype`
|
||||
|
||||
**External Links:** https://github.com/apache/superset/pull/40444
|
||||
|
||||
---
|
||||
|
||||
## 7. Import / delete UI
|
||||
|
||||
**Goal:** Add an actions column to the extensions list with buttons to delete an
|
||||
extension, set-as-default (chatbot extensions only), and import a new extension.
|
||||
|
||||
**Build:**
|
||||
|
||||
- Import an extension bundle, refreshing the list on success.
|
||||
- Delete an installed extension.
|
||||
- A "set as default chatbot" control, shown only for chatbot extensions.
|
||||
|
||||
**Out of scope:** the settings endpoint itself (#40443); the resolver (#40440).
|
||||
|
||||
**Depends on:** #40442/#40443 for the settings + chatbot-selection plumbing.
|
||||
|
||||
**Done when:** an admin can import, delete, and set a default chatbot from the
|
||||
actions column, with the list reflecting changes.
|
||||
|
||||
Base branch: `enxdev/chat-prototype`
|
||||
|
||||
**External Links:** https://github.com/apache/superset/pull/40450
|
||||
|
||||
---
|
||||
|
||||
## ~~8. Fault isolation & error boundaries~~ — CLOSED (no ticket needed)
|
||||
|
||||
The protective fault-isolation mechanisms are **already implemented** across the
|
||||
mount and eager-loading PRs, so no standalone ticket is required:
|
||||
|
||||
- Render/lifecycle throw → host `ErrorBoundary` around the `superset.chatbot` slot
|
||||
(#40440, reinforced by the `ChatbotRenderer` wrapper in #40441).
|
||||
- Bundle-load failure → `.catch()` + danger toast in `ExtensionsLoader` (#40441).
|
||||
- `activate()` throw → host try/catch in `ExtensionsLoader` (#40441).
|
||||
- Escaped async rejection → `unhandledrejection` hook in `ExtensionsStartup` (#40441).
|
||||
- Failed-activation cleanup → driven by `deactivateExtension` (ticket 3 / #40441).
|
||||
|
||||
The host stays safe under every failure class today. The only unbuilt pieces were the
|
||||
**optional** "chatbot failed — Reload page" notification and structured
|
||||
failure-class/telemetry logging — both judged not worth a ticket (the original spec
|
||||
itself marked the reload notification "optional"). File a fresh ticket only if that
|
||||
UX is later wanted.
|
||||
|
||||
(Original link, for reference only: PR #40433 `feat(extensions): adds chatbot P1-P2` —
|
||||
closed/superseded; never a dedicated fault-isolation PR.)
|
||||
|
||||
---
|
||||
|
||||
## Notes on consolidation
|
||||
|
||||
- **Lifecycle/teardown** was a separate ticket pointing at the same PR as **Eager
|
||||
loading** (#40441) — merged into ticket 3 above. (This is the only true duplicate.)
|
||||
- The **Permissions** ticket (#40443) is kept as-is. Note its PR also contains
|
||||
backend settings-persistence code, but the original ticket only ever scoped the
|
||||
permission-safe context surface — so the ticket stays "Permissions" and no
|
||||
persistence ticket is invented.
|
||||
- The **Permissions** ticket previously had a truncated base branch
|
||||
(`enxdev/chat-protot`) — corrected to `enxdev/chat-prototype`.
|
||||
- **Fault isolation** was **closed without a ticket** (see the struck-through section
|
||||
above): its protective mechanisms already shipped in #40440/#40441, and the only
|
||||
unbuilt pieces (the optional "Reload page" notification + structured telemetry
|
||||
logging) were judged not worth a ticket.
|
||||
@@ -24,6 +24,12 @@ assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
### Dataset import validates catalog against the target connection
|
||||
|
||||
Importing a dataset now validates the `catalog` field against the target database connection. When the connection has multi-catalog disabled (`allow_multi_catalog` off) and the dataset's catalog is not the connection's default catalog, the import fails instead of silently persisting the non-default catalog. This matches the validation already enforced on the dataset update path and prevents imported datasets from querying an unintended database.
|
||||
|
||||
If you relied on importing datasets with a non-default catalog, enable "Allow changing catalogs" on the target connection, or set the dataset's catalog to the connection's default before importing.
|
||||
|
||||
### Granular Export Controls
|
||||
|
||||
A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission:
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
# Configuration for docker-compose-light.yml - disables Redis and uses minimal services
|
||||
|
||||
# Import all settings from the main config first
|
||||
import os
|
||||
|
||||
from flask_caching.backends.filesystemcache import FileSystemCache
|
||||
|
||||
from superset_config import * # noqa: F403
|
||||
@@ -36,3 +38,32 @@ THUMBNAIL_CACHE_CONFIG = CACHE_CONFIG
|
||||
|
||||
# Disable Celery entirely for lightweight mode
|
||||
CELERY_CONFIG = None # type: ignore[assignment,misc]
|
||||
|
||||
# Honor SUPERSET_FEATURE_<NAME> env vars on top of any flags inherited from
|
||||
# superset_config. Lets local dev/e2e enable features (e.g. EMBEDDED_SUPERSET)
|
||||
# without editing shipped config files. Only the literal string "true"
|
||||
# (case-insensitive) is treated as enabled — "1"/"yes"/"on" are not, matching
|
||||
# the strict-string convention used elsewhere in Superset's env parsing.
|
||||
FEATURE_FLAGS = {
|
||||
**FEATURE_FLAGS, # noqa: F405
|
||||
**{
|
||||
name[len("SUPERSET_FEATURE_") :]: value.strip().lower() == "true"
|
||||
for name, value in os.environ.items()
|
||||
if name.startswith("SUPERSET_FEATURE_")
|
||||
},
|
||||
}
|
||||
|
||||
if os.environ.get("SUPERSET_FEATURE_EMBEDDED_SUPERSET", "").strip().lower() == "true":
|
||||
# Disable Talisman so /embedded/<uuid> doesn't return X-Frame-Options:SAMEORIGIN.
|
||||
# Without this, browsers refuse to render Superset inside an iframe from a
|
||||
# different origin (i.e. the embedded SDK use case). Production/CI configures
|
||||
# Talisman with explicit `frame-ancestors`; for the lightweight local stack we
|
||||
# just turn it off.
|
||||
TALISMAN_ENABLED = False
|
||||
|
||||
# Guest tokens (used by the embedded SDK) inherit the "Public" role's perms.
|
||||
# Out of the box Public has zero perms, so embedded dashboards immediately fail
|
||||
# their first call (`/api/v1/me/roles/`) with 403. Mirror Public to Gamma —
|
||||
# the standard read-only viewer role — so the embedded flow can authenticate
|
||||
# and load dashboard data in local dev.
|
||||
PUBLIC_ROLE_LIKE = "Gamma"
|
||||
|
||||
@@ -25,9 +25,17 @@
|
||||
command = "yarn install && yarn build"
|
||||
# Output directory (relative to base)
|
||||
publish = "build"
|
||||
# Skip builds when no docs changes (exit 0 = skip, exit 1 = build)
|
||||
# Checks for changes in docs/ and README.md (which gets pulled into docs)
|
||||
ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF -- . ../README.md"
|
||||
# Skip builds when no docs changes (exit 0 = skip, non-zero = build).
|
||||
# Checks for changes in docs/ and README.md (which gets pulled into docs).
|
||||
#
|
||||
# $CACHED_COMMIT_REF is the last *deployed* commit. On a PR's first build it
|
||||
# is empty, so the original `git diff` errored and Netlify fell back to
|
||||
# building -- which is why every PR built a docs preview once even with no
|
||||
# docs changes. When it is empty we instead diff the whole branch against its
|
||||
# merge-base with master, so non-docs PRs are skipped from the very first
|
||||
# build. Subsequent builds (and the master production build) keep the cheaper
|
||||
# incremental $CACHED_COMMIT_REF diff. Any failure exits non-zero -> build.
|
||||
ignore = 'if [ -n "$CACHED_COMMIT_REF" ]; then git diff --quiet "$CACHED_COMMIT_REF" "$COMMIT_REF" -- . ../README.md; else git fetch origin master --depth=100 >/dev/null 2>&1; git diff --quiet "$(git merge-base origin/master "$COMMIT_REF" 2>/dev/null || echo origin/master)" "$COMMIT_REF" -- . ../README.md; fi'
|
||||
|
||||
[build.environment]
|
||||
# Node version matching docs/.nvmrc
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.60.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
@@ -109,8 +109,8 @@
|
||||
"globals": "^17.6.0",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.59.4",
|
||||
"webpack": "^5.107.1"
|
||||
"typescript-eslint": "^8.60.0",
|
||||
"webpack": "^5.107.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -132,7 +132,9 @@
|
||||
"lodash": "4.18.1",
|
||||
"lodash-es": "4.18.1",
|
||||
"yaml": "1.10.3",
|
||||
"uuid": "11.1.1"
|
||||
"uuid": "11.1.1",
|
||||
"serialize-javascript": "7.0.5",
|
||||
"d3-color": "3.1.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
|
||||
}
|
||||
|
||||
199
docs/yarn.lock
199
docs/yarn.lock
@@ -4812,100 +4812,110 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.59.4", "@typescript-eslint/eslint-plugin@^8.59.3":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz#c67bfee32caae9cb587dce1ac59c3bf43b659707"
|
||||
integrity sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==
|
||||
"@typescript-eslint/eslint-plugin@8.60.0", "@typescript-eslint/eslint-plugin@^8.59.3":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz#8fc1e0a950c43270eaf0212dc060f7edaa42f9cf"
|
||||
integrity sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@typescript-eslint/scope-manager" "8.59.4"
|
||||
"@typescript-eslint/type-utils" "8.59.4"
|
||||
"@typescript-eslint/utils" "8.59.4"
|
||||
"@typescript-eslint/visitor-keys" "8.59.4"
|
||||
"@typescript-eslint/scope-manager" "8.60.0"
|
||||
"@typescript-eslint/type-utils" "8.60.0"
|
||||
"@typescript-eslint/utils" "8.60.0"
|
||||
"@typescript-eslint/visitor-keys" "8.60.0"
|
||||
ignore "^7.0.5"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/parser@8.59.4", "@typescript-eslint/parser@^8.59.3":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.4.tgz#77d99e3b27663e7a22cf12c3fb769db509e5e93c"
|
||||
integrity sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==
|
||||
"@typescript-eslint/parser@8.60.0", "@typescript-eslint/parser@^8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.60.0.tgz#38d611b8e658cb10850d4975e8a175a222fbcd6a"
|
||||
integrity sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.59.4"
|
||||
"@typescript-eslint/types" "8.59.4"
|
||||
"@typescript-eslint/typescript-estree" "8.59.4"
|
||||
"@typescript-eslint/visitor-keys" "8.59.4"
|
||||
"@typescript-eslint/scope-manager" "8.60.0"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/typescript-estree" "8.60.0"
|
||||
"@typescript-eslint/visitor-keys" "8.60.0"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/project-service@8.59.4":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.4.tgz#5830535a0e7a3ae806e2669964f47a74c4bc6b0e"
|
||||
integrity sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==
|
||||
"@typescript-eslint/project-service@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.60.0.tgz#b82ab12e64d005d0c7163d1240c432381f1bde0f"
|
||||
integrity sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.59.4"
|
||||
"@typescript-eslint/types" "^8.59.4"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.60.0"
|
||||
"@typescript-eslint/types" "^8.60.0"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.59.4":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz#507d1258c758147dac1adee9517a205a8ac1e046"
|
||||
integrity sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==
|
||||
"@typescript-eslint/scope-manager@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz#7617a4617c043fe235dcf066f9a40f106cfd2fd5"
|
||||
integrity sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.59.4"
|
||||
"@typescript-eslint/visitor-keys" "8.59.4"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/visitor-keys" "8.60.0"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.59.4", "@typescript-eslint/tsconfig-utils@^8.59.4":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz#218ba229d96dde35212e3a76a7d0a6bc831398be"
|
||||
integrity sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==
|
||||
"@typescript-eslint/tsconfig-utils@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz#3af78c48956227a407dea9626b8db8ca53f130d2"
|
||||
integrity sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==
|
||||
|
||||
"@typescript-eslint/type-utils@8.59.4":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz#359fc53ba39a1f1860fddda40ebe5bfe0d87faed"
|
||||
integrity sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==
|
||||
"@typescript-eslint/tsconfig-utils@^8.60.0":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz#bee8b942a13679a878101c9c74577d732062ed93"
|
||||
integrity sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==
|
||||
|
||||
"@typescript-eslint/type-utils@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz#6971a61bc4f3a1b2df45dcc14e26a43a88a4cb6a"
|
||||
integrity sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.59.4"
|
||||
"@typescript-eslint/typescript-estree" "8.59.4"
|
||||
"@typescript-eslint/utils" "8.59.4"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/typescript-estree" "8.60.0"
|
||||
"@typescript-eslint/utils" "8.60.0"
|
||||
debug "^4.4.3"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/types@8.59.4", "@typescript-eslint/types@^8.59.4":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.4.tgz#c29d5c21bfbaa8347ddc677d3ac1fcd2db0f848e"
|
||||
integrity sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==
|
||||
"@typescript-eslint/types@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.0.tgz#e77ad768e933263b1960b2fe79de75fe1cc6e7db"
|
||||
integrity sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.59.4":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz#d005e5e1fb425526f39685594bed34a04ad755ea"
|
||||
integrity sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==
|
||||
"@typescript-eslint/types@^8.60.0":
|
||||
version "8.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.60.1.tgz#ccdc482ba9e17f9723a10ce240b5e67dad3046c4"
|
||||
integrity sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz#c102196a44414481190041c99eea1d854e66001b"
|
||||
integrity sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.59.4"
|
||||
"@typescript-eslint/tsconfig-utils" "8.59.4"
|
||||
"@typescript-eslint/types" "8.59.4"
|
||||
"@typescript-eslint/visitor-keys" "8.59.4"
|
||||
"@typescript-eslint/project-service" "8.60.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.60.0"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/visitor-keys" "8.60.0"
|
||||
debug "^4.4.3"
|
||||
minimatch "^10.2.2"
|
||||
semver "^7.7.3"
|
||||
tinyglobby "^0.2.15"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/utils@8.59.4":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.4.tgz#8ccd2b08aecc72c7efc0d7ac6695631d199d256e"
|
||||
integrity sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==
|
||||
"@typescript-eslint/utils@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.60.0.tgz#6110cddaef87606ae4ca6f8bf81bb5949fc8e098"
|
||||
integrity sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.9.1"
|
||||
"@typescript-eslint/scope-manager" "8.59.4"
|
||||
"@typescript-eslint/types" "8.59.4"
|
||||
"@typescript-eslint/typescript-estree" "8.59.4"
|
||||
"@typescript-eslint/scope-manager" "8.60.0"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
"@typescript-eslint/typescript-estree" "8.60.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.59.4":
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz#1ac23b747b011f5cbdb449da97769f6c5f3a9355"
|
||||
integrity sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==
|
||||
"@typescript-eslint/visitor-keys@8.60.0":
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz#f2c41eedd3d7b03b808369fb2e3fb40a93783ec2"
|
||||
integrity sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.59.4"
|
||||
"@typescript-eslint/types" "8.60.0"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
|
||||
"@ungap/structured-clone@^1.0.0":
|
||||
@@ -6538,14 +6548,9 @@ d3-chord@3:
|
||||
dependencies:
|
||||
d3-path "1 - 3"
|
||||
|
||||
"d3-color@1 - 2":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz"
|
||||
integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==
|
||||
|
||||
"d3-color@1 - 3", d3-color@3:
|
||||
"d3-color@1 - 2", "d3-color@1 - 3", d3-color@3, d3-color@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
|
||||
integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
|
||||
|
||||
d3-contour@4:
|
||||
@@ -7253,10 +7258,10 @@ encodeurl@~2.0.0:
|
||||
resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz"
|
||||
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
|
||||
|
||||
enhanced-resolve@^5.21.4:
|
||||
version "5.21.5"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.21.5.tgz#8f80167d009d8f01267ad61035e59fe5c94ac3a6"
|
||||
integrity sha512-mLCNbrQli11K1ySUmuNt4ZUB3OpGIDq4q2vTBTf5cL2lpsRjI9QKqSD0ndjW8FyvcW/Jj46gMe9syyHAsvMa/A==
|
||||
enhanced-resolve@^5.22.0:
|
||||
version "5.22.1"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz#c34bc3f414298496fc244b21bbe316440782da17"
|
||||
integrity sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.3.3"
|
||||
@@ -13362,12 +13367,10 @@ serialize-error@^8.1.0:
|
||||
dependencies:
|
||||
type-fest "^0.20.2"
|
||||
|
||||
serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
|
||||
integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
|
||||
dependencies:
|
||||
randombytes "^2.1.0"
|
||||
serialize-javascript@7.0.5, serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
|
||||
version "7.0.5"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-7.0.5.tgz#c798cc0552ffbb08981914a42a8756e339d0d5b1"
|
||||
integrity sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==
|
||||
|
||||
serve-handler@^6.1.7:
|
||||
version "6.1.7"
|
||||
@@ -14379,15 +14382,15 @@ types-ramda@^0.30.1:
|
||||
dependencies:
|
||||
ts-toolbelt "^9.6.0"
|
||||
|
||||
typescript-eslint@^8.59.4:
|
||||
version "8.59.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.59.4.tgz#834e3b53f4d1a764a985ceb8592c4a95d6a8da7c"
|
||||
integrity sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==
|
||||
typescript-eslint@^8.60.0:
|
||||
version "8.60.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.60.0.tgz#6686fecb1f4f367c0bf0075828e93b7ecacbc62b"
|
||||
integrity sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.59.4"
|
||||
"@typescript-eslint/parser" "8.59.4"
|
||||
"@typescript-eslint/typescript-estree" "8.59.4"
|
||||
"@typescript-eslint/utils" "8.59.4"
|
||||
"@typescript-eslint/eslint-plugin" "8.60.0"
|
||||
"@typescript-eslint/parser" "8.60.0"
|
||||
"@typescript-eslint/typescript-estree" "8.60.0"
|
||||
"@typescript-eslint/utils" "8.60.0"
|
||||
|
||||
typescript@~6.0.3:
|
||||
version "6.0.3"
|
||||
@@ -14937,20 +14940,20 @@ webpack-merge@^6.0.1:
|
||||
flat "^5.0.2"
|
||||
wildcard "^2.0.1"
|
||||
|
||||
webpack-sources@^3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.4.1.tgz#009d110999ebd9fb3a6fa8d32eec6f84d940e65d"
|
||||
integrity sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==
|
||||
webpack-sources@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.5.0.tgz#87bf7f5801a4e985b1f1c92b64b9620a02f76d08"
|
||||
integrity sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==
|
||||
|
||||
webpack-virtual-modules@^0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz"
|
||||
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
|
||||
|
||||
webpack@^5.107.1, webpack@^5.88.1, webpack@^5.95.0:
|
||||
version "5.107.1"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.107.1.tgz#01ad63131b7c413f607cc00a8136f467c1f10af0"
|
||||
integrity sha512-mvdIWxj/H6QsfgDdH9djne3a5dYcmEmtsXGESkypaGN5jXjF/b+9KDlmTDQ2TKlFUeA2fI9Y65kihD30JOdB+Q==
|
||||
webpack@^5.107.2, webpack@^5.88.1, webpack@^5.95.0:
|
||||
version "5.107.2"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.107.2.tgz#dea14dcb177b46b29de15f952f7303691ee2b596"
|
||||
integrity sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==
|
||||
dependencies:
|
||||
"@types/estree" "^1.0.8"
|
||||
"@types/json-schema" "^7.0.15"
|
||||
@@ -14961,7 +14964,7 @@ webpack@^5.107.1, webpack@^5.88.1, webpack@^5.95.0:
|
||||
acorn-import-phases "^1.0.3"
|
||||
browserslist "^4.28.1"
|
||||
chrome-trace-event "^1.0.2"
|
||||
enhanced-resolve "^5.21.4"
|
||||
enhanced-resolve "^5.22.0"
|
||||
es-module-lexer "^2.1.0"
|
||||
eslint-scope "5.1.1"
|
||||
events "^3.2.0"
|
||||
@@ -14974,7 +14977,7 @@ webpack@^5.107.1, webpack@^5.88.1, webpack@^5.95.0:
|
||||
tapable "^2.3.0"
|
||||
terser-webpack-plugin "^5.5.0"
|
||||
watchpack "^2.5.1"
|
||||
webpack-sources "^3.4.1"
|
||||
webpack-sources "^3.5.0"
|
||||
|
||||
webpackbar@^7.0.0:
|
||||
version "7.0.0"
|
||||
|
||||
138
extensions/chat/README.md
Normal file
138
extensions/chat/README.md
Normal file
@@ -0,0 +1,138 @@
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
# Reference Chatbot Extension
|
||||
|
||||
Canonical environment-validation extension for the `superset.chatbot`
|
||||
contribution area. **Not** a product chatbot — there is no LLM, no backend,
|
||||
no persistence. Its purpose is to exercise the extension platform end-to-end:
|
||||
|
||||
- `views.registerView` at `superset.chatbot` (singleton resolution)
|
||||
- Lifecycle activation + a master disposable that tears down everything
|
||||
- `commands.registerCommand` for `core.chatbot__open|close|toggle`
|
||||
- Mock streaming with `AbortController` cancellation on dispose
|
||||
- Defense-in-depth React error boundary inside the panel
|
||||
- A single P3 page-context seam that lights up automatically as the
|
||||
`dashboard` / `explore` / `dataset` / `navigation` namespaces become
|
||||
available at runtime on the host
|
||||
|
||||
It is intended as the reference implementation third-party chatbot extension
|
||||
authors copy. Anything that ships as host-internal (the mount point, the
|
||||
admin picker, the `getActiveChatbot` resolver) is **not** here — see the
|
||||
host side at `superset-frontend/src/components/ChatbotMount/` and
|
||||
`superset-frontend/src/core/chatbot/`.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
extensions/chat/
|
||||
├── extension.json Manifest (app.chatbot view + commands)
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── webpack.config.js ModuleFederation → window.superset
|
||||
├── jest.config.js Self-contained unit tests
|
||||
└── src/
|
||||
├── index.tsx MF entry — calls activate() once
|
||||
├── activate.ts Returns master disposable
|
||||
├── commands.ts core.chatbot__open|close|toggle
|
||||
├── state.ts Module-scoped open/closed + emitter
|
||||
├── ReferenceChatbot.tsx Root component (bubble ↔ panel)
|
||||
├── components/
|
||||
│ ├── Bubble.tsx
|
||||
│ ├── Panel.tsx
|
||||
│ └── ErrorBoundary.tsx
|
||||
├── streaming/
|
||||
│ ├── mockStream.ts AsyncIterable<string> + AbortSignal
|
||||
│ └── registry.ts Cross-component abort tracking
|
||||
├── context/
|
||||
│ └── pageContext.ts P3 namespace seam (defensive)
|
||||
└── __tests__/
|
||||
├── sdkMock.ts In-memory @apache-superset/core mock
|
||||
└── activate.test.tsx
|
||||
```
|
||||
|
||||
## Run the unit tests
|
||||
|
||||
```bash
|
||||
cd extensions/chat
|
||||
npm install # first time only
|
||||
npx jest
|
||||
```
|
||||
|
||||
The tests mock `@apache-superset/core` via `src/__tests__/sdkMock.ts` so they
|
||||
do not depend on host runtime wiring.
|
||||
|
||||
## Build / bundle for deployment
|
||||
|
||||
```bash
|
||||
# from the extension folder
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# packaging into a .supx is handled by the Superset extensions CLI
|
||||
pip install apache-superset-extensions-cli
|
||||
superset-extensions bundle # produces apache-superset.reference-chatbot-0.1.0.supx
|
||||
```
|
||||
|
||||
Drop the `.supx` into the `EXTENSIONS_PATH` of a Superset instance that has
|
||||
`FEATURE_FLAGS = { "ENABLE_EXTENSIONS": True }`.
|
||||
|
||||
## Selecting it as the active chatbot
|
||||
|
||||
The host's singleton picker reads `active_chatbot_id` from the admin
|
||||
settings endpoint (`/api/v1/extensions/settings`). Set it to:
|
||||
|
||||
```
|
||||
apache-superset.reference-chatbot
|
||||
```
|
||||
|
||||
If no admin selection exists, the host falls back to the first-to-register
|
||||
chatbot — installing this extension alone is enough for the bubble to appear.
|
||||
|
||||
## P3 integration seams
|
||||
|
||||
All page-context derivation lives in [`src/context/pageContext.ts`](src/context/pageContext.ts).
|
||||
Each namespace branch (`dashboard`, `explore`, `dataset`, `navigation`) is
|
||||
called defensively — when the host implementation lands, the returned value
|
||||
becomes non-undefined automatically with no other change in the extension.
|
||||
|
||||
The panel re-reads context on `popstate`. Once `navigation.onDidChangePage`
|
||||
is live on the host, the panel's `useEffect` should subscribe to it instead;
|
||||
that is the only file in the extension that needs to change for full P3
|
||||
context sync.
|
||||
|
||||
## Known intentional non-features
|
||||
|
||||
- No conversation persistence — by design (extension scope per SIP §2).
|
||||
- No real network. The mock stream is a `setTimeout` token emitter so the
|
||||
cancellation contract is exercised without external dependencies.
|
||||
- No keyboard shortcut binding (Cmd+K). Extensions own that, but it adds
|
||||
surface area not needed for platform validation.
|
||||
- No notification badge / icon mutation. SIP §3.2 recommends static icons;
|
||||
the bubble re-renders freely already.
|
||||
|
||||
## TODOs
|
||||
|
||||
- **P1**: if/when the host gains `deactivate(): Promise<void>`, wrap the
|
||||
master disposer in `activate.ts` to flush async work before returning.
|
||||
- **P3**: replace the `popstate` listener in `Panel.tsx` with
|
||||
`navigation.onDidChangePage` once that event is wired up host-side.
|
||||
- **P4**: if the host pre-registers `core.chatbot__*` as host-owned intents,
|
||||
swap `commands.registerCommand` for the implementation hook in
|
||||
`commands.ts`. Command IDs do not change.
|
||||
40
extensions/chat/extension.json
Normal file
40
extensions/chat/extension.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"publisher": "apache-superset",
|
||||
"name": "reference-chatbot",
|
||||
"displayName": "Reference Chatbot",
|
||||
"description": "Canonical environment-validation chatbot extension for the superset.chatbot contribution area. Exercises registration, lifecycle, singleton resolution, commands, fault isolation, and streaming teardown. Not a product chatbot.",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"permissions": [],
|
||||
"contributes": {
|
||||
"views": {
|
||||
"app": {
|
||||
"chatbot": [
|
||||
{
|
||||
"id": "apache-superset.reference-chatbot",
|
||||
"name": "Reference Chatbot",
|
||||
"description": "Validates the chatbot extension environment end-to-end.",
|
||||
"icon": "Bubble"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"id": "core.chatbot__open",
|
||||
"title": "Open chatbot",
|
||||
"description": "Opens the reference chatbot panel."
|
||||
},
|
||||
{
|
||||
"id": "core.chatbot__close",
|
||||
"title": "Close chatbot",
|
||||
"description": "Closes the reference chatbot panel."
|
||||
},
|
||||
{
|
||||
"id": "core.chatbot__toggle",
|
||||
"title": "Toggle chatbot",
|
||||
"description": "Toggles the reference chatbot panel."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
53
extensions/chat/jest.config.js
Normal file
53
extensions/chat/jest.config.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const path = require('path');
|
||||
|
||||
// When run as a standalone package (`cd extensions/chat && npm test`), modules
|
||||
// resolve from this folder's own node_modules. When run from the superset-frontend
|
||||
// workspace (CI, dev convenience), resolve ts-jest there too.
|
||||
const tsJest = (() => {
|
||||
try {
|
||||
require.resolve('ts-jest');
|
||||
return 'ts-jest';
|
||||
} catch {
|
||||
return path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'superset-frontend',
|
||||
'node_modules',
|
||||
'ts-jest',
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
module.exports = {
|
||||
testEnvironment: 'jsdom',
|
||||
rootDir: __dirname,
|
||||
testMatch: ['<rootDir>/src/**/*.test.{ts,tsx}'],
|
||||
// When running from the extension folder without node_modules installed,
|
||||
// resolve react / react-dom from the superset-frontend workspace.
|
||||
modulePaths: [path.resolve(__dirname, '..', '..', 'superset-frontend', 'node_modules')],
|
||||
moduleNameMapper: {
|
||||
'^@apache-superset/core$': '<rootDir>/src/__tests__/sdkMock.ts',
|
||||
},
|
||||
transform: {
|
||||
'^.+\\.tsx?$': [tsJest, { tsconfig: '<rootDir>/tsconfig.test.json' }],
|
||||
},
|
||||
};
|
||||
26
extensions/chat/package.json
Normal file
26
extensions/chat/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@apache-superset/reference-chatbot",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "Reference chatbot extension that validates the Superset chatbot extension platform.",
|
||||
"scripts": {
|
||||
"start": "webpack serve --mode development",
|
||||
"build": "webpack --stats-error-details --mode production"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "^0.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apache-superset/core": "^0.1.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"ts-loader": "^9.5.0",
|
||||
"typescript": "^5.0.0",
|
||||
"webpack": "^5.0.0",
|
||||
"webpack-cli": "^5.0.0",
|
||||
"webpack-dev-server": "^5.0.0"
|
||||
}
|
||||
}
|
||||
45
extensions/chat/src/ReferenceChatbot.tsx
Normal file
45
extensions/chat/src/ReferenceChatbot.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import { commands } from '@apache-superset/core';
|
||||
import { Bubble } from './components/Bubble';
|
||||
import { Panel } from './components/Panel';
|
||||
import { ExtensionErrorBoundary } from './components/ErrorBoundary';
|
||||
import { isOpen, subscribe } from './state';
|
||||
|
||||
/**
|
||||
* Root extension component. Mirrors module-state into React via `subscribe`
|
||||
* so the bubble↔panel transition is driven by the same command handlers
|
||||
* that external callers use (`core.chatbot__open`, `__close`, `__toggle`).
|
||||
*/
|
||||
export const ReferenceChatbot: React.FC = () => {
|
||||
const [open, setOpenState] = useState<boolean>(isOpen());
|
||||
|
||||
useEffect(() => subscribe(setOpenState), []);
|
||||
|
||||
return (
|
||||
<ExtensionErrorBoundary>
|
||||
{open ? (
|
||||
<Panel onClose={() => commands.executeCommand('core.chatbot__close')} />
|
||||
) : (
|
||||
<Bubble onClick={() => commands.executeCommand('core.chatbot__open')} />
|
||||
)}
|
||||
</ExtensionErrorBoundary>
|
||||
);
|
||||
};
|
||||
144
extensions/chat/src/__tests__/activate.test.tsx
Normal file
144
extensions/chat/src/__tests__/activate.test.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 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 { commands } from '@apache-superset/core';
|
||||
import { registry, reset } from './sdkMock';
|
||||
import { activate, VIEW_ID, CHATBOT_LOCATION } from '../activate';
|
||||
import { isOpen } from '../state';
|
||||
import { streamReply } from '../streaming/mockStream';
|
||||
import {
|
||||
registerActiveController,
|
||||
unregisterActiveController,
|
||||
abortAllActiveControllers,
|
||||
} from '../streaming/registry';
|
||||
|
||||
beforeEach(() => {
|
||||
reset();
|
||||
});
|
||||
|
||||
test('registers one view at superset.chatbot and three chatbot commands', () => {
|
||||
const disposable = activate();
|
||||
|
||||
try {
|
||||
expect(registry.views.size).toBe(1);
|
||||
const entry = registry.views.get(VIEW_ID);
|
||||
expect(entry?.location).toBe(CHATBOT_LOCATION);
|
||||
expect(entry?.view.icon).toBe('Bubble');
|
||||
|
||||
expect(Array.from(registry.commands.keys()).sort()).toEqual([
|
||||
'core.chatbot__close',
|
||||
'core.chatbot__open',
|
||||
'core.chatbot__toggle',
|
||||
]);
|
||||
} finally {
|
||||
disposable.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('executeCommand drives open/close/toggle through module state', async () => {
|
||||
const disposable = activate();
|
||||
try {
|
||||
expect(isOpen()).toBe(false);
|
||||
|
||||
await commands.executeCommand('core.chatbot__open');
|
||||
expect(isOpen()).toBe(true);
|
||||
|
||||
await commands.executeCommand('core.chatbot__toggle');
|
||||
expect(isOpen()).toBe(false);
|
||||
|
||||
await commands.executeCommand('core.chatbot__toggle');
|
||||
expect(isOpen()).toBe(true);
|
||||
|
||||
await commands.executeCommand('core.chatbot__close');
|
||||
expect(isOpen()).toBe(false);
|
||||
} finally {
|
||||
disposable.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('disposing the master disposable unregisters view + commands', () => {
|
||||
const disposable = activate();
|
||||
expect(registry.views.size).toBe(1);
|
||||
expect(registry.commands.size).toBe(3);
|
||||
|
||||
disposable.dispose();
|
||||
|
||||
expect(registry.views.size).toBe(0);
|
||||
expect(registry.commands.size).toBe(0);
|
||||
});
|
||||
|
||||
test('disposal is idempotent', () => {
|
||||
const disposable = activate();
|
||||
disposable.dispose();
|
||||
expect(() => disposable.dispose()).not.toThrow();
|
||||
expect(registry.views.size).toBe(0);
|
||||
});
|
||||
|
||||
test('re-activate after dispose works (validates replace semantics)', () => {
|
||||
const first = activate();
|
||||
first.dispose();
|
||||
|
||||
const second = activate();
|
||||
try {
|
||||
expect(registry.views.size).toBe(1);
|
||||
expect(registry.commands.size).toBe(3);
|
||||
expect(isOpen()).toBe(false); // resetState() cleared open flag
|
||||
} finally {
|
||||
second.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('aborting an active controller stops the stream cleanly', async () => {
|
||||
const controller = new AbortController();
|
||||
registerActiveController(controller);
|
||||
|
||||
const iter = streamReply('hello world', controller.signal);
|
||||
const received: string[] = [];
|
||||
|
||||
const consume = (async () => {
|
||||
for await (const tok of iter) received.push(tok);
|
||||
})();
|
||||
|
||||
// Abort after a single tick — the iterator must return without throwing.
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
abortAllActiveControllers();
|
||||
await expect(consume).resolves.toBeUndefined();
|
||||
|
||||
unregisterActiveController(controller);
|
||||
expect(received.length).toBeLessThan(20); // would be ~20+ tokens if uncancelled
|
||||
});
|
||||
|
||||
test('disposing the extension aborts any in-flight controller', async () => {
|
||||
const disposable = activate();
|
||||
const controller = new AbortController();
|
||||
registerActiveController(controller);
|
||||
|
||||
const iter = streamReply('a longer prompt to ensure many tokens', controller.signal);
|
||||
const consume = (async () => {
|
||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
for await (const _tok of iter) {
|
||||
// drain
|
||||
}
|
||||
})();
|
||||
|
||||
await new Promise(r => setTimeout(r, 30));
|
||||
disposable.dispose();
|
||||
|
||||
await expect(consume).resolves.toBeUndefined();
|
||||
expect(controller.signal.aborted).toBe(true);
|
||||
});
|
||||
119
extensions/chat/src/__tests__/sdkMock.ts
Normal file
119
extensions/chat/src/__tests__/sdkMock.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* In-memory mock of `@apache-superset/core` for unit-testing the extension.
|
||||
*
|
||||
* Mirrors only the surfaces the reference chatbot consumes:
|
||||
* - views.registerView returns a disposable that removes the view
|
||||
* - commands.registerCommand / executeCommand round-trip handlers
|
||||
* - sqlLab.getCurrentTab returns undefined (no SQL Lab in tests)
|
||||
*
|
||||
* The mock is intentionally observable: tests can read `registry.views` and
|
||||
* `registry.commands` to assert contract compliance.
|
||||
*/
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
type Provider = () => ReactElement;
|
||||
|
||||
interface ViewDescriptor {
|
||||
id: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface DisposableLike {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
interface RegisteredView {
|
||||
view: ViewDescriptor;
|
||||
location: string;
|
||||
provider: Provider;
|
||||
}
|
||||
|
||||
interface RegisteredCommand {
|
||||
id: string;
|
||||
title: string;
|
||||
handler: (...args: any[]) => any;
|
||||
}
|
||||
|
||||
export const registry = {
|
||||
views: new Map<string, RegisteredView>(),
|
||||
commands: new Map<string, RegisteredCommand>(),
|
||||
};
|
||||
|
||||
export const reset = (): void => {
|
||||
registry.views.clear();
|
||||
registry.commands.clear();
|
||||
};
|
||||
|
||||
export const views = {
|
||||
registerView(
|
||||
view: ViewDescriptor,
|
||||
location: string,
|
||||
provider: Provider,
|
||||
): DisposableLike {
|
||||
registry.views.set(view.id, { view, location, provider });
|
||||
return {
|
||||
dispose: () => {
|
||||
registry.views.delete(view.id);
|
||||
},
|
||||
};
|
||||
},
|
||||
getViews(location: string) {
|
||||
return Array.from(registry.views.values())
|
||||
.filter(v => v.location === location)
|
||||
.map(v => v.view);
|
||||
},
|
||||
};
|
||||
|
||||
export const commands = {
|
||||
registerCommand(
|
||||
command: { id: string; title: string },
|
||||
handler: (...args: any[]) => any,
|
||||
): DisposableLike {
|
||||
registry.commands.set(command.id, {
|
||||
id: command.id,
|
||||
title: command.title,
|
||||
handler,
|
||||
});
|
||||
return {
|
||||
dispose: () => {
|
||||
registry.commands.delete(command.id);
|
||||
},
|
||||
};
|
||||
},
|
||||
async executeCommand(id: string, ...rest: any[]): Promise<unknown> {
|
||||
const cmd = registry.commands.get(id);
|
||||
return cmd?.handler(...rest);
|
||||
},
|
||||
getCommands() {
|
||||
return Array.from(registry.commands.values()).map(c => ({
|
||||
id: c.id,
|
||||
title: c.title,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
export const sqlLab = {
|
||||
getCurrentTab: () => undefined,
|
||||
};
|
||||
88
extensions/chat/src/activate.ts
Normal file
88
extensions/chat/src/activate.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { views } from '@apache-superset/core';
|
||||
import { ReferenceChatbot } from './ReferenceChatbot';
|
||||
import { registerChatbotCommands } from './commands';
|
||||
import { abortAllActiveControllers } from './streaming/registry';
|
||||
import { resetState } from './state';
|
||||
|
||||
export const VIEW_ID = 'apache-superset.reference-chatbot';
|
||||
export const CHATBOT_LOCATION = 'superset.chatbot';
|
||||
|
||||
interface DisposableLike {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the reference chatbot and returns a single disposable that
|
||||
* tears down everything it created. Idempotent across activate/dispose cycles.
|
||||
*
|
||||
* Cleanup order matters: stop in-flight streams first so listeners do not
|
||||
* receive late tokens, then unregister commands (so user clicks during teardown
|
||||
* become no-ops), then unregister the view (so the host's ChatbotMount unmounts
|
||||
* the React tree), and finally reset module state.
|
||||
*
|
||||
* Returns a plain `{ dispose }` object rather than constructing a Disposable
|
||||
* from the SDK — the SDK class is host-injected and only reliably available
|
||||
* via window.superset at runtime, while plain disposable-likes work in both
|
||||
* runtime and unit-test contexts.
|
||||
*
|
||||
* TODO(P1): when the host gains an async `deactivate(): Promise<void>` hook,
|
||||
* wrap the master disposer to flush in-flight async work before returning.
|
||||
*/
|
||||
export const activate = (): DisposableLike => {
|
||||
const commandDisposables = registerChatbotCommands();
|
||||
const viewDisposable = views.registerView(
|
||||
{
|
||||
id: VIEW_ID,
|
||||
name: 'Reference Chatbot',
|
||||
icon: 'Bubble',
|
||||
description: 'Validates the chatbot extension environment end-to-end.',
|
||||
},
|
||||
CHATBOT_LOCATION,
|
||||
() => React.createElement(ReferenceChatbot),
|
||||
);
|
||||
|
||||
let disposed = false;
|
||||
return {
|
||||
dispose() {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
try {
|
||||
abortAllActiveControllers();
|
||||
} catch {
|
||||
// streams are best-effort during teardown
|
||||
}
|
||||
commandDisposables.forEach(d => {
|
||||
try {
|
||||
d.dispose();
|
||||
} catch {
|
||||
// a single command failing to unregister must not block the rest
|
||||
}
|
||||
});
|
||||
try {
|
||||
viewDisposable.dispose();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
resetState();
|
||||
},
|
||||
};
|
||||
};
|
||||
47
extensions/chat/src/commands.ts
Normal file
47
extensions/chat/src/commands.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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 { commands } from '@apache-superset/core';
|
||||
import { isOpen, setOpen } from './state';
|
||||
|
||||
interface DisposableLike {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the three chatbot intent commands and returns their disposables.
|
||||
*
|
||||
* TODO(P4): if/when the host pre-registers `core.chatbot__*` as host-owned
|
||||
* intents that extensions implement instead of own, swap registerCommand for
|
||||
* the implementation hook. The command ids stay the same so call sites do not
|
||||
* change.
|
||||
*/
|
||||
export const registerChatbotCommands = (): DisposableLike[] => [
|
||||
commands.registerCommand(
|
||||
{ id: 'core.chatbot__open', title: 'Open chatbot' },
|
||||
() => setOpen(true),
|
||||
),
|
||||
commands.registerCommand(
|
||||
{ id: 'core.chatbot__close', title: 'Close chatbot' },
|
||||
() => setOpen(false),
|
||||
),
|
||||
commands.registerCommand(
|
||||
{ id: 'core.chatbot__toggle', title: 'Toggle chatbot' },
|
||||
() => setOpen(!isOpen()),
|
||||
),
|
||||
];
|
||||
46
extensions/chat/src/components/Bubble.tsx
Normal file
46
extensions/chat/src/components/Bubble.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 from 'react';
|
||||
|
||||
interface Props {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const Bubble: React.FC<Props> = ({ onClick }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
data-test="reference-chatbot-bubble"
|
||||
aria-label="Open reference chatbot"
|
||||
style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: '50%',
|
||||
border: 'none',
|
||||
background: '#1f6feb',
|
||||
color: '#fff',
|
||||
fontSize: 24,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 4px 14px rgba(0,0,0,0.18)',
|
||||
}}
|
||||
>
|
||||
?
|
||||
</button>
|
||||
);
|
||||
66
extensions/chat/src/components/ErrorBoundary.tsx
Normal file
66
extensions/chat/src/components/ErrorBoundary.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 React from 'react';
|
||||
|
||||
interface State {
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defense-in-depth boundary. The host already wraps the mount in its own
|
||||
* ErrorBoundary; this one keeps a panel crash from also bringing down the
|
||||
* bubble next to it.
|
||||
*/
|
||||
export class ExtensionErrorBoundary extends React.Component<
|
||||
React.PropsWithChildren<{}>,
|
||||
State
|
||||
> {
|
||||
state: State = { error: null };
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error): void {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[reference-chatbot] render error', error);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<div
|
||||
data-test="reference-chatbot-error"
|
||||
style={{
|
||||
padding: 12,
|
||||
border: '1px solid #f5222d',
|
||||
borderRadius: 6,
|
||||
background: '#fff1f0',
|
||||
color: '#a8071a',
|
||||
fontSize: 12,
|
||||
maxWidth: 320,
|
||||
}}
|
||||
>
|
||||
Reference chatbot crashed: {this.state.error.message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <>{this.props.children}</>;
|
||||
}
|
||||
}
|
||||
307
extensions/chat/src/components/Panel.tsx
Normal file
307
extensions/chat/src/components/Panel.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* 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, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { streamReply } from '../streaming/mockStream';
|
||||
import { getPageContext, PageContext, subscribeToPageChanges } from '../context/pageContext';
|
||||
import { registerActiveController, unregisterActiveController } from '../streaming/registry';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
from: 'user' | 'bot';
|
||||
text: string;
|
||||
}
|
||||
|
||||
let messageSeq = 0;
|
||||
|
||||
/**
|
||||
* Builds the full set of context fields the host exposes for the current
|
||||
* surface, as ordered [label, value] rows. Whatever the host provides for where
|
||||
* the user is, the panel shows — nothing is summarized away. Returns an empty
|
||||
* array for surfaces with no active entity (list/home pages), where the
|
||||
* `page:` line alone is the context.
|
||||
*/
|
||||
const contextRows = (ctx: PageContext): Array<[string, string]> => {
|
||||
const rows: Array<[string, string]> = [];
|
||||
const push = (label: string, value: unknown) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
rows.push([label, String(value)]);
|
||||
}
|
||||
};
|
||||
|
||||
const chart = ctx.chart as
|
||||
| {
|
||||
chartId?: number | null;
|
||||
chartName?: string | null;
|
||||
vizType?: string;
|
||||
datasourceId?: number | null;
|
||||
datasourceName?: string | null;
|
||||
}
|
||||
| undefined;
|
||||
if (chart) {
|
||||
push('chart', chart.chartName ?? (chart.chartId == null ? '(unsaved)' : ''));
|
||||
push('chartId', chart.chartId);
|
||||
push('viz', chart.vizType);
|
||||
push('datasource', chart.datasourceName);
|
||||
push('datasourceId', chart.datasourceId);
|
||||
}
|
||||
|
||||
const dashboard = ctx.dashboard as
|
||||
| { dashboardId?: number; title?: string; filters?: Array<{ label: string; value: unknown }> }
|
||||
| undefined;
|
||||
if (dashboard) {
|
||||
push('dashboard', dashboard.title);
|
||||
push('dashboardId', dashboard.dashboardId);
|
||||
const filters = dashboard.filters ?? [];
|
||||
if (filters.length) {
|
||||
push(
|
||||
'filters',
|
||||
filters.map(f => `${f.label}=${JSON.stringify(f.value)}`).join(', '),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const dataset = ctx.dataset as
|
||||
| {
|
||||
datasetId?: number;
|
||||
datasetName?: string;
|
||||
schema?: string | null;
|
||||
catalog?: string | null;
|
||||
databaseName?: string | null;
|
||||
isVirtual?: boolean;
|
||||
}
|
||||
| undefined;
|
||||
if (dataset) {
|
||||
push('dataset', dataset.datasetName);
|
||||
push('datasetId', dataset.datasetId);
|
||||
push('schema', dataset.schema);
|
||||
push('catalog', dataset.catalog);
|
||||
push('database', dataset.databaseName);
|
||||
if (typeof dataset.isVirtual === 'boolean') {
|
||||
push('virtual', dataset.isVirtual);
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.sqlLab) {
|
||||
push('tab', ctx.sqlLab.title);
|
||||
}
|
||||
|
||||
return rows;
|
||||
};
|
||||
|
||||
export const Panel: React.FC<Props> = ({ onClose }) => {
|
||||
const [input, setInput] = useState('');
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [streaming, setStreaming] = useState(false);
|
||||
const [pageContext, setPageContext] = useState<PageContext>(() => getPageContext());
|
||||
const controllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
useEffect(
|
||||
() => subscribeToPageChanges(() => setPageContext(getPageContext())),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
// Component unmount cancels any in-flight stream.
|
||||
controllerRef.current?.abort();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const send = useCallback(async () => {
|
||||
const prompt = input.trim();
|
||||
if (!prompt || streaming) return;
|
||||
setInput('');
|
||||
const userMsg: Message = { id: ++messageSeq, from: 'user', text: prompt };
|
||||
const botMsg: Message = { id: ++messageSeq, from: 'bot', text: '' };
|
||||
setMessages(prev => [...prev, userMsg, botMsg]);
|
||||
setStreaming(true);
|
||||
|
||||
const controller = new AbortController();
|
||||
controllerRef.current = controller;
|
||||
registerActiveController(controller);
|
||||
|
||||
try {
|
||||
for await (const token of streamReply(prompt, controller.signal)) {
|
||||
setMessages(prev =>
|
||||
prev.map(m => (m.id === botMsg.id ? { ...m, text: m.text + token } : m)),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
unregisterActiveController(controller);
|
||||
controllerRef.current = null;
|
||||
setStreaming(false);
|
||||
}
|
||||
}, [input, streaming]);
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
controllerRef.current?.abort();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-test="reference-chatbot-panel"
|
||||
style={{
|
||||
width: 360,
|
||||
maxHeight: 480,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: '#fff',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 8px 24px rgba(0,0,0,0.18)',
|
||||
overflow: 'hidden',
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
<header
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
background: '#1f6feb',
|
||||
color: '#fff',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<span>Reference Chatbot</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Close chatbot"
|
||||
data-test="reference-chatbot-close"
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div
|
||||
data-test="reference-chatbot-context"
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
background: '#f6f8fa',
|
||||
borderBottom: '1px solid #eaecef',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11,
|
||||
color: '#57606a',
|
||||
wordBreak: 'break-all',
|
||||
}}
|
||||
>
|
||||
<div>page: {pageContext.pageType}</div>
|
||||
{contextRows(pageContext).map(([label, value]) => (
|
||||
<div key={label}>
|
||||
{label}: {value}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: 12 }}>
|
||||
{messages.length === 0 && (
|
||||
<p style={{ color: '#8c8c8c' }}>
|
||||
Ask anything — replies are canned tokens streamed by the reference extension.
|
||||
</p>
|
||||
)}
|
||||
{messages.map(m => (
|
||||
<div
|
||||
key={m.id}
|
||||
data-test={`reference-chatbot-msg-${m.from}`}
|
||||
style={{
|
||||
margin: '6px 0',
|
||||
textAlign: m.from === 'user' ? 'right' : 'left',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '4px 8px',
|
||||
borderRadius: 6,
|
||||
background: m.from === 'user' ? '#1f6feb' : '#eef0f3',
|
||||
color: m.from === 'user' ? '#fff' : '#1f2328',
|
||||
maxWidth: '85%',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
{m.text || '…'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<footer
|
||||
style={{
|
||||
padding: 8,
|
||||
borderTop: '1px solid #eaecef',
|
||||
display: 'flex',
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<input
|
||||
aria-label="Chat input"
|
||||
data-test="reference-chatbot-input"
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
send();
|
||||
}
|
||||
}}
|
||||
placeholder="Type a message"
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '4px 8px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
{streaming ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={cancel}
|
||||
data-test="reference-chatbot-cancel"
|
||||
style={{ padding: '4px 10px' }}
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={send}
|
||||
data-test="reference-chatbot-send"
|
||||
disabled={!input.trim()}
|
||||
style={{ padding: '4px 10px' }}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
)}
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
159
extensions/chat/src/context/pageContext.ts
Normal file
159
extensions/chat/src/context/pageContext.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Single integration seam for the P3 namespaces.
|
||||
*
|
||||
* Each surface namespace is consumed via a try/catch — the host may ship a
|
||||
* version where a namespace function is declared but not yet implemented at
|
||||
* runtime, and the reference extension must keep working in that case. As
|
||||
* each namespace lights up on the host, that branch starts returning real
|
||||
* data without any change here.
|
||||
*
|
||||
* Route inference is the fallback when navigation.getPageType() is absent.
|
||||
*/
|
||||
|
||||
import * as core from '@apache-superset/core';
|
||||
|
||||
export type PageType =
|
||||
| 'home'
|
||||
| 'dashboard'
|
||||
| 'dashboard_list'
|
||||
| 'chart'
|
||||
| 'chart_list'
|
||||
| 'sqllab'
|
||||
| 'query_history'
|
||||
| 'saved_queries'
|
||||
| 'dataset'
|
||||
| 'dataset_list'
|
||||
| 'unknown';
|
||||
|
||||
export interface PageContext {
|
||||
pageType: PageType;
|
||||
dashboard?: unknown;
|
||||
chart?: unknown;
|
||||
dataset?: unknown;
|
||||
sqlLab?: { tabId: string; title: string };
|
||||
href: string;
|
||||
}
|
||||
|
||||
const tryCall = <T>(fn: () => T | undefined): T | undefined => {
|
||||
try {
|
||||
return fn();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const inferPageType = (pathname: string): PageType => {
|
||||
if (pathname.startsWith('/sqllab/history')) return 'query_history';
|
||||
if (pathname.startsWith('/savedqueryview/list')) return 'saved_queries';
|
||||
if (pathname.startsWith('/sqllab')) return 'sqllab';
|
||||
if (pathname.startsWith('/dashboard/list')) return 'dashboard_list';
|
||||
if (
|
||||
pathname.startsWith('/superset/dashboard') ||
|
||||
pathname.startsWith('/dashboard')
|
||||
)
|
||||
return 'dashboard';
|
||||
if (pathname.startsWith('/chart/list')) return 'chart_list';
|
||||
if (pathname.startsWith('/explore') || pathname.startsWith('/chart'))
|
||||
return 'chart';
|
||||
if (pathname.startsWith('/tablemodelview/list')) return 'dataset_list';
|
||||
if (pathname.startsWith('/tablemodelview') || pathname.startsWith('/dataset'))
|
||||
return 'dataset';
|
||||
if (pathname === '/' || pathname.startsWith('/superset/welcome'))
|
||||
return 'home';
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
const readSqlLabTab = (): PageContext['sqlLab'] => {
|
||||
const tab = tryCall(() => (core as any).sqlLab?.getCurrentTab?.());
|
||||
return tab ? { tabId: tab.id, title: tab.title } : undefined;
|
||||
};
|
||||
|
||||
const readPageType = (pathname: string): PageType => {
|
||||
const fromNav = tryCall(() => (core as any).navigation?.getPageType?.());
|
||||
return (fromNav as PageType | undefined) ?? inferPageType(pathname);
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribe to page-context changes and invoke `onChange` whenever any part of
|
||||
* the context may have changed. Returns a cleanup function.
|
||||
*
|
||||
* Three classes of change are watched:
|
||||
* - Navigation (`navigation.onDidChangePage`, or `popstate` as a fallback for
|
||||
* hosts without the namespace) — the user moved to a different surface.
|
||||
* - Entity hydration (`explore.onDidChangeChart`, `dashboard.onDidChangeDashboard`,
|
||||
* `dataset.onDidChangeDataset`) — the surface's entity loaded or changed
|
||||
* *after* navigation settled. This matters because a surface (notably Explore)
|
||||
* can finish hydrating several seconds after the route change fires, so a
|
||||
* navigation-only subscription would read empty entity context and never
|
||||
* refresh once the real data arrives.
|
||||
* - In-surface SQL Lab changes (`sqlLab.onDidChangeActiveTab`,
|
||||
* `sqlLab.onDidChangeTabTitle`) — switching or renaming a tab does not change
|
||||
* the route, so without these the panel would keep showing the first tab.
|
||||
*/
|
||||
export const subscribeToPageChanges = (onChange: () => void): (() => void) => {
|
||||
const disposers: Array<() => void> = [];
|
||||
|
||||
const nav = tryCall(() => (core as any).navigation);
|
||||
if (nav?.onDidChangePage) {
|
||||
const sub = nav.onDidChangePage(onChange);
|
||||
disposers.push(() => sub.dispose());
|
||||
} else {
|
||||
window.addEventListener('popstate', onChange);
|
||||
disposers.push(() => window.removeEventListener('popstate', onChange));
|
||||
}
|
||||
|
||||
// Entity-context change events. Each is optional — a host may not implement a
|
||||
// given namespace yet — so subscribe defensively and collect any disposer.
|
||||
const subscribeEntity = (
|
||||
getNamespace: () => any,
|
||||
method: string,
|
||||
): void => {
|
||||
const sub = tryCall(() => getNamespace()?.[method]?.(onChange));
|
||||
if (sub?.dispose) {
|
||||
disposers.push(() => sub.dispose());
|
||||
}
|
||||
};
|
||||
subscribeEntity(() => (core as any).explore, 'onDidChangeChart');
|
||||
subscribeEntity(() => (core as any).dashboard, 'onDidChangeDashboard');
|
||||
subscribeEntity(() => (core as any).dataset, 'onDidChangeDataset');
|
||||
// SQL Lab tab switches/renames happen without a route change.
|
||||
subscribeEntity(() => (core as any).sqlLab, 'onDidChangeActiveTab');
|
||||
subscribeEntity(() => (core as any).sqlLab, 'onDidChangeTabTitle');
|
||||
|
||||
return () => disposers.forEach(dispose => dispose());
|
||||
};
|
||||
|
||||
export const getPageContext = (): PageContext => {
|
||||
const { pathname, href } =
|
||||
typeof window !== 'undefined'
|
||||
? window.location
|
||||
: { pathname: '', href: '' };
|
||||
|
||||
return {
|
||||
pageType: readPageType(pathname),
|
||||
dashboard: tryCall(() => (core as any).dashboard?.getCurrentDashboard?.()),
|
||||
chart: tryCall(() => (core as any).explore?.getCurrentChart?.()),
|
||||
dataset: tryCall(() => (core as any).dataset?.getCurrentDataset?.()),
|
||||
sqlLab: readSqlLabTab(),
|
||||
href,
|
||||
};
|
||||
};
|
||||
30
extensions/chat/src/index.tsx
Normal file
30
extensions/chat/src/index.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module Federation entry. The host loads `./index` and invokes the factory;
|
||||
* the side effect below registers the view + commands. The host's loader
|
||||
* intercepts registerView calls to collect disposables for deactivation, so
|
||||
* returning the master Disposable here is also captured by the test harness
|
||||
* for direct assertion.
|
||||
*/
|
||||
|
||||
import { activate } from './activate';
|
||||
|
||||
export const disposable = activate();
|
||||
57
extensions/chat/src/state.ts
Normal file
57
extensions/chat/src/state.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module-scoped open/closed state plus a tiny emitter the UI subscribes to.
|
||||
*
|
||||
* Lives entirely inside the extension — never reaches into the host store.
|
||||
* Reset on dispose so re-activation starts cleanly.
|
||||
*/
|
||||
|
||||
export type OpenStateListener = (open: boolean) => void;
|
||||
|
||||
let open = false;
|
||||
const listeners = new Set<OpenStateListener>();
|
||||
|
||||
export const isOpen = (): boolean => open;
|
||||
|
||||
export const setOpen = (next: boolean): void => {
|
||||
if (next === open) return;
|
||||
open = next;
|
||||
listeners.forEach(fn => {
|
||||
try {
|
||||
fn(open);
|
||||
} catch {
|
||||
// A listener throwing must not block other listeners or flip our state back.
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const subscribe = (fn: OpenStateListener): (() => void) => {
|
||||
listeners.add(fn);
|
||||
return () => {
|
||||
listeners.delete(fn);
|
||||
};
|
||||
};
|
||||
|
||||
/** Drains listeners and resets state. Called from the master Disposable. */
|
||||
export const resetState = (): void => {
|
||||
open = false;
|
||||
listeners.clear();
|
||||
};
|
||||
73
extensions/chat/src/streaming/mockStream.ts
Normal file
73
extensions/chat/src/streaming/mockStream.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mock streaming reply used to validate stream teardown semantics.
|
||||
*
|
||||
* The reference chatbot is environment-validation only — there is no LLM.
|
||||
* This iterator yields canned tokens on a timer and exits cleanly when its
|
||||
* AbortSignal is fired. Disposal of the extension aborts any in-flight
|
||||
* controller, which is the contract that proves async cancellation works.
|
||||
*/
|
||||
|
||||
const TICK_MS = 40;
|
||||
|
||||
const buildReply = (prompt: string): string => {
|
||||
const trimmed = prompt.trim();
|
||||
if (!trimmed) {
|
||||
return 'Reference chatbot online. Send a message to validate streaming.';
|
||||
}
|
||||
return (
|
||||
`[reference-chatbot] received "${trimmed}". ` +
|
||||
'Streaming token-by-token to validate cancellation and teardown.'
|
||||
);
|
||||
};
|
||||
|
||||
const sleep = (ms: number, signal: AbortSignal): Promise<void> =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (signal.aborted) {
|
||||
reject(new DOMException('aborted', 'AbortError'));
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
signal.removeEventListener('abort', onAbort);
|
||||
resolve();
|
||||
}, ms);
|
||||
const onAbort = () => {
|
||||
clearTimeout(timer);
|
||||
reject(new DOMException('aborted', 'AbortError'));
|
||||
};
|
||||
signal.addEventListener('abort', onAbort, { once: true });
|
||||
});
|
||||
|
||||
export async function* streamReply(
|
||||
prompt: string,
|
||||
signal: AbortSignal,
|
||||
): AsyncIterableIterator<string> {
|
||||
const tokens = buildReply(prompt).split(/(\s+)/);
|
||||
for (const token of tokens) {
|
||||
if (signal.aborted) return;
|
||||
try {
|
||||
await sleep(TICK_MS, signal);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
yield token;
|
||||
}
|
||||
}
|
||||
46
extensions/chat/src/streaming/registry.ts
Normal file
46
extensions/chat/src/streaming/registry.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module-scoped registry of in-flight stream AbortControllers.
|
||||
*
|
||||
* Lets the master Disposable abort any running stream even when the panel
|
||||
* is unmounted by a route change or by re-activation of the extension.
|
||||
*/
|
||||
|
||||
const active = new Set<AbortController>();
|
||||
|
||||
export const registerActiveController = (c: AbortController): void => {
|
||||
active.add(c);
|
||||
};
|
||||
|
||||
export const unregisterActiveController = (c: AbortController): void => {
|
||||
active.delete(c);
|
||||
};
|
||||
|
||||
export const abortAllActiveControllers = (): void => {
|
||||
active.forEach(c => {
|
||||
try {
|
||||
c.abort();
|
||||
} catch {
|
||||
// ignore — abort() should not throw, but stay defensive.
|
||||
}
|
||||
});
|
||||
active.clear();
|
||||
};
|
||||
16
extensions/chat/tsconfig.json
Normal file
16
extensions/chat/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2019",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"lib": ["dom", "es2019"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/__tests__"]
|
||||
}
|
||||
16
extensions/chat/tsconfig.test.json
Normal file
16
extensions/chat/tsconfig.test.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@apache-superset/core": ["src/__tests__/sdkMock.ts"]
|
||||
},
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"../../superset-frontend/node_modules/@types"
|
||||
],
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": []
|
||||
}
|
||||
108
extensions/chat/webpack.config.js
Normal file
108
extensions/chat/webpack.config.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { ModuleFederationPlugin } = require('webpack').container;
|
||||
const packageConfig = require('./package.json');
|
||||
const extensionConfig = require('./extension.json');
|
||||
|
||||
const MODULE_FEDERATION_NAME = 'apacheSuperset_referenceChatbot';
|
||||
|
||||
/**
|
||||
* Emits the `manifest.json` the host reads from the extension `dist/` root.
|
||||
*
|
||||
* The host (`superset/extensions/utils.py`) expects an extension dist laid out
|
||||
* as `dist/manifest.json` plus the federated bundle under `dist/frontend/dist/`.
|
||||
* The manifest carries `extension.json` verbatim, plus the composite `id` and a
|
||||
* `frontend` block naming the content-hashed `remoteEntry` so the host can load
|
||||
* the right file. Because the hash is only known after the build, the manifest
|
||||
* is written from the final asset names rather than checked in.
|
||||
*/
|
||||
class EmitManifestPlugin {
|
||||
apply(compiler) {
|
||||
compiler.hooks.afterEmit.tap('EmitManifestPlugin', compilation => {
|
||||
const assets = Object.keys(compilation.assets);
|
||||
const remoteEntry = assets.find(name => /^remoteEntry\..*\.js$/.test(name));
|
||||
if (!remoteEntry) {
|
||||
throw new Error('EmitManifestPlugin: no remoteEntry asset was emitted');
|
||||
}
|
||||
const manifest = {
|
||||
...extensionConfig,
|
||||
id: `${extensionConfig.publisher}.${extensionConfig.name}`,
|
||||
frontend: {
|
||||
remoteEntry,
|
||||
moduleFederationName: MODULE_FEDERATION_NAME,
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, 'dist', 'manifest.json'),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const isProd = argv.mode === 'production';
|
||||
|
||||
return {
|
||||
entry: isProd ? {} : './src/index.tsx',
|
||||
mode: isProd ? 'production' : 'development',
|
||||
devtool: isProd ? false : 'eval-cheap-module-source-map',
|
||||
devServer: {
|
||||
port: 3030,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
},
|
||||
output: {
|
||||
clean: true,
|
||||
filename: isProd ? undefined : '[name].[contenthash].js',
|
||||
chunkFilename: '[name].[contenthash].js',
|
||||
path: path.resolve(__dirname, 'dist', 'frontend', 'dist'),
|
||||
publicPath: `/api/v1/extensions/${extensionConfig.publisher}/${extensionConfig.name}/`,
|
||||
},
|
||||
resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'] },
|
||||
externalsType: 'window',
|
||||
externals: { '@apache-superset/core': 'superset' },
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ },
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new ModuleFederationPlugin({
|
||||
name: MODULE_FEDERATION_NAME,
|
||||
filename: 'remoteEntry.[contenthash].js',
|
||||
exposes: { './index': './src/index.tsx' },
|
||||
shared: {
|
||||
react: {
|
||||
singleton: true,
|
||||
requiredVersion: packageConfig.peerDependencies.react,
|
||||
import: false,
|
||||
},
|
||||
'react-dom': {
|
||||
singleton: true,
|
||||
requiredVersion: packageConfig.peerDependencies['react-dom'],
|
||||
import: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
new EmitManifestPlugin(),
|
||||
],
|
||||
};
|
||||
};
|
||||
138
extensions/chat2/README.md
Normal file
138
extensions/chat2/README.md
Normal file
@@ -0,0 +1,138 @@
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
# Reference Chatbot Extension
|
||||
|
||||
Canonical environment-validation extension for the `superset.chatbot`
|
||||
contribution area. **Not** a product chatbot — there is no LLM, no backend,
|
||||
no persistence. Its purpose is to exercise the extension platform end-to-end:
|
||||
|
||||
- `views.registerView` at `superset.chatbot` (singleton resolution)
|
||||
- Lifecycle activation + a master disposable that tears down everything
|
||||
- `commands.registerCommand` for `core.chatbot__open|close|toggle`
|
||||
- Mock streaming with `AbortController` cancellation on dispose
|
||||
- Defense-in-depth React error boundary inside the panel
|
||||
- A single P3 page-context seam that lights up automatically as the
|
||||
`dashboard` / `explore` / `dataset` / `navigation` namespaces become
|
||||
available at runtime on the host
|
||||
|
||||
It is intended as the reference implementation third-party chatbot extension
|
||||
authors copy. Anything that ships as host-internal (the mount point, the
|
||||
admin picker, the `getActiveChatbot` resolver) is **not** here — see the
|
||||
host side at `superset-frontend/src/components/ChatbotMount/` and
|
||||
`superset-frontend/src/core/chatbot/`.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
extensions/chat/
|
||||
├── extension.json Manifest (app.chatbot view + commands)
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── webpack.config.js ModuleFederation → window.superset
|
||||
├── jest.config.js Self-contained unit tests
|
||||
└── src/
|
||||
├── index.tsx MF entry — calls activate() once
|
||||
├── activate.ts Returns master disposable
|
||||
├── commands.ts core.chatbot__open|close|toggle
|
||||
├── state.ts Module-scoped open/closed + emitter
|
||||
├── ReferenceChatbot.tsx Root component (bubble ↔ panel)
|
||||
├── components/
|
||||
│ ├── Bubble.tsx
|
||||
│ ├── Panel.tsx
|
||||
│ └── ErrorBoundary.tsx
|
||||
├── streaming/
|
||||
│ ├── mockStream.ts AsyncIterable<string> + AbortSignal
|
||||
│ └── registry.ts Cross-component abort tracking
|
||||
├── context/
|
||||
│ └── pageContext.ts P3 namespace seam (defensive)
|
||||
└── __tests__/
|
||||
├── sdkMock.ts In-memory @apache-superset/core mock
|
||||
└── activate.test.tsx
|
||||
```
|
||||
|
||||
## Run the unit tests
|
||||
|
||||
```bash
|
||||
cd extensions/chat
|
||||
npm install # first time only
|
||||
npx jest
|
||||
```
|
||||
|
||||
The tests mock `@apache-superset/core` via `src/__tests__/sdkMock.ts` so they
|
||||
do not depend on host runtime wiring.
|
||||
|
||||
## Build / bundle for deployment
|
||||
|
||||
```bash
|
||||
# from the extension folder
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# packaging into a .supx is handled by the Superset extensions CLI
|
||||
pip install apache-superset-extensions-cli
|
||||
superset-extensions bundle # produces apache-superset.reference-chatbot-0.1.0.supx
|
||||
```
|
||||
|
||||
Drop the `.supx` into the `EXTENSIONS_PATH` of a Superset instance that has
|
||||
`FEATURE_FLAGS = { "ENABLE_EXTENSIONS": True }`.
|
||||
|
||||
## Selecting it as the active chatbot
|
||||
|
||||
The host's singleton picker reads `active_chatbot_id` from the admin
|
||||
settings endpoint (`/api/v1/extensions/settings`). Set it to:
|
||||
|
||||
```
|
||||
apache-superset.reference-chatbot
|
||||
```
|
||||
|
||||
If no admin selection exists, the host falls back to the first-to-register
|
||||
chatbot — installing this extension alone is enough for the bubble to appear.
|
||||
|
||||
## P3 integration seams
|
||||
|
||||
All page-context derivation lives in [`src/context/pageContext.ts`](src/context/pageContext.ts).
|
||||
Each namespace branch (`dashboard`, `explore`, `dataset`, `navigation`) is
|
||||
called defensively — when the host implementation lands, the returned value
|
||||
becomes non-undefined automatically with no other change in the extension.
|
||||
|
||||
The panel re-reads context on `popstate`. Once `navigation.onDidChangePage`
|
||||
is live on the host, the panel's `useEffect` should subscribe to it instead;
|
||||
that is the only file in the extension that needs to change for full P3
|
||||
context sync.
|
||||
|
||||
## Known intentional non-features
|
||||
|
||||
- No conversation persistence — by design (extension scope per SIP §2).
|
||||
- No real network. The mock stream is a `setTimeout` token emitter so the
|
||||
cancellation contract is exercised without external dependencies.
|
||||
- No keyboard shortcut binding (Cmd+K). Extensions own that, but it adds
|
||||
surface area not needed for platform validation.
|
||||
- No notification badge / icon mutation. SIP §3.2 recommends static icons;
|
||||
the bubble re-renders freely already.
|
||||
|
||||
## TODOs
|
||||
|
||||
- **P1**: if/when the host gains `deactivate(): Promise<void>`, wrap the
|
||||
master disposer in `activate.ts` to flush async work before returning.
|
||||
- **P3**: replace the `popstate` listener in `Panel.tsx` with
|
||||
`navigation.onDidChangePage` once that event is wired up host-side.
|
||||
- **P4**: if the host pre-registers `core.chatbot__*` as host-owned intents,
|
||||
swap `commands.registerCommand` for the implementation hook in
|
||||
`commands.ts`. Command IDs do not change.
|
||||
23
extensions/chat2/extension.json
Normal file
23
extensions/chat2/extension.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"publisher": "apache-superset",
|
||||
"name": "alt-chatbot",
|
||||
"displayName": "Alt Chatbot",
|
||||
"description": "Second chatbot for testing multi-chatbot selection in the superset.chatbot contribution area.",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"permissions": [],
|
||||
"contributes": {
|
||||
"views": {
|
||||
"app": {
|
||||
"chatbot": [
|
||||
{
|
||||
"id": "apache-superset.alt-chatbot",
|
||||
"name": "Alt Chatbot",
|
||||
"description": "Second chatbot for testing singleton resolution.",
|
||||
"icon": "Star"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
extensions/chat2/jest.config.js
Normal file
53
extensions/chat2/jest.config.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const path = require('path');
|
||||
|
||||
// When run as a standalone package (`cd extensions/chat && npm test`), modules
|
||||
// resolve from this folder's own node_modules. When run from the superset-frontend
|
||||
// workspace (CI, dev convenience), resolve ts-jest there too.
|
||||
const tsJest = (() => {
|
||||
try {
|
||||
require.resolve('ts-jest');
|
||||
return 'ts-jest';
|
||||
} catch {
|
||||
return path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'superset-frontend',
|
||||
'node_modules',
|
||||
'ts-jest',
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
module.exports = {
|
||||
testEnvironment: 'jsdom',
|
||||
rootDir: __dirname,
|
||||
testMatch: ['<rootDir>/src/**/*.test.{ts,tsx}'],
|
||||
// When running from the extension folder without node_modules installed,
|
||||
// resolve react / react-dom from the superset-frontend workspace.
|
||||
modulePaths: [path.resolve(__dirname, '..', '..', 'superset-frontend', 'node_modules')],
|
||||
moduleNameMapper: {
|
||||
'^@apache-superset/core$': '<rootDir>/src/__tests__/sdkMock.ts',
|
||||
},
|
||||
transform: {
|
||||
'^.+\\.tsx?$': [tsJest, { tsconfig: '<rootDir>/tsconfig.test.json' }],
|
||||
},
|
||||
};
|
||||
26
extensions/chat2/package.json
Normal file
26
extensions/chat2/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@apache-superset/alt-chatbot",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "Second chatbot extension for testing multi-chatbot selection in the Superset chatbot contribution area.",
|
||||
"scripts": {
|
||||
"start": "webpack serve --mode development",
|
||||
"build": "webpack --stats-error-details --mode production"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "^0.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apache-superset/core": "^0.1.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"ts-loader": "^9.5.0",
|
||||
"typescript": "^5.0.0",
|
||||
"webpack": "^5.0.0",
|
||||
"webpack-cli": "^5.0.0",
|
||||
"webpack-dev-server": "^5.0.0"
|
||||
}
|
||||
}
|
||||
46
extensions/chat2/src/ReferenceChatbot.tsx
Normal file
46
extensions/chat2/src/ReferenceChatbot.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import { Bubble } from './components/Bubble';
|
||||
import { Panel } from './components/Panel';
|
||||
import { ExtensionErrorBoundary } from './components/ErrorBoundary';
|
||||
import { isOpen, setOpen, subscribe } from './state';
|
||||
|
||||
/**
|
||||
* Root extension component. Mirrors module-state into React via `subscribe`.
|
||||
*
|
||||
* Unlike the Reference Chatbot, Alt registers no `core.chatbot__*` commands
|
||||
* (those ids are globally owned by Reference), so the bubble↔panel transition
|
||||
* drives the local open-state directly via `setOpen`.
|
||||
*/
|
||||
export const ReferenceChatbot: React.FC = () => {
|
||||
const [open, setOpenState] = useState<boolean>(isOpen());
|
||||
|
||||
useEffect(() => subscribe(setOpenState), []);
|
||||
|
||||
return (
|
||||
<ExtensionErrorBoundary>
|
||||
{open ? (
|
||||
<Panel onClose={() => setOpen(false)} />
|
||||
) : (
|
||||
<Bubble onClick={() => setOpen(true)} />
|
||||
)}
|
||||
</ExtensionErrorBoundary>
|
||||
);
|
||||
};
|
||||
131
extensions/chat2/src/__tests__/activate.test.tsx
Normal file
131
extensions/chat2/src/__tests__/activate.test.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 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 { registry, reset } from './sdkMock';
|
||||
import { activate, VIEW_ID, CHATBOT_LOCATION } from '../activate';
|
||||
import { isOpen, setOpen } from '../state';
|
||||
import { streamReply } from '../streaming/mockStream';
|
||||
import {
|
||||
registerActiveController,
|
||||
unregisterActiveController,
|
||||
abortAllActiveControllers,
|
||||
} from '../streaming/registry';
|
||||
|
||||
beforeEach(() => {
|
||||
reset();
|
||||
});
|
||||
|
||||
test('registers one view at superset.chatbot and no commands', () => {
|
||||
const disposable = activate();
|
||||
|
||||
try {
|
||||
expect(registry.views.size).toBe(1);
|
||||
const entry = registry.views.get(VIEW_ID);
|
||||
expect(entry?.location).toBe(CHATBOT_LOCATION);
|
||||
expect(entry?.view.icon).toBe('Star');
|
||||
|
||||
// Alt Chatbot is view-only — the core.chatbot__* command ids are owned by
|
||||
// the Reference Chatbot, so Alt registers none of its own.
|
||||
expect(registry.commands.size).toBe(0);
|
||||
} finally {
|
||||
disposable.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('setOpen drives open/close through module state', () => {
|
||||
const disposable = activate();
|
||||
try {
|
||||
expect(isOpen()).toBe(false);
|
||||
setOpen(true);
|
||||
expect(isOpen()).toBe(true);
|
||||
setOpen(false);
|
||||
expect(isOpen()).toBe(false);
|
||||
} finally {
|
||||
disposable.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('disposing the master disposable unregisters the view', () => {
|
||||
const disposable = activate();
|
||||
expect(registry.views.size).toBe(1);
|
||||
|
||||
disposable.dispose();
|
||||
|
||||
expect(registry.views.size).toBe(0);
|
||||
expect(registry.commands.size).toBe(0);
|
||||
});
|
||||
|
||||
test('disposal is idempotent', () => {
|
||||
const disposable = activate();
|
||||
disposable.dispose();
|
||||
expect(() => disposable.dispose()).not.toThrow();
|
||||
expect(registry.views.size).toBe(0);
|
||||
});
|
||||
|
||||
test('re-activate after dispose works (validates replace semantics)', () => {
|
||||
const first = activate();
|
||||
first.dispose();
|
||||
|
||||
const second = activate();
|
||||
try {
|
||||
expect(registry.views.size).toBe(1);
|
||||
expect(isOpen()).toBe(false); // resetState() cleared open flag
|
||||
} finally {
|
||||
second.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('aborting an active controller stops the stream cleanly', async () => {
|
||||
const controller = new AbortController();
|
||||
registerActiveController(controller);
|
||||
|
||||
const iter = streamReply('hello world', controller.signal);
|
||||
const received: string[] = [];
|
||||
|
||||
const consume = (async () => {
|
||||
for await (const tok of iter) received.push(tok);
|
||||
})();
|
||||
|
||||
// Abort after a single tick — the iterator must return without throwing.
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
abortAllActiveControllers();
|
||||
await expect(consume).resolves.toBeUndefined();
|
||||
|
||||
unregisterActiveController(controller);
|
||||
expect(received.length).toBeLessThan(20); // would be ~20+ tokens if uncancelled
|
||||
});
|
||||
|
||||
test('disposing the extension aborts any in-flight controller', async () => {
|
||||
const disposable = activate();
|
||||
const controller = new AbortController();
|
||||
registerActiveController(controller);
|
||||
|
||||
const iter = streamReply('a longer prompt to ensure many tokens', controller.signal);
|
||||
const consume = (async () => {
|
||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
for await (const _tok of iter) {
|
||||
// drain
|
||||
}
|
||||
})();
|
||||
|
||||
await new Promise(r => setTimeout(r, 30));
|
||||
disposable.dispose();
|
||||
|
||||
await expect(consume).resolves.toBeUndefined();
|
||||
expect(controller.signal.aborted).toBe(true);
|
||||
});
|
||||
119
extensions/chat2/src/__tests__/sdkMock.ts
Normal file
119
extensions/chat2/src/__tests__/sdkMock.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* In-memory mock of `@apache-superset/core` for unit-testing the extension.
|
||||
*
|
||||
* Mirrors only the surfaces the reference chatbot consumes:
|
||||
* - views.registerView returns a disposable that removes the view
|
||||
* - commands.registerCommand / executeCommand round-trip handlers
|
||||
* - sqlLab.getCurrentTab returns undefined (no SQL Lab in tests)
|
||||
*
|
||||
* The mock is intentionally observable: tests can read `registry.views` and
|
||||
* `registry.commands` to assert contract compliance.
|
||||
*/
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
type Provider = () => ReactElement;
|
||||
|
||||
interface ViewDescriptor {
|
||||
id: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface DisposableLike {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
interface RegisteredView {
|
||||
view: ViewDescriptor;
|
||||
location: string;
|
||||
provider: Provider;
|
||||
}
|
||||
|
||||
interface RegisteredCommand {
|
||||
id: string;
|
||||
title: string;
|
||||
handler: (...args: any[]) => any;
|
||||
}
|
||||
|
||||
export const registry = {
|
||||
views: new Map<string, RegisteredView>(),
|
||||
commands: new Map<string, RegisteredCommand>(),
|
||||
};
|
||||
|
||||
export const reset = (): void => {
|
||||
registry.views.clear();
|
||||
registry.commands.clear();
|
||||
};
|
||||
|
||||
export const views = {
|
||||
registerView(
|
||||
view: ViewDescriptor,
|
||||
location: string,
|
||||
provider: Provider,
|
||||
): DisposableLike {
|
||||
registry.views.set(view.id, { view, location, provider });
|
||||
return {
|
||||
dispose: () => {
|
||||
registry.views.delete(view.id);
|
||||
},
|
||||
};
|
||||
},
|
||||
getViews(location: string) {
|
||||
return Array.from(registry.views.values())
|
||||
.filter(v => v.location === location)
|
||||
.map(v => v.view);
|
||||
},
|
||||
};
|
||||
|
||||
export const commands = {
|
||||
registerCommand(
|
||||
command: { id: string; title: string },
|
||||
handler: (...args: any[]) => any,
|
||||
): DisposableLike {
|
||||
registry.commands.set(command.id, {
|
||||
id: command.id,
|
||||
title: command.title,
|
||||
handler,
|
||||
});
|
||||
return {
|
||||
dispose: () => {
|
||||
registry.commands.delete(command.id);
|
||||
},
|
||||
};
|
||||
},
|
||||
async executeCommand(id: string, ...rest: any[]): Promise<unknown> {
|
||||
const cmd = registry.commands.get(id);
|
||||
return cmd?.handler(...rest);
|
||||
},
|
||||
getCommands() {
|
||||
return Array.from(registry.commands.values()).map(c => ({
|
||||
id: c.id,
|
||||
title: c.title,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
export const sqlLab = {
|
||||
getCurrentTab: () => undefined,
|
||||
};
|
||||
83
extensions/chat2/src/activate.ts
Normal file
83
extensions/chat2/src/activate.ts
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.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { views } from '@apache-superset/core';
|
||||
import { ReferenceChatbot } from './ReferenceChatbot';
|
||||
import { abortAllActiveControllers } from './streaming/registry';
|
||||
import { resetState } from './state';
|
||||
|
||||
export const VIEW_ID = 'apache-superset.alt-chatbot';
|
||||
export const CHATBOT_LOCATION = 'superset.chatbot';
|
||||
|
||||
interface DisposableLike {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the reference chatbot and returns a single disposable that
|
||||
* tears down everything it created. Idempotent across activate/dispose cycles.
|
||||
*
|
||||
* Cleanup order matters: stop in-flight streams first so listeners do not
|
||||
* receive late tokens, then unregister commands (so user clicks during teardown
|
||||
* become no-ops), then unregister the view (so the host's ChatbotMount unmounts
|
||||
* the React tree), and finally reset module state.
|
||||
*
|
||||
* Returns a plain `{ dispose }` object rather than constructing a Disposable
|
||||
* from the SDK — the SDK class is host-injected and only reliably available
|
||||
* via window.superset at runtime, while plain disposable-likes work in both
|
||||
* runtime and unit-test contexts.
|
||||
*
|
||||
* TODO(P1): when the host gains an async `deactivate(): Promise<void>` hook,
|
||||
* wrap the master disposer to flush in-flight async work before returning.
|
||||
*/
|
||||
export const activate = (): DisposableLike => {
|
||||
// Alt Chatbot deliberately registers no commands: the `core.chatbot__*`
|
||||
// command ids are owned by the Reference Chatbot, and command ids are global,
|
||||
// so a second registrant would collide. Alt is a view-only chatbot used to
|
||||
// exercise multi-chatbot selection.
|
||||
const viewDisposable = views.registerView(
|
||||
{
|
||||
id: VIEW_ID,
|
||||
name: 'Alt Chatbot',
|
||||
icon: 'Star',
|
||||
description: 'Second chatbot for testing singleton resolution.',
|
||||
},
|
||||
CHATBOT_LOCATION,
|
||||
() => React.createElement(ReferenceChatbot),
|
||||
);
|
||||
|
||||
let disposed = false;
|
||||
return {
|
||||
dispose() {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
try {
|
||||
abortAllActiveControllers();
|
||||
} catch {
|
||||
// streams are best-effort during teardown
|
||||
}
|
||||
try {
|
||||
viewDisposable.dispose();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
resetState();
|
||||
},
|
||||
};
|
||||
};
|
||||
46
extensions/chat2/src/components/Bubble.tsx
Normal file
46
extensions/chat2/src/components/Bubble.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 from 'react';
|
||||
|
||||
interface Props {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const Bubble: React.FC<Props> = ({ onClick }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
data-test="reference-chatbot-bubble"
|
||||
aria-label="Open Alt chatbot"
|
||||
style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: '50%',
|
||||
border: 'none',
|
||||
background: '#2da44e',
|
||||
color: '#fff',
|
||||
fontSize: 24,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 4px 14px rgba(0,0,0,0.18)',
|
||||
}}
|
||||
>
|
||||
?
|
||||
</button>
|
||||
);
|
||||
66
extensions/chat2/src/components/ErrorBoundary.tsx
Normal file
66
extensions/chat2/src/components/ErrorBoundary.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 React from 'react';
|
||||
|
||||
interface State {
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defense-in-depth boundary. The host already wraps the mount in its own
|
||||
* ErrorBoundary; this one keeps a panel crash from also bringing down the
|
||||
* bubble next to it.
|
||||
*/
|
||||
export class ExtensionErrorBoundary extends React.Component<
|
||||
React.PropsWithChildren<{}>,
|
||||
State
|
||||
> {
|
||||
state: State = { error: null };
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error): void {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[reference-chatbot] render error', error);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<div
|
||||
data-test="reference-chatbot-error"
|
||||
style={{
|
||||
padding: 12,
|
||||
border: '1px solid #f5222d',
|
||||
borderRadius: 6,
|
||||
background: '#fff1f0',
|
||||
color: '#a8071a',
|
||||
fontSize: 12,
|
||||
maxWidth: 320,
|
||||
}}
|
||||
>
|
||||
Reference chatbot crashed: {this.state.error.message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <>{this.props.children}</>;
|
||||
}
|
||||
}
|
||||
307
extensions/chat2/src/components/Panel.tsx
Normal file
307
extensions/chat2/src/components/Panel.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* 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, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { streamReply } from '../streaming/mockStream';
|
||||
import { getPageContext, PageContext, subscribeToPageChanges } from '../context/pageContext';
|
||||
import { registerActiveController, unregisterActiveController } from '../streaming/registry';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
from: 'user' | 'bot';
|
||||
text: string;
|
||||
}
|
||||
|
||||
let messageSeq = 0;
|
||||
|
||||
/**
|
||||
* Builds the full set of context fields the host exposes for the current
|
||||
* surface, as ordered [label, value] rows. Whatever the host provides for where
|
||||
* the user is, the panel shows — nothing is summarized away. Returns an empty
|
||||
* array for surfaces with no active entity (list/home pages), where the
|
||||
* `page:` line alone is the context.
|
||||
*/
|
||||
const contextRows = (ctx: PageContext): Array<[string, string]> => {
|
||||
const rows: Array<[string, string]> = [];
|
||||
const push = (label: string, value: unknown) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
rows.push([label, String(value)]);
|
||||
}
|
||||
};
|
||||
|
||||
const chart = ctx.chart as
|
||||
| {
|
||||
chartId?: number | null;
|
||||
chartName?: string | null;
|
||||
vizType?: string;
|
||||
datasourceId?: number | null;
|
||||
datasourceName?: string | null;
|
||||
}
|
||||
| undefined;
|
||||
if (chart) {
|
||||
push('chart', chart.chartName ?? (chart.chartId == null ? '(unsaved)' : ''));
|
||||
push('chartId', chart.chartId);
|
||||
push('viz', chart.vizType);
|
||||
push('datasource', chart.datasourceName);
|
||||
push('datasourceId', chart.datasourceId);
|
||||
}
|
||||
|
||||
const dashboard = ctx.dashboard as
|
||||
| { dashboardId?: number; title?: string; filters?: Array<{ label: string; value: unknown }> }
|
||||
| undefined;
|
||||
if (dashboard) {
|
||||
push('dashboard', dashboard.title);
|
||||
push('dashboardId', dashboard.dashboardId);
|
||||
const filters = dashboard.filters ?? [];
|
||||
if (filters.length) {
|
||||
push(
|
||||
'filters',
|
||||
filters.map(f => `${f.label}=${JSON.stringify(f.value)}`).join(', '),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const dataset = ctx.dataset as
|
||||
| {
|
||||
datasetId?: number;
|
||||
datasetName?: string;
|
||||
schema?: string | null;
|
||||
catalog?: string | null;
|
||||
databaseName?: string | null;
|
||||
isVirtual?: boolean;
|
||||
}
|
||||
| undefined;
|
||||
if (dataset) {
|
||||
push('dataset', dataset.datasetName);
|
||||
push('datasetId', dataset.datasetId);
|
||||
push('schema', dataset.schema);
|
||||
push('catalog', dataset.catalog);
|
||||
push('database', dataset.databaseName);
|
||||
if (typeof dataset.isVirtual === 'boolean') {
|
||||
push('virtual', dataset.isVirtual);
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.sqlLab) {
|
||||
push('tab', ctx.sqlLab.title);
|
||||
}
|
||||
|
||||
return rows;
|
||||
};
|
||||
|
||||
export const Panel: React.FC<Props> = ({ onClose }) => {
|
||||
const [input, setInput] = useState('');
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [streaming, setStreaming] = useState(false);
|
||||
const [pageContext, setPageContext] = useState<PageContext>(() => getPageContext());
|
||||
const controllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
useEffect(
|
||||
() => subscribeToPageChanges(() => setPageContext(getPageContext())),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
// Component unmount cancels any in-flight stream.
|
||||
controllerRef.current?.abort();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const send = useCallback(async () => {
|
||||
const prompt = input.trim();
|
||||
if (!prompt || streaming) return;
|
||||
setInput('');
|
||||
const userMsg: Message = { id: ++messageSeq, from: 'user', text: prompt };
|
||||
const botMsg: Message = { id: ++messageSeq, from: 'bot', text: '' };
|
||||
setMessages(prev => [...prev, userMsg, botMsg]);
|
||||
setStreaming(true);
|
||||
|
||||
const controller = new AbortController();
|
||||
controllerRef.current = controller;
|
||||
registerActiveController(controller);
|
||||
|
||||
try {
|
||||
for await (const token of streamReply(prompt, controller.signal)) {
|
||||
setMessages(prev =>
|
||||
prev.map(m => (m.id === botMsg.id ? { ...m, text: m.text + token } : m)),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
unregisterActiveController(controller);
|
||||
controllerRef.current = null;
|
||||
setStreaming(false);
|
||||
}
|
||||
}, [input, streaming]);
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
controllerRef.current?.abort();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-test="reference-chatbot-panel"
|
||||
style={{
|
||||
width: 360,
|
||||
maxHeight: 480,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: '#fff',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 8px 24px rgba(0,0,0,0.18)',
|
||||
overflow: 'hidden',
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
<header
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
background: '#2da44e',
|
||||
color: '#fff',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<span>Alt Chatbot</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Close chatbot"
|
||||
data-test="reference-chatbot-close"
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div
|
||||
data-test="reference-chatbot-context"
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
background: '#f6f8fa',
|
||||
borderBottom: '1px solid #eaecef',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11,
|
||||
color: '#57606a',
|
||||
wordBreak: 'break-all',
|
||||
}}
|
||||
>
|
||||
<div>page: {pageContext.pageType}</div>
|
||||
{contextRows(pageContext).map(([label, value]) => (
|
||||
<div key={label}>
|
||||
{label}: {value}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: 12 }}>
|
||||
{messages.length === 0 && (
|
||||
<p style={{ color: '#8c8c8c' }}>
|
||||
Ask anything — replies are canned tokens streamed by the Alt Chatbot extension.
|
||||
</p>
|
||||
)}
|
||||
{messages.map(m => (
|
||||
<div
|
||||
key={m.id}
|
||||
data-test={`reference-chatbot-msg-${m.from}`}
|
||||
style={{
|
||||
margin: '6px 0',
|
||||
textAlign: m.from === 'user' ? 'right' : 'left',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '4px 8px',
|
||||
borderRadius: 6,
|
||||
background: m.from === 'user' ? '#2da44e' : '#eef0f3',
|
||||
color: m.from === 'user' ? '#fff' : '#1f2328',
|
||||
maxWidth: '85%',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
{m.text || '…'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<footer
|
||||
style={{
|
||||
padding: 8,
|
||||
borderTop: '1px solid #eaecef',
|
||||
display: 'flex',
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<input
|
||||
aria-label="Chat input"
|
||||
data-test="reference-chatbot-input"
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
send();
|
||||
}
|
||||
}}
|
||||
placeholder="Type a message"
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '4px 8px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
{streaming ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={cancel}
|
||||
data-test="reference-chatbot-cancel"
|
||||
style={{ padding: '4px 10px' }}
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={send}
|
||||
data-test="reference-chatbot-send"
|
||||
disabled={!input.trim()}
|
||||
style={{ padding: '4px 10px' }}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
)}
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
159
extensions/chat2/src/context/pageContext.ts
Normal file
159
extensions/chat2/src/context/pageContext.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Single integration seam for the P3 namespaces.
|
||||
*
|
||||
* Each surface namespace is consumed via a try/catch — the host may ship a
|
||||
* version where a namespace function is declared but not yet implemented at
|
||||
* runtime, and the reference extension must keep working in that case. As
|
||||
* each namespace lights up on the host, that branch starts returning real
|
||||
* data without any change here.
|
||||
*
|
||||
* Route inference is the fallback when navigation.getPageType() is absent.
|
||||
*/
|
||||
|
||||
import * as core from '@apache-superset/core';
|
||||
|
||||
export type PageType =
|
||||
| 'home'
|
||||
| 'dashboard'
|
||||
| 'dashboard_list'
|
||||
| 'chart'
|
||||
| 'chart_list'
|
||||
| 'sqllab'
|
||||
| 'query_history'
|
||||
| 'saved_queries'
|
||||
| 'dataset'
|
||||
| 'dataset_list'
|
||||
| 'unknown';
|
||||
|
||||
export interface PageContext {
|
||||
pageType: PageType;
|
||||
dashboard?: unknown;
|
||||
chart?: unknown;
|
||||
dataset?: unknown;
|
||||
sqlLab?: { tabId: string; title: string };
|
||||
href: string;
|
||||
}
|
||||
|
||||
const tryCall = <T>(fn: () => T | undefined): T | undefined => {
|
||||
try {
|
||||
return fn();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const inferPageType = (pathname: string): PageType => {
|
||||
if (pathname.startsWith('/sqllab/history')) return 'query_history';
|
||||
if (pathname.startsWith('/savedqueryview/list')) return 'saved_queries';
|
||||
if (pathname.startsWith('/sqllab')) return 'sqllab';
|
||||
if (pathname.startsWith('/dashboard/list')) return 'dashboard_list';
|
||||
if (
|
||||
pathname.startsWith('/superset/dashboard') ||
|
||||
pathname.startsWith('/dashboard')
|
||||
)
|
||||
return 'dashboard';
|
||||
if (pathname.startsWith('/chart/list')) return 'chart_list';
|
||||
if (pathname.startsWith('/explore') || pathname.startsWith('/chart'))
|
||||
return 'chart';
|
||||
if (pathname.startsWith('/tablemodelview/list')) return 'dataset_list';
|
||||
if (pathname.startsWith('/tablemodelview') || pathname.startsWith('/dataset'))
|
||||
return 'dataset';
|
||||
if (pathname === '/' || pathname.startsWith('/superset/welcome'))
|
||||
return 'home';
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
const readSqlLabTab = (): PageContext['sqlLab'] => {
|
||||
const tab = tryCall(() => (core as any).sqlLab?.getCurrentTab?.());
|
||||
return tab ? { tabId: tab.id, title: tab.title } : undefined;
|
||||
};
|
||||
|
||||
const readPageType = (pathname: string): PageType => {
|
||||
const fromNav = tryCall(() => (core as any).navigation?.getPageType?.());
|
||||
return (fromNav as PageType | undefined) ?? inferPageType(pathname);
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribe to page-context changes and invoke `onChange` whenever any part of
|
||||
* the context may have changed. Returns a cleanup function.
|
||||
*
|
||||
* Three classes of change are watched:
|
||||
* - Navigation (`navigation.onDidChangePage`, or `popstate` as a fallback for
|
||||
* hosts without the namespace) — the user moved to a different surface.
|
||||
* - Entity hydration (`explore.onDidChangeChart`, `dashboard.onDidChangeDashboard`,
|
||||
* `dataset.onDidChangeDataset`) — the surface's entity loaded or changed
|
||||
* *after* navigation settled. This matters because a surface (notably Explore)
|
||||
* can finish hydrating several seconds after the route change fires, so a
|
||||
* navigation-only subscription would read empty entity context and never
|
||||
* refresh once the real data arrives.
|
||||
* - In-surface SQL Lab changes (`sqlLab.onDidChangeActiveTab`,
|
||||
* `sqlLab.onDidChangeTabTitle`) — switching or renaming a tab does not change
|
||||
* the route, so without these the panel would keep showing the first tab.
|
||||
*/
|
||||
export const subscribeToPageChanges = (onChange: () => void): (() => void) => {
|
||||
const disposers: Array<() => void> = [];
|
||||
|
||||
const nav = tryCall(() => (core as any).navigation);
|
||||
if (nav?.onDidChangePage) {
|
||||
const sub = nav.onDidChangePage(onChange);
|
||||
disposers.push(() => sub.dispose());
|
||||
} else {
|
||||
window.addEventListener('popstate', onChange);
|
||||
disposers.push(() => window.removeEventListener('popstate', onChange));
|
||||
}
|
||||
|
||||
// Entity-context change events. Each is optional — a host may not implement a
|
||||
// given namespace yet — so subscribe defensively and collect any disposer.
|
||||
const subscribeEntity = (
|
||||
getNamespace: () => any,
|
||||
method: string,
|
||||
): void => {
|
||||
const sub = tryCall(() => getNamespace()?.[method]?.(onChange));
|
||||
if (sub?.dispose) {
|
||||
disposers.push(() => sub.dispose());
|
||||
}
|
||||
};
|
||||
subscribeEntity(() => (core as any).explore, 'onDidChangeChart');
|
||||
subscribeEntity(() => (core as any).dashboard, 'onDidChangeDashboard');
|
||||
subscribeEntity(() => (core as any).dataset, 'onDidChangeDataset');
|
||||
// SQL Lab tab switches/renames happen without a route change.
|
||||
subscribeEntity(() => (core as any).sqlLab, 'onDidChangeActiveTab');
|
||||
subscribeEntity(() => (core as any).sqlLab, 'onDidChangeTabTitle');
|
||||
|
||||
return () => disposers.forEach(dispose => dispose());
|
||||
};
|
||||
|
||||
export const getPageContext = (): PageContext => {
|
||||
const { pathname, href } =
|
||||
typeof window !== 'undefined'
|
||||
? window.location
|
||||
: { pathname: '', href: '' };
|
||||
|
||||
return {
|
||||
pageType: readPageType(pathname),
|
||||
dashboard: tryCall(() => (core as any).dashboard?.getCurrentDashboard?.()),
|
||||
chart: tryCall(() => (core as any).explore?.getCurrentChart?.()),
|
||||
dataset: tryCall(() => (core as any).dataset?.getCurrentDataset?.()),
|
||||
sqlLab: readSqlLabTab(),
|
||||
href,
|
||||
};
|
||||
};
|
||||
30
extensions/chat2/src/index.tsx
Normal file
30
extensions/chat2/src/index.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module Federation entry. The host loads `./index` and invokes the factory;
|
||||
* the side effect below registers the view + commands. The host's loader
|
||||
* intercepts registerView calls to collect disposables for deactivation, so
|
||||
* returning the master Disposable here is also captured by the test harness
|
||||
* for direct assertion.
|
||||
*/
|
||||
|
||||
import { activate } from './activate';
|
||||
|
||||
export const disposable = activate();
|
||||
57
extensions/chat2/src/state.ts
Normal file
57
extensions/chat2/src/state.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module-scoped open/closed state plus a tiny emitter the UI subscribes to.
|
||||
*
|
||||
* Lives entirely inside the extension — never reaches into the host store.
|
||||
* Reset on dispose so re-activation starts cleanly.
|
||||
*/
|
||||
|
||||
export type OpenStateListener = (open: boolean) => void;
|
||||
|
||||
let open = false;
|
||||
const listeners = new Set<OpenStateListener>();
|
||||
|
||||
export const isOpen = (): boolean => open;
|
||||
|
||||
export const setOpen = (next: boolean): void => {
|
||||
if (next === open) return;
|
||||
open = next;
|
||||
listeners.forEach(fn => {
|
||||
try {
|
||||
fn(open);
|
||||
} catch {
|
||||
// A listener throwing must not block other listeners or flip our state back.
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const subscribe = (fn: OpenStateListener): (() => void) => {
|
||||
listeners.add(fn);
|
||||
return () => {
|
||||
listeners.delete(fn);
|
||||
};
|
||||
};
|
||||
|
||||
/** Drains listeners and resets state. Called from the master Disposable. */
|
||||
export const resetState = (): void => {
|
||||
open = false;
|
||||
listeners.clear();
|
||||
};
|
||||
73
extensions/chat2/src/streaming/mockStream.ts
Normal file
73
extensions/chat2/src/streaming/mockStream.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mock streaming reply used to validate stream teardown semantics.
|
||||
*
|
||||
* The reference chatbot is environment-validation only — there is no LLM.
|
||||
* This iterator yields canned tokens on a timer and exits cleanly when its
|
||||
* AbortSignal is fired. Disposal of the extension aborts any in-flight
|
||||
* controller, which is the contract that proves async cancellation works.
|
||||
*/
|
||||
|
||||
const TICK_MS = 40;
|
||||
|
||||
const buildReply = (prompt: string): string => {
|
||||
const trimmed = prompt.trim();
|
||||
if (!trimmed) {
|
||||
return 'Reference chatbot online. Send a message to validate streaming.';
|
||||
}
|
||||
return (
|
||||
`[reference-chatbot] received "${trimmed}". ` +
|
||||
'Streaming token-by-token to validate cancellation and teardown.'
|
||||
);
|
||||
};
|
||||
|
||||
const sleep = (ms: number, signal: AbortSignal): Promise<void> =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (signal.aborted) {
|
||||
reject(new DOMException('aborted', 'AbortError'));
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
signal.removeEventListener('abort', onAbort);
|
||||
resolve();
|
||||
}, ms);
|
||||
const onAbort = () => {
|
||||
clearTimeout(timer);
|
||||
reject(new DOMException('aborted', 'AbortError'));
|
||||
};
|
||||
signal.addEventListener('abort', onAbort, { once: true });
|
||||
});
|
||||
|
||||
export async function* streamReply(
|
||||
prompt: string,
|
||||
signal: AbortSignal,
|
||||
): AsyncIterableIterator<string> {
|
||||
const tokens = buildReply(prompt).split(/(\s+)/);
|
||||
for (const token of tokens) {
|
||||
if (signal.aborted) return;
|
||||
try {
|
||||
await sleep(TICK_MS, signal);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
yield token;
|
||||
}
|
||||
}
|
||||
46
extensions/chat2/src/streaming/registry.ts
Normal file
46
extensions/chat2/src/streaming/registry.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module-scoped registry of in-flight stream AbortControllers.
|
||||
*
|
||||
* Lets the master Disposable abort any running stream even when the panel
|
||||
* is unmounted by a route change or by re-activation of the extension.
|
||||
*/
|
||||
|
||||
const active = new Set<AbortController>();
|
||||
|
||||
export const registerActiveController = (c: AbortController): void => {
|
||||
active.add(c);
|
||||
};
|
||||
|
||||
export const unregisterActiveController = (c: AbortController): void => {
|
||||
active.delete(c);
|
||||
};
|
||||
|
||||
export const abortAllActiveControllers = (): void => {
|
||||
active.forEach(c => {
|
||||
try {
|
||||
c.abort();
|
||||
} catch {
|
||||
// ignore — abort() should not throw, but stay defensive.
|
||||
}
|
||||
});
|
||||
active.clear();
|
||||
};
|
||||
16
extensions/chat2/tsconfig.json
Normal file
16
extensions/chat2/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2019",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"lib": ["dom", "es2019"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/__tests__"]
|
||||
}
|
||||
16
extensions/chat2/tsconfig.test.json
Normal file
16
extensions/chat2/tsconfig.test.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@apache-superset/core": ["src/__tests__/sdkMock.ts"]
|
||||
},
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"../../superset-frontend/node_modules/@types"
|
||||
],
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": []
|
||||
}
|
||||
108
extensions/chat2/webpack.config.js
Normal file
108
extensions/chat2/webpack.config.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { ModuleFederationPlugin } = require('webpack').container;
|
||||
const packageConfig = require('./package.json');
|
||||
const extensionConfig = require('./extension.json');
|
||||
|
||||
const MODULE_FEDERATION_NAME = 'apacheSuperset_altChatbot';
|
||||
|
||||
/**
|
||||
* Emits the `manifest.json` the host reads from the extension `dist/` root.
|
||||
*
|
||||
* The host (`superset/extensions/utils.py`) expects an extension dist laid out
|
||||
* as `dist/manifest.json` plus the federated bundle under `dist/frontend/dist/`.
|
||||
* The manifest carries `extension.json` verbatim, plus the composite `id` and a
|
||||
* `frontend` block naming the content-hashed `remoteEntry` so the host can load
|
||||
* the right file. Because the hash is only known after the build, the manifest
|
||||
* is written from the final asset names rather than checked in.
|
||||
*/
|
||||
class EmitManifestPlugin {
|
||||
apply(compiler) {
|
||||
compiler.hooks.afterEmit.tap('EmitManifestPlugin', compilation => {
|
||||
const assets = Object.keys(compilation.assets);
|
||||
const remoteEntry = assets.find(name => /^remoteEntry\..*\.js$/.test(name));
|
||||
if (!remoteEntry) {
|
||||
throw new Error('EmitManifestPlugin: no remoteEntry asset was emitted');
|
||||
}
|
||||
const manifest = {
|
||||
...extensionConfig,
|
||||
id: `${extensionConfig.publisher}.${extensionConfig.name}`,
|
||||
frontend: {
|
||||
remoteEntry,
|
||||
moduleFederationName: MODULE_FEDERATION_NAME,
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, 'dist', 'manifest.json'),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const isProd = argv.mode === 'production';
|
||||
|
||||
return {
|
||||
entry: isProd ? {} : './src/index.tsx',
|
||||
mode: isProd ? 'production' : 'development',
|
||||
devtool: isProd ? false : 'eval-cheap-module-source-map',
|
||||
devServer: {
|
||||
port: 3031,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
},
|
||||
output: {
|
||||
clean: true,
|
||||
filename: isProd ? undefined : '[name].[contenthash].js',
|
||||
chunkFilename: '[name].[contenthash].js',
|
||||
path: path.resolve(__dirname, 'dist', 'frontend', 'dist'),
|
||||
publicPath: `/api/v1/extensions/${extensionConfig.publisher}/${extensionConfig.name}/`,
|
||||
},
|
||||
resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'] },
|
||||
externalsType: 'window',
|
||||
externals: { '@apache-superset/core': 'superset' },
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ },
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new ModuleFederationPlugin({
|
||||
name: MODULE_FEDERATION_NAME,
|
||||
filename: 'remoteEntry.[contenthash].js',
|
||||
exposes: { './index': './src/index.tsx' },
|
||||
shared: {
|
||||
react: {
|
||||
singleton: true,
|
||||
requiredVersion: packageConfig.peerDependencies.react,
|
||||
import: false,
|
||||
},
|
||||
'react-dom': {
|
||||
singleton: true,
|
||||
requiredVersion: packageConfig.peerDependencies['react-dom'],
|
||||
import: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
new EmitManifestPlugin(),
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -71,7 +71,7 @@ dependencies = [
|
||||
"marshmallow>=3.0, <4",
|
||||
"marshmallow-union>=0.1",
|
||||
"msgpack>=1.0.0, <1.2",
|
||||
"nh3>=0.2.11, <0.4",
|
||||
"nh3>=0.3.5, <0.4",
|
||||
"numpy>1.23.5, <2.3",
|
||||
"packaging",
|
||||
# --------------------------
|
||||
@@ -123,7 +123,7 @@ bigquery = [
|
||||
]
|
||||
clickhouse = ["clickhouse-connect>=0.13.0, <2.0"]
|
||||
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
|
||||
crate = ["sqlalchemy-cratedb>=0.40.1, <1"]
|
||||
crate = ["sqlalchemy-cratedb>=0.41.0, <1"]
|
||||
d1 = [
|
||||
"superset-engine-d1>=0.1.0",
|
||||
"sqlalchemy-d1>=0.1.0",
|
||||
@@ -135,7 +135,7 @@ databricks = [
|
||||
"databricks-sqlalchemy==1.0.5",
|
||||
]
|
||||
db2 = ["ibm-db-sa>0.3.8, <=0.4.4"]
|
||||
denodo = ["denodo-sqlalchemy>=1.0.6,<2.1.0"]
|
||||
denodo = ["denodo-sqlalchemy>=2.0.5,<2.1.0"]
|
||||
dremio = ["sqlalchemy-dremio>=1.2.1, <4"]
|
||||
drill = ["sqlalchemy-drill>=1.1.10, <2"]
|
||||
druid = ["pydruid>=0.6.5,<0.7"]
|
||||
@@ -147,6 +147,7 @@ exasol = ["sqlalchemy-exasol >= 2.4.0, < 8.0"]
|
||||
excel = ["xlrd>=1.2.0, <1.3"]
|
||||
fastmcp = [
|
||||
"fastmcp>=3.2.4,<4.0",
|
||||
"joserfc>=1.0.0,<2.0",
|
||||
# tiktoken backs the response-size-guard token estimator. Without
|
||||
# it, the middleware falls back to a coarser character-based
|
||||
# heuristic that under-counts JSON-heavy MCP responses.
|
||||
@@ -190,7 +191,7 @@ risingwave = ["sqlalchemy-risingwave"]
|
||||
shillelagh = ["shillelagh[all]>=1.4.4, <2"]
|
||||
singlestore = ["sqlalchemy-singlestoredb>=1.1.1, <2"]
|
||||
snowflake = ["snowflake-sqlalchemy>=1.2.4, <2"]
|
||||
sqlite = ["syntaqlite>=0.1.0"]
|
||||
sqlite = ["syntaqlite>=0.1.0,<0.5.0"]
|
||||
spark = [
|
||||
"pyhive[hive]>=0.6.5;python_version<'3.11'",
|
||||
"pyhive[hive_pure_sasl]>=0.7",
|
||||
@@ -225,7 +226,7 @@ development = [
|
||||
"progress>=1.5,<2",
|
||||
"psutil",
|
||||
"pyfakefs",
|
||||
"pyinstrument>=4.0.2,<6",
|
||||
"pyinstrument>=5.1.2,<6",
|
||||
"pylint",
|
||||
"pytest<8.0.0", # hairy issue with pytest >=8 where current_app proxies are not set in time
|
||||
"pytest-asyncio",
|
||||
@@ -235,7 +236,7 @@ development = [
|
||||
"ruff",
|
||||
"sqloxide",
|
||||
"statsd",
|
||||
"syntaqlite>=0.1.0",
|
||||
"syntaqlite>=0.4.2,<0.5.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -176,7 +176,7 @@ holidays==0.82
|
||||
# via apache-superset (pyproject.toml)
|
||||
humanize==4.12.3
|
||||
# via apache-superset (pyproject.toml)
|
||||
idna==3.10
|
||||
idna==3.15
|
||||
# via
|
||||
# email-validator
|
||||
# requests
|
||||
@@ -241,7 +241,7 @@ msgpack==1.0.8
|
||||
# via apache-superset (pyproject.toml)
|
||||
msgspec==0.19.0
|
||||
# via flask-session
|
||||
nh3==0.2.21
|
||||
nh3==0.3.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
numexpr==2.10.2
|
||||
# via -r requirements/base.in
|
||||
|
||||
@@ -52,7 +52,7 @@ attrs==25.3.0
|
||||
# referencing
|
||||
# requests-cache
|
||||
# trio
|
||||
authlib==1.6.9
|
||||
authlib==1.6.12
|
||||
# via fastmcp
|
||||
babel==2.17.0
|
||||
# via
|
||||
@@ -183,6 +183,7 @@ cryptography==46.0.7
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
# authlib
|
||||
# joserfc
|
||||
# paramiko
|
||||
# pyjwt
|
||||
# pyopenssl
|
||||
@@ -317,7 +318,7 @@ flask-wtf==1.2.2
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
fonttools==4.55.0
|
||||
fonttools==4.60.2
|
||||
# via matplotlib
|
||||
freezegun==1.5.1
|
||||
# via apache-superset
|
||||
@@ -422,7 +423,7 @@ humanize==4.12.3
|
||||
# apache-superset
|
||||
identify==2.5.36
|
||||
# via pre-commit
|
||||
idna==3.10
|
||||
idna==3.15
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# anyio
|
||||
@@ -471,6 +472,8 @@ jmespath==1.1.0
|
||||
# via
|
||||
# boto3
|
||||
# botocore
|
||||
joserfc==1.6.8
|
||||
# via apache-superset
|
||||
jsonpath-ng==1.7.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -566,7 +569,7 @@ msgspec==0.19.0
|
||||
# flask-session
|
||||
mysqlclient==2.2.6
|
||||
# via apache-superset
|
||||
nh3==0.2.21
|
||||
nh3==0.3.5
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -768,7 +771,7 @@ pygments==2.20.0
|
||||
# rich
|
||||
pyhive==0.7.0
|
||||
# via apache-superset
|
||||
pyinstrument==4.4.0
|
||||
pyinstrument==5.1.2
|
||||
# via apache-superset
|
||||
pyjwt==2.12.0
|
||||
# via
|
||||
@@ -838,7 +841,7 @@ python-dotenv==1.2.2
|
||||
# apache-superset
|
||||
# fastmcp
|
||||
# pydantic-settings
|
||||
python-ldap==3.4.4
|
||||
python-ldap==3.4.5
|
||||
# via apache-superset
|
||||
python-multipart==0.0.29
|
||||
# via mcp
|
||||
@@ -1005,7 +1008,7 @@ starlette==0.49.1
|
||||
# via mcp
|
||||
statsd==4.0.1
|
||||
# via apache-superset
|
||||
syntaqlite==0.1.0
|
||||
syntaqlite==0.4.2
|
||||
# via apache-superset
|
||||
tabulate==0.10.0
|
||||
# via
|
||||
@@ -1090,7 +1093,7 @@ vine==5.1.0
|
||||
# amqp
|
||||
# celery
|
||||
# kombu
|
||||
virtualenv==20.29.2
|
||||
virtualenv==20.36.1
|
||||
# via pre-commit
|
||||
watchdog==6.0.0
|
||||
# via
|
||||
|
||||
@@ -69,6 +69,24 @@ DEFAULT_INDEX = TRANSLATIONS_DIR / "translation_index.json"
|
||||
DEFAULT_MODEL = "claude-sonnet-4-6"
|
||||
DEFAULT_BATCH_SIZE = 50
|
||||
|
||||
_ASF_LICENSE_HEADER = """\
|
||||
# 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.
|
||||
"""
|
||||
|
||||
# Language names for the prompt, keyed by ISO code
|
||||
LANGUAGE_NAMES: dict[str, str] = {
|
||||
"ar": "Arabic",
|
||||
@@ -95,6 +113,19 @@ LANGUAGE_NAMES: dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
def _ensure_license_header(po_path: Path, *, dry_run: bool = False) -> None:
|
||||
"""Prepend the ASF license header to the .po file if it is missing."""
|
||||
content = po_path.read_text(encoding="utf-8")
|
||||
if "Licensed to the Apache Software Foundation" not in content:
|
||||
if dry_run:
|
||||
print(
|
||||
f"[dry-run] Would add ASF license header to {po_path}", file=sys.stderr
|
||||
)
|
||||
else:
|
||||
po_path.write_text(_ASF_LICENSE_HEADER + content, encoding="utf-8")
|
||||
print(f"Added ASF license header to {po_path}", file=sys.stderr)
|
||||
|
||||
|
||||
def _lang_name(code: str) -> str:
|
||||
"""Return a human-readable language name for an ISO language code."""
|
||||
return LANGUAGE_NAMES.get(code, code)
|
||||
@@ -510,6 +541,8 @@ def backfill(
|
||||
with open(index_path, encoding="utf-8") as f:
|
||||
index: dict[str, Any] = json.load(f)
|
||||
|
||||
_ensure_license_header(po_path, dry_run=dry_run)
|
||||
|
||||
print(f"Loading {po_path} …", file=sys.stderr)
|
||||
cat = polib.pofile(str(po_path))
|
||||
|
||||
|
||||
@@ -44,13 +44,14 @@ Typical CI workflow
|
||||
1. Create a base-branch worktree alongside the PR worktree
|
||||
2. Run babel_update.sh in the base worktree (extract from BASE source)
|
||||
3. Record baseline: python ... --count --translations-dir BASE_TREE > before.json
|
||||
4. Run babel_update.sh in the PR worktree (extract from PR source) starting
|
||||
from the same pristine BASE translations
|
||||
4. Run babel_update.sh in the PR worktree (extract from PR source and keep
|
||||
any committed PR .po updates)
|
||||
5. Compare: python ... --compare before.json [--report report.md]
|
||||
|
||||
Comparing two babel_update outputs that started from the same BASE .po files
|
||||
isolates regressions caused by the PR's source diff from any pre-existing
|
||||
drift on the base branch.
|
||||
Running babel_update on the base branch first isolates regressions caused by
|
||||
the PR's source diff from any pre-existing drift on the base branch, while the
|
||||
PR worktree run still allows committed .po updates to restore lost
|
||||
translations.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
@@ -22,7 +22,12 @@ set -e
|
||||
# If not already running in Docker, run this script inside Docker
|
||||
if [ -z "$RUNNING_IN_DOCKER" ]; then
|
||||
# Extract "current" Python version from CI config (single source of truth)
|
||||
PYTHON_VERSION=$(grep -A 1 'if.*"current"' .github/actions/setup-backend/action.yml | grep 'PYTHON_VERSION=' | sed 's/.*PYTHON_VERSION=\([0-9.]*\).*/\1/')
|
||||
PYTHON_VERSION=$(grep -A 1 'if.*"current"' .github/actions/setup-backend/action.yml | grep 'RESOLVED_VERSION=' | sed 's/.*RESOLVED_VERSION="\([0-9.]*\)".*/\1/')
|
||||
|
||||
if [ -z "$PYTHON_VERSION" ]; then
|
||||
echo "Failed to determine Python version from .github/actions/setup-backend/action.yml" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running in Docker (Python ${PYTHON_VERSION} on Linux)..."
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ const restrictedImportsRules = {
|
||||
'no-jest-mock-console': {
|
||||
name: 'jest-mock-console',
|
||||
message: 'Please use native Jest spies, i.e. jest.spyOn(console, "warn")',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
498
superset-frontend/cypress-base/package-lock.json
generated
498
superset-frontend/cypress-base/package-lock.json
generated
@@ -1717,9 +1717,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@cypress/code-coverage/node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
@@ -1739,9 +1740,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@cypress/request": {
|
||||
"version": "2.88.12",
|
||||
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz",
|
||||
"integrity": "sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==",
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.10.tgz",
|
||||
"integrity": "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"aws-sign2": "~0.7.0",
|
||||
"aws4": "^1.8.0",
|
||||
@@ -1749,16 +1751,16 @@
|
||||
"combined-stream": "~1.0.6",
|
||||
"extend": "~3.0.2",
|
||||
"forever-agent": "~0.6.1",
|
||||
"form-data": "~2.3.2",
|
||||
"http-signature": "~1.3.6",
|
||||
"form-data": "~4.0.4",
|
||||
"http-signature": "~1.4.0",
|
||||
"is-typedarray": "~1.0.0",
|
||||
"isstream": "~0.1.2",
|
||||
"json-stringify-safe": "~5.0.1",
|
||||
"mime-types": "~2.1.19",
|
||||
"performance-now": "^2.1.0",
|
||||
"qs": "~6.10.3",
|
||||
"qs": "~6.14.1",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"tough-cookie": "^5.0.0",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
@@ -1766,14 +1768,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@cypress/request/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@cypress/webpack-preprocessor": {
|
||||
"version": "5.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@cypress/webpack-preprocessor/-/webpack-preprocessor-5.17.0.tgz",
|
||||
@@ -2956,6 +2950,7 @@
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
@@ -2964,6 +2959,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
@@ -3128,6 +3124,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
@@ -3235,18 +3232,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
@@ -3260,6 +3245,22 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -3554,7 +3555,8 @@
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cosmiconfig": {
|
||||
"version": "6.0.0",
|
||||
@@ -3804,6 +3806,7 @@
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
"integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0"
|
||||
},
|
||||
@@ -3948,6 +3951,7 @@
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
"integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.1.0"
|
||||
@@ -4493,7 +4497,8 @@
|
||||
"integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
]
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
@@ -4681,42 +4686,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
|
||||
"integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"safe-buffer": "^5.2.1"
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.12"
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fromentries": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz",
|
||||
@@ -4858,6 +4842,7 @@
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
||||
"integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
@@ -5163,13 +5148,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-signature": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz",
|
||||
"integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz",
|
||||
"integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"jsprim": "^2.0.2",
|
||||
"sshpk": "^1.14.1"
|
||||
"sshpk": "^1.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
@@ -5620,7 +5606,8 @@
|
||||
"node_modules/jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="
|
||||
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
@@ -5642,7 +5629,8 @@
|
||||
"node_modules/json-schema": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
|
||||
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
|
||||
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
|
||||
"license": "(AFL-2.1 OR BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
@@ -5698,6 +5686,7 @@
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "1.0.0",
|
||||
"extsprintf": "1.3.0",
|
||||
@@ -6688,9 +6677,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
|
||||
"integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==",
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -7016,11 +7009,6 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/psl": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
|
||||
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ=="
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
@@ -7034,16 +7022,19 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.10.4",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz",
|
||||
"integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==",
|
||||
"version": "6.15.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
@@ -7346,11 +7337,6 @@
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
|
||||
},
|
||||
"node_modules/reselect": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz",
|
||||
@@ -7468,7 +7454,8 @@
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.19.1",
|
||||
@@ -7570,13 +7557,72 @@
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -7713,9 +7759,10 @@
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"node_modules/sshpk": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
|
||||
"integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==",
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
|
||||
"integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asn1": "~0.2.3",
|
||||
"assert-plus": "^1.0.0",
|
||||
@@ -8019,6 +8066,24 @@
|
||||
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
||||
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^6.1.86"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
|
||||
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
|
||||
@@ -8039,17 +8104,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
||||
"integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"psl": "^1.1.33",
|
||||
"punycode": "^2.1.1",
|
||||
"universalify": "^0.2.0",
|
||||
"url-parse": "^1.5.3"
|
||||
"tldts": "^6.1.32"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/trim-lines": {
|
||||
@@ -8118,7 +8181,8 @@
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
|
||||
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
@@ -8302,14 +8366,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
|
||||
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/untildify": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
|
||||
@@ -8357,21 +8413,17 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url-parse": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"dependencies": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",
|
||||
"integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "bin/uuid"
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/uvu": {
|
||||
@@ -8405,6 +8457,7 @@
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"core-util-is": "1.0.2",
|
||||
@@ -9848,7 +9901,7 @@
|
||||
"execa": "4.1.0",
|
||||
"globby": "11.0.4",
|
||||
"istanbul-lib-coverage": "3.0.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"nyc": "15.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -9872,9 +9925,9 @@
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"requires": {
|
||||
"argparse": "^2.0.1"
|
||||
}
|
||||
@@ -9890,9 +9943,9 @@
|
||||
}
|
||||
},
|
||||
"@cypress/request": {
|
||||
"version": "2.88.12",
|
||||
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz",
|
||||
"integrity": "sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==",
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.10.tgz",
|
||||
"integrity": "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==",
|
||||
"requires": {
|
||||
"aws-sign2": "~0.7.0",
|
||||
"aws4": "^1.8.0",
|
||||
@@ -9900,25 +9953,18 @@
|
||||
"combined-stream": "~1.0.6",
|
||||
"extend": "~3.0.2",
|
||||
"forever-agent": "~0.6.1",
|
||||
"form-data": "^2.3.4",
|
||||
"http-signature": "~1.3.6",
|
||||
"form-data": "~4.0.4",
|
||||
"http-signature": "~1.4.0",
|
||||
"is-typedarray": "~1.0.0",
|
||||
"isstream": "~0.1.2",
|
||||
"json-stringify-safe": "~5.0.1",
|
||||
"mime-types": "~2.1.19",
|
||||
"performance-now": "^2.1.0",
|
||||
"qs": "~6.10.3",
|
||||
"qs": "^6.14.2",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"tough-cookie": "^5.0.0",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
}
|
||||
"uuid": "^11.1.1"
|
||||
}
|
||||
},
|
||||
"@cypress/webpack-preprocessor": {
|
||||
@@ -11167,15 +11213,6 @@
|
||||
"write-file-atomic": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
@@ -11185,6 +11222,15 @@
|
||||
"function-bind": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"requires": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -11400,7 +11446,7 @@
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="
|
||||
},
|
||||
"cosmiconfig": {
|
||||
"version": "6.0.0",
|
||||
@@ -11440,7 +11486,7 @@
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-11.2.0.tgz",
|
||||
"integrity": "sha512-u61UGwtu7lpsNWLUma/FKNOsrjcI6wleNmda/TyKHe0dOBcVjbCPlp1N6uwFZ0doXev7f/91YDpU9bqDCFeBLA==",
|
||||
"requires": {
|
||||
"@cypress/request": "^2.88.10",
|
||||
"@cypress/request": "^3.0.0",
|
||||
"@cypress/xvfb": "^1.2.4",
|
||||
"@types/node": "^14.14.31",
|
||||
"@types/sinonjs__fake-timers": "8.1.1",
|
||||
@@ -12254,23 +12300,15 @@
|
||||
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
|
||||
"integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"safe-buffer": "^5.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
}
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"fromentries": {
|
||||
@@ -12596,13 +12634,13 @@
|
||||
"integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A=="
|
||||
},
|
||||
"http-signature": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz",
|
||||
"integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz",
|
||||
"integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==",
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"jsprim": "^2.0.2",
|
||||
"sshpk": "^1.14.1"
|
||||
"sshpk": "^1.18.0"
|
||||
}
|
||||
},
|
||||
"human-signals": {
|
||||
@@ -12809,7 +12847,7 @@
|
||||
"make-dir": "^3.0.0",
|
||||
"p-map": "^3.0.0",
|
||||
"rimraf": "^3.0.0",
|
||||
"uuid": "^3.3.3"
|
||||
"uuid": "^11.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"p-map": {
|
||||
@@ -13640,9 +13678,9 @@
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
||||
},
|
||||
"object-inspect": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
|
||||
"integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g=="
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
@@ -13873,11 +13911,6 @@
|
||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.2.0.tgz",
|
||||
"integrity": "sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg=="
|
||||
},
|
||||
"psl": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
|
||||
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ=="
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
@@ -13890,14 +13923,16 @@
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.10.4",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz",
|
||||
"integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==",
|
||||
"version": "6.15.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||
"requires": {
|
||||
"side-channel": "^1.0.4"
|
||||
"side-channel": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"querystringify": {
|
||||
@@ -14121,11 +14156,6 @@
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
||||
},
|
||||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
|
||||
},
|
||||
"reselect": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz",
|
||||
@@ -14291,13 +14321,47 @@
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
|
||||
},
|
||||
"side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"side-channel-list": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4"
|
||||
}
|
||||
},
|
||||
"side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"requires": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
}
|
||||
},
|
||||
"side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"requires": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"signal-exit": {
|
||||
@@ -14402,9 +14466,9 @@
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"sshpk": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
|
||||
"integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==",
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
|
||||
"integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
|
||||
"requires": {
|
||||
"asn1": "~0.2.3",
|
||||
"assert-plus": "^1.0.0",
|
||||
@@ -14600,6 +14664,19 @@
|
||||
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
|
||||
"peer": true
|
||||
},
|
||||
"tldts": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
||||
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
||||
"requires": {
|
||||
"tldts-core": "^6.1.86"
|
||||
}
|
||||
},
|
||||
"tldts-core": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
|
||||
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="
|
||||
},
|
||||
"tmp": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
|
||||
@@ -14614,14 +14691,11 @@
|
||||
}
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
||||
"integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||
"requires": {
|
||||
"psl": "^1.1.33",
|
||||
"punycode": "^2.1.1",
|
||||
"universalify": "^0.2.0",
|
||||
"url-parse": "^1.5.3"
|
||||
"tldts": "^6.1.32"
|
||||
}
|
||||
},
|
||||
"trim-lines": {
|
||||
@@ -14794,11 +14868,6 @@
|
||||
"unist-util-is": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"universalify": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
|
||||
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="
|
||||
},
|
||||
"untildify": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
|
||||
@@ -14823,19 +14892,10 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"requires": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",
|
||||
"integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="
|
||||
},
|
||||
"uvu": {
|
||||
"version": "0.5.6",
|
||||
|
||||
@@ -36,6 +36,12 @@
|
||||
"overrides": {
|
||||
"cypress": {
|
||||
"form-data": "^2.3.4"
|
||||
},
|
||||
"qs": "^6.14.2",
|
||||
"uuid": "^11.1.1",
|
||||
"@cypress/request": "^3.0.0",
|
||||
"@cypress/code-coverage": {
|
||||
"js-yaml": "4.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1391
superset-frontend/package-lock.json
generated
1391
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -204,7 +204,7 @@
|
||||
"query-string": "9.3.1",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^18.2.0",
|
||||
"react-arborist": "^3.7.0",
|
||||
"react-arborist": "^3.8.0",
|
||||
"react-checkbox-tree": "^1.8.0",
|
||||
"react-diff-viewer-continued": "^4.2.2",
|
||||
"react-dnd": "^11.1.3",
|
||||
@@ -243,22 +243,22 @@
|
||||
"yargs": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.28.6",
|
||||
"@babel/cli": "^7.29.7",
|
||||
"@babel/compat-data": "^7.28.4",
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/eslint-parser": "^7.28.6",
|
||||
"@babel/eslint-parser": "^7.29.7",
|
||||
"@babel/node": "^7.29.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
|
||||
"@babel/plugin-transform-runtime": "^7.29.0",
|
||||
"@babel/preset-env": "^7.29.5",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@babel/preset-typescript": "^7.28.5",
|
||||
"@babel/plugin-transform-export-namespace-from": "^7.29.7",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.29.7",
|
||||
"@babel/plugin-transform-runtime": "^7.29.7",
|
||||
"@babel/preset-env": "^7.29.7",
|
||||
"@babel/preset-react": "^7.29.7",
|
||||
"@babel/preset-typescript": "^7.29.7",
|
||||
"@babel/register": "^7.29.3",
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"@babel/runtime-corejs3": "^7.29.2",
|
||||
"@babel/types": "^7.28.6",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/jest": "^11.14.2",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
@@ -305,7 +305,7 @@
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.4",
|
||||
"@typescript-eslint/eslint-plugin": "^8.60.0",
|
||||
"@typescript-eslint/parser": "^8.59.4",
|
||||
"babel-jest": "^30.4.1",
|
||||
"babel-loader": "^10.1.1",
|
||||
@@ -394,6 +394,7 @@
|
||||
"npm": "^10.8.1"
|
||||
},
|
||||
"overrides": {
|
||||
"uuid": "$uuid",
|
||||
"core-js": "^3.38.1",
|
||||
"puppeteer": "^22.4.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
|
||||
@@ -18,6 +18,22 @@
|
||||
"types": "./lib/authentication/index.d.ts",
|
||||
"default": "./lib/authentication/index.js"
|
||||
},
|
||||
"./dashboard": {
|
||||
"types": "./lib/dashboard/index.d.ts",
|
||||
"default": "./lib/dashboard/index.js"
|
||||
},
|
||||
"./dataset": {
|
||||
"types": "./lib/dataset/index.d.ts",
|
||||
"default": "./lib/dataset/index.js"
|
||||
},
|
||||
"./explore": {
|
||||
"types": "./lib/explore/index.d.ts",
|
||||
"default": "./lib/explore/index.js"
|
||||
},
|
||||
"./navigation": {
|
||||
"types": "./lib/navigation/index.d.ts",
|
||||
"default": "./lib/navigation/index.js"
|
||||
},
|
||||
"./commands": {
|
||||
"types": "./lib/commands/index.d.ts",
|
||||
"default": "./lib/commands/index.js"
|
||||
@@ -73,11 +89,11 @@
|
||||
"author": "Apache Software Foundation",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.28.6",
|
||||
"@babel/cli": "^7.29.7",
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/preset-env": "^7.29.5",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@babel/preset-typescript": "^7.28.5",
|
||||
"@babel/preset-env": "^7.29.7",
|
||||
"@babel/preset-react": "^7.29.7",
|
||||
"@babel/preset-typescript": "^7.29.7",
|
||||
"typescript": "^5.0.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@types/lodash": "^4.17.24",
|
||||
|
||||
@@ -213,6 +213,55 @@ export declare interface Event<T> {
|
||||
(listener: (e: T) => any, thisArgs?: any): Disposable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context handed to an extension's `activate` function.
|
||||
*
|
||||
* The extension binds the lifetime of everything it registers to this object by
|
||||
* pushing the returned {@link Disposable}s onto `subscriptions`. Because the
|
||||
* context is owned by the extension for as long as it is active, registrations
|
||||
* performed asynchronously (after an `await`, in a timer, or in an event
|
||||
* callback) are tracked just the same as synchronous ones — the host disposes
|
||||
* the whole `subscriptions` array on deactivation.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export function activate(context: ExtensionContext) {
|
||||
* context.subscriptions.push(
|
||||
* commands.registerCommand('my_ext.hello', () => {}),
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface ExtensionContext {
|
||||
/**
|
||||
* Disposables to be cleaned up when the extension is deactivated. Push every
|
||||
* {@link Disposable} returned by a `register*` call here.
|
||||
*/
|
||||
subscriptions: { dispose(): void }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape of an extension's entry module (its `./index`).
|
||||
*
|
||||
* Extensions are encouraged to export an `activate(context)` function so that
|
||||
* their registrations are tracked via `context.subscriptions` regardless of
|
||||
* whether they run synchronously or asynchronously. For backward compatibility,
|
||||
* a module may instead register its contributions as top-level side effects when
|
||||
* the module is evaluated; such registrations are only tracked when performed
|
||||
* synchronously during module evaluation.
|
||||
*/
|
||||
export interface ExtensionModule {
|
||||
/**
|
||||
* Called by the host once the extension module has loaded. May be async; the
|
||||
* host awaits it before considering the extension active.
|
||||
*/
|
||||
activate?(context: ExtensionContext): void | Promise<void>;
|
||||
/**
|
||||
* Optional hook called before the host disposes `context.subscriptions`.
|
||||
*/
|
||||
deactivate?(): void | Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a Superset extension with its metadata.
|
||||
* Extensions are modular components that can extend Superset's functionality
|
||||
|
||||
@@ -43,6 +43,9 @@ export type SqlLabLocation =
|
||||
| 'results'
|
||||
| 'queryHistory';
|
||||
|
||||
/** Valid locations within the app shell (persist across all routes). */
|
||||
export type AppLocation = 'chatbot';
|
||||
|
||||
/**
|
||||
* Nested structure for view contributions by scope and location.
|
||||
* @example
|
||||
@@ -55,6 +58,7 @@ export type SqlLabLocation =
|
||||
*/
|
||||
export interface ViewContributions {
|
||||
sqllab?: Partial<Record<SqlLabLocation, View[]>>;
|
||||
app?: Partial<Record<AppLocation, View[]>>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Dashboard namespace for Superset extensions (P3).
|
||||
*
|
||||
* Exposes dashboard identity and filter state as a stable semantic API.
|
||||
* Extensions must not depend on the Redux dashboard slice structure directly.
|
||||
*/
|
||||
|
||||
import { Event } from '../common';
|
||||
|
||||
/**
|
||||
* A single native filter's current selected value(s).
|
||||
* The value type is intentionally kept as `unknown` because filter values
|
||||
* are heterogeneous (date ranges, string lists, numbers, etc.).
|
||||
*/
|
||||
export interface FilterValue {
|
||||
/** The filter's stable id. */
|
||||
filterId: string;
|
||||
/** Display label of the filter. */
|
||||
label: string;
|
||||
/** Currently applied value, or `null` when the filter is cleared. */
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized dashboard context exposed to extensions on the Dashboard page.
|
||||
*/
|
||||
export interface DashboardContext {
|
||||
/** Numeric dashboard id. */
|
||||
dashboardId: number;
|
||||
/** Display title of the dashboard. */
|
||||
title: string;
|
||||
/**
|
||||
* Active native filter values keyed by filter id.
|
||||
* Only includes filters that have a value applied.
|
||||
*/
|
||||
filters: FilterValue[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normalized dashboard context for the page currently being viewed,
|
||||
* or `undefined` when the user is not on a Dashboard page.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const dash = dashboard.getCurrentDashboard();
|
||||
* if (dash) {
|
||||
* console.log(dash.title, dash.filters);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export declare function getCurrentDashboard(): DashboardContext | undefined;
|
||||
|
||||
/**
|
||||
* Event fired when the dashboard identity or its active filter values change.
|
||||
* Fired on native filter value changes and on navigation to a different dashboard.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sub = dashboard.onDidChangeDashboard(dash => {
|
||||
* chatbot.updateContext({ dashboard: dash });
|
||||
* });
|
||||
* sub.dispose();
|
||||
* ```
|
||||
*/
|
||||
export declare const onDidChangeDashboard: Event<DashboardContext>;
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Dataset namespace for Superset extensions (P3).
|
||||
*
|
||||
* Exposes the dataset currently being viewed as a stable semantic API.
|
||||
* Aligned with backend-enforced dataset visibility and column-access semantics.
|
||||
*/
|
||||
|
||||
import { Event } from '../common';
|
||||
|
||||
/**
|
||||
* Normalized dataset context exposed to extensions on the Dataset page.
|
||||
*/
|
||||
export interface DatasetContext {
|
||||
/** Numeric dataset id. */
|
||||
datasetId: number;
|
||||
/** Display name (table name or virtual dataset name). */
|
||||
datasetName: string;
|
||||
/** Schema the dataset belongs to, if applicable. */
|
||||
schema: string | null;
|
||||
/** Catalog the dataset belongs to, if applicable. */
|
||||
catalog: string | null;
|
||||
/** Database name backing this dataset. */
|
||||
databaseName: string | null;
|
||||
/** Whether this is a virtual (SQL-defined) dataset. */
|
||||
isVirtual: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normalized dataset context for the page currently being viewed,
|
||||
* or `undefined` when the user is not on a Dataset page.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const ds = dataset.getCurrentDataset();
|
||||
* if (ds) {
|
||||
* console.log(ds.datasetName, ds.schema);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export declare function getCurrentDataset(): DatasetContext | undefined;
|
||||
|
||||
/**
|
||||
* Event fired when the focused dataset changes (e.g. the user navigates to a
|
||||
* different dataset detail page).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sub = dataset.onDidChangeDataset(ds => {
|
||||
* chatbot.updateContext({ dataset: ds });
|
||||
* });
|
||||
* sub.dispose();
|
||||
* ```
|
||||
*/
|
||||
export declare const onDidChangeDataset: Event<DatasetContext>;
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Explore namespace for Superset extensions (P3).
|
||||
*
|
||||
* Exposes the current chart/explore context as a stable semantic API.
|
||||
* Normalized over Explore Redux state — extensions must not depend on
|
||||
* the Redux slice structure directly.
|
||||
*/
|
||||
|
||||
import { Event } from '../common';
|
||||
|
||||
/**
|
||||
* Normalized chart context exposed to extensions during an Explore session.
|
||||
* Covers saved chart identity and transient editing context; excludes raw
|
||||
* form-data internals and datasource-implementation details.
|
||||
*/
|
||||
export interface ChartContext {
|
||||
/** The saved chart id, or `null` when the chart has not been persisted. */
|
||||
chartId: number | null;
|
||||
/** Display name of the saved chart, or `null` for a new/unsaved chart. */
|
||||
chartName: string | null;
|
||||
/** The visualization type currently selected in the editor. */
|
||||
vizType: string;
|
||||
/** Id of the datasource backing the chart (physical or virtual dataset). */
|
||||
datasourceId: number | null;
|
||||
/** Human-readable datasource name. */
|
||||
datasourceName: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normalized chart context for the active Explore session, or
|
||||
* `undefined` when the user is not on the Explore page.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const chart = explore.getCurrentChart();
|
||||
* if (chart) {
|
||||
* console.log(chart.vizType, chart.chartName);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export declare function getCurrentChart(): ChartContext | undefined;
|
||||
|
||||
/**
|
||||
* Event fired when the chart context changes within the active Explore session
|
||||
* (e.g. when the viz type, datasource, or saved name changes).
|
||||
* Not fired during route changes — subscribe to `navigation.onDidChangePage` for those.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sub = explore.onDidChangeChart(chart => {
|
||||
* chatbot.updateContext({ chart });
|
||||
* });
|
||||
* sub.dispose();
|
||||
* ```
|
||||
*/
|
||||
export declare const onDidChangeChart: Event<ChartContext>;
|
||||
@@ -19,9 +19,13 @@
|
||||
export * as common from './common';
|
||||
export * as authentication from './authentication';
|
||||
export * as commands from './commands';
|
||||
export * as dashboard from './dashboard';
|
||||
export * as dataset from './dataset';
|
||||
export * as editors from './editors';
|
||||
export * as explore from './explore';
|
||||
export * as extensions from './extensions';
|
||||
export * as menus from './menus';
|
||||
export * as navigation from './navigation';
|
||||
export * as sqlLab from './sqlLab';
|
||||
export * as views from './views';
|
||||
export * as contributions from './contributions';
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Navigation namespace for Superset extensions (P3).
|
||||
*
|
||||
* Exposes the current application surface so extensions can react to route
|
||||
* changes without polling. Entity-level context (chart, dashboard, dataset)
|
||||
* is intentionally not included here — use the surface-specific namespace
|
||||
* (`explore`, `dashboard`, `dataset`) to retrieve entity payloads.
|
||||
*/
|
||||
|
||||
import { Event } from '../common';
|
||||
|
||||
/**
|
||||
* The set of top-level application surfaces.
|
||||
*
|
||||
* `'explore'`, `'dashboard'` and `'dataset'` are the single-entity
|
||||
* editing/viewing surfaces where `explore.getCurrentChart()` /
|
||||
* `dashboard.getCurrentDashboard()` / `dataset.getCurrentDataset()` resolve to a
|
||||
* concrete entity. `'chart_list'`, `'dashboard_list'` and `'dataset_list'` are
|
||||
* the browse/list surfaces, distinct from those because no single entity is
|
||||
* active. `'sqllab'` is the SQL editor where `sqlLab.getCurrentTab()` resolves;
|
||||
* `'query_history'` and `'saved_queries'` are the related SQL Lab browse pages,
|
||||
* which are not the editor. `'other'` covers any route not explicitly enumerated.
|
||||
*/
|
||||
export type PageType =
|
||||
| 'dashboard'
|
||||
| 'dashboard_list'
|
||||
| 'explore'
|
||||
| 'chart_list'
|
||||
| 'sqllab'
|
||||
| 'query_history'
|
||||
| 'saved_queries'
|
||||
| 'dataset'
|
||||
| 'dataset_list'
|
||||
| 'home'
|
||||
| 'other';
|
||||
|
||||
/**
|
||||
* Returns the current page surface type.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const pageType = navigation.getPageType();
|
||||
* if (pageType === 'dashboard') {
|
||||
* const ctx = dashboard.getCurrentDashboard();
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export declare function getPageType(): PageType;
|
||||
|
||||
/**
|
||||
* Event fired whenever the user navigates to a different surface.
|
||||
* Use the surface-specific namespace to read entity context after the event.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sub = navigation.onDidChangePage(pageType => {
|
||||
* if (pageType === 'dashboard') {
|
||||
* const ctx = dashboard.getCurrentDashboard();
|
||||
* }
|
||||
* });
|
||||
* // later:
|
||||
* sub.dispose();
|
||||
* ```
|
||||
*/
|
||||
export declare const onDidChangePage: Event<PageType>;
|
||||
@@ -48,6 +48,12 @@ export interface View {
|
||||
name: string;
|
||||
/** Optional description of the view, for display in contribution manifests. */
|
||||
description?: string;
|
||||
/**
|
||||
* Optional icon identifier for the view, used in admin pickers and manifest
|
||||
* listings. Static — set once at registerView() time.
|
||||
* Dynamic icon states (e.g. notification badge) are the extension's concern.
|
||||
*/
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,12 +62,12 @@ export interface View {
|
||||
* The view provider function is called when the UI renders the location,
|
||||
* and should return a React element to display.
|
||||
*
|
||||
* @param view The view descriptor (id and name).
|
||||
* @param view The view descriptor (id, name, and optional icon/description).
|
||||
* @param location The location where this view should appear (e.g. "sqllab.panels").
|
||||
* @param provider A function that returns the React element to render.
|
||||
* @returns A Disposable that unregisters the view when disposed.
|
||||
*
|
||||
* @example
|
||||
* @example SQL Lab panel
|
||||
* ```typescript
|
||||
* views.registerView(
|
||||
* { id: 'my_ext.result_stats', name: 'Result Stats' },
|
||||
@@ -69,6 +75,15 @@ export interface View {
|
||||
* () => <ResultStatsPanel />,
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @example Chatbot bubble (`superset.chatbot` — singleton, host renders one)
|
||||
* ```typescript
|
||||
* views.registerView(
|
||||
* { id: 'my_ext.chatbot', name: 'My Chatbot', icon: 'Bubble' },
|
||||
* 'superset.chatbot',
|
||||
* () => <ChatbotApp />,
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export declare function registerView(
|
||||
view: View,
|
||||
@@ -76,6 +91,21 @@ export declare function registerView(
|
||||
provider: () => ReactElement,
|
||||
): Disposable;
|
||||
|
||||
/**
|
||||
* Narrowed descriptor for chatbot contributions (`superset.chatbot` location).
|
||||
*
|
||||
* Extension authors should use this type when calling `registerView` for the
|
||||
* chatbot area. It is identical to {@link View} but makes the registration
|
||||
* intent explicit and allows future narrowing (e.g. required `icon`).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const chatbot: ChatbotView = { id: 'my_ext.chatbot', name: 'My Chatbot', icon: 'Bubble' };
|
||||
* views.registerView(chatbot, 'superset.chatbot', () => <ChatbotApp />);
|
||||
* ```
|
||||
*/
|
||||
export type ChatbotView = View;
|
||||
|
||||
/**
|
||||
* Retrieves all views registered at a specific location.
|
||||
*
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.2.3",
|
||||
"@apache-superset/core": "*",
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"@babel/runtime": "^7.29.7",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@visx/responsive": "^3.12.0",
|
||||
"ace-builds": "^1.44.0",
|
||||
|
||||
@@ -519,7 +519,8 @@ const Select = forwardRef(
|
||||
handleSelectAll();
|
||||
}}
|
||||
>
|
||||
{t('Select all')} {`(${formatNumber('SMART_NUMBER', bulkSelectCounts.selectable)})`}
|
||||
{t('Select all')}{' '}
|
||||
{`(${formatNumber('SMART_NUMBER', bulkSelectCounts.selectable)})`}
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
@@ -536,7 +537,8 @@ const Select = forwardRef(
|
||||
handleDeselectAll();
|
||||
}}
|
||||
>
|
||||
{t('Clear')} {`(${formatNumber('SMART_NUMBER', bulkSelectCounts.deselectable)})`}
|
||||
{t('Clear')}{' '}
|
||||
{`(${formatNumber('SMART_NUMBER', bulkSelectCounts.deselectable)})`}
|
||||
</Button>
|
||||
</StyledBulkActionsContainer>
|
||||
),
|
||||
|
||||
@@ -95,6 +95,7 @@ export default defineConfig({
|
||||
testIgnore: [
|
||||
'**/tests/auth/**/*.spec.ts',
|
||||
'**/tests/sqllab/**/*.spec.ts',
|
||||
'**/tests/embedded/**/*.spec.ts',
|
||||
...(process.env.INCLUDE_EXPERIMENTAL ? [] : ['**/experimental/**']),
|
||||
],
|
||||
use: {
|
||||
@@ -132,6 +133,29 @@ export default defineConfig({
|
||||
// No storageState = clean browser with no cached cookies
|
||||
},
|
||||
},
|
||||
// Strict 'true' check: non-empty strings like 'false' or '0' would
|
||||
// otherwise enable the embedded project, matching the env-parsing
|
||||
// convention used in docker/pythonpath_dev/superset_config_docker_light.py.
|
||||
...(process.env.INCLUDE_EMBEDDED?.toLowerCase() === 'true'
|
||||
? [
|
||||
{
|
||||
// Embedded dashboard tests - validates the full embedding flow:
|
||||
// external app -> SDK -> iframe -> guest token -> dashboard render.
|
||||
// Each spec file mutates per-dashboard embedding state (UUID,
|
||||
// allowed_domains) on a single shared Superset, so files must not
|
||||
// run in parallel even if more are added later.
|
||||
name: 'chromium-embedded',
|
||||
testMatch: '**/tests/embedded/**/*.spec.ts',
|
||||
fullyParallel: false,
|
||||
use: {
|
||||
browserName: 'chromium' as const,
|
||||
testIdAttribute: 'data-test',
|
||||
// Uses admin auth for API calls to configure embedding and get guest tokens
|
||||
storageState: 'playwright/.auth/user.json',
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
|
||||
// Web server setup - disabled in CI (Flask started separately in workflow)
|
||||
|
||||
@@ -32,9 +32,25 @@ import { Tabs } from './Tabs';
|
||||
export class EditableTabs extends Tabs {
|
||||
/**
|
||||
* Clicks the add-tab button rendered by antd in editable-card mode.
|
||||
*
|
||||
* When the tab strip overflows, antd renders two `Add tab` buttons:
|
||||
* one hidden inside `.ant-tabs-nav-list` (visibility: hidden) and one
|
||||
* visible inside `.ant-tabs-nav-operations`. Scope the click to the
|
||||
* visible operations container so we never match the hidden inline copy.
|
||||
*/
|
||||
async addTab(): Promise<void> {
|
||||
await this.element.getByRole('button', { name: 'Add tab' }).click();
|
||||
const operationsButton = this.element
|
||||
.locator('.ant-tabs-nav-operations')
|
||||
.getByRole('button', { name: 'Add tab' });
|
||||
if ((await operationsButton.count()) > 0) {
|
||||
await operationsButton.click();
|
||||
return;
|
||||
}
|
||||
// No overflow yet — the inline nav-list button is the only one rendered.
|
||||
await this.element
|
||||
.locator('.ant-tabs-nav-list')
|
||||
.getByRole('button', { name: 'Add tab' })
|
||||
.click();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,10 @@ export class EditDatasetModal extends Modal {
|
||||
UNLOCK_ICON: '[data-test="unlock"]',
|
||||
};
|
||||
|
||||
// FAST_DEBOUNCE in @superset-ui/core is 250 ms; pad slightly so the
|
||||
// debounced onChange has reliably flushed before we click Save.
|
||||
private static readonly TEXT_CONTROL_DEBOUNCE_FLUSH_MS = 350;
|
||||
|
||||
private readonly tabs: Tabs;
|
||||
private readonly specificLocator: Locator;
|
||||
|
||||
@@ -94,6 +98,7 @@ export class EditDatasetModal extends Modal {
|
||||
*/
|
||||
async fillName(name: string): Promise<void> {
|
||||
await this.nameInput.fill(name);
|
||||
await this.waitForTextControlDebounce();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,5 +193,17 @@ export class EditDatasetModal extends Modal {
|
||||
await dateFormatInput.element.waitFor({ state: 'visible' });
|
||||
await dateFormatInput.clear();
|
||||
await dateFormatInput.fill(format);
|
||||
await this.waitForTextControlDebounce();
|
||||
}
|
||||
|
||||
/**
|
||||
* TextControl debounces its onChange by FAST_DEBOUNCE (250 ms) before
|
||||
* propagating the value to the parent form. Wait past that window so a
|
||||
* subsequent Save click captures the new value rather than the stale state.
|
||||
*/
|
||||
private async waitForTextControlDebounce(): Promise<void> {
|
||||
await this.page.waitForTimeout(
|
||||
EditDatasetModal.TEXT_CONTROL_DEBOUNCE_FLUSH_MS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
95
superset-frontend/playwright/embedded-app/index.html
Normal file
95
superset-frontend/playwright/embedded-app/index.html
Normal file
@@ -0,0 +1,95 @@
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Embedded Dashboard Test App</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; height: 100%; }
|
||||
#superset-container { width: 100%; height: 100vh; }
|
||||
#superset-container iframe { width: 100%; height: 100%; border: none; }
|
||||
#error { color: red; padding: 20px; display: none; }
|
||||
#status { padding: 10px; font-family: monospace; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="status">Initializing embedded dashboard...</div>
|
||||
<div id="error"></div>
|
||||
<div id="superset-container" data-test="embedded-container"></div>
|
||||
|
||||
<script src="/sdk/index.js"></script>
|
||||
<script>
|
||||
(async function () {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const uuid = params.get('uuid');
|
||||
const supersetDomain = params.get('supersetDomain');
|
||||
|
||||
if (!uuid || !supersetDomain) {
|
||||
document.getElementById('error').style.display = 'block';
|
||||
document.getElementById('error').textContent =
|
||||
'Missing required query params: uuid, supersetDomain';
|
||||
return;
|
||||
}
|
||||
|
||||
const statusEl = document.getElementById('status');
|
||||
|
||||
// fetchGuestToken is injected by Playwright via page.exposeFunction()
|
||||
async function fetchGuestToken() {
|
||||
if (typeof window.__fetchGuestToken !== 'function') {
|
||||
throw new Error('No guest token source available');
|
||||
}
|
||||
statusEl.textContent = 'Fetching guest token...';
|
||||
const token = await window.__fetchGuestToken();
|
||||
statusEl.textContent = 'Guest token received, loading dashboard...';
|
||||
return token;
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse optional UI config from query params
|
||||
const uiConfig = {};
|
||||
if (params.get('hideTitle') === 'true') uiConfig.hideTitle = true;
|
||||
if (params.get('hideTab') === 'true') uiConfig.hideTab = true;
|
||||
if (params.get('hideChartControls') === 'true') uiConfig.hideChartControls = true;
|
||||
|
||||
const dashboard = await supersetEmbeddedSdk.embedDashboard({
|
||||
id: uuid,
|
||||
supersetDomain: supersetDomain,
|
||||
mountPoint: document.getElementById('superset-container'),
|
||||
fetchGuestToken: fetchGuestToken,
|
||||
dashboardUiConfig: Object.keys(uiConfig).length > 0 ? uiConfig : undefined,
|
||||
debug: params.get('debug') === 'true',
|
||||
});
|
||||
|
||||
statusEl.textContent = 'Dashboard embedded successfully';
|
||||
// Expose dashboard API on window for Playwright assertions
|
||||
window.__embeddedDashboard = dashboard;
|
||||
} catch (err) {
|
||||
// Browser exceptions can be strings, DOMExceptions, or arbitrary
|
||||
// thrown values; only Error instances reliably expose `.message`.
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
document.getElementById('error').style.display = 'block';
|
||||
document.getElementById('error').textContent = 'Embed failed: ' + message;
|
||||
statusEl.textContent = 'Error';
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -132,26 +132,14 @@ export interface DashboardResult {
|
||||
published?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a dashboard by its title
|
||||
* @param page - Playwright page instance (provides authentication context)
|
||||
* @param title - The dashboard_title to search for
|
||||
* @returns Dashboard object if found, null if not found
|
||||
*/
|
||||
export async function getDashboardByName(
|
||||
async function getDashboardByFilter(
|
||||
page: Page,
|
||||
title: string,
|
||||
col: 'dashboard_title' | 'slug',
|
||||
value: string,
|
||||
): Promise<DashboardResult | null> {
|
||||
const filter = {
|
||||
filters: [
|
||||
{
|
||||
col: 'dashboard_title',
|
||||
opr: 'eq',
|
||||
value: title,
|
||||
},
|
||||
],
|
||||
};
|
||||
const queryParam = rison.encode(filter);
|
||||
const queryParam = rison.encode({
|
||||
filters: [{ col, opr: 'eq', value }],
|
||||
});
|
||||
const response = await apiGet(
|
||||
page,
|
||||
`${ENDPOINTS.DASHBOARD}?q=${queryParam}`,
|
||||
@@ -169,3 +157,29 @@ export async function getDashboardByName(
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a dashboard by its title
|
||||
* @param page - Playwright page instance (provides authentication context)
|
||||
* @param title - The dashboard_title to search for
|
||||
* @returns Dashboard object if found, null if not found
|
||||
*/
|
||||
export async function getDashboardByName(
|
||||
page: Page,
|
||||
title: string,
|
||||
): Promise<DashboardResult | null> {
|
||||
return getDashboardByFilter(page, 'dashboard_title', title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a dashboard by its slug
|
||||
* @param page - Playwright page instance (provides authentication context)
|
||||
* @param slug - The slug to search for
|
||||
* @returns Dashboard object if found, null if not found
|
||||
*/
|
||||
export async function getDashboardBySlug(
|
||||
page: Page,
|
||||
slug: string,
|
||||
): Promise<DashboardResult | null> {
|
||||
return getDashboardByFilter(page, 'slug', slug);
|
||||
}
|
||||
|
||||
133
superset-frontend/playwright/helpers/api/embedded.ts
Normal file
133
superset-frontend/playwright/helpers/api/embedded.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Page } from '@playwright/test';
|
||||
import { apiPost, apiPut } from './requests';
|
||||
import { ENDPOINTS as DASHBOARD_ENDPOINTS } from './dashboard';
|
||||
|
||||
export const ENDPOINTS = {
|
||||
SECURITY_LOGIN: 'api/v1/security/login',
|
||||
GUEST_TOKEN: 'api/v1/security/guest_token/',
|
||||
} as const;
|
||||
|
||||
export interface EmbeddedConfig {
|
||||
uuid: string;
|
||||
allowed_domains: string[];
|
||||
dashboard_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable embedding on a dashboard and return the embedded UUID.
|
||||
* Uses PUT (upsert) to preserve UUID across repeated calls.
|
||||
* @param page - Playwright page instance (provides authentication context)
|
||||
* @param dashboardIdOrSlug - Numeric dashboard id or slug
|
||||
* @param allowedDomains - Domains allowed to embed; empty array allows all
|
||||
* @returns Embedded config with UUID, allowed_domains, and dashboard_id
|
||||
*/
|
||||
export async function apiEnableEmbedding(
|
||||
page: Page,
|
||||
dashboardIdOrSlug: number | string,
|
||||
allowedDomains: string[] = [],
|
||||
): Promise<EmbeddedConfig> {
|
||||
const response = await apiPut(
|
||||
page,
|
||||
`${DASHBOARD_ENDPOINTS.DASHBOARD}${dashboardIdOrSlug}/embedded`,
|
||||
{ allowed_domains: allowedDomains },
|
||||
);
|
||||
const body = await response.json();
|
||||
return body.result as EmbeddedConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login as admin and return the JWT access token used by the guest_token
|
||||
* endpoint. Cache the result at suite level (`beforeAll`) and pass it into
|
||||
* `getGuestToken` to avoid a fresh login on every test.
|
||||
*
|
||||
* Defaults match `playwright/global-setup.ts` so credentials come from one
|
||||
* source (env vars). Overrides via `options` win.
|
||||
*/
|
||||
export async function getAccessToken(
|
||||
page: Page,
|
||||
options?: { username?: string; password?: string },
|
||||
): Promise<string> {
|
||||
const username =
|
||||
options?.username ?? process.env.PLAYWRIGHT_ADMIN_USERNAME ?? 'admin';
|
||||
const password =
|
||||
options?.password ?? process.env.PLAYWRIGHT_ADMIN_PASSWORD ?? 'general';
|
||||
const loginResponse = await apiPost(
|
||||
page,
|
||||
ENDPOINTS.SECURITY_LOGIN,
|
||||
{
|
||||
username,
|
||||
password,
|
||||
provider: 'db',
|
||||
refresh: true,
|
||||
},
|
||||
{ allowMissingCsrf: true },
|
||||
);
|
||||
const loginBody = await loginResponse.json();
|
||||
return loginBody.access_token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a guest token for an embedded dashboard.
|
||||
* If `accessToken` is provided, the login round-trip is skipped — preferred
|
||||
* for tests that fetch many tokens from a single suite.
|
||||
* @returns Signed guest token string
|
||||
*/
|
||||
export async function getGuestToken(
|
||||
page: Page,
|
||||
dashboardId: number | string,
|
||||
options?: {
|
||||
accessToken?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
rls?: Array<{ dataset: number; clause: string }>;
|
||||
},
|
||||
): Promise<string> {
|
||||
const accessToken =
|
||||
options?.accessToken ??
|
||||
(await getAccessToken(page, {
|
||||
username: options?.username,
|
||||
password: options?.password,
|
||||
}));
|
||||
const rls = options?.rls ?? [];
|
||||
|
||||
// The guest_token endpoint authenticates via JWT Bearer, but `page.request`
|
||||
// inherits the session cookie from storageState, so Flask-WTF still requires
|
||||
// a matching X-CSRFToken (plus a same-origin Referer). Route through
|
||||
// `apiPost` so CSRF + Referer headers are built consistently with every
|
||||
// other mutation helper; only the Authorization header is added here.
|
||||
const guestResponse = await apiPost(
|
||||
page,
|
||||
ENDPOINTS.GUEST_TOKEN,
|
||||
{
|
||||
user: {
|
||||
username: 'embedded_test_user',
|
||||
first_name: 'Embedded',
|
||||
last_name: 'TestUser',
|
||||
},
|
||||
resources: [{ type: 'dashboard', id: String(dashboardId) }],
|
||||
rls,
|
||||
},
|
||||
{ headers: { Authorization: `Bearer ${accessToken}` } },
|
||||
);
|
||||
const guestBody = await guestResponse.json();
|
||||
return guestBody.token;
|
||||
}
|
||||
@@ -26,6 +26,40 @@ export interface ApiRequestOptions {
|
||||
allowMissingCsrf?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Werkzeug (Flask's dev server, used in CI) periodically drops connections
|
||||
* mid-request under concurrent load — surfacing as `socket hang up`,
|
||||
* `ECONNRESET`, or `ERR_EMPTY_RESPONSE`. These are transport-layer
|
||||
* failures, not application errors, so retrying is safe.
|
||||
*
|
||||
* The matcher is intentionally narrow: only retry on signatures that
|
||||
* indicate the server never produced a response. Application errors
|
||||
* (4xx/5xx, HTTP-level CSRF rejection) bubble up unchanged.
|
||||
*/
|
||||
const TRANSIENT_NETWORK_ERROR =
|
||||
/socket hang up|ECONNRESET|ERR_EMPTY_RESPONSE|ECONNREFUSED|EPIPE/i;
|
||||
const TRANSIENT_RETRY_ATTEMPTS = 3;
|
||||
const TRANSIENT_RETRY_BACKOFF_MS = 250;
|
||||
|
||||
async function withTransientRetry<T>(fn: () => Promise<T>): Promise<T> {
|
||||
let lastError: unknown;
|
||||
for (let attempt = 0; attempt < TRANSIENT_RETRY_ATTEMPTS; attempt += 1) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (!TRANSIENT_NETWORK_ERROR.test(String(error))) {
|
||||
throw error;
|
||||
}
|
||||
// Linear backoff — werkzeug recovers in 100–300 ms after a drop.
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, TRANSIENT_RETRY_BACKOFF_MS * (attempt + 1));
|
||||
});
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base URL for Referer header
|
||||
* Reads from environment variable configured in playwright.config.ts
|
||||
@@ -39,7 +73,7 @@ function getBaseUrl(): string {
|
||||
return url.endsWith('/') ? url : `${url}/`;
|
||||
}
|
||||
|
||||
interface CsrfResult {
|
||||
export interface CsrfResult {
|
||||
token: string;
|
||||
error?: string;
|
||||
}
|
||||
@@ -49,11 +83,13 @@ interface CsrfResult {
|
||||
* Superset provides a CSRF token via api/v1/security/csrf_token/
|
||||
* The session cookie is automatically included by page.request
|
||||
*/
|
||||
async function getCsrfToken(page: Page): Promise<CsrfResult> {
|
||||
export async function getCsrfToken(page: Page): Promise<CsrfResult> {
|
||||
try {
|
||||
const response = await page.request.get('api/v1/security/csrf_token/', {
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
const response = await withTransientRetry(() =>
|
||||
page.request.get('api/v1/security/csrf_token/', {
|
||||
failOnStatusCode: false,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!response.ok()) {
|
||||
return {
|
||||
@@ -107,11 +143,13 @@ export async function apiGet(
|
||||
url: string,
|
||||
options?: ApiRequestOptions,
|
||||
): Promise<APIResponse> {
|
||||
return page.request.get(url, {
|
||||
headers: options?.headers,
|
||||
params: options?.params,
|
||||
failOnStatusCode: options?.failOnStatusCode ?? true,
|
||||
});
|
||||
return withTransientRetry(() =>
|
||||
page.request.get(url, {
|
||||
headers: options?.headers,
|
||||
params: options?.params,
|
||||
failOnStatusCode: options?.failOnStatusCode ?? true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,12 +164,14 @@ export async function apiPost(
|
||||
): Promise<APIResponse> {
|
||||
const headers = await buildHeaders(page, options);
|
||||
|
||||
return page.request.post(url, {
|
||||
data,
|
||||
headers,
|
||||
params: options?.params,
|
||||
failOnStatusCode: options?.failOnStatusCode ?? true,
|
||||
});
|
||||
return withTransientRetry(() =>
|
||||
page.request.post(url, {
|
||||
data,
|
||||
headers,
|
||||
params: options?.params,
|
||||
failOnStatusCode: options?.failOnStatusCode ?? true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,12 +186,14 @@ export async function apiPut(
|
||||
): Promise<APIResponse> {
|
||||
const headers = await buildHeaders(page, options);
|
||||
|
||||
return page.request.put(url, {
|
||||
data,
|
||||
headers,
|
||||
params: options?.params,
|
||||
failOnStatusCode: options?.failOnStatusCode ?? true,
|
||||
});
|
||||
return withTransientRetry(() =>
|
||||
page.request.put(url, {
|
||||
data,
|
||||
headers,
|
||||
params: options?.params,
|
||||
failOnStatusCode: options?.failOnStatusCode ?? true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,12 +208,14 @@ export async function apiPatch(
|
||||
): Promise<APIResponse> {
|
||||
const headers = await buildHeaders(page, options);
|
||||
|
||||
return page.request.patch(url, {
|
||||
data,
|
||||
headers,
|
||||
params: options?.params,
|
||||
failOnStatusCode: options?.failOnStatusCode ?? true,
|
||||
});
|
||||
return withTransientRetry(() =>
|
||||
page.request.patch(url, {
|
||||
data,
|
||||
headers,
|
||||
params: options?.params,
|
||||
failOnStatusCode: options?.failOnStatusCode ?? true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,9 +229,11 @@ export async function apiDelete(
|
||||
): Promise<APIResponse> {
|
||||
const headers = await buildHeaders(page, options);
|
||||
|
||||
return page.request.delete(url, {
|
||||
headers,
|
||||
params: options?.params,
|
||||
failOnStatusCode: options?.failOnStatusCode ?? true,
|
||||
});
|
||||
return withTransientRetry(() =>
|
||||
page.request.delete(url, {
|
||||
headers,
|
||||
params: options?.params,
|
||||
failOnStatusCode: options?.failOnStatusCode ?? true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
57
superset-frontend/playwright/helpers/navigation.ts
Normal file
57
superset-frontend/playwright/helpers/navigation.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Page, Response } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Werkzeug (Flask's dev server, used in CI) periodically drops connections
|
||||
* during page navigation under concurrent load — surfacing as
|
||||
* `ERR_EMPTY_RESPONSE`, `ERR_CONNECTION_RESET`, or a socket hang up. These
|
||||
* are transport-layer failures, not application errors, so retrying the
|
||||
* navigation is safe: the next request hits a fresh werkzeug worker thread.
|
||||
*
|
||||
* Application errors (4xx/5xx, JS exceptions during load) bubble up
|
||||
* unchanged — the matcher is narrow on purpose.
|
||||
*/
|
||||
const TRANSIENT_NAV_ERROR =
|
||||
/ERR_EMPTY_RESPONSE|ERR_CONNECTION_RESET|ERR_CONNECTION_CLOSED|socket hang up|ECONNRESET/i;
|
||||
const NAV_RETRY_ATTEMPTS = 3;
|
||||
const NAV_RETRY_BACKOFF_MS = 400;
|
||||
|
||||
export async function gotoWithRetry(
|
||||
page: Page,
|
||||
url: string,
|
||||
options?: Parameters<Page['goto']>[1],
|
||||
): Promise<Response | null> {
|
||||
let lastError: unknown;
|
||||
for (let attempt = 0; attempt < NAV_RETRY_ATTEMPTS; attempt += 1) {
|
||||
try {
|
||||
return await page.goto(url, options);
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (!TRANSIENT_NAV_ERROR.test(String(error))) {
|
||||
throw error;
|
||||
}
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, NAV_RETRY_BACKOFF_MS * (attempt + 1));
|
||||
});
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import { expect, Locator, Page } from '@playwright/test';
|
||||
import { Button, Select } from '../components/core';
|
||||
import { gotoWithRetry } from '../helpers/navigation';
|
||||
|
||||
/**
|
||||
* Chart Creation Page object for the "Create a new chart" wizard.
|
||||
@@ -74,7 +75,7 @@ export class ChartCreationPage {
|
||||
* Navigate to the chart creation page
|
||||
*/
|
||||
async goto(): Promise<void> {
|
||||
await this.page.goto('chart/add');
|
||||
await gotoWithRetry(this.page, 'chart/add');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
import { Table } from '../components/core';
|
||||
import { BulkSelect } from '../components/ListView';
|
||||
import { gotoWithRetry } from '../helpers/navigation';
|
||||
import { URL } from '../utils/urls';
|
||||
|
||||
/**
|
||||
@@ -52,14 +53,14 @@ export class ChartListPage {
|
||||
* (ListviewsDefaultCardView feature flag may enable card view).
|
||||
*/
|
||||
async goto(): Promise<void> {
|
||||
await this.page.goto(`${URL.CHART_LIST}?viewMode=table`);
|
||||
await gotoWithRetry(this.page, `${URL.CHART_LIST}?viewMode=table`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the chart list page in card view.
|
||||
*/
|
||||
async gotoCardView(): Promise<void> {
|
||||
await this.page.goto(`${URL.CHART_LIST}?viewMode=card`);
|
||||
await gotoWithRetry(this.page, `${URL.CHART_LIST}?viewMode=card`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import { Page } from '@playwright/test';
|
||||
import { Button, Select } from '../components/core';
|
||||
import { gotoWithRetry } from '../helpers/navigation';
|
||||
|
||||
/**
|
||||
* Create Dataset Page object for the dataset creation wizard.
|
||||
@@ -75,7 +76,7 @@ export class CreateDatasetPage {
|
||||
* Navigate to the create dataset page
|
||||
*/
|
||||
async goto(): Promise<void> {
|
||||
await this.page.goto('dataset/add/');
|
||||
await gotoWithRetry(this.page, 'dataset/add/');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
import { Button, Table } from '../components/core';
|
||||
import { BulkSelect } from '../components/ListView';
|
||||
import { gotoWithRetry } from '../helpers/navigation';
|
||||
import { URL } from '../utils/urls';
|
||||
|
||||
/**
|
||||
@@ -52,7 +53,7 @@ export class DashboardListPage {
|
||||
* (ListviewsDefaultCardView feature flag may enable card view).
|
||||
*/
|
||||
async goto(): Promise<void> {
|
||||
await this.page.goto(`${URL.DASHBOARD_LIST}?viewMode=table`);
|
||||
await gotoWithRetry(this.page, `${URL.DASHBOARD_LIST}?viewMode=table`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import { Page, Download } from '@playwright/test';
|
||||
import { Menu } from '../components/core';
|
||||
import { gotoWithRetry } from '../helpers/navigation';
|
||||
import { TIMEOUT } from '../utils/constants';
|
||||
|
||||
/**
|
||||
@@ -43,7 +44,7 @@ export class DashboardPage {
|
||||
* @param slug - The dashboard slug (e.g., 'world_health')
|
||||
*/
|
||||
async gotoBySlug(slug: string): Promise<void> {
|
||||
await this.page.goto(`superset/dashboard/${slug}/`);
|
||||
await gotoWithRetry(this.page, `superset/dashboard/${slug}/`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,7 +52,7 @@ export class DashboardPage {
|
||||
* @param id - The dashboard ID
|
||||
*/
|
||||
async gotoById(id: number): Promise<void> {
|
||||
await this.page.goto(`superset/dashboard/${id}/`);
|
||||
await gotoWithRetry(this.page, `superset/dashboard/${id}/`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
import { Button, Table } from '../components/core';
|
||||
import { BulkSelect } from '../components/ListView';
|
||||
import { gotoWithRetry } from '../helpers/navigation';
|
||||
import { URL } from '../utils/urls';
|
||||
|
||||
/**
|
||||
@@ -54,7 +55,7 @@ export class DatasetListPage {
|
||||
* Navigate to the dataset list page
|
||||
*/
|
||||
async goto(): Promise<void> {
|
||||
await this.page.goto(URL.DATASET_LIST);
|
||||
await gotoWithRetry(this.page, URL.DATASET_LIST);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user