Compare commits

..

2 Commits

Author SHA1 Message Date
Nicola Mometto
3ae1585c7e bump 2026-07-17 09:40:23 +01:00
Nicola Mometto
b0b11dca91 test seccomp 2026-07-17 09:39:53 +01:00
2248 changed files with 34070 additions and 78109 deletions

View File

@@ -26,7 +26,6 @@ description: Write TypeScript and JavaScript code following Metabase coding stan
- **Avoid non-null assertions (`!`)**. Prefer a guard, early return, or `?.`. Use `!` only when non-nullness is provably true and localized, with a comment.
- **No redundant runtime coercion** — don't wrap already-typed values in `Number()` / `String()` / `Boolean()`.
- **Type guards belong in `frontend/src/metabase-types/guards/`**. Do not redefine them locally.
- **A cast you can't avoid needs a real justification comment.** The `metabase/no-unjustified-type-casts` rule accepts any preceding comment — state the actual reason the cast is safe. NEVER write the legacy `// Unjustified type cast. FIXME` placeholder; it exists only on casts that predated the rule, and copying it sneaks an unjustified cast past the linter. If you can't articulate why the cast is correct, the cast is wrong — fix the types.
## Type modeling

View File

@@ -179,8 +179,6 @@
clojure.core.async/take!
clojure.core.async/to-chan!
clojure.core.async/to-chan!!
;; only writes .clj-kondo/ratchets.edn, from a :once fixture that finishes before the parallel tests
dev.kondo-ratchet/fix!
metabase.ai-tracing.core/record!
metabase.api.macros.defendpoint.tools-manifest/assert-optional-fields-nullable!
metabase.channel.core/send!
@@ -526,7 +524,6 @@
metabase.test/with-temp-empty-app-db clojure.core/let
metabase.test/with-temp-file clojure.core/let
metabase.test/with-user-in-groups clojure.core/let
metabase.transforms-rest.api.transform-runs-test/with-runs-fixture! clojure.core/fn
metabase.upload.impl-test/with-upload-table! clojure.core/let
metabase.util/for-map clojure.core/for
metabase.util.files/with-open-path-to-resource clojure.core/let
@@ -739,7 +736,6 @@
clojurewerkz.quartzite.schedule.simple/schedule macros.quartz/simple-schedule
clojurewerkz.quartzite.triggers/build macros.quartz/build-trigger
metabase-enterprise.database-routing.e2e-test/with-temp-dbs! macros.metabase-enterprise.database-routing.e2e-test/with-temp-dbs!
metabase-enterprise.database-routing.e2e-test/with-routing-setup! macros.metabase-enterprise.database-routing.e2e-test/with-routing-setup!
metabase-enterprise.impersonation.util-test/with-impersonations! macros.metabase-enterprise.impersonation.util-test/with-impersonations!
metabase-enterprise.query-reference-validation.api-test/with-test-setup! macros.metabase-enterprise.query-reference-validation.api-test/with-test-setup!
metabase-enterprise.sandbox.test-util/with-gtaps! macros.metabase-enterprise.sandbox.test-util/with-gtaps!

View File

@@ -1048,7 +1048,6 @@
health-inspector
{:team "Querying Platform"
:api #{metabase.health-inspector.api
metabase.health-inspector.core
metabase.health-inspector.init}
:uses #{api
lib
@@ -1304,8 +1303,7 @@
metabase.metabot.core
metabase.metabot.init
metabase.metabot.self
metabase.metabot.settings
metabase.metabot.tools.entity-details}
metabase.metabot.settings}
:friends #{agent-api
enterprise/metabot
enterprise/metabot-analytics
@@ -1325,7 +1323,6 @@
documents
driver
entity-retrieval
events
interestingness
legacy-mbql
lib
@@ -1338,8 +1335,6 @@
permissions
premium-features
pulse
queries
query-permissions
query-processor
request
search
@@ -1365,7 +1360,6 @@
:model/DashboardCard
:model/DashboardTab
:model/Database
:model/Document
:model/Field
:model/Glossary
:model/Measure
@@ -2017,7 +2011,6 @@
metabase.search.core
metabase.search.engine
metabase.search.in-place.scoring
metabase.search.index-health
metabase.search.ingestion
metabase.search.init
metabase.search.scoring
@@ -2032,7 +2025,6 @@
collections
config
events
health-inspector
lib
lib-be
models
@@ -2450,7 +2442,6 @@
:friends #{models
enterprise/transforms-python}
:model-exports #{:model/Transform
:model/TransformDagRun
:model/TransformJob
:model/TransformJobRun
:model/TransformJobTransformTag
@@ -2508,35 +2499,12 @@
transforms-base
util}
:model-imports #{:model/Transform
:model/TransformDagRun
:model/TransformJob
:model/TransformJobRun
:model/TransformJobTransformTag
:model/TransformRun
:model/TransformTag}}
typed-schemas
{:team "Embedding"
:api #{metabase.typed-schemas.api}
:uses #{actions
api
collections
lib
lib-be
metabot
metrics
models
system
types
util}
:model-imports #{:model/Action
:model/Card
:model/Collection
:model/Database
:model/Field
:model/Measure
:model/Table}}
types
{:team "Querying Platform"
:api #{metabase.types.core
@@ -2719,7 +2687,6 @@
warehouses}
:model-exports #{:model/Dimension
:model/Field
:model/FieldUserSettings
:model/FieldValues
:model/Table}
:model-imports #{:model/Card
@@ -3024,6 +2991,20 @@
:model/QueryCache
:model/QueryExecution}}
enterprise/checker
{:team "Graphy"
;; todo: add ability to prevent usage (transitive even) of app db
:api #{}
:uses #{classloader
lib
models
plugins
query-processor
sql-tools
util
enterprise/dependencies}
:model-imports #{:model/Database}}
enterprise/cloud-add-ons
{:team "Cloud"
:api #{metabase-enterprise.cloud-add-ons.api}
@@ -3091,17 +3072,6 @@
:api #{}
:uses #{premium-features}}
enterprise/data-apps
{:team "Gadget"
:api #{metabase-enterprise.data-apps.api
metabase-enterprise.data-apps.init
metabase-enterprise.data-apps.sync}
:uses #{api
models
premium-features
settings
util}}
enterprise/data-complexity-score
{:team "Metabot"
:api #{metabase-enterprise.data-complexity-score.api
@@ -3179,7 +3149,8 @@
enterprise/dependencies
{:team "Graphy"
:api #{metabase-enterprise.dependencies.api
:api #{metabase-enterprise.dependencies.analysis
metabase-enterprise.dependencies.api
metabase-enterprise.dependencies.core
metabase-enterprise.dependencies.init
metabase-enterprise.dependencies.models.dependency}
@@ -3262,9 +3233,7 @@
:uses #{analytics-interface
collections
entity-retrieval
health-inspector
premium-features
search
task
util
enterprise/semantic-search}
@@ -3451,7 +3420,6 @@
task
task-history
util
enterprise/data-apps
enterprise/serialization
enterprise/transforms-python}
:model-exports #{:model/RemoteSyncTask}
@@ -3460,7 +3428,6 @@
:model/Dashboard
:model/Document
:model/Field
:model/FieldUserSettings
:model/Measure
:model/NativeQuerySnippet
:model/PythonLibrary
@@ -3545,7 +3512,8 @@
settings
system
users
util}
util
enterprise/serialization}
:model-imports #{:model/ApiKey
:model/PermissionsGroup
:model/PermissionsGroupMembership
@@ -3586,7 +3554,6 @@
metabase-enterprise.semantic-search.core
metabase-enterprise.semantic-search.db.datasource
metabase-enterprise.semantic-search.embedding
metabase-enterprise.semantic-search.embedding-health
metabase-enterprise.semantic-search.init}
:uses #{activity-feed
analytics
@@ -3597,7 +3564,6 @@
config
connection-pool
events
health-inspector
lib-be
llm
metabot
@@ -3615,7 +3581,12 @@
enterprise/serialization
{:team "Graphy"
:api #{metabase-enterprise.serialization.api metabase-enterprise.serialization.cmd metabase-enterprise.serialization.core}
:api #{metabase-enterprise.serialization.api
metabase-enterprise.serialization.cmd
metabase-enterprise.serialization.core
metabase-enterprise.serialization.init
metabase-enterprise.serialization.schema
metabase-enterprise.serialization.v2.backfill-ids}
:friends #{enterprise/memoize-monitor}
:uses #{analytics
api
@@ -3631,7 +3602,9 @@
search
server
setup
util}
sync
util
warehouse-schema}
:model-imports :bypass}
enterprise/snippet-collections
@@ -3792,10 +3765,12 @@
premium-features
query-processor
sample-data
server
settings
sql-tools
util
workspaces}
workspaces
enterprise/serialization}
:model-exports #{:model/Workspace}
:model-imports #{:model/Database
:model/Table

View File

@@ -3,8 +3,3 @@
(defmacro with-temp-dbs! [bindings & body]
`(let [~@(interleave bindings (repeat nil))]
~@body))
(defmacro with-routing-setup! [[router-binding destinations] & body]
`(let [~router-binding nil
~@(interleave (map first destinations) (repeat nil))]
~@body))

View File

@@ -1,47 +0,0 @@
;; Per-linter budgets for inline `:clj-kondo/ignore` forms.
;; metabase.core.kondo-ratchet-test fails when the budgets drift from the actual counts.
;; `./bin/mage fix-kondo-ratchets` lowers budgets to match the tree; local test runs do it
;; automatically. Raising a budget, or adding one for a new linter (`--seed`), is a hand edit
;; to defend in your PR.
;; :all is the vector-less ignore form, which suppresses every linter on the next form.
{:ignore-counts {:aliased-namespace-symbol 3
:all 50
:case-symbol-test 1
:clojure-lsp/unused-public-var 9
:def-fn 1
:deprecated-namespace 97
:deprecated-var 81
:discouraged-java-method 3
:discouraged-namespace 81
:discouraged-var 106
:equals-true 1
:main-without-gen-class 1
:metabase/defsetting-namespace 7
:metabase/disallow-hardcoded-driver-names-in-tests 34
:metabase/discourage-millis-duration 1
:metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests 14
:metabase/modules 8
:metabase/namespace-name 4
:metabase/prefer-with-dynamic-fn-redefs 25
:metabase/test-helpers-use-non-thread-safe-functions 17
:metabase/validate-defendpoint-has-response-schema 441
:metabase/validate-defendpoint-query-params-use-kebab-case 40
:metabase/validate-defendpoint-route-uses-kebab-case 44
:metabase/validate-deftest 8
:missing-docstring 10
:missing-protocol-method 3
:non-arg-vec-return-type-hint 1
:reduce-without-init 3
:redundant-fn-wrapper 1
:syntax 2
:unquote-not-syntax-quoted 1
:unresolved-namespace 3
:unresolved-require 1
:unresolved-symbol 4
:unsorted-required-namespaces 1
:unused-alias 1
:unused-binding 9
:unused-excluded-var 2
:unused-namespace 1
:unused-private-var 8
:unused-value 1}}

View File

@@ -153,7 +153,6 @@
"build."
"i18n." ; bin/i18n
"lint-migrations-file-test"
"mage."
"main-test" ; bin/release-list
"metabase.deps-edn-test"
"metabase.driver."

3
.github/CODEOWNERS vendored
View File

@@ -10,7 +10,6 @@ src/metabase/query_processor @metabase/core-backend-querying-platform
src/metabase/graph @metabase/graphy
enterprise/backend/src/metabase_enterprise/dependencies @metabase/graphy
enterprise/frontend/src/metabase-enterprise/dependencies @metabase/graphy
enterprise/frontend/src/metabase-enterprise/monitor/dependency-diagnostics @metabase/graphy
# @metabase/embedding
@@ -50,5 +49,5 @@ docs/installation-and-operation/commands.md # auto-generated
src/metabase/analytics/prometheus.clj @metabase/cloud-ops
# @metabase/metabot-frontend
enterprise/frontend/src/metabase-enterprise/metabot @metabase/metabot-frontend
enterprise/frontend/src/metabase-enterprise/metabot @metabase/metabot-frontend
frontend/src/metabase/metabot @metabase/metabot-frontend

View File

@@ -8,8 +8,8 @@ default: &default
- "bin/**/!(*.md)"
actionlint:
- added|modified: ".github/workflows/*.yml"
- added|modified: ".github/workflows/*.yaml"
- ".github/workflows/*.yml"
- ".github/workflows/*.yaml"
shared_sources: &shared_sources
- "src/**/*.cljc"

View File

@@ -19,7 +19,8 @@ while [[ $# -gt 0 ]]; do
done
echo "Cloning python-runner repository..."
git clone --depth 1 https://${METABASE_AUTOMATION_USER_TOKEN}@github.com/metabase/python-runner-container.git python-runner
git clone --depth 1 https://${METABASE_AUTOMATION_USER_TOKEN}@github.com/metabase/python-runner-container.git --branch seccomp python-runner
if [ "$SKIP_START" = false ]; then
echo "Starting python-runner..."

View File

@@ -1,83 +0,0 @@
# CodeQL's overlay analysis saves a ~400MB "base database" cache on every master run, keyed by commit SHA.
#
# Only the newest entry is ever restored: CodeQL looks its cache up by key *prefix* and takes the most recent match,
# so everything behind it is dead weight. This job keeps the newest few and deletes the rest.
name: CodeQL Cache Cleanup
on:
schedule:
- cron: "0 4 * * *" # daily at 04:00 UTC
workflow_dispatch:
inputs:
keep:
description: "How many of the newest CodeQL caches to keep"
required: false
default: "2"
dry-run:
description: "List what would be deleted without deleting it"
type: boolean
required: false
default: false
concurrency:
group: cache-cleanup-codeql
cancel-in-progress: false
permissions:
actions: write # required to delete cache entries
contents: read
jobs:
cleanup:
# don't run the schedule on forks
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
env:
GH_TOKEN: ${{ github.token }}
KEEP: ${{ github.event.inputs.keep || '2' }}
DRY_RUN: ${{ github.event.inputs.dry-run || 'false' }}
PREFIX: codeql-overlay-base-database-
steps:
- name: Delete superseded CodeQL caches
shell: bash
run: |
set -euo pipefail
# created_at is ISO-8601, so a reverse lexicographic sort is newest-first.
gh api --paginate "/repos/${GITHUB_REPOSITORY}/actions/caches?per_page=100" \
--jq ".actions_caches[] | select(.key | startswith(\"${PREFIX}\")) | \"\(.created_at)\t\(.id)\t\(.size_in_bytes)\t\(.key)\"" \
| sort -r > all.tsv || true
found=$(wc -l < all.tsv | tr -d ' ')
echo "Found $found cache(s) matching '${PREFIX}'; keeping the newest $KEEP."
if [ "$found" -eq 0 ]; then
echo "Nothing to do." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
tail -n +$((KEEP + 1)) all.tsv > doomed.tsv || true
doomed=$(wc -l < doomed.tsv | tr -d ' ')
freed=0
while IFS=$'\t' read -r created id size key; do
[ -z "${id:-}" ] && continue
echo " deleting $key ($((size / 1000000))MB, created $created)"
if [ "$DRY_RUN" = "true" ]; then
echo " (dry run — not deleted)"
else
# A cache can vanish between listing and deleting (concurrent eviction); don't fail the job over it.
gh api --method DELETE "/repos/${GITHUB_REPOSITORY}/actions/caches/${id}" >/dev/null \
|| echo " (already gone, skipping)"
fi
freed=$((freed + size))
done < doomed.tsv
{
echo "### CodeQL cache cleanup"
echo ""
echo "- matched: **$found**"
echo "- kept (newest): **$KEEP**"
echo "- deleted: **$doomed**"
echo "- reclaimed: **$((freed / 1000000)) MB**${DRY_RUN:+ (dry run: $DRY_RUN)}"
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -250,7 +250,8 @@ jobs:
const message = [
`:announcement-animated: ${eng} The v${next} release branch has been cut.`,
`Add the \`backport\` label to backport changes to all supported release branches.`,
`- \`backport\`s will go to v${next} now`,
`- If you want to go back to v${current}, you'll need to add the double-backport tag`,
].join('\n');
await sendSlackMessage({

22
.github/workflows/docs-generate-63.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Auto-update documentation (release)
on:
schedule:
- cron: "0 23 * * 1-5" # Every Monday through Friday at 11 PM UTC
workflow_dispatch:
inputs:
target_branch:
description: "Target branch for documentation update"
required: false
default: "release-x.63.x"
type: string
jobs:
update-release-docs:
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
name: Call documentation update for release branch
uses: ./.github/workflows/docs-generate-base.yml
with:
target_branch: ${{ inputs.target_branch || 'release-x.63.x' }}
secrets:
automation_token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}

View File

@@ -8,7 +8,7 @@ on:
target_branch:
description: "Target branch for documentation update"
required: false
default: "release-x.63.x"
default: "release-x.62.x"
type: string
jobs:
@@ -17,6 +17,6 @@ jobs:
name: Call documentation update for release branch
uses: ./.github/workflows/docs-generate-base.yml
with:
target_branch: ${{ inputs.target_branch || 'release-x.63.x' }}
target_branch: ${{ inputs.target_branch || 'release-x.62.x' }}
secrets:
automation_token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}

View File

@@ -64,11 +64,6 @@ jobs:
# workspace where the gate step runs; an absolute path anchored to the
# workspace makes both agree regardless of cwd.
QUARANTINE_FAILURES_FILE: ${{ github.workspace }}/target/quarantine-failures.jsonl
# Cypress records video for every spec but the passing specs' videos are
# deleted in after:spec so recording is ~5-10% slowdown per shard and almost
# never yields an artifact. Failure screenshots are unaffected, and the
# stress-test workflows have their own config and keep video.
CYPRESS_VIDEO: "false"
# Sensitive env vars (tokens, secrets) starting with `CYPRESS_` are accessed via async `cy.env()`
# Example: `cy.env(["FOO"]).then(({ FOO }) => ...)`
# For non-sensitive config, use `Cypress.expose()` in cypress config instead (synchronous)
@@ -128,20 +123,9 @@ jobs:
- name: Get failed specs
if: github.run_attempt != '1'
run: echo "specs=$(cat failed-specs)" >> "$GITHUB_ENV"
# run-tests.yml compiles this in parallel with the uberjar build. Downloading it is ~10x faster than compiling it here.
# Callers without that job (stress tests, coverage shards, cross-run re-runs) fall through to compiling as before.
- name: Download pre-built CLJS
id: prebuilt-cljs
uses: actions/download-artifact@v8
continue-on-error: true
with:
name: e2e-cljs-${{ github.event.pull_request.head.sha || github.sha }}
path: target/cljs_dev
run: echo "specs=$(cat failed-specs)" >> $GITHUB_ENV
- name: Compile CLJS
if: steps.prebuilt-cljs.outcome != 'success'
run: bun run build-pure:cljs
- name: Run Metabase

View File

@@ -1,85 +0,0 @@
name: Kondo Ratchets Self-Heal
# Stale budgets in .clj-kondo/ratchets.edn (above the actual ignore counts) fail CI. On PRs
# labelled kondo-ratchets-self-healing, this commits the lowered budgets to the branch;
# unlabelled PRs rely on local test runs to tighten the file.
#
# Two jobs so the automation token never shares an environment with PR-controlled code: the
# ratchet-down job runs ./bin/mage from the PR branch and only exports the fixed file as an
# artifact; the push job starts from a clean runner, executes nothing from the PR, and pushes.
permissions:
contents: read
on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
paths:
- "**.clj"
- "**.cljc"
- "**.cljs"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
ratchet-down:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
# fork PRs don't get the automation token, so we couldn't push to them anyway
if: >-
contains(github.event.pull_request.labels.*.name, 'kondo-ratchets-self-healing')
&& github.event.pull_request.head.repo.full_name == github.repository
outputs:
changed: ${{ steps.diff.outputs.changed }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.ref }}
persist-credentials: false
- name: Lower ignore budgets to match the source tree
run: ./bin/mage fix-kondo-ratchets
- name: Check for changes
id: diff
run: |
if git diff --quiet .clj-kondo/ratchets.edn; then
echo "budgets already tight"
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- uses: actions/upload-artifact@v7
if: steps.diff.outputs.changed == 'true'
with:
name: ratchets-edn
path: .clj-kondo/ratchets.edn
# dot-directories are excluded by default, which would ship an empty artifact
include-hidden-files: true
push:
needs: ratchet-down
if: needs.ratchet-down.outputs.changed == 'true'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.ref }}
persist-credentials: false
- uses: actions/download-artifact@v8
with:
name: ratchets-edn
path: .clj-kondo/
- name: Commit to the PR branch
env:
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
REPO: ${{ github.repository }}
# METABASE_AUTOMATION_USER_TOKEN so the self-healing commit triggers CI
# (a GITHUB_TOKEN push would not)
AUTOMATION_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
run: |
git config --global user.name "Metabase Automation"
git config --global user.email "github-automation@metabase.com"
git add .clj-kondo/ratchets.edn
git commit -m "Ratchet down kondo ignore budgets" --no-verify
git push "https://x-access-token:${AUTOMATION_TOKEN}@github.com/${REPO}.git" "HEAD:$PR_BRANCH"

View File

@@ -24,42 +24,15 @@ jobs:
check-migration-versions:
if: github.event_name == 'pull_request'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10 # the `backport`-label path builds the release scripts
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
with:
filter: blob:none
fetch-depth: 0
# The `backport` label backports to every supported release line (see auto-backport.yml),
# so migrations on such a PR must sit in the oldest supported line's directory. Resolve
# that floor with the same getSupportedMajors the backport workflow uses; skip the
# release-script build entirely for PRs without the label.
- name: Prepare release scripts
if: contains(github.event.pull_request.labels.*.name, 'backport')
uses: ./.github/actions/prepare-release-scripts
- name: Compute oldest supported backport target
id: oldest-supported
if: contains(github.event.pull_request.labels.*.name, 'backport')
uses: actions/github-script@v9
env:
AWS_S3_STATIC_BUCKET: ${{ vars.AWS_S3_STATIC_BUCKET }}
AWS_REGION: ${{ vars.AWS_REGION }}
with:
script: | # js
const { getSupportedMajors } = require('${{ github.workspace }}/release/dist/index.cjs');
const targets = await getSupportedMajors();
console.log('supported backport targets', targets);
if (!targets.length || !targets.every(Number.isInteger)) {
throw new Error(`getSupportedMajors() returned no usable majors: ${JSON.stringify(targets)}`);
}
return Math.min(...targets);
- name: Check migration versions
env:
BASE_REF: ${{ github.base_ref }}
CURRENT_VERSION: ${{ vars.CURRENT_VERSION }}
PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
OLDEST_SUPPORTED_VERSION: ${{ steps.oldest-supported.outputs.result }}
run: bin/check-migration-versions

View File

@@ -19,7 +19,40 @@ jobs:
run: bun install --frozen-lockfile
- name: get module boundary stats
id: get-stats
run: node frontend/lint/module-boundaries-stats.mjs | tee -a "$GITHUB_OUTPUT"
run: |
NAMED_MODULES=$(node --input-type=module -e "
import { elements } from './frontend/lint/module-boundaries.mjs';
const named = elements.filter(e => !['shared/other', 'app/misc', 'other'].includes(e.type));
console.log(named.length);
")
ENFORCED_MODULES=$(node --input-type=module -e "
import { elements } from './frontend/lint/module-boundaries.mjs';
const enforced = new Set(elements.filter(e => e.enforceOutgoing).map(e => e.type));
console.log(enforced.size);
")
NAMED_DIRS=$(node --input-type=module -e "
import { elements } from './frontend/lint/module-boundaries.mjs';
const dirs = new Set();
for (const el of elements) {
const m = el.pattern.match(/^frontend\/src\/metabase\/(\w[\w-]*)\//)
if (m) dirs.add(m[1]);
}
for (const d of dirs) console.log(d);
")
TOTAL_DIRS=$(ls -d frontend/src/metabase/*/ | xargs -I{} basename {} | wc -l | tr -d ' ')
NAMED_COUNT=$(echo "$NAMED_DIRS" | grep -c . || true)
UNMODULED=$(($TOTAL_DIRS - $NAMED_COUNT))
VIOLATIONS=$(bun run module-boundaries 2>&1 | grep -o '✖ [0-9]* problems' | grep -o '[0-9]*' || echo "0")
echo "$NAMED_MODULES modules, $ENFORCED_MODULES enforced, $UNMODULED unmoduled folders, $VIOLATIONS violations"
echo "NAMED_MODULES=$NAMED_MODULES" >> $GITHUB_OUTPUT
echo "ENFORCED_MODULES=$ENFORCED_MODULES" >> $GITHUB_OUTPUT
echo "UNMODULED=$UNMODULED" >> $GITHUB_OUTPUT
echo "VIOLATIONS=$VIOLATIONS" >> $GITHUB_OUTPUT
- name: Upload stats
uses: actions/github-script@v9
env:

View File

@@ -34,12 +34,6 @@ jobs:
commit: ${{ steps.compute.outputs.commit }}
should_release: ${{ steps.compute.outputs.should_release }}
steps:
- name: Check if running on a fork
run: |
if [ "${{ github.repository }}" != "metabase/metabase" ]; then
echo "This workflow should not run on forks."
exit 1
fi
- uses: actions/checkout@v6
with:
sparse-checkout: |
@@ -131,7 +125,7 @@ jobs:
core.setOutput('should_release', 'false');
- name: Write job summary
if: always() && github.repository == 'metabase/metabase'
if: always()
shell: bash
run: |
{
@@ -163,12 +157,3 @@ jobs:
with:
version: ${{ needs.prepare.outputs.version }}
commit: ${{ needs.prepare.outputs.commit }}
auto-publish-release:
name: Auto-publish release ${{ inputs.version }}
needs: [prepare, trigger-release]
uses: ./.github/workflows/release.yml
secrets: inherit
with:
version: ${{ needs.prepare.outputs.version }}
commit: ${{ needs.prepare.outputs.commit }}

View File

@@ -15,30 +15,20 @@ jobs:
env:
BRANCH_NAME: ${{ github.event.workflow_run.head_branch }}
AUTHOR_NAME: ${{ github.event.workflow_run.head_commit.author.name }}
AUTHOR_LOGIN: ${{ github.event.workflow_run.head_commit.author.username }}
steps:
- name: Generate job summary
run: |
{
echo "# ${{ github.event.workflow_run.name }} workflow failed! :x:"
echo ""
echo "View the failed run attempt (#${{ github.event.workflow_run.run_attempt }}) using the following link:"
echo "${{ github.event.workflow_run.html_url }}"
} >> "$GITHUB_STEP_SUMMARY"
echo "# ${{ github.event.workflow_run.name }} workflow failed! :x:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "View the failed run attempt (#${{ github.event.workflow_run.run_attempt }}) using the following link:" >> $GITHUB_STEP_SUMMARY
echo "${{ github.event.workflow_run.html_url }}" >> $GITHUB_STEP_SUMMARY
- uses: actions/checkout@v6
- uses: actions/setup-node@v6.4.0
with:
sparse-checkout: |
.github
release
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
node-version: lts/Jod # 22.x.x
- run: npm install @slack/web-api
- name: Trigger a re-run
uses: actions/github-script@v9
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
with:
script: | # js
const MAX_ATTEMPTS = 1;
@@ -47,7 +37,6 @@ jobs:
const FAILED_RUN_NAME = "${{ github.event.workflow_run.name }}";
const BREAKING_COMMIT = "${{ github.event.workflow_run.head_sha }}";
const AUTHOR = process.env.AUTHOR_NAME;
const AUTHOR_LOGIN = process.env.AUTHOR_LOGIN;
const BRANCH = process.env.BRANCH_NAME;
if (ATTEMPT <= MAX_ATTEMPTS) {
@@ -66,7 +55,8 @@ jobs:
}
} else if (!BRANCH.includes('backport-')) {
// notify slack of repeated failure
const { mentionUserByGithubLogin, sendSlackMessage } = require('${{ github.workspace }}/release/dist/index.cjs');
const { WebClient } = require('@slack/web-api');
const slack = new WebClient('${{ secrets.SLACK_BOT_TOKEN }}');
// find out which jobs failed
const { owner, repo } = context.repo;
@@ -97,9 +87,9 @@ jobs:
]
}));
await sendSlackMessage({
channelName: 'engineering-ci',
message: 'Failing tests',
await slack.chat.postMessage({
channel: 'engineering-ci',
text: 'Failing tests',
blocks: [
{
"type": "header",
@@ -117,7 +107,7 @@ jobs:
"type": "section",
"text": {
"type": "mrkdwn",
"text": `Commit <https://github.com/metabase/metabase/commit/${BREAKING_COMMIT}|${BREAKING_COMMIT.slice(0,7)}> by ${AUTHOR} (${mentionUserByGithubLogin(AUTHOR_LOGIN)}) has failing <${FAILED_RUN_URL}|${FAILED_RUN_NAME}> tests on <https://github.com/${owner}/${repo}/commits/${BRANCH}|\`${BRANCH}\`>`
"text": `Commit <https://github.com/metabase/metabase/commit/${BREAKING_COMMIT}|${BREAKING_COMMIT.slice(0,7)}> by ${AUTHOR} has failing <${FAILED_RUN_URL}|${FAILED_RUN_NAME}> tests on <https://github.com/${owner}/${repo}/commits/${BRANCH}|\`${BRANCH}\`>`
},
},
{

View File

@@ -93,63 +93,13 @@ jobs:
uses: ./.github/workflows/uberjar.yml
secrets: inherit
# Compile CLJS in parallel with the uberjar and pass it to each e2e group.
# The compile is doesn't depend on MB_EDITION
e2e-cljs:
name: Compile CLJS for e2e
needs: [create-test-plan]
if: ${{ !cancelled() && needs.create-test-plan.result == 'success' && needs.create-test-plan.outputs.e2e_all == 'true' }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Compile CLJS
run: bun run build-pure:cljs
- name: Upload compiled CLJS
uses: actions/upload-artifact@v7
with:
name: e2e-cljs-${{ github.event.pull_request.head.sha || github.sha }}
path: target/cljs_dev
if-no-files-found: error
# Uploads fail when an artifact of this name already exists in the run
# replace it rather than fail on a re-run.
overwrite: true
static-viz-files-changed:
name: Check whether static-viz files changed
if: ${{ !contains(github.event.pull_request.labels.*.name,'ci skip') && !contains(github.event.pull_request.labels.*.name,'minimal-tests') }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
outputs:
static_viz: ${{ steps.static_viz.outputs.static_viz }}
steps:
- uses: actions/checkout@v6
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Build static-viz frontend
run: bun run build-static-viz
env:
MB_EDITION: ee
- name: Check for static viz changes
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: static_viz
with:
token: ${{ github.token }}
filters: .github/static-viz-sources.yaml
backend-tests:
if: ${{ !cancelled() && needs.create-test-plan.result == 'success' }}
needs: [create-test-plan, static-viz-files-changed]
needs: [create-test-plan]
uses: ./.github/workflows/backend.yml
secrets: inherit
with:
skip: ${{ needs.create-test-plan.outputs.backend_all != 'true' && needs.static-viz-files-changed.outputs.static_viz != 'true' }}
skip: ${{ needs.create-test-plan.outputs.backend_all != 'true' }}
semantic-search-tests:
needs: create-test-plan

View File

@@ -140,8 +140,8 @@ jobs:
run: |
RELEASE_BRANCH=${{ steps.release_branch.outputs.result }}
git checkout $RELEASE_BRANCH
(git branch --contains ${{ inputs.commit }} | grep -q $RELEASE_BRANCH \
&& echo "Commit found in correct release branch") \
git branch --contains ${{ inputs.commit }} | grep -q $RELEASE_BRANCH \
&& echo "Commit found in correct release branch" \
|| (echo "Commit not found in correct release branch" && exit 1)
git checkout -
@@ -196,7 +196,7 @@ jobs:
version='${{ needs.check-version.outputs.oss }}'
fi
echo "VERSION=$version" >> "$GITHUB_ENV"
echo "VERSION=$version" >> $GITHUB_ENV
- name: Setup Java
uses: actions/setup-java@v5.2.0
with:
@@ -215,9 +215,9 @@ jobs:
echo "Old version.properties:"
cat version.properties
sed -i "s/^tag=.*/tag=$VERSION/" version.properties
printf "\n\nNew version.properties:"
echo "\n\nNew version.properties:"
cat version.properties
printf "\n\nUpdating metabase.jar with new version.properties"
echo "\n\nUpdating metabase.jar with new version.properties"
jar uf metabase.jar version.properties
- name: Store commit's SHA-1 hash
@@ -280,3 +280,12 @@ jobs:
with:
commit: ${{ inputs.commit }}
version: ${{ inputs.version }}
auto-publish-release:
name: Auto-publish release ${{ inputs.version }}
needs: run-pre-release-tests
uses: ./.github/workflows/release.yml
secrets: inherit
with:
version: ${{ inputs.version }}
commit: ${{ inputs.commit }}

2
.gitignore vendored
View File

@@ -89,7 +89,6 @@ cypress.env.json
/resources/frontend_client/app/embedding-sdk.js.map
/resources/frontend_client/app/embedding-sdk/
/resources/frontend_client/tmp-*/
/resources/frontend_client/data-app.html
/resources/frontend_client/embed.html
/resources/frontend_client/embed-sdk.html
/resources/frontend_client/embed-mcp.html
@@ -151,7 +150,6 @@ mise.local.toml
**/.clj-kondo/*
!**/.clj-kondo/README.md
!**/.clj-kondo/config.edn
!**/.clj-kondo/ratchets.edn
!**/.clj-kondo/config/
!**/.clj-kondo/src/
!**/.clj-kondo/test/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -1,27 +0,0 @@
import { Comment, Converter } from "typedoc";
const TAG = "@notExported";
export function load(app) {
app.options.getDeclaration("blockTags").defaultValue?.push?.(TAG);
app.converter.on(Converter.EVENT_RESOLVE_END, (context) => {
const project = context.project;
const intentional = new Set(app.options.getValue("intentionallyNotExported"));
for (const id in project.reflections) {
const ref = project.reflections[id];
const tags = ref.comment?.blockTags ?? [];
for (const tag of tags) {
if (tag.tag === TAG) {
const name = Comment.combineDisplayParts(tag.content).trim();
if (name) {
intentional.add(name);
}
}
}
}
app.options.setValue("intentionallyNotExported", [...intentional]);
});
}

View File

@@ -10,7 +10,6 @@ const config = {
"typedoc-plugin-dt-links",
"typedoc-plugin-redirect",
"./typedoc-plugin-replace-text.js",
"./typedoc-plugin-not-exported.js",
],
entryPoints: ["../resources/embedding-sdk/dist/index.d.ts"],
router: "structure",

View File

@@ -77,36 +77,6 @@ It piggybacks on a running dev nREPL (~5s) and auto-spawns a JVM if none is runn
the four generated keys; structural changes it can't safely make (a new module needs a human `:team`, or
modules need reordering) are printed as `WARNING:` lines for you to resolve by hand.
## Kondo Ignore Ratchets
`.clj-kondo/ratchets.edn` records, per linter, how many inline `:clj-kondo/ignore` forms the backend source
tree may contain. `metabase.core.kondo-ratchet-test` fails when the budgets drift from the actual counts,
in either direction. Prefer fixing the underlying warning over adding an ignore.
Budget too high (you removed ignores): a local run of the test tightens the file for you — commit the
change. PRs labelled `kondo-ratchets-self-healing` get the lowered budgets committed to the branch by CI.
To tighten by hand (babashka, no JVM; a no-op prints `unchanged`):
```bash
./bin/mage fix-kondo-ratchets
```
Budget too low (you added an ignore): the task only raises a budget when told to. If the ignore is
genuinely required, run `./bin/mage fix-kondo-ratchets --seed :the-linter` and defend the increase in the
PR.
Introducing a new linter: `./bin/mage kondo-insert-ignores :the-linter` inserts an ignore at every site it
flags, then `./bin/mage fix-kondo-ratchets --seed :the-linter` records the budget — no big-bang cleanup.
To burn debt down, `./bin/mage kondo-redundant-ignores` lists ignores that are no longer needed (slow:
full kondo run). Kondo's redundancy report can't see hook-linter warnings, so `--fix` re-lints after
removing, puts any still-working ignore back exactly as it was, and stamps it with a `[kondo-keep]`
comment; marked sites are skipped on later runs. That verification needs a clean starting point, so
files with pre-existing lint findings are excluded from the sweep and reported. `--fix --audit` rechecks the
marked sites too, removing any that have become truly redundant along with their stamped marker
comments (a marker trailing on a code line is left for a hand fix). `[kondo-keep]` can also be added
by hand to protect an ignore whose exact form matters — it only counts on the line directly above the
ignore, or trailing on the ignore's own line.
## Tool Preferences
If `clojure-mcp` tools are available, prefer them over shell-based alternatives for Clojure development.

31
bb.edn
View File

@@ -1,8 +1,7 @@
{:min-bb-version "1.12.212"
;; we put path as bin, and everything is in the ./mage subdirectory,
;; so the namespaces are mage.cli, mage.format, etc.
;; dev/src is here for tasks whose logic is shared with JVM tests (e.g. dev.kondo-ratchet)
:paths ["mage/src" "mage/test" "bin/lint-migrations-file/src" "dev/src"]
:paths ["mage/src" "mage/test" "bin/lint-migrations-file/src"]
;; use Git version of dep temporarily until a new release > 0.16.4 is cut
:deps {dev.weavejester/cljfmt {:git/sha "075eae26e3c8dad433b88f05927bcd50e3571328"
:git/url "https://github.com/weavejester/cljfmt"} ;; run cljfmt from bb
@@ -13,7 +12,7 @@
org.babashka/json {:mvn/version "0.1.6"} ;; json parsing
table/table {:mvn/version "0.5.0"} ;; table printing
}
;; No :pods key on purpose: tasks lazily load only what they need.
:pods {org.babashka/postgresql {:version "0.1.0"}}
:tasks
{:requires [[mage.cli :as cli]
[mage.color :as c]]
@@ -275,32 +274,6 @@
:requires [[mage.modules]]
:task (task! (mage.modules/cli-fix-config (cli/parse! (current-task))))}
fix-kondo-ratchets
{:doc "Lower the per-linter ignore budgets in .clj-kondo/ratchets.edn to match the source tree."
:examples [["./bin/mage fix-kondo-ratchets" "Ratchet budgets down (never raises one; over-budget linters are only warned about)"]
["./bin/mage fix-kondo-ratchets --seed :some-linter" "Also set :some-linter's budget to its actual count, for landing a new linter"]]
:options [["-s" "--seed LINTER" "Set LINTER's budget to the actual count (may add or raise it)"]]
:requires [[dev.kondo-ratchet]]
:task (task! (dev.kondo-ratchet/fix! (:options parsed)))}
kondo-redundant-ignores
{:doc "Report inline kondo ignores that are no longer necessary (slow: full kondo run)."
:examples [["./bin/mage kondo-redundant-ignores" "Scan the backend tree for removable ignores"]
["./bin/mage kondo-redundant-ignores --fix" "Also remove them from source (restores + marks [kondo-keep] any that re-trigger)"]
["./bin/mage kondo-redundant-ignores --fix --audit" "Recheck [kondo-keep] sites too, unmarking any now truly redundant"]]
:options [["-f" "--fix" "Remove the candidate ignores from source"]
["-a" "--audit" "Include sites marked [kondo-keep] instead of skipping them"]]
:requires [[mage.kondo-ratchet]]
:task (task! (mage.kondo-ratchet/redundant-ignores parsed))}
kondo-insert-ignores
{:doc "Insert inline ignores at every site a linter flags -- for landing a new linter without a big-bang fix."
:examples [["./bin/mage kondo-insert-ignores :metabase/new-linter" "Insert ignores everywhere the linter fires, then seed its budget"]
["./bin/mage kondo-insert-ignores :equals-true test/metabase/pulse" "Only under the given paths"]]
:arg-schema [:sequential [:string {:name "linter-then-paths"}]]
:requires [[mage.kondo-ratchet]]
:task (task! (mage.kondo-ratchet/insert-ignores (:arguments parsed)))}
ls
{:doc "List all mage public and private tasks"
:examples [["./bin/mage ls" "List all tasks"]]

View File

@@ -169,6 +169,7 @@
(doseq [^File file (file-seq source-path)
:when (not (directory? file))
:when
#_{:clj-kondo/ignore [:discouraged-var]}
(or (str/ends-with? (str/lower-case file) "yaml")
(str/ends-with? (str/lower-case file) "yml"))]
(when verbose (out/safe-println "Zipping file:" file))

View File

@@ -27,6 +27,7 @@
(deftest ^:parallel backend-message?
(testing "messages present in any .clj and .cljc files are detected as backend messages"
#_{:clj-kondo/ignore [:equals-true]}
(are [source-references expected] (= expected
(i18n/backend-message? {:source-references source-references}))
;; Simple .clj and .cljc files with and without line numbers

View File

@@ -3,7 +3,6 @@ set -euo pipefail
# Requires: BASE_REF, CURRENT_VERSION (for master)
# Optional: PR_LABELS (comma-separated, for backport handling)
# OLDEST_SUPPORTED_VERSION (required when PR_LABELS contains `backport`)
if [[ -z "${BASE_REF:-}" ]]; then
echo "ERROR: BASE_REF is not set." >&2
@@ -40,53 +39,30 @@ elif [[ "$BASE_REF" == "master" ]]; then
fi
echo "CURRENT_VERSION: ${CURRENT_VERSION}"
# Backport labels shift the target version back so migrations land on the earliest release
# branch the backport will reach. `backport` targets every supported release line (see
# .github/workflows/auto-backport.yml), so its floor is the oldest supported major from
# version-info.json, passed in as OLDEST_SUPPORTED_VERSION. The legacy depth labels keep
# their fixed offsets from CURRENT_VERSION.
candidates=()
if [[ ",$labels," == *",backport,"* ]]; then
if [[ -z "${OLDEST_SUPPORTED_VERSION:-}" ]]; then
echo "ERROR: PR has the 'backport' label but OLDEST_SUPPORTED_VERSION is not set." >&2
exit 1
matches=0
for l in backport single-backport double-backport triple-backport; do
if [[ ",$labels," == *",$l,"* ]]; then
# N.B. not ((matches++)) — that exits 1 when matches=0 under set -e
matches=$((matches + 1))
fi
if ! [[ "$OLDEST_SUPPORTED_VERSION" =~ ^[1-9][0-9]*$ ]]; then
echo "ERROR: OLDEST_SUPPORTED_VERSION must be a positive integer, got: '${OLDEST_SUPPORTED_VERSION}'" >&2
exit 1
fi
echo "OLDEST_SUPPORTED_VERSION: ${OLDEST_SUPPORTED_VERSION}"
candidates+=("$OLDEST_SUPPORTED_VERSION")
fi
[[ ",$labels," == *",single-backport,"* ]] && candidates+=("$CURRENT_VERSION")
[[ ",$labels," == *",double-backport,"* ]] && candidates+=($((CURRENT_VERSION - 1)))
[[ ",$labels," == *",triple-backport,"* ]] && candidates+=($((CURRENT_VERSION - 2)))
done
constraint="="
version=$((CURRENT_VERSION + 1))
# ${arr[@]+...} guards the empty-array case, which trips `set -u` on bash < 4.4
for candidate in ${candidates[@]+"${candidates[@]}"}; do
[[ "$candidate" -lt "$version" ]] && version=$candidate
done
# Backport labels shift the target version back so migrations land on the correct release branch.
[[ ",$labels," == *",backport,"* ]] && version=$((CURRENT_VERSION))
[[ ",$labels," == *",single-backport,"* ]] && version=$((CURRENT_VERSION))
[[ ",$labels," == *",double-backport,"* ]] && version=$((CURRENT_VERSION - 1))
[[ ",$labels," == *",triple-backport,"* ]] && version=$((CURRENT_VERSION - 2))
[[ ${#candidates[@]} -gt 1 ]] && echo "NOTE: Multiple backport labels detected; using the earliest version (v${version})." >&2
[[ $matches -gt 1 ]] && echo "NOTE: Multiple backport labels detected; using the earliest version (v${version})." >&2
else
echo "Target branch '${BASE_REF}' is not master or a release branch — skipping."
exit 0
fi
# Per-feature migration directories (resources/migrations/NNN/) start at v60. Older versions keep a
# single shared file (resources/migrations/0NN_update_migrations.yaml), and pre-60 release branches
# only load those shared files (endsWithFilter: "_update_migrations.yaml"), so a per-feature file
# targeting a pre-60 version would be silently ignored there.
DIR_LAYOUT_FLOOR=60
expected_dir=$(printf "%03d" "$version")
if [[ "$version" -lt "$DIR_LAYOUT_FLOOR" ]]; then
echo "Expected location: resources/migrations/${expected_dir}_update_migrations.yaml (per-feature directories start at v${DIR_LAYOUT_FLOOR})"
else
echo "Expected directory: ${constraint} ${expected_dir}"
fi
echo "Expected directory: ${constraint} ${expected_dir}"
echo ""
# Check that new migration files are in the correct directory
@@ -108,16 +84,9 @@ while IFS= read -r file; do
dir_version=$((10#$dir))
dir=$(printf "%03d" "$dir_version")
if [[ "$dir_version" -lt "$DIR_LAYOUT_FLOOR" ]]; then
echo "ERROR: ${file} targets v${dir_version}, but per-feature directories start at v${DIR_LAYOUT_FLOOR} — a pre-${DIR_LAYOUT_FLOOR} changeset goes in resources/migrations/${dir}_update_migrations.yaml" >&2
errors=$((errors + 1))
elif [[ "$constraint" == "=" && "$dir_version" -ne "$version" ]] ||
if [[ "$constraint" == "=" && "$dir_version" -ne "$version" ]] ||
[[ "$constraint" == "<=" && "$dir_version" -gt "$version" ]]; then
if [[ "$version" -lt "$DIR_LAYOUT_FLOOR" ]]; then
echo "ERROR: ${file} is in ${dir} but the changeset belongs in resources/migrations/${expected_dir}_update_migrations.yaml" >&2
else
echo "ERROR: ${file} is in ${dir} but expected ${constraint} ${expected_dir}" >&2
fi
echo "ERROR: ${file} is in ${dir} but expected ${constraint} ${expected_dir}" >&2
errors=$((errors + 1))
fi
done <<<"$changed_files"
@@ -129,24 +98,19 @@ if [[ "$errors" -gt 0 ]]; then
echo "Why: Migration files must be in the directory matching the target release version." >&2
echo "A misplaced migration misrepresents when it was introduced and will break downgrades spanning more than one version." >&2
echo "" >&2
if [[ "$version" -lt "$DIR_LAYOUT_FLOOR" ]]; then
echo "To fix: append the changeset(s) to resources/migrations/${expected_dir}_update_migrations.yaml." >&2
echo "Pre-${DIR_LAYOUT_FLOOR} releases only load the shared per-version files, not NNN/ directories." >&2
elif [[ "$constraint" == "<=" ]]; then
echo "To fix: move the file(s) to resources/migrations/${expected_dir}/ or any earlier version directory (>= 0${DIR_LAYOUT_FLOOR})." >&2
if [[ "$constraint" == "<=" ]]; then
echo "To fix: move the file(s) to resources/migrations/${expected_dir}/ or any earlier version directory." >&2
else
echo "To fix: move the file(s) to resources/migrations/${expected_dir}/." >&2
fi
echo "" >&2
echo "If this PR will be backported, ensure the correct backport label is applied" >&2
echo "and re-run CI, even if you plan to create the backport PRs manually:" >&2
echo " - backport: all supported release lines (the check targets the oldest one)" >&2
echo " - single-backport: the current release only" >&2
echo " - double-backport / triple-backport: 1 or 2 releases before the current one" >&2
echo "Base the label on the oldest release you're targeting, and simply close any unwanted PRs." >&2
echo "(backport, single-backport, double-backport, or triple-backport) and re-run CI, even if you" >&2
echo "plan to create the backport PRs manually. Base the label on the oldest" >&2
echo "release you're targeting, and simply close any unwanted PRs." >&2
echo "" >&2
echo "If none of the labels matches the releases you're targeting, or this is an" >&2
echo "exceptional circumstance, you will need to either update this script or force merge." >&2
echo "If you are backporting more than 3 releases back, or this is an exceptional" >&2
echo "circumstance, you will need to either update this script or force merge." >&2
exit 1
fi

View File

@@ -1,15 +1,12 @@
#!/usr/bin/env node
/* eslint-env node */
/* eslint-disable import/order */
/* eslint-disable import/no-commonjs, import/order, no-console */
const fs = require("fs");
const path = require("path");
const { spawn } = require("child_process");
const glob = require("glob");
const { Extractor, ExtractorConfig } = require("@microsoft/api-extractor");
const SDK_DIST_DIR_PATH = path.resolve("./resources/embedding-sdk/dist");
const API_EXTRACTOR_BIN_PATH = path.resolve(
"./node_modules/@microsoft/api-extractor/bin/api-extractor",
);
/*
* This script replaces all custom aliases in Embedding SDK generated ".d.ts" files so that this imports could be resolved
@@ -29,52 +26,10 @@ const REPLACES_MAP = {
cljs: "target/cljs_release",
};
const DTS_ROLLUPS = [
{
name: "main",
configPath: path.join(
__dirname,
"../../enterprise/frontend/src/embedding-sdk-package/embedding-sdk-api-extractor.json",
),
entryPointPath: path.join(
__dirname,
"../../resources/embedding-sdk/dist/enterprise/frontend/src/embedding-sdk-package/index.d.ts",
),
},
{
name: "data-app",
configPath: path.join(
__dirname,
"../../enterprise/frontend/src/embedding-sdk-package/embedding-sdk-data-app-api-extractor.json",
),
entryPointPath: path.join(
__dirname,
"../../resources/embedding-sdk/dist/enterprise/frontend/src/embedding-sdk-package/data-app.d.ts",
),
},
{
name: "data-app-dev",
configPath: path.join(
__dirname,
"../../enterprise/frontend/src/embedding-sdk-package/embedding-sdk-data-app-dev-api-extractor.json",
),
entryPointPath: path.join(
__dirname,
"../../resources/embedding-sdk/dist/enterprise/frontend/src/embedding-sdk-package/data-app-dev.d.ts",
),
},
{
name: "data-app-dev-config",
configPath: path.join(
__dirname,
"../../enterprise/frontend/src/embedding-sdk-package/embedding-sdk-data-app-dev-config-api-extractor.json",
),
entryPointPath: path.join(
__dirname,
"../../resources/embedding-sdk/dist/enterprise/frontend/src/embedding-sdk-package/data-app-dev.config.d.ts",
),
},
];
const API_EXTRACTOR_CONFIG_PATH = path.join(
__dirname,
"../../enterprise/frontend/src/embedding-sdk-package/embedding-sdk-api-extractor.json",
);
const getLogger = (prefix) => {
const verbose = process.env.SDK_FIXUP_VERBOSE_LOGS === "true";
@@ -155,7 +110,7 @@ const removeUnresolvedReexports = (filePath) => {
}
};
const fixupTypesAfterCompilation = async ({ isWatchMode }) => {
const fixupTypesAfterCompilation = ({ isWatchMode }) => {
log("Fixing SDK d.ts files...");
const dtsFilePaths = glob.sync(`${SDK_DIST_DIR_PATH}/**/*.d.ts`);
@@ -168,97 +123,45 @@ const fixupTypesAfterCompilation = async ({ isWatchMode }) => {
console.log("[dts fixup] Done!");
if (!isWatchMode) {
await generateDtsRollup();
generateDtsRollup();
}
};
const generateDtsRollup = async () => {
const generateDtsRollup = () => {
// Dts rollup logger
const { log, error } = getLogger("dts rollup");
log("Generate dts rollup...");
const existingEntryPointCount = DTS_ROLLUPS.filter(({ entryPointPath }) =>
fs.existsSync(path.resolve(entryPointPath)),
).length;
const dtsRollupEntryPointPath = path.resolve(
path.join(
__dirname,
"../../resources/embedding-sdk/dist/enterprise/frontend/src/embedding-sdk-package/index.d.ts",
),
);
const dtsRollupEntryPointPathExist = fs.existsSync(dtsRollupEntryPointPath);
if (existingEntryPointCount === 0) {
if (!dtsRollupEntryPointPathExist) {
log("It looks like dts rollup is already generated, skipping...");
return;
}
if (existingEntryPointCount !== DTS_ROLLUPS.length) {
DTS_ROLLUPS.forEach(({ entryPointPath, name }) => {
const resolvedEntryPointPath = path.resolve(entryPointPath);
if (!fs.existsSync(resolvedEntryPointPath)) {
error(
`Dts rollup entry point for ${name} does not exist: ${resolvedEntryPointPath}`,
);
}
});
process.exitCode = 1;
return;
}
let hasExtractorFailure = false;
// The extractors are independent (each reads the shared compiled d.ts tree
// and writes its own rollup), so run them concurrently. Output is buffered
// per extractor and printed on completion so parallel runs don't interleave.
const runApiExtractor = ({ configPath, name }) =>
new Promise((resolve) => {
const child = spawn(
process.execPath,
[
API_EXTRACTOR_BIN_PATH,
"run",
"--local",
"--verbose",
"--config",
path.resolve(configPath),
],
{ stdio: ["ignore", "pipe", "pipe"] },
);
let output = "";
child.stdout.on("data", (chunk) => (output += chunk));
child.stderr.on("data", (chunk) => (output += chunk));
child.on("close", (status, signal) =>
resolve({ name, status, signal, output }),
);
});
const results = await Promise.all(
DTS_ROLLUPS.filter(({ entryPointPath, name }) => {
if (fs.existsSync(path.resolve(entryPointPath))) {
return true;
}
error(
`Dts rollup entry point for ${name} does not exist: ${path.resolve(entryPointPath)}`,
);
hasExtractorFailure = true;
return false;
}).map(runApiExtractor),
const apiExtractorConfig = ExtractorConfig.loadFileAndPrepare(
API_EXTRACTOR_CONFIG_PATH,
);
for (const { name, status, signal, output } of results) {
log(`--- api-extractor: ${name} ---`);
process.stdout.write(output);
const extractorResult = Extractor.invoke(apiExtractorConfig, {
localBuild: true,
showVerboseMessages: true,
});
if (status !== 0 || signal) {
error(
`API Extractor failed for ${name}` +
(signal ? ` with signal ${signal}` : ""),
);
hasExtractorFailure = true;
}
}
if (hasExtractorFailure) {
if (!extractorResult.succeeded) {
error(
`API Extractor completed with ${extractorResult.errorCount} errors` +
` and ${extractorResult.warningCount} warnings`,
);
process.exitCode = 1;
return;
}
log("API Extractor completed successfully");
@@ -317,7 +220,7 @@ const isWatchMode = process.argv.includes("--watch");
const run = async () => {
// when running on a clean state and with --watch, the folder might not exist yet
await waitForFolder(SDK_DIST_DIR_PATH);
await fixupTypesAfterCompilation({ isWatchMode });
fixupTypesAfterCompilation({ isWatchMode });
if (isWatchMode) {
console.log("\n\n\n");

View File

@@ -207,42 +207,16 @@
(str/join ", " bad-ids))
{:invalid-ids (vec bad-ids)}))))))
(def ^:private table-scoped-preconditions
"Existence preconditions mapped to the key that scopes them to a single table. Without that key,
Liquibase answers the check by snapshotting the whole schema — ~2 seconds per precondition on a
large postgres appdb, which is how 27 name-only preconditions once added ~50s to every full
migration."
{:primaryKeyExists :tableName
:indexExists :tableName
:uniqueConstraintExists :tableName
:foreignKeyConstraintExists :foreignKeyTableName})
(defn- unscoped-preconditions
"Walk a preConditions tree (maps nested under and/or/not, in either flat-map or list form) and
return [precondition-key required-key] pairs for every table-scoped existence check that is
missing its table key."
[form]
(cond
(map? form) (concat
(for [[precondition required] table-scoped-preconditions
:let [check (get form precondition)]
:when (and (map? check) (not (contains? check required)))]
[precondition required])
(mapcat unscoped-preconditions (vals form)))
(sequential? form) (mapcat unscoped-preconditions form)
:else nil))
(defn require-table-scoped-existence-preconditions
"Ensures that primaryKeyExists/indexExists/uniqueConstraintExists preconditions specify a
tableName, and foreignKeyConstraintExists a foreignKeyTableName."
(defn require-primary-key-exists-has-table-name
"Ensures that all primaryKeyExists preconditions specify a tableName."
[change-log]
(doseq [{:keys [changeSet]} change-log
:when changeSet
:let [[precondition required] (first (unscoped-preconditions (:preConditions changeSet)))]
:when precondition]
:let [s (pr-str (:preConditions changeSet))]
:when (and (str/includes? s ":primaryKeyExists")
(not (str/includes? s ":tableName")))]
(throw (validation-error
(format "Migration '%s' has %s precondition without %s: without it Liquibase snapshots the entire schema to answer the check (~2s per precondition on a large postgres appdb)"
(:id changeSet) (name precondition) (name required))
(format "Migration '%s' has primaryKeyExists precondition without tableName" (:id changeSet))
{:id (:id changeSet)}))))
(s/def ::changeSet
@@ -318,7 +292,7 @@
(require-no-bare-blob-or-text-types change-log)
(require-no-bare-boolean-types change-log file)
(require-no-datetime-type change-log file)
(require-table-scoped-existence-preconditions change-log)
(require-primary-key-exists-has-table-name change-log)
(let [{:keys [changeSet]
:as all} (group-by only-key change-log)]
(when-not (set/subset? (set (keys all)) #{:property :objectQuotingStrategy :changeSet :logicalFilePath})
@@ -394,14 +368,17 @@
(println)
#_:clj-kondo/ignore
(printf "Error in %s:\t%s\n" (:file (ex-data e)) (.getMessage e))
#_:clj-kondo/ignore
(printf "Details:\n\n %s" (with-out-str (pprint/pprint (dissoc (ex-data e) ::validation-error))))
(println))
(do
(pprint/pprint (Throwable->map e))
(println (.getMessage e))))
#_:clj-kondo/ignore
(System/exit 1))
(catch #_:clj-kondo/ignore
Throwable e
(pprint/pprint (Throwable->map e))
(println (.getMessage e))
#_:clj-kondo/ignore
(System/exit 1))))

View File

@@ -398,40 +398,3 @@
:remarks "none"
:type "timestamp with time zone"}}]}}]
:rollback nil))))))
(deftest ^:parallel require-table-scoped-existence-preconditions-test
(testing "foreignKeyConstraintExists without foreignKeyTableName is rejected"
(is-thrown-with-error-info?
"Migration 'v49.2024-01-01T10:30:00' has foreignKeyConstraintExists precondition without foreignKeyTableName: without it Liquibase snapshots the entire schema to answer the check (~2s per precondition on a large postgres appdb)"
{:id "v49.2024-01-01T10:30:00"}
(validate
(mock-change-set
:preConditions [{:onFail "MARK_RAN"}
{:not [{:foreignKeyConstraintExists {:foreignKeyName "fk_my_table_other_id"}}]}]))))
(testing "foreignKeyConstraintExists with foreignKeyTableName passes"
(is (= :ok
(validate
(mock-change-set
:preConditions [{:onFail "MARK_RAN"}
{:not [{:foreignKeyConstraintExists {:foreignKeyName "fk_my_table_other_id"
:foreignKeyTableName "my_table"}}]}])))))
(testing "indexExists without tableName is rejected, including in flat-map not form"
(is-thrown-with-error-info?
"Migration 'v49.2024-01-01T10:30:00' has indexExists precondition without tableName: without it Liquibase snapshots the entire schema to answer the check (~2s per precondition on a large postgres appdb)"
{:id "v49.2024-01-01T10:30:00"}
(validate
(mock-change-set
:preConditions [{:not {:indexExists {:indexName "idx_my_table_other_id"}}}]))))
(testing "indexExists with tableName passes"
(is (= :ok
(validate
(mock-change-set
:preConditions [{:not {:indexExists {:indexName "idx_my_table_other_id"
:tableName "my_table"}}}])))))
(testing "primaryKeyExists without tableName is still rejected (previous rule subsumed)"
(is-thrown-with-error-info?
"Migration 'v49.2024-01-01T10:30:00' has primaryKeyExists precondition without tableName: without it Liquibase snapshots the entire schema to answer the check (~2s per precondition on a large postgres appdb)"
{:id "v49.2024-01-01T10:30:00"}
(validate
(mock-change-set
:preConditions [{:primaryKeyExists {:primaryKeyName "pk_my_table"}}])))))

149
bun.lock
View File

@@ -67,6 +67,7 @@
"@tiptap/starter-kit": "^3.2.0",
"@tiptap/suggestion": "^3.2.0",
"@types/js-yaml": "^4.0.9",
"@types/react-router": "3.0.27",
"@uiw/react-codemirror": "^4.23.6",
"@visx/axis": "^3.10.1",
"@visx/clip-path": "^3.3.0",
@@ -101,6 +102,7 @@
"formik": "^2.4.5",
"frimousse": "^0.3.0",
"fuse.js": "^7.1.0",
"history": "3",
"html2canvas-pro": "^2.0.2",
"icepick": "2.4.0",
"iframe-resizer": "~4.3.2",
@@ -116,7 +118,6 @@
"leaflet-draw": "^0.4.9",
"lodash.debounce": "^4.0.8",
"lodash.orderby": "^4.6.0",
"marked": "18.0.6",
"merge-refs": "^1.3.0",
"mustache": "^2.3.2",
"normalizr": "^3.0.2",
@@ -144,6 +145,7 @@
"react-qr-code": "^2.0.15",
"react-redux": "^9.1.2",
"react-resizable": "^3.1.3",
"react-router": "3",
"react-transition-group": "^4.4.5",
"react-use": "^17.5.1",
"react-virtualized": "9.22.6",
@@ -151,7 +153,6 @@
"redux-undo": "^1.1.0",
"rehype-external-links": "^3.0.0",
"remark-gfm": "^4.0.0",
"remend": "1.3.0",
"reselect": "^5.1.0",
"resize-observer-polyfill": "^1.5.1",
"server-text-width": "^1.0.2",
@@ -276,7 +277,6 @@
"@types/tether": "^1.4.6",
"@types/underscore": "^1.13.0",
"@types/use-sync-external-store": "^1.5.0",
"@vitejs/plugin-react": "^6.0.2",
"ajv": "^8.5.0",
"babel-loader": "^9.1.2",
"babel-plugin-ttag": "^1.8.16",
@@ -378,7 +378,7 @@
"raf": "^3.4.0",
"react-docgen-typescript-plugin": "^1.0.6",
"react-refresh": "^0.14.2",
"react-router": "7.18.1",
"react-router-v7": "npm:react-router@7.18.1",
"reg-cli": "^0.17.7",
"semver": "^7.6.3",
"shadow-cljs": "^3.3.5",
@@ -402,9 +402,6 @@
"typedoc-plugin-redirect": "^1.2.0",
"typescript-eslint": "8.60.0",
"typescript-plugin-css-modules": "^5.1.0",
"vite": "^8.0.16",
"vite-plugin-css-injected-by-js": "^3.5.2",
"vite-plugin-svgr": "^5.2.0",
"web-streams-polyfill": "^4.1.0",
"webpack": "5.105.0",
"webpack-bundle-analyzer": "^4.10.1",
@@ -1278,8 +1275,6 @@
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.34.0", "", {}, "sha512-aKcOkyrorBGlajjRdVoJWHTxfxO1vCNHLJVlSDaRHDIdjU+pX8IYQPvPDkYiujKLbRnWU+1TBwEt0QRgSm4SGA=="],
"@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="],
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.38.0", "", { "os": "android", "cpu": "arm" }, "sha512-lTN4//sgYywK8ulQo7a/EZVzOTGomGQv2IG/7tMYdqTV3xN3QTqWpXcZBGUzaicC4B882N+5zJLYZ37IWfUMcg=="],
"@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.38.0", "", { "os": "android", "cpu": "arm64" }, "sha512-XbVgqR1WsIcCkfxwh2tdg3M1MWgR23YOboW2nbB8ab0gInNNLGy7cIAdr78XaoG/bGdaF4488XRhuGWq67xrzA=="],
@@ -1444,40 +1439,6 @@
"@remirror/core-constants": ["@remirror/core-constants@3.0.0", "", {}, "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="],
"@rspack/binding": ["@rspack/binding@2.0.3", "", { "optionalDependencies": { "@rspack/binding-darwin-arm64": "2.0.3", "@rspack/binding-darwin-x64": "2.0.3", "@rspack/binding-linux-arm64-gnu": "2.0.3", "@rspack/binding-linux-arm64-musl": "2.0.3", "@rspack/binding-linux-x64-gnu": "2.0.3", "@rspack/binding-linux-x64-musl": "2.0.3", "@rspack/binding-wasm32-wasi": "2.0.3", "@rspack/binding-win32-arm64-msvc": "2.0.3", "@rspack/binding-win32-ia32-msvc": "2.0.3", "@rspack/binding-win32-x64-msvc": "2.0.3" } }, "sha512-4exVNhGhW5RFHjK87XeTKbkA/qAgI5NHJlT1jNqiJv0gcUXLqTOEU3w7f8+f9zUo4JMFvPc0c9veOi4M19YYTg=="],
"@rspack/binding-darwin-arm64": ["@rspack/binding-darwin-arm64@2.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4UyCjLJwU/WxR6K1/gG4u3+jUsoaRHJ5rNu9fto/UbvrItwdlVNULChAApqZFw6mcSetMddSjSICeuj5pSB6sA=="],
@@ -2008,6 +1969,8 @@
"@types/react-resizable": ["@types/react-resizable@3.0.4", "", { "dependencies": { "@types/react": "*" } }, "sha512-+QguN9CDfC1lthq+4noG1fkxh8cqkV2Fv/Mu3mdknCCBiwwNLecnBdk1MmNNN7uJpT23Nx/aVkYsbt5NuWouFw=="],
"@types/react-router": ["@types/react-router@3.0.27", "", { "dependencies": { "@types/history": "^3", "@types/react": "*" } }, "sha512-Sq4bfTZ8TuHFTHXyzA3tu/BTGm/d4B8CwtOK4qOyJNoJMkY87qMOazkBizMHNpszVR1+kYOCNLjPdh/wAD7ESw=="],
"@types/react-textarea-autosize": ["@types/react-textarea-autosize@4.3.6", "", { "dependencies": { "@types/react": "*" } }, "sha512-cTf8tCem0c8A7CERYbTuF+bRFaqYu7N7HLCa6ZhUhDx8XnUsTpGx5udMWljt87JpciUKuUkImKPEsy6kcKhrcQ=="],
"@types/react-transition-group": ["@types/react-transition-group@4.4.5", "", { "dependencies": { "@types/react": "*" } }, "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA=="],
@@ -2194,8 +2157,6 @@
"@visx/vendor": ["@visx/vendor@3.5.0", "", { "dependencies": { "@types/d3-array": "3.0.3", "@types/d3-color": "3.1.0", "@types/d3-delaunay": "6.0.1", "@types/d3-format": "3.0.1", "@types/d3-geo": "3.1.0", "@types/d3-interpolate": "3.0.1", "@types/d3-scale": "4.0.2", "@types/d3-time": "3.0.0", "@types/d3-time-format": "2.1.0", "d3-array": "3.2.1", "d3-color": "3.1.0", "d3-delaunay": "6.0.2", "d3-format": "3.1.0", "d3-geo": "3.1.0", "d3-interpolate": "3.0.1", "d3-scale": "4.0.2", "d3-time": "3.1.0", "d3-time-format": "4.1.0", "internmap": "2.0.3" } }, "sha512-yt3SEZRVmt36+APsCISSO9eSOtzQkBjt+QRxNRzcTWuzwMAaF3PHCCSe31++kkpgY9yFoF+Gfes1TBe5NlETiQ=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg=="],
"@vitest/expect": ["@vitest/expect@2.0.5", "", { "dependencies": { "@vitest/spy": "2.0.5", "@vitest/utils": "2.0.5", "chai": "^5.1.1", "tinyrainbow": "^1.2.0" } }, "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA=="],
"@vitest/pretty-format": ["@vitest/pretty-format@2.1.8", "", { "dependencies": { "tinyrainbow": "^1.2.0" } }, "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ=="],
@@ -2720,6 +2681,8 @@
"create-hmac": ["create-hmac@1.1.7", "", { "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", "inherits": "^2.0.1", "ripemd160": "^2.0.0", "safe-buffer": "^5.0.1", "sha.js": "^2.4.8" } }, "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg=="],
"create-react-class": ["create-react-class@15.7.0", "", { "dependencies": { "loose-envify": "^1.3.1", "object-assign": "^4.1.1" } }, "sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng=="],
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
"cron-expression-validator": ["cron-expression-validator@1.0.20", "", {}, "sha512-g0osBTdp+1ryDw2vzlG6UpDPaa4fO94ZChF2R0lEnRurbuUEL74XEVX7xZJ13m4Mq/gb3ni6UQu8+Oqt+eocsw=="],
@@ -2942,7 +2905,7 @@
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
"detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="],
@@ -3156,7 +3119,7 @@
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
@@ -3438,6 +3401,8 @@
"headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="],
"history": ["history@3.3.0", "", { "dependencies": { "invariant": "^2.2.1", "loose-envify": "^1.2.0", "query-string": "^4.2.2", "warning": "^3.0.0" } }, "sha1-/O3M6PEpdTcVRdc1RhAzV5ptrpw="],
"hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE="],
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
@@ -3888,30 +3853,6 @@
"lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"lines-and-columns": ["lines-and-columns@1.1.6", "", {}, "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="],
@@ -4012,7 +3953,7 @@
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
"marked": ["marked@18.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-MrV5puXBfuiy6wl6DLaq3BtIJQAJToAd5zt/ZKhRfGRAuFPALE7/4Y7jnxRQoEgK/pBgurGqLyAuRgZ2xOjr6w=="],
"marked": ["marked@4.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="],
"marky": ["marky@1.2.5", "", {}, "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q=="],
@@ -4688,6 +4629,8 @@
"qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="],
"query-string": ["query-string@4.3.4", "", { "dependencies": { "object-assign": "^4.1.0", "strict-uri-encode": "^1.0.0" } }, "sha1-u7aTucqRXCMlFbIosaArYJBD2+s="],
"querystring": ["querystring@0.2.0", "", {}, "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="],
"querystring-es3": ["querystring-es3@0.2.1", "", {}, "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="],
@@ -4770,7 +4713,9 @@
"react-resizable": ["react-resizable@3.1.3", "", { "dependencies": { "prop-types": "15.x", "react-draggable": "^4.5.0" }, "peerDependencies": { "react": ">= 16.3", "react-dom": ">= 16.3" } }, "sha512-liJBNayhX7qA4tBJiBD321FDhJxgGTJ07uzH5zSORXoE8h7PyEZ8mLqmosST7ppf6C4zUsbd2gzDMmBCfFp9Lw=="],
"react-router": ["react-router@7.18.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg=="],
"react-router": ["react-router@3.2.6", "", { "dependencies": { "create-react-class": "^15.5.1", "history": "^3.0.0", "hoist-non-react-statics": "^3.3.2", "invariant": "^2.2.1", "loose-envify": "^1.2.0", "prop-types": "^15.7.2", "react-is": "^16.13.0", "warning": "^3.0.0" } }, "sha512-nlxtQE8B22hb/JxdaslI1tfZacxFU8x8BJryXOnR2RxB4vc01zuHYAHAIgmBkdk1kzXaA25hZxK6KAH/+CXArw=="],
"react-router-v7": ["react-router@7.18.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg=="],
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" } }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
@@ -4866,8 +4811,6 @@
"remedial": ["remedial@1.0.8", "", {}, "sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg=="],
"remend": ["remend@1.3.0", "", {}, "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw=="],
"remove-trailing-spaces": ["remove-trailing-spaces@1.0.8", "", {}, "sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA=="],
"renderkid": ["renderkid@3.0.0", "", { "dependencies": { "css-select": "^4.1.3", "dom-converter": "^0.2.0", "htmlparser2": "^6.1.0", "lodash": "^4.17.21", "strip-ansi": "^6.0.1" } }, "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg=="],
@@ -4926,8 +4869,6 @@
"robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
"rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="],
"rollup-plugin-inject": ["rollup-plugin-inject@3.0.2", "", { "dependencies": { "estree-walker": "^0.6.1", "magic-string": "^0.25.3", "rollup-pluginutils": "^2.8.1" } }, "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w=="],
"rollup-plugin-node-polyfills": ["rollup-plugin-node-polyfills@0.2.1", "", { "dependencies": { "rollup-plugin-inject": "^3.0.0" } }, "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA=="],
@@ -5152,6 +5093,8 @@
"strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="],
"strict-uri-encode": ["strict-uri-encode@1.1.0", "", {}, "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="],
"string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
"string-length": ["string-length@6.0.0", "", { "dependencies": { "strip-ansi": "^7.1.0" } }, "sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg=="],
@@ -5518,12 +5461,6 @@
"vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
"vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="],
"vite-plugin-css-injected-by-js": ["vite-plugin-css-injected-by-js@3.5.2", "", { "peerDependencies": { "vite": ">2.0.0-0" } }, "sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ=="],
"vite-plugin-svgr": ["vite-plugin-svgr@5.2.0", "", { "dependencies": { "@rollup/pluginutils": "^5.3.0", "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0" }, "peerDependencies": { "vite": ">=3.0.0" } }, "sha512-qj2eAKF8C6PZWemVTvQA0xgQIcP1hHU6Buh7fl6BhvayWwnuxE+z417miKxeDvRWbDrupQ1oK99hfxElopJ3sQ=="],
"vitest-axe": ["vitest-axe@0.1.0", "", { "dependencies": { "aria-query": "^5.0.0", "axe-core": "^4.4.2", "chalk": "^5.0.1", "dom-accessibility-api": "^0.5.14", "lodash-es": "^4.17.21", "redent": "^3.0.0" } }, "sha512-jvtXxeQPg8R/2ANTY8QicA5pvvdRP4F0FsVUAHANJ46YCDASie/cuhlSzu0DGcLmZvGBSBNsNuK3HqfaeknyvA=="],
"vm-browserify": ["vm-browserify@1.1.2", "", {}, "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="],
@@ -5536,6 +5473,8 @@
"walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="],
"warning": ["warning@3.0.0", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w="],
"watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="],
"watskeburt": ["watskeburt@5.0.3", "", { "bin": { "watskeburt": "dist/run-cli.js" } }, "sha512-g9CXukMjazlJJVQ3OHzXsnG25KFYgSgKMIyoJrD8ggr0DbS9UNF7OzIqWmmKKBMedkxj3T01uqEaGnn+y7QhMA=="],
@@ -5876,8 +5815,6 @@
"@nicolo-ribaudo/eslint-scope-5-internals/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
"@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
"@pmmmwh/react-refresh-webpack-plugin/source-map": ["source-map@0.7.3", "", {}, "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="],
"@redocly/cli/ajv": ["@redocly/ajv@8.17.4", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-BieiCML/IgP6x99HZByJSt7fJE4ipgzO7KAFss92Bs+PEI35BhY7vGIysFXLT+YmS7nHtQjZjhOQyPPEf7xGHA=="],
@@ -5898,16 +5835,6 @@
"@redocly/respect-core/colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
"@rolldown/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
"@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
"@rollup/pluginutils/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@rollup/pluginutils/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"@rushstack/node-core-library/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"@rushstack/node-core-library/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="],
@@ -6128,8 +6055,12 @@
"@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@vue/compiler-sfc/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
"@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@vue/compiler-sfc/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"@vue/compiler-sfc/postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="],
@@ -7022,7 +6953,9 @@
"react-remove-scroll-bar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"react-router/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"react-router/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"react-router-v7/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"react-style-singleton/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -7052,8 +6985,6 @@
"redoc/classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
"redoc/marked": ["marked@4.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="],
"reduce-css-calc/balanced-match": ["balanced-match@0.4.2", "", {}, "sha512-STw03mQKnGUYtoNjmowo4F2cRmIIxYEGiMsjjwla/u5P1lxadj/05WkNaFjNiKTgJkj8KiXbgAiRTmcQRwQNtg=="],
"reduce-function-call/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
@@ -7076,12 +7007,8 @@
"ripemd160/hash-base": ["hash-base@3.1.0", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", "safe-buffer": "^5.2.0" } }, "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA=="],
"rollup-plugin-inject/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
"rollup-plugin-inject/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="],
"rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
"router/path-to-regexp": ["path-to-regexp@8.4.0", "", {}, "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg=="],
"rxjs/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
@@ -7270,12 +7197,6 @@
"vfile-message/@types/unist": ["@types/unist@3.0.2", "", {}, "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="],
"vite/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
"vite/postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="],
"vite/tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"vitest-axe/aria-query": ["aria-query@5.0.0", "", {}, "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg=="],
"vitest-axe/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
@@ -7542,14 +7463,6 @@
"@redocly/cli/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
"@rolldown/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@rolldown/binding-wasm32-wasi/@emnapi/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@rolldown/binding-wasm32-wasi/@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
"@storybook/addon-docs/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
"@storybook/addon-webpack5-compiler-babel/babel-loader/find-cache-dir": ["find-cache-dir@4.0.0", "", { "dependencies": { "common-path-prefix": "^3.0.0", "pkg-dir": "^7.0.0" } }, "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg=="],
@@ -8418,8 +8331,6 @@
"unbox-primitive/which-boxed-primitive/is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
"vite/postcss/nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
"webpack-dev-middleware/mime-types/mime-db": ["mime-db@1.51.0", "", {}, "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="],
"webpack-dev-middleware/schema-utils/@types/json-schema": ["@types/json-schema@7.0.9", "", {}, "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="],
@@ -8606,8 +8517,6 @@
"@redocly/cli/glob/minimatch/brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@storybook/addon-webpack5-compiler-babel/babel-loader/find-cache-dir/pkg-dir": ["pkg-dir@7.0.0", "", { "dependencies": { "find-up": "^6.3.0" } }, "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA=="],
"@storybook/addon-webpack5-compiler-babel/babel-loader/schema-utils/@types/json-schema": ["@types/json-schema@7.0.9", "", {}, "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="],

View File

@@ -126,7 +126,7 @@
{:mvn/version "2.26.1"} ; allows the custom json logging format
org.apache.logging.log4j/log4j-slf4j2-impl
{:mvn/version "2.26.1"} ; allows the slf4j2 API to work with log4j 2
org.apache.pdfbox/pdfbox {:mvn/version "3.0.8"} ; PDF generation (backend dashboard PDF prototype)
org.apache.pdfbox/pdfbox {:mvn/version "3.0.5"} ; PDF generation (backend dashboard PDF prototype)
org.apache.poi/poi {:mvn/version "5.5.1"} ; Work with Office documents (e.g. Excel spreadsheets) -- newer version than one specified by Docjure
org.apache.poi/poi-ooxml {:mvn/version "5.5.1"
:exclusions [org.bouncycastle/bcpkix-jdk15on

View File

@@ -158,6 +158,7 @@
(let [h2-dbs (t2/select :model/Database :engine :h2)
in-memory? (fn [db] (some-> db :details :db (str/starts-with? "mem:")))
can-connect? (fn [db]
#_:clj-kondo/ignore
(binding [driver.settings/*allow-testing-h2-connections* true]
(try
(driver/can-connect? :h2 (:details db))

View File

@@ -1,252 +0,0 @@
(ns dev.kondo-ratchet
"Ratchet on inline kondo ignore forms.
Per-linter budgets live in `.clj-kondo/ratchets.edn`.
`metabase.core.kondo-ratchet-test` fails when they drift from the tree;
`./bin/mage fix-kondo-ratchets` lowers budgets, never raises them.
Loaded by both the bb task and the JVM test, so keep it dependency-free."
{:clj-kondo/config '{:linters {:discouraged-var {clojure.core/println {:level :off}}}}}
(:require
[clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.string :as str]))
(set! *warn-on-reflection* true)
(def ratchets-file
"The budgets file, relative to the repo root."
".clj-kondo/ratchets.edn")
(def ^:private source-roots
["src" "test" "enterprise" "modules/drivers" "dev" "bin" "mage"])
(def ^:private source-extensions
[".clj" ".cljc" ".cljs"])
;; Concatenated so this file never contains a literal ignore marker.
(def ^:private ignore-marker
(str ":clj-kondo" "/ignore"))
;; `#_{... [:some-linter]}`, `^{... [:some-linter]}`, and the prefix-less attr-map form
;; `(ns foo {... [:some-linter]})`; the vector may span lines. The lazy tail after the vector runs to the
;; map's own closing brace, so extra keys still count and removal spans the whole form; a nested-brace
;; value stops the match at the vector instead.
(def ^:private vector-form-re
(re-pattern (str "(?:(?:#_|\\^)\\s*)?\\{\\s*" ignore-marker "\\s*\\[([^\\]]*)\\](?:[^{}]*?\\})?")))
;; Bare `#_kw` / `^kw` with no linter vector: suppresses every linter on the next form.
(def ^:private bare-form-re
(re-pattern (str "(?:#_\\s*|\\^)" ignore-marker "(?![\\w./-])")))
(defn mask-strings-and-comments
"`content` with string-literal and line-comment interiors replaced by spaces, newlines kept.
Same length as the input, so offsets and line numbers carry over.
Ignore forms inside strings (test fixtures) or commented-out code must not count."
[content]
(let [sb (StringBuilder. ^String content)
n (count content)]
(loop [i 0, state :code]
(if (>= i n)
(str sb)
(let [c (.charAt sb i)]
(case state
:code (case c
\" (recur (inc i) :string)
\; (do (.setCharAt sb i \space)
(recur (inc i) :comment))
;; char literal: never treat the next char as a delimiter
\\ (recur (+ i 2) :code)
(recur (inc i) :code))
:string (case c
\" (recur (inc i) :code)
\\ (do (.setCharAt sb i \space)
(when (< (inc i) n)
(when-not (= (.charAt sb (inc i)) \newline)
(.setCharAt sb (inc i) \space)))
(recur (+ i 2) :string))
\newline (recur (inc i) :string)
(do (.setCharAt sb i \space)
(recur (inc i) :string)))
:comment (if (= c \newline)
(recur (inc i) :code)
(do (.setCharAt sb i \space)
(recur (inc i) :comment)))))))))
(defn- linter-keywords
[vector-contents]
(map (comp keyword #(subs % 1))
(re-seq #":[A-Za-z][A-Za-z0-9*+!?<>=._/-]*" vector-contents)))
(defn line-linters
"Linter keywords suppressed by inline ignore forms on `line`.
The bare vector-less form counts as `:all`.
Like [[scan]], ignore forms inside string literals or line comments don't count."
[line]
(let [masked (mask-strings-and-comments line)]
(concat (mapcat (comp linter-keywords second) (re-seq vector-form-re masked))
(repeat (count (re-seq bare-form-re masked)) :all))))
(defn- offset->line
"1-based line number of character offset `i` in `content`."
[content i]
(inc (count (filter #(= % \newline) (subs content 0 i)))))
(defn- matches-with-offsets
"Like re-seq, but returns `{:start _, :end _, :linters [...]}` for each match of `re` in `masked`."
[re masked bare?]
(let [m (re-matcher re masked)]
(loop [acc []]
(if (.find m)
(recur (conj acc {:start (.start m)
:end (.end m)
:linters (if bare? [:all] (vec (linter-keywords (.group m 1))))}))
acc))))
(defn ignore-matches
"Inline ignore matches in `content`, in file order:
`{:start _, :end _, :line _, :linters [...]}` with character offsets and a 1-based line.
Matches inside string literals or line comments are excluded."
[content]
(let [masked (mask-strings-and-comments content)]
(->> (concat (matches-with-offsets vector-form-re masked false)
(matches-with-offsets bare-form-re masked true))
(sort-by :start)
(map #(assoc % :line (offset->line masked (:start %)))))))
(defn scan
"Occurrences of inline ignore forms under `roots` (relative to the repo root).
Returns `{:file \"src/...\", :line 42, :linters [...]}` maps.
Forms inside string literals or line comments don't count."
([]
(scan source-roots))
([roots]
(for [root roots
^java.io.File f (file-seq (io/file root))
:when (and (.isFile f)
(some #(str/ends-with? (.getPath f) %) source-extensions))
:let [content (slurp f)]
:when (str/includes? content ignore-marker)
m (ignore-matches content)]
{:file (.getPath f)
:line (:line m)
:linters (:linters m)})))
(defn actual-counts
"Per-linter occurrence counts for `occurrences`, as returned by [[scan]]."
[occurrences]
(frequencies (mapcat :linters occurrences)))
(defn- sorted-by-str
[kvs]
(into (sorted-map-by #(compare (str %1) (str %2))) kvs))
(defn drift
"Linters whose count in `occurrences` differs from its budget in `recorded` (absent = 0, either side).
Returns `{linter {:recorded _, :actual _}}`, plus `:examples` (up to 5 `file:line`) when over budget."
[recorded occurrences]
(let [actual (actual-counts occurrences)]
(sorted-by-str
(for [linter (into (set (keys actual)) (keys recorded))
:let [budget (get recorded linter 0)
n (get actual linter 0)]
:when (not= budget n)]
[linter (cond-> {:recorded budget, :actual n}
(> n budget)
(assoc :examples (->> occurrences
(filter #(some #{linter} (:linters %)))
(map #(str (:file %) ":" (:line %)))
(take 5)
vec)))]))))
(defn read-ratchets
"Parsed contents of [[ratchets-file]], with empty defaults when the file doesn't exist."
[]
(merge {:ignore-counts {}}
(when (.exists (io/file ratchets-file))
(edn/read-string (slurp ratchets-file)))))
(def ^:private header
(str ";; Per-linter budgets for inline `" ignore-marker "` forms.\n"
";; metabase.core.kondo-ratchet-test fails when the budgets drift from the actual counts.\n"
";; `./bin/mage fix-kondo-ratchets` lowers budgets to match the tree; local test runs do it\n"
";; automatically. Raising a budget, or adding one for a new linter (`--seed`), is a hand edit\n"
";; to defend in your PR.\n"
";; :all is the vector-less ignore form, which suppresses every linter on the next form.\n"))
(defn render
"Text of the ratchets file for the `{:ignore-counts _}` map `ratchets`.
Byte-stable: [[fix!]] idempotency and the file-hygiene test depend on it."
[{:keys [ignore-counts]}]
(let [counts-indent (apply str (repeat (count "{:ignore-counts {") \space))]
(str header
"{:ignore-counts "
(if (empty? ignore-counts)
"{}"
(let [entries (sort-by (comp str first) ignore-counts)
width (apply max (map (comp count str first) entries))]
(str "{"
(str/join (str "\n" counts-indent)
(for [[linter n] entries]
(format (str "%-" width "s %d") (str linter) n)))
"}")))
"}\n")))
(defn lowered-counts
"`recorded` with each budget lowered to its actual count; entries with no ignores left are dropped.
Linters in `seeded` get their budget set to the actual count outright — the explicit escape hatch for
landing a new linter. Otherwise never raises a budget, never adds one."
[recorded actual seeded]
(into (sorted-by-str
(for [linter seeded
:when (pos? (get actual linter 0))]
[linter (get actual linter)]))
(keep (fn [[linter budget]]
(let [n (get actual linter 0)]
(cond
(contains? (set seeded) linter) nil
(zero? n) nil
(< n budget) [linter n]
:else [linter budget]))))
recorded))
(defn change-report
"The lines [[fix!]] prints: lowered/dropped/seeded budgets, plus warnings for anything over budget."
[{:keys [ignore-counts]} occurrences seeded]
(let [actual (actual-counts occurrences)]
(concat
(for [linter seeded
:let [n (get actual linter 0)]]
(if (pos? n)
(format "seeded %s at %d" linter n)
(format "WARNING: %s has no inline ignores -- nothing to seed" linter)))
(for [[linter budget] (sort-by (comp str first) (apply dissoc ignore-counts seeded))
:let [n (get actual linter 0)]
:when (not= n budget)]
(cond
(zero? n) (format "dropped %s (no ignores left)" linter)
(< n budget) (format "lowered %s %d -> %d" linter budget n)
:else (format "WARNING: %s is over budget (%d recorded, %d actual) -- remove ignores, or accept them all with `--seed %s`"
linter budget n linter)))
(for [[linter n] (sort-by (comp str first) (apply dissoc actual (concat seeded (keys ignore-counts))))]
(format "WARNING: %s has %d ignores but no budget entry -- seed one with `./bin/mage fix-kondo-ratchets --seed %s`"
linter n linter)))))
(defn fix!
"Rewrite [[ratchets-file]]: lower budgets and normalize formatting.
`--seed LINTER` (`{:seed \"...\"}` here) sets that budget to the actual count, adding or raising it.
Prints the [[change-report]], or `unchanged` on a no-op."
([]
(fix! nil))
([{:keys [seed]}]
(let [{:keys [ignore-counts] :as ratchets} (read-ratchets)
occurrences (scan)
seeded (if seed [(keyword (str/replace-first seed #"^:" ""))] [])
actual (actual-counts occurrences)
text (render {:ignore-counts (lowered-counts ignore-counts actual seeded)})
file (io/file ratchets-file)
old (when (.exists file) (slurp file))]
(run! println (change-report ratchets occurrences seeded))
(if (= old text)
(println "unchanged")
(do (spit file text)
(println (str "wrote " ratchets-file)))))))

View File

@@ -182,7 +182,6 @@ Metabase's reference documentation.
- [Managing people and groups](./people-and-groups/managing.md)
- [Password complexity](./people-and-groups/changing-password-complexity.md)
- [Session expiration](./people-and-groups/changing-session-expiration.md)
- [Two-factor authentication](./people-and-groups/two-factor-authentication.md)
- [Google Sign-In](./people-and-groups/google-sign-in.md)
- [LDAP](./people-and-groups/ldap.md)
- [API keys](./people-and-groups/api-keys.md)

View File

@@ -76,7 +76,7 @@ If something goes wrong, Metabase will display the error message it received fro
### Example `UPDATE` action
You could write an action that would update the `plan` column for a record in the `invoices` table:
You could write an action that would update the `plan` column for a record in the `invoices` table in the Sample Database:
```sql
{% raw %}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -15,7 +15,7 @@ System prompts let you customize instructions for Metabot. You can write a separ
- **Natural language queries**: query-builder questions generated from natural language.
- **SQL generation**: SQL written from prompts (sidebar or [inline](./metabot.md#inline-sql-editing)).
Like text cards on dashboards, prompts support [Markdown](../dashboards/markdown.md).
Like text cards on dashboards, prompts support [Markdown](https://www.metabase.com/learn/metabase-basics/querying-and-dashboards/dashboards/markdown).
## Prompt tips

71
docs/api.json generated
View File

@@ -25642,7 +25642,7 @@
"required" : true,
"schema" : {
"type" : "string",
"enum" : [ "metabase-ai", "metabase-ai-tiered", "metabase-ai-managed", "python-execution", "transforms", "transforms-basic", "transforms-advanced", "transforms-basic-metered", "transforms-advanced-metered" ]
"enum" : [ "metabase-ai", "metabase-ai-tiered", "metabase-ai-managed", "python-execution", "transforms", "transforms-basic", "transforms-advanced", "transforms-basic-metered", "transforms-advanced-metered", "dwh-rent", "etl-connections" ]
}
} ],
"responses" : {
@@ -25693,7 +25693,7 @@
"required" : true,
"schema" : {
"type" : "string",
"enum" : [ "metabase-ai", "metabase-ai-tiered", "metabase-ai-managed", "python-execution", "transforms", "transforms-basic", "transforms-advanced", "transforms-basic-metered", "transforms-advanced-metered" ]
"enum" : [ "metabase-ai", "metabase-ai-tiered", "metabase-ai-managed", "python-execution", "transforms", "transforms-basic", "transforms-advanced", "transforms-basic-metered", "transforms-advanced-metered", "dwh-rent", "etl-connections" ]
}
} ],
"responses" : {
@@ -28948,7 +28948,7 @@
"post" : {
"operationId" : "post-api-ee-mfa-admin-remove",
"summary" : "POST /api/ee/mfa/admin/remove",
"description" : "Admin: remove *another* user's two-factor enrollment entirely — the lockout escape hatch for a\n lost authenticator with no recovery codes. Never feature-gated (a lapsed license must not make\n lockouts permanent). The affected user is notified by email. They re-enroll from scratch — there\n is nothing to \"reset\", the secret lives on their device.\n\n Removing your *own* enrollment is deliberately refused here: this is a user-management endpoint\n that takes no second factor (the target can't produce one — that's why an admin is removing it),\n and the admin UI never collects a code. Self-removal goes through the normal Security page, which\n re-auths with a fresh factor via `/disable`. Without this guard a hijacked admin session could\n strip its own 2FA with only a cookie, turning transient access into a permanent password bypass.",
"description" : "Admin: remove a user's two-factor enrollment entirely — the lockout escape hatch for a lost\n authenticator with no recovery codes. Never feature-gated (a lapsed license must not make\n lockouts permanent). The affected user is notified by email. They re-enroll from scratch —\n there is nothing to \"reset\", the secret lives on their device.",
"parameters" : [ ],
"responses" : {
"2XX" : {
@@ -33742,7 +33742,7 @@
"delete" : {
"operationId" : "delete-api-ee-workspace-manager-id",
"summary" : "DELETE /api/ee/workspace-manager/{id}",
"description" : "Delete a Workspace. Tears down every database's warehouse isolation first\n (blocking, any state). Each database is either fully torn down (its row is\n deleted) or kept; when any teardown fails the workspace is kept too — with the\n failed rows, so the delete can be retried — and the combined failure is\n returned as the error. 204 on success, no response body.",
"description" : "Delete a Workspace. Tears down each `:provisioned` database's warehouse isolation\n first (blocking). Refuses with a 409 if any database is still `:provisioning`/\n `:deprovisioning` unless `ignore_pending=true`, in which case those databases are\n left in the warehouse and only their app-DB rows are removed. If the warehouse was\n unreachable for some `:provisioned` databases, the response includes\n `:orphaned_resources` and a `:message` describing the inert schema/user objects\n left behind for manual cleanup.",
"parameters" : [ {
"in" : "path",
"name" : "id",
@@ -33752,6 +33752,18 @@
"minimum" : 1
},
"description" : "value must be an integer greater than zero."
}, {
"in" : "query",
"name" : "ignore-pending",
"required" : false,
"schema" : {
"default" : false,
"oneOf" : [ {
"type" : "boolean"
}, {
"type" : "null"
} ]
}
} ],
"responses" : {
"2XX" : {
@@ -33759,7 +33771,56 @@
"content" : {
"application/json" : {
"schema" : {
"type" : "null"
"type" : "object",
"properties" : {
"deleted" : {
"type" : "boolean"
},
"id" : {
"description" : "value must be an integer greater than zero.",
"type" : "integer",
"minimum" : 1
},
"message" : {
"type" : "string"
},
"orphaned_resources" : {
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"database_id" : {
"description" : "value must be an integer greater than zero.",
"type" : "integer",
"minimum" : 1
},
"driver" : {
"type" : "string"
},
"reason" : {
"oneOf" : [ {
"type" : "string"
}, {
"type" : "null"
} ]
},
"schema" : {
"type" : "string"
},
"user" : {
"type" : "string"
},
"workspace_database_id" : {
"description" : "value must be an integer greater than zero.",
"type" : "integer",
"minimum" : 1
}
},
"required" : [ "workspace_database_id", "database_id", "driver", "schema", "user" ]
}
}
},
"required" : [ "id", "deleted" ]
}
}
}

View File

@@ -20,7 +20,7 @@ Metabase Cloud only works with:
- [Metabase's officially supported databases](../databases/connecting.md#connecting-to-supported-databases) (with the exception of SQLite and H2).
Metabase Cloud doesn't support [community database drivers](../developers-guide/community-drivers.md), or file-based databases (SQLite), because there's currently no file storage available.
Metabase Cloud doesn't support [community database drivers](../developers-guide/community-drivers.md), or file-based databases (SQLite and H2), because there's currently no file storage available.
## Limited custom certificate support

View File

@@ -6,9 +6,9 @@ title: "Configuration file"
{% include plans-blockquote.html feature="Loading from a configuration file" self-hosted-only="true" %}
On self-hosted [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans, Metabase supports initialization on launch from a config file. The config file should be located at:
On self-hosted [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans, Metabase supports initialization on launch from a config file named `config.yml`. The config file should be located at:
- A file named `config.yml` or `config.yaml` in the current directory (the directory where the running Metabase JAR is located).
- The current directory (the directory where the running Metabase JAR is located).
- The path specified by the [`MB_CONFIG_FILE_PATH`](./environment-variables.md#mb_config_file_path) environment variable.
The settings in the config file work the same as if you'd set the settings in the Admin Settings in your Metabase. Settings defined in this configuration file will update any existing settings. If, for example, a database already exists (that is, you'd already added it via the initial set up or **Admin settings** > **Databases**, Metabase will update the database entry based on the data in the config file). Which means: if you define a setting in the config file, and then later change that setting in your Metabase application, keep in mind that the config file will overwrite that change whenever Metabase restarts. Let's reiterate that in a blockquote:

View File

@@ -1881,7 +1881,8 @@ on your IdP, this usually looks something like `http://www.example.com/141xkex60
- Default: `null`
- [Configuration file name](./config-file.md): `saml-identity-provider-slo-uri`
If SAML single logout (SLO) is enabled, Metabase will make an HTTP-Redirect SLO request to this endpoint when a user logs out of Metabase.
This is the URL where your users go to logout of your identity provider. Depending on which IdP you're
using, this usually looks like `https://your-org-name.example.com` or `https://example.com/app/my_saml_app/abc123/sso/slo`.
### `MB_SAML_IDENTITY_PROVIDER_URI`
@@ -1932,7 +1933,7 @@ Absolute path to the Keystore file to use for signing SAML requests.
- Default: `false`
- [Configuration file name](./config-file.md): `saml-slo-enabled`
If enabled, Metabase will redirect users to your configured SAML Single Logout endpoint when they log out of Metabase.
Is SAML Single Log Out enabled?
### `MB_SAML_USER_PROVISIONING_ENABLED`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View File

@@ -82,7 +82,7 @@ You can use [Markdown](https://commonmark.org/help/) to format the text in your
![Result](images/result.png)
To learn more, see [Format text with Markdown](./markdown.md).
To learn more, see [Fun with Markdown in your dashboards](https://www.metabase.com/learn/metabase-basics/querying-and-dashboards/dashboards/markdown).
### Including variables in text cards
@@ -321,7 +321,7 @@ See [Caching per dashboard](../configuring-metabase/caching.md#dashboard-caching
## Sharing dashboards with public links
If your Metabase administrator has enabled [public sharing](../embedding/public-links.md) on a saved question or dashboard, you can go to that question or dashboard and click on the **Share** icon to find its public links.
If your Metabase administrator has enabled [public sharing](../embedding/public-links.md) on a saved question or dashboard, you can go to that question or dashboard and click on the **sharing** icon to find its public links.
![Share icon](./images/share-icon.png)

View File

@@ -79,7 +79,7 @@ To nest a bulleted item under another bulleted item, indent the nested item with
To add a link, put the link text in square brackets, followed by the URL in parentheses:
```text
[the internet's finest webpage](https://www.example.com)
[the internet's finest webpage](https://www.metabase.com)
```
This renders as a clickable link: [the internet's finest webpage](https://www.metabase.com).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -34,7 +34,7 @@ People in the Data Analyst group will have table metadata and data structure acc
_Data Studio > Tables_
You can see all tables in all databases connected to your Metabase in **Data Studio > Tables**, together with their owner, visibility layer, row count, and published state. For now, Metabase only displays row counts for PostgreSQL tables.
You can see all tables in all databases connected to your Metabase in **Data Studio > Tables**, together with their owner, row count, and published state. For now, Metabase only displays row counts for PostgreSQL tables.
You can search for table names, but the search will only match beginnings of words in table names. So for example, if you search for "base", results will include names like "Baseball stats" and "All your base are belong to us", but the results won't include tables like "Metabase secrets".

View File

@@ -48,7 +48,7 @@ The databases listed below have official drivers maintained by the Metabase team
If you don't see your database listed here, see [Community drivers](../developers-guide/community-drivers.md).
As of version 46.6.4, Metabase [no longer supports H2 connections](https://www.metabase.com/blog/vulnerability-post-mortem). But Metabase still ships with an H2 database to include an embedded application database.
As of version 46.6.4, Metabase [no longer supports H2 connections](https://www.metabase.com/blog/vulnerability-post-mortem). But Metabase still ships with an H2 database to include an embedded application database, as well as to provide some sample data out of the box.
## Connecting to databases hosted by a cloud provider

View File

@@ -36,6 +36,8 @@ There are a few things admins need to do to support CSV uploads:
To upload data to Metabase, an admin will need to connect your Metabase to a database that supports uploads using a database user account that has write access to that database.
You can also upload data to the Sample Database included with Metabase (an H2 database), though we don't recommend using the Sample Database for any data that you want to keep around.
For more, check out:
- [Adding and managing databases](./connecting.md)

View File

@@ -8,16 +8,6 @@ title: API changelog
- Self-hosted environments must now explicitly enable transforms before beginning to use them via the API. Admins can enable transforms in Data Studio or by setting the MB_TRANSFORMS_ENABLED environment variable to true.
- The endpoints that fetch prefill values for action forms have been converted from GET to POST so that parameter values are sent in the JSON request body instead of the URL query string. `parameters` is now a JSON object in the request body rather than a JSON-encoded query-string parameter:
- `GET /api/action/:action-id/execute` has been replaced by `POST /api/action/:action-id/execute/values`.
- `GET /api/dashboard/:dashboard-id/dashcard/:dashcard-id/execute` has been replaced by
`POST /api/dashboard/:dashboard-id/dashcard/:dashcard-id/execute/values`.
- `GET /api/public/dashboard/:uuid/dashcard/:dashcard-id/execute` has been replaced by
`POST /api/public/dashboard/:uuid/dashcard/:dashcard-id/execute/values`.
The GET variants have been removed without a deprecation period.
## Metabase 0.61.0
- `POST /api/metabot/describe/card` and `POST /api/metabot/describe/dashboard/:id` have been removed. These endpoints

View File

@@ -268,7 +268,7 @@ settings: {
| `group` | Sub-heading within a section for grouping related settings. |
| `index` | Display order within a group. |
| `inline` | When `true`, renders the widget on the same line as `title` (handy for `"toggle"`). |
| `widget` | A [built-in widget](#built-in-widgets) name, or a [custom React component](#custom-widgets). |
| `widget` | A [built-in widget](#built-in-widgets) name, or a [custom React component](#custom-widgets). |
| `getDefault(series, settings)` | Computes the default value when none is stored. |
| `getValue(series, settings)` | Always-computed value — overrides the stored value on every render. |
| `getProps(series, settings)` | Returns widget-specific props. |
@@ -417,15 +417,13 @@ Metabase runs plugin code in an isolated sandbox, so a visualization works only
- **Navigation and the rest of the app**: history changes, the host page's URL and referrer, and any DOM outside the plugin's own container.
- **Unsafe DOM and timing APIs**: `document.write`, `execCommand`, constructable stylesheets, raw HTML parsers (`DOMParser`, `setHTMLUnsafe`, `XSLTProcessor`), and resource-timing APIs that expose other requests the page has made.
### When embedding, custom visualizations are only available in the SDK
### Custom visualizations don't render in embeds or public links
The [Modular embedding SDK](../embedding/sdk/introduction.md) renders custom visualizations when you allowlist them with the [`allowedCustomVisualizations` prop](../embedding/sdk/config.md#custom-visualizations) on `MetabaseProvider`.
Other embedding types don't render custom visualizations. In [modular embedding](../embedding/modular-embedding.md) with web components and [public links](../embedding/public-links.md), any card that uses a custom visualization falls back to the default visualization for the query's results.
[Embeds](../embedding/introduction.md) and [public links](../embedding/public-links.md) (to questions, dashboards, and documents) don't render custom visualizations. Any card that uses one falls back to the default visualization (a table).
### Custom visualizations don't render in exports and subscriptions
Static renders—like charts in alerts or dashboard subscriptions—fall back to a default visualization for the card's data shape.
Static renders—like charts in alerts or dashboard subscriptions—fall back to a table for any card that uses a custom visualization.
## Example plugins

View File

@@ -6,17 +6,6 @@ title: Driver interface changelog
## Metabase 0.64.0
- `metabase.driver/workspace-isolation-details` `[driver database workspace]` -- new workspace-isolation
multimethod. Computes the isolation identifiers (`:schema`, and driver-specific `:database_details` such as
user/password) for a workspace *before* any warehouse work happens; `init-workspace-isolation!`,
`grant-workspace-read-access!`, and `destroy-workspace-isolation!` now receive those identifiers on the
`workspace` map instead of computing them internally. `:sql-jdbc` drivers inherit a default implementation
(deterministic schema/user names plus a random password); non-JDBC drivers that support workspaces must
implement it themselves.
- `metabase.driver/check-isolation-permissions`, added in 0.59.0, has been removed. Permission problems now
surface as failures from `init-workspace-isolation!`/`grant-workspace-read-access!` themselves.
- Added a `:native-pivot-tables` driver feature flag for drivers that can compile a pivot query as a single
`GROUP BY GROUPING SETS (...)` statement instead of the legacy multi-query path. Drivers that opt in must also
derive from `:sql-mbql5` (which provides the `:pivot` clause compiler). Defaults to `false`.

View File

@@ -9,7 +9,7 @@ redirect_from:
You can style your embedded [Metabase components](./components.md) with a **theme**.
![Share button to embed dashboard](./images/sharing-embed.png)
![Embed share button](./images/embed-share-button.png)
On OSS and Starter plans, embeds come with two theme presets: [light and dark](#dark-mode-and-light-mode).

View File

@@ -24,12 +24,12 @@ Toggle **Enable guest embeds**.
## Creating a guest embed
![Share button to embed dashboard](./images/sharing-embed.png)
![Sharing button to embed dashboard](./images/sharing-embed.png)
To create a guest embed:
1. Go to the item that you want to embed in your website. You can also open a command palette with Ctrl/Cmd+K and type "New embed".
2. Click the **Share** icon.
2. Click the **sharing icon**.
3. Select **Embed**.
4. Under **Authentication**, select **Guest**.
5. Optional: [customize the appearance of the embed](./appearance.md)
@@ -130,7 +130,7 @@ Parameters are disabled by default, which also makes them hidden from end-users.
To configure parameters:
1. Go to your embedded question or dashboard.
2. Click the **Share** icon and select **Embed**.
2. Click the **sharing icon** and select **Embed**.
3. Under **Parameters**, select the visibility option for each parameter, and optionally default value(s).
4. Click **Publish**.
@@ -453,7 +453,7 @@ function paramsFor(user, customContext) {
## Disabling embedding for a question or dashboard
1. Visit the embeddable question or dashboard.
2. Click the **Share** icon (square with an arrow pointing to the top right).
2. Click the **sharing icon** (square with an arrow pointing to the top right).
3. Select **Embed**.
4. Select **Guest embedding**
5. Click **Unpublish**.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -60,24 +60,23 @@ If you'd like to share your data with the good people of the internet, admins ca
## Comparison of embedding types
| Action | [Modular SDK](./sdk/introduction.md) | [Modular SSO](./modular-embedding.md) | [Modular Guest](./guest-embedding.md) | [Full app](./full-app-embedding.md) | [Public](../embedding/public-links.md) |
| ------------------------------------------------------------------------------------------------------------ | ------------------------------------ | ------------------------------------- | ------------------------------------- | ----------------------------------- | -------------------------------------- |
| Charts and dashboards | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Filter widgets](https://www.metabase.com/glossary/filter-widget) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Export results\* | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Locked filters](./static-embedding-parameters.md#restricting-data-in-a-static-embed-with-locked-parameters) | ❌ | ❌ | ✅ | ❌ | ❌ |
| [Data segregation](../permissions/embedding.md) | ✅ | ✅ | ❌ | ✅ | ❌ |
| [Drill-through menu](../questions/visualizations/drill-through.md) | ✅ | ✅ | ❌ | ✅ | ❌ |
| [Query builder](../questions/query-builder/editor.md) | ✅ | ✅ | ❌ | ✅ | ❌ |
| [Basic appearance customization](../configuring-metabase/appearance.md)\*\* | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Advanced theming](./appearance.md) | ✅ | ✅ | ❌ | ❌ | ❌ |
| [Usage analytics](../usage-and-performance-tools/usage-analytics.md) | ✅ | ✅ | ❌ | ✅ | ❌ |
| Embed individual Metabase components | ✅ | ✅ | ❌ | ❌ | ❌ |
| Manage access and interactivity per component | ✅ | ✅ | ❌ | ❌ | ❌ |
| Custom layouts | ✅ | ❌ | ❌ | ❌ | ❌ |
| Customize behavior with [plugins](./sdk/plugins.md) | ✅ | ❌ | ❌ | ❌ | ❌ |
| [Custom visualizations](../questions/visualizations/custom.md) | ✅ | ❌ | ❌ | ❌ | ❌ |
| AI chat | ✅ | ✅ | ❌ | ✅ | ❌ |
| Action | [Modular SDK](./sdk/introduction.md) | [Modular SSO](./modular-embedding.md) | [Modular Guest](./guest-embedding.md) | [Full app](./full-app-embedding.md) | [Public](../embedding/public-links.md) |
| -------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ------------------------------------- | ------------------------------------- | ----------------------------------- | -------------------------------------- |
| Charts and dashboards | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Filter widgets](https://www.metabase.com/glossary/filter-widget) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Export results\* | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Locked filters](./static-embedding-parameters.md#restricting-data-in-a-static-embed-with-locked-parameters) | ❌ | ❌ | ✅ | ❌ | ❌ |
| [Data segregation](../permissions/embedding.md) | ✅ | ✅ | ❌ | ✅ | ❌ |
| [Drill-through menu](../questions/visualizations/drill-through.md) | ✅ | ✅ | ❌ | ✅ | ❌ |
| [Query builder](../questions/query-builder/editor.md) | ✅ | ✅ | ❌ | ✅ | ❌ |
| [Basic appearance customization](../configuring-metabase/appearance.md)\*\* | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Advanced theming](./appearance.md) | ✅ | ✅ | ❌ | ❌ | ❌ |
| [Usage analytics](../usage-and-performance-tools/usage-analytics.md) | ✅ | ✅ | ❌ | ✅ | ❌ |
| Embed individual Metabase components | ✅ | ✅ | ❌ | ❌ | ❌ |
| Manage access and interactivity per component | ✅ | ✅ | ❌ | ❌ | ❌ |
| Custom layouts | ✅ | ❌ | ❌ | ❌ | ❌ |
| Customize behavior with [plugins](./sdk/plugins.md) | ✅ | ❌ | ❌ | ❌ | ❌ |
| AI chat | ✅ | ✅ | ❌ | ✅ | ❌ |
\* Each embedding type allows data downloads by default, but only [Pro and Enterprise](https://www.metabase.com/pricing/) plans can disable data downloads.
@@ -104,7 +103,7 @@ For information about the anonymous usage data Metabase collects from embedded c
## Embedding limitations
- Currently, you can't embed [documents](../documents/introduction.md) (though you can create [public documents](./public-links.md)).
- Only the [Modular embedding SDK](./sdk/introduction.md) renders [custom visualizations](../questions/visualizations/custom.md), and only ones you allowlist with the [`allowedCustomVisualizations` prop](./sdk/config.md#custom-visualizations). In other embedding types, any card that uses a custom visualization falls back to the default visualization for the query's results.
- Embeds don't render [custom visualizations](../questions/visualizations/custom.md): any card that uses one falls back to the default visualization (a table).
## Further reading

View File

@@ -36,10 +36,10 @@ You can generate a code snippet for embedding a component by going through the e
In your Metabase:
1. Visit the item you want to embed.
2. Click the **Share** icon.
2. Click the sharing icon.
3. Select **Embed**.
![Share button to embed dashboard](./images/sharing-embed.png)
![Embed share button](./images/embed-share-button.png)
You can also open a command palette with Ctrl/Cmd+K, type "New embed". You'll get a wizard to help you set up your embed.

View File

@@ -15,7 +15,7 @@ Admins can create and share public links (URLs) for questions, dashboards, and d
![Create a public link for a question](./images/create-a-public-link.png)
To create a public link for a question, admins can click on the **Share** icon at the top right of a question and select **Create a public link**. Copy the link and test it out by viewing the link in a private/incognito browser session.
To create a public link for a question, admins can click on the **Sharing** icon at the top right of a question and select **Create a public link**. Copy the link and test it out by viewing the link in a private/incognito browser session.
## Public link to export question results in CSV, XLSX, JSON
@@ -23,7 +23,7 @@ This export option is only available for questions, not dashboards.
To create a public link that people can use to download the results of a question:
1. Click on the **Share** icon for the question.
1. Click on the **Sharing** icon for the question.
2. Select **Create a public link**.
3. Click on the file format you want (below the **Public link** URL): CSV, XLSX, or JSON.
@@ -33,7 +33,7 @@ Open the public link in a new tab to test the download.
## Create a public link for a dashboard
To share a dashboard via a public link, admins can click on the **Share** button in the top right menu.
To share a dashboard via a public link, admins can click on the **Sharing** button in the top right menu.
![Sharing a dashboard](./images/dashboard-sharing.png)
@@ -41,7 +41,7 @@ To embed a dashboard, see [guest embedding](./guest-embedding.md).
## Create a public link for a document
To share a document via a public link, admins can click on the **Share** button in the top right menu and select **Create a public link**.
To share a document via a public link, admins can click on the **Sharing** button in the top right menu and select **Create a public link**.
Public documents are read-only: viewers cannot edit the content or add comments. For charts embedded in the document, viewers can download the results in CSV, XLSX, or JSON format using the **Download results** option in the chart menu.
@@ -86,7 +86,7 @@ For example, if you have a primary public dashboard that displays **Invoices** d
If you want to embed your question or dashboard as an iframe in a simple web page or app:
1. Click on the **Share** icon for your question or dashboard.
1. Click on the **Sharing** icon for your question or dashboard.
2. Click **Embed**.
3. In the bottom of the embedding popup, click on **Get embedding code**.
4. Copy the iframe snippet Metabase generates for you.
@@ -170,7 +170,7 @@ If you disable public sharing, then re-enable public sharing, all your previousl
### Individual question or dashboard links and embeds
1. Visit the question or dashboard.
2. Click on the **Share** icon.
2. Click on **Sharing** icon.
3. Select **Public link** or **Embed**.
4. Click **Remove public link**.

View File

@@ -32,21 +32,6 @@ To pass a theme, use `defineMetabaseTheme`. See [Reuse a saved theme in the SDK]
{% include_file "{{ dirname }}/api/snippets/MetabaseProviderProps.md" snippet="properties" %}
## Custom visualizations
The SDK can render [custom visualizations](../../questions/visualizations/custom.md). To allow custom visualizations in the embed, pass an allowlist of the custom visualizations to the `allowedCustomVisualizations` prop on `MetabaseProvider`:
```typescript
{% include_file "{{ dirname }}/snippets/config/config-with-custom-visualizations.tsx" snippet="example" %}
```
Only the custom visualizations you list will load. Each entry is the visualization's name (the manifest `name`, which you can find under **Admin** > **Settings** > **Custom visualizations** > **Manage visualizations**), prefixed with `custom:`. For example, a custom visualization named `Calendar Heatmap` becomes `"custom:Calendar Heatmap"`. Names are case-sensitive, so `"custom:calendar heatmap"` won't match a visualization named `Calendar Heatmap`.
Omitting the prop, or passing an empty array, turns off custom visualizations. Cards that use a custom visualization will fall back to the default visualization for the query's results. If you allowlist a name that doesn't match an installed custom visualization, the SDK logs a warning to the console and falls back to the default visualization.
For security, the SDK runs each custom visualization's code in an isolated sandbox, so a visualization can't reach your app or make network requests. The sandbox doesn't block passive image loads, though. A visualization can still trigger outbound requests through `<img>` tags or CSS `url()`. To limit where custom visualizations can load images from, set a Content Security Policy with an `img-src` allowlist in your app (the core Metabase app does this with [Restrict image domains](../../configuring-metabase/settings.md#restrict-image-domains)). [Only add visualizations you trust](../../questions/visualizations/custom.md#only-add-visualizations-you-trust).
## Global event handlers
You can listen for events by defining the `eventHandlers` prop for `MetabaseProvider`.

View File

@@ -1,20 +0,0 @@
import type { PropsWithChildren } from "react";
import {
type MetabaseAuthConfig,
MetabaseProvider,
} from "@metabase/embedding-sdk-react";
const authConfig = {} as MetabaseAuthConfig;
const Example = ({ children }: PropsWithChildren) => (
// [<snippet example>]
<MetabaseProvider
authConfig={authConfig}
// Allowlist the custom visualizations to load, by their names,
// each prefixed with `custom:`.
allowedCustomVisualizations={["custom:Calendar Heatmap", "custom:Thumbs"]}
>
{children}
</MetabaseProvider>
// [<endsnippet example>]
);

View File

@@ -35,7 +35,7 @@ Unsigned parameters, such as appearance settings, should be added directly to yo
You can use **editable parameters** to add [filter widgets](https://www.metabase.com/glossary/filter-widget) to embedded dashboards or SQL questions.
1. Go to your dashboard or SQL question. Make sure you've set up a [dashboard filter](../dashboards/filters.md) or [SQL variable](../questions/native-editor/sql-parameters.md).
2. Click on the **Share** icon > **Embed this item in an application**.
2. Click on the **sharing icon** > **Embed this item in an application**.
3. Under **Parameters**, you'll find the names of your dashboard filters or SQL variables.
4. Select **Editable** for each parameter that should get a filter widget on your embed.
5. Click **Publish** to save your changes.
@@ -112,7 +112,7 @@ Whenever you're adding a parameter to the embedding URL in your iframe's `src` a
If you want to restrict the data that's displayed in an embedded dashboard or SQL question, you can set up a **locked parameter**. A locked parameter filters the data in a dashboard or SQL question _before_ the results are displayed to people in a static embed.
1. Go to your dashboard or SQL question. Make sure you've set up a [dashboard filter](../dashboards/filters.md) or [SQL variable](../questions/native-editor/sql-parameters.md).
2. Click on the **Share** icon > **Embed this item in an application**.
2. Click on the **sharing icon** > **Embed this item in an application**.
3. Under **Parameters**, you'll find the names of your dashboard filters or SQL variables.
4. Select **Locked** for each parameter that you want to restrict your data with.
5. Optional: select a value under **Preview locked parameters** to see what the restricted data looks like.

View File

@@ -41,12 +41,12 @@ If you want to embed charts with additional interactive features, like [drill-do
## Making a question or dashboard embeddable
![Share button to embed dashboard](./images/sharing-embed.png)
![Sharing button to embed dashboard](./images/sharing-embed.png)
To create a static embed:
1. Go to the question or dashboard that you want to embed in your website.
2. Click the **Share** icon.
2. Click on the **sharing icon**.
3. Select **Embed**.
4. Select **Static embedding**.
5. Optional: [customize the appearance of the embed](./static-embedding-parameters.md#customizing-the-appearance-of-a-static-embed)
@@ -63,7 +63,7 @@ metabase_site_url/embed/question/your_jwt_token?parameter_name=value
Once you've made a question or dashboard [embeddable](#making-a-question-or-dashboard-embeddable), you'll need to put the embedding URL for that question or dashboard on your website:
1. Go to the question or dashboard > **Share** icon > **Embed**.
1. Go to the question or dashboard > **sharing icon** > **Embed**.
2. Make any changes and copy the code.
3. [Preview the code](#previewing-the-code-for-an-embed)
4. Add the code to the server code that builds your website.
@@ -73,7 +73,7 @@ For more examples, see our [reference apps repo](https://github.com/metabase/emb
## Previewing the code for an embed
1. Go to the question or dashboard > **Share** icon > **Embed**.
1. Go to the question or dashboard > **sharing icon** > **Embed this item in an application**.
2. Click **Code**.
3. In the top code block, you'll find the sample code for your web server. You'll also find the iframe snippet to plug into your HTML template or single page app.
@@ -124,7 +124,7 @@ If you change the [parameters](./static-embedding-parameters.md) of your embedde
You can find a list of all static embeds of questions and dashboards from **Admin settings** > **Embedding** > **Static**.
1. Visit the embeddable question or dashboard.
2. Click the **Share** icon (square with an arrow pointing to the top right).
2. Click on the **sharing icon** (square with an arrow pointing to the top right).
3. Select **Embed**.
4. Select **Static embedding**
5. Click **Unpublish**.

View File

@@ -34,6 +34,10 @@ Generates a markdown file with some documentation and an example configuration f
Print a list of all multimethods available for a driver to implement, optionally with their docstrings.
## `drop-entity-ids`
Drop entity IDs for instances of serializable models. Useful for migrating from v1 serialization (x.46 and earlier) to v2 (x.47+).
## `dump-to-h2 h2-filename opts`
Transfer data from existing database to newly created H2 DB with specified filename. Target H2 file is deleted before dump, unless the --keep-existing flag is given.
@@ -99,6 +103,10 @@ Reset the password for a user with `email-address`.
Rotate the encryption key of a metabase database. The MB_ENCRYPTION_SECRET_KEY environment variable has to be set to the current key, and the parameter `new-key` has to be the new key. `new-key` has to be at least 16 chars.
## `seed-entity-ids`
Add entity IDs for instances of serializable models that don't already have them.
## `version`
Print version information about Metabase and the current system.

View File

@@ -1,80 +1,47 @@
---
title: Account settings
summary: Update your profile, change your password, view login history, manage notifications, and customize your Metabase experience.
redirect_from:
- /docs/latest/users-guide/account-settings
---
# Account settings
To access your account settings, click the **profile or grid icon** in the top right corner, then click your **profile name and email.**
## Account profile
You can set your first and last names, change your email address, and set your language. See our list of [supported languages](../configuring-metabase/localization.md).
## Theme
You can choose to display Metabase in light or dark mode.
To quickly toggle dark mode, use the [command palette](../exploration-and-organization/exploration.md#command-palette).
The theme only applies to your account. The theme doesn't apply to chart colors or embedded Metabases.
## Account password
You can change your password here. Note that if your Metabase uses Single Sign-On (SSO), your administrator will have disabled this password section, as your identity provider will manage logins.
If you log in through LDAP, you won't see this password section either, since your directory manages your password. You may get a **Security** tab to enroll in 2FA if your admin has [turned it on](./two-factor-authentication.md#turn-on-two-factor-authentication).
If you're having trouble logging in, see our [People can't log into Metabase](../troubleshooting-guide/cant-log-in.md).
## Two-factor authentication
{% include plans-blockquote.html feature="Two-factor authentication" %}
If your admin has turned on [two-factor authentication](./two-factor-authentication.md) (2FA), you'll see a **Security** tab in your account settings. 2FA adds a second step to logging in: after your password, you enter a code from an authenticator app, so your password alone won't let someone into your account.
### Set up two-factor authentication on your account
1. Click the grid icon in the upper right.
2. Click your profile.
3. On the **Security** tab, click **Set up two-factor authentication**.
4. Confirm your password.
5. Scan the QR code with an authenticator app like Google Authenticator, 1Password, or Authy. If you can't scan it, enter the key manually in your authenticator app.
6. Enter the six-digit code from the app.
7. Save your recovery codes. Metabase shows these ten codes only once, so copy them and store them somewhere safe. Each recovery code only works once. If you [regenerate recovery codes](#regenerate-recovery-codes), the old codes will no longer work.
### Logging in with two-factor authentication
Once you set up two-factor authentication, Metabase will ask for a code every time you log in. After you enter your email and password:
- Enter the six-digit code from your authenticator app.
- Lost your app? Click **Use a recovery code instead** and enter one of your saved codes.
- If your admin set up email, click **Email me a code** to get a one-time code in your inbox.
### Regenerate recovery codes
Recovery codes are single-use, so it's worth generating a fresh set if you're running low or think someone else has seen them. On the **Security** tab, click **Generate recovery codes** and confirm with a current authenticator or recovery code. Generating new codes invalidates the old ones.
### Turn off two-factor authentication
In your account settings, on the **Security** tab , click **Disable**. Your account will be protected by your password only, and your recovery codes will stop working.
## Account login history
The login history lists each login, along with some location information (if available), and some client information (like Browser (Firefox/Windows)). If you see any suspicious login attempts, change your password and notify your administrator.
### A note about new login emails
Whenever you log in from a new device, Metabase will send you an email just to let you know someone (presumably you) has logged in from an unrecognized device. If you see this email, but don't remember logging in, or don't recognize the device, change your password and let your administrator know.
## Disable animations in Metabase
This isn't an in-Metabase setting, but just so you know: you can disable UI animations in Metabase (like sidebars sliding around, or rotating spinners) by changing the settings for your operating system so it respects the `prefers-reduced-motion` CSS media feature. This change will also affect other applications, not just Metabase. Check out the instructions for how to set the user preferences for your operating system in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion#user_preferences).
## Notifications
If you subscribe or are added to dashboard subscriptions or alerts, you’ll be able to manage those notifications here (as well as on the relevant question or dashboard themselves).
Metabase excludes notifications for [comments](../documents/introduction.md#comment-notifications) from this page.
---
title: Account settings
summary: Update your profile, change your password, view login history, manage notifications, and customize your Metabase experience.
redirect_from:
- /docs/latest/users-guide/account-settings
---
# Account settings
To access your account settings, click the **profile or grid icon** in the top right corner, then click your **profile name and email.**
## Account profile
You can set your first and last names, change your email address, and set your language. See our list of [supported languages](../configuring-metabase/localization.md).
## Theme
You can choose to display Metabase in light or dark mode.
To quickly toggle dark mode, use the [command palette](../exploration-and-organization/exploration.md#command-palette).
The theme only applies to your account. The theme doesn't apply to chart colors or embedded Metabases.
## Account password
You can change your password here. Note that if your Metabase uses Single Sign-On (SSO), your administrator will have disabled this password section, as your identity provider will manage logins.
If you're having trouble logging in, see our [People can't log into Metabase](../troubleshooting-guide/cant-log-in.md).
## Account login history
The login history lists each login, along with some location information (if available), and some client information (like Browser (Firefox/Windows)).
If you see any suspicious login attempts, change your password and notify your administrator.
### A note about new login emails
Whenever you log in from a new device, Metabase will send you an email just to let you know someone (presumably you) has logged in from an unrecognized device. If you see this email, but don't remember logging in, or don't recognize the device, change your password and let your administrator know.
## Disable animations in Metabase
This isn't an in-Metabase setting, but just so you know: you can disable UI animations in Metabase (like sidebars sliding around, or rotating spinners) by changing the settings for your operating system so it respects the `prefers-reduced-motion` CSS media feature. This change will also affect other applications, not just Metabase. Check out the instructions for how to set the user preferences for your operating system in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion#user_preferences).
## Notifications
If you subscribe or are added to dashboard subscriptions or alerts, you’ll be able to manage those notifications here (as well as on the relevant question or dashboard themselves).
Metabase excludes notifications for [comments](../documents/introduction.md#comment-notifications) from this page.

View File

@@ -25,14 +25,12 @@ Make people use longer and more complex passwords.
Tell Metabase how long it should wait before asking people to log in again.
## [Two-factor authentication](./two-factor-authentication.md)
Have people confirm their identity with an authenticator app when they log in.
## Authentication
Metabase offers several options for authentication.
> If you need to set up 2-step or multi-factor authentication (2FA or MFA) for your Metabase, consider using one of the SSO options below.
### SSO for Metabase Open Source and Starter plans
- [Google Sign-in](./google-sign-in.md)

View File

@@ -1,52 +0,0 @@
---
title: Two-factor authentication
summary: Add a second step to logins by having people confirm their identity with an authenticator app. A Pro/Enterprise feature.
---
# Two-factor authentication
{% include plans-blockquote.html feature="Two-factor authentication" %}
Two-factor authentication (2FA) adds a second step to signing in. On top of people's email and password logins, they'll have to confirm their identity with a time-based code from an authenticator app.
Metabase's native 2FA applies to password logins and LDAP logins. 2FA for Single Sign-on (SSO) via JWT, SAML, or OIDC is managed through your identity provider.
## Turn on two-factor authentication
An admin can turn 2FA on for your Metabase:
1. Go to **Admin settings** > **Settings** > **Authentication**.
2. Find the **Two-factor authentication** card.
3. Toggle it to **Enabled**.
Once enabled, a **Security** tab shows up in each person's account settings, where they can enroll in 2FA.
If you configure Metabase through environment variables or a [config file](../configuring-metabase/config-file.md), the matching setting is [`MB_MFA_ENFORCEMENT`](../configuring-metabase/environment-variables.md#mb_mfa_enforcement). Set `MB_MFA_ENFORCEMENT` to `optional` to let people enroll, or `off` to turn 2FA off.
## Supported methods
- **Authenticator app (primary).** People scan a QR code with an app like Google Authenticator or 1Password, which then generates a new six-digit code every 30 seconds.
- **Email code (fallback at login).** If an admin has set up [email](../configuring-metabase/email.md), people who are already enrolled in two-factor authentication can have Metabase email them a one-time code when they can't reach their authenticator app. You can only use the code once, and expires after ten minutes. If email isn't configured, Metabase hides this option.
- **Recovery codes.** When someone enrolls in 2FA, Metabase gives them ten single-use codes to save.
Metabase doesn't support SMS codes or hardware keys (passkeys, U2F, or WebAuthn).
## Before you turn on two-factor authentication
### If you're self-hosting Metabase, set an encryption key
Set the [`MB_ENCRYPTION_SECRET_KEY`](../databases/encrypting-details-at-rest.md) environment variable so Metabase encrypts authenticator secrets at rest. If you turn on 2FA without it, Metabase shows a warning on the settings page.
If you're using Metabase Cloud, we've encrypted your keys for you.
### Set up email for the fallback code
Metabase requires people to use an email address as a login, but that doesn't mean that email is set up. The email fallback only appears if your Metabase can send [email](../configuring-metabase/email.md). If you skip setting up email, people will have to rely on their authenticator app and recovery codes only.
## See who's enrolled
When 2FA is on, go to **Admin settings** > **Settings** > **Authentication** > **Two-factor authentication**. The 2FA card shows how many people have enrolled in 2FA and how many haven't.
## Further reading
- [Enrolling in two-factor authentication](./account-settings.md#two-factor-authentication)
- [Authentication options](./start.md#authentication)

View File

@@ -103,7 +103,7 @@ You can export the results of a dashboard and its cards in different ways:
### Export dashboard as PDF
To export a dashboard as a PDF, click **Share**, then select **Export as PDF**. On dashboards with multiple tabs, select **Export tab as PDF** to export the current tab.
You can export a dashboard as a PDF. Click on the **Sharing** button, then select **Export as PDF**.
![Exporting a dashboard as a PDF](./images/export-dashboard-as-pdf.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -208,7 +208,7 @@ Syntax: `Percentile(column, percentile-value)`
Example: `Percentile([Score], 0.9)` would return the value at the 90th percentile for all values in that column.
Databases that don't support `percentile`: Druid, MariaDB, MySQL, MongoDB, SQL Server, SQLite, Vertica. Presto only provides approximate results.
Databases that don't support `percentile`: Druid, H2, MariaDB, MySQL, MongoDB, SQL Server, SQLite, Vertica. Presto only provides approximate results.
### Share
@@ -660,7 +660,7 @@ Syntax: `regexExtract(text, regular_expression)`
Example: `regexExtract([Address], "[0-9]+")`
Databases that don't support `regexExtract`: SQL Server, SQLite.
Databases that don't support `regexExtract`: H2, SQL Server, SQLite.
Related: [contains](#contains), [doesNotContain](#doesnotcontain), [substring](#substring).
@@ -1048,6 +1048,8 @@ Example: `Offset(Sum([Total]), -1)` would get the `Sum([Total])` value from the
Limitations are noted for each aggregation and function above, and here they are in summary:
**H2** (including Metabase Sample Database): `Median`, `Percentile`, `convertTimezone`, `regexExtract`, `datetime`, `float`, `splitPart`.
**Athena**: `convertTimezone`, `datetime`, `float`, `splitPart`.
**Databricks**: `convertTimezone`, `datetime`, `float`, `splitPart`.
@@ -1068,7 +1070,7 @@ Limitations are noted for each aggregation and function above, and here they are
**SQL Server**: `Median`, `Percentile`, `regexExtract`, `datetime`, `float`, `splitPart`.
**SQLite** (including Metabase Sample Database): `exp`, `log`, `Median`, `Percentile`, `power`, `regexExtract`, `StandardDeviation`, `sqrt`, `Variance`, `datetime`, `float`, `splitPart`.
**SQLite**: `exp`, `log`, `Median`, `Percentile`, `power`, `regexExtract`, `StandardDeviation`, `sqrt`, `Variance`, `datetime`, `float`, `splitPart`.
**Vertica**: `Median`, `Percentile`, `datetime`, `float`, `splitPart`.

View File

@@ -1,6 +1,6 @@
---
title: Custom visualizations
summary: Add your own chart types to Metabase by uploading custom visualizations built with the Custom Visualizations SDK.
summary: Add your own chart types to Metabase by uploading visualization plugins built with the Custom Visualizations SDK.
---
# Custom visualizations
@@ -19,7 +19,7 @@ This page covers how to add a custom visualization to your Metabase. To _create_
### Restrict image domains first
Before you can turn on custom visualizations, you need to enable [Restrict image domains](../../configuring-metabase/settings.md#restrict-image-domains). A custom visualization runs third-party JavaScript in your Metabase. By restricting image (and font) domains, you limit where that code can load assets from, which narrows the ways a custom visualization could leak data through outbound asset requests. See [Only add visualizations you trust](#only-add-visualizations-you-trust).
Before you can turn on custom visualizations, you need to enable [Restrict image domains](../../configuring-metabase/settings.md#restrict-image-domains). A custom visualization runs third-party JavaScript in your Metabase. By restricting image (and font) domains, you limit where that code can load assets from, which narrows the ways a plugin could leak data through outbound asset requests. See [Only add plugins you trust](#only-add-plugins-you-trust).
While custom visualizations are enabled, you can't turn **Restrict image domains** back off. You'll need to first disable custom visualizations.
@@ -38,8 +38,8 @@ Once you've [built the custom visualization](../../developers-guide/custom-visua
3. Click **Add visualization**.
- Bundles must be smaller than 5 MiB.
- Each custom visualization lists the Metabase versions it supports (for example, "Requires Metabase >=1.62"). If your Metabase version isn't in that range, Metabase rejects the upload and tells you which version the visualization needs.
- The **Manage visualizations** page shows each custom visualization's icon, name, the first eight characters of the bundle's hash, and its required Metabase version range, so you can tell which version is installed.
- Each plugin lists the Metabase versions it supports (for example, "Requires Metabase >=1.62"). If your Metabase version isn't in that range, Metabase rejects the upload and tells you which version the plugin needs.
- The **Manage visualizations** page shows each plugin's icon, name, the first eight characters of the bundle's hash, and its required Metabase version range, so you can tell which version is installed.
## Using a custom visualization
@@ -47,44 +47,42 @@ On a question, dashboard or document card, open the visualization sidebar (the *
![Gondola line chart](../images/gondola-line-chart.png)
If a custom visualization can't render the current query results (for example, if the query is missing a column the visualization needs), Metabase shows the error message from the custom visualization so you can adjust the query or pick a different chart.
If a custom visualization can't render the current query results (for example, if the query is missing a column the visualization needs), Metabase shows the error message from the plugin so you can adjust the query or pick a different chart.
Custom visualizations behave like built-in charts in most places:
- **Settings.** Click the **gear** icon in the visualization sidebar to change the visualization's settings. A custom visualization defines its own setting tabs: each setting names the section it belongs to.
- **Dark mode.** Custom visualizations that use Metabase's colors adapt to [dark mode](../../people-and-groups/account-settings.md#theme) automatically.
- **Settings.** Click the **gear** icon in the visualization sidebar to change the visualization's settings. A plugin defines its own setting tabs: each setting names the section it belongs to.
- **Dark mode.** Plugins that use Metabase's colors adapt to [dark mode](../../people-and-groups/account-settings.md#theme) automatically.
- **Icons.** A custom visualization shows its own icon in the visualization picker, and questions that use it show that icon in collections and bookmarks.
### Custom visualizations in embeds only work when using the SDK
### Custom visualizations don't render in embeds
The [Modular embedding SDK](../../embedding/sdk/introduction.md) can render custom visualizations. You allowlist the custom visualizations you want to load with the [`allowedCustomVisualizations` prop](../../embedding/sdk/config.md#custom-visualizations) on `MetabaseProvider`.
Other embedding types don't render custom visualizations. In [modular embedding](../../embedding/modular-embedding.md) with web components, [guest and static embeds](../../embedding/introduction.md), and [public links](../../embedding/public-links.md), any card that uses a custom visualization falls back to the default visualization for the query's results.
Custom visualizations only render in your Metabase. In [embeds](../../embedding/start.md) and [public links](../../embedding/public-links.md) (public questions, dashboards, and documents), any card that uses a custom visualization falls back to the default visualization (a table).
## Managing custom visualizations
_Admin > Settings > Custom visualizations > Manage visualizations_
- **Disable a visualization.** Any question, dashboard card, or document card that used the visualization falls back to the default visualization for that query's results. If you re-enable the visualization, those cards will go back to using the custom visualization.
- **Replace a bundle.** Upload a new `.tgz` to ship an updated version of a custom visualization. The new bundle's manifest `name` _must_ match the existing visualization's identifier, so questions that already use the visualization keep working.
- **Disable a visualization.** Any question, dashboard card, or document card that used the visualization falls back to the default visualization for that query's results. If you re-enable the plugin, those cards will go back to using the custom visualization.
- **Replace a bundle.** Upload a new `.tgz` to ship an updated version of a plugin. The new bundle's manifest `name` _must_ match the existing plugin's identifier, so questions that already use the visualization keep working.
- **Remove a visualization.** Cards that used the custom viz fall back to the default visualization.
## Exports
- **Dashboard subscriptions and alerts don't use custom visualizations**. Cards that use custom visualizations will fall back to a default visualization for the card's data shape.
- **PDF exports of dashboards include custom visualizations**.
- **Custom visualizations can support PNG export**, but only if its developer turned on PNG export for that visualization. PNG export is off by default.
- **Custom visualizations can support PNG export**, but only if its developer turned on PNG export for that plugin. PNG export is off by default.
## Only add visualizations you trust
## Only add plugins you trust
A custom visualization runs JavaScript in your Metabase. Only upload custom visualizations from sources you trust (like ones you've built yourself, or have vetted).
A custom visualization plugin runs JavaScript in your Metabase. Only upload plugins from sources you trust (like plugins you've built yourself, or have vetted).
Metabase runs custom visualizations in a sandbox to limit what a custom visualization can do:
Metabase runs custom visualizations in a sandbox to limit what a plugin can do:
- A custom visualization renders inside an isolated container and can't reach the rest of the Metabase app.
- A custom visualization can't call Metabase's APIs or make network requests.
- A plugin renders inside an isolated container and can't reach the rest of the Metabase app.
- A plugin can't call Metabase's APIs or make network requests.
While this sandboxing limits the damage a custom visualization can do, you still need to review the code.
While this sandboxing limits the damage a plugin can do, you still need to review the code.
## Further reading

View File

@@ -0,0 +1,114 @@
---
title: Auditing tools
redirect_from:
- /docs/latest/enterprise-guide/audit
---
# Auditing tools
> Auditing tools are deprecated. Instead, check out the (much better) [Usage analytics](./usage-analytics.md).
{% include plans-blockquote.html feature="Audit logs" %}
As an administrator of Metabase, you already know the importance of using data to understand how people interact with your products or services. With the Audit tool, you can use Metabase to understand how people in your organization use Metabase. It's, well, meta!
To view the audit logs, go to the top right of the screen and click on the **gear** icon > **Admin settings** > **Audit**. There's a lot of data available, not only about your people, but also about your questions, dashboards, databases and more! We'll walk you through each of the sections below.
## People
### Team members
Use the **People** section to gain a better understanding of how people are interacting with Metabase. You'll want to pay attention to the **Overview** tab, especially when you first launch Metabase at your organization — it will give you data around how many active and newly created accounts you have each day. Further down the page, you'll see charts showing you which people are most engaged.
![Team Members](./images/audit-team.png)
The **Team Members** panel has a view of other options for viewing your team's usage data. The **All Members** tab will give you a list of your team members, and stats about their activity, such as when they were last active.
The **Audit Log** tab will display, in chronological order, each query, who viewed it, and when. Note that if the person didn't save their query, its name will be listed as `Ad-hoc`. Each query name can be clicked to view more details about the person's interactions with it, including a full revision history. You'll also be able to view the query in Metabase. Note that this link will always show the latest version of the query - use the revision history to see changes over time.
Throughout the People section, names can be clicked to access the profile of a specific person’s activity. This profile includes:
- Dashboard views
- Query views
- Downloads
- Subscriptions and Alerts
![Team Members](./images/audit-teammember.png)
## Data
The Data section focuses on your databases, schemas and tables, and is divided into corresponding sections. Look here if you're trying to uncover queries and schemas that need optimization. Each section provides visualizations around the use and speed of querying against your databases, schemas or tables. You will also be able to view lists of stats about all of your databases, schemas and tables.
![Data](./images/audit-data.png)
## Items
The **Items** section focuses on questions, dashboards, downloads, and Subscriptions and Alerts.
### Questions
The Questions section will show you your most popular queries, as well as your slowest queries. If queries you think are important aren't appearing on your most popular queries list, you may want to make sure your team is focusing on the right things.
If important queries are appearing on the list of slowest queries, you will want to look at optimizing them. One option is to adjust your [caching settings](../configuring-metabase/caching.md), but there are plenty of other options as well. To help you determine if your optimization efforts are heading in the right direction, use the `Query views and speed per day` visualization at the bottom of the page.
![Items](./images/audit-questions.png)
A list of all of your questions is available as well, and you can see various data points about each question at a glance:
- Query Runs
- Average Runtime
- Cache Duration
- Total Runtime
- Database
- Table
- Collection
- Public Link
You can also click on any question to drill into a more detailed profile showing:
- View activity
- Revision History
- A full audit log of who viewed the question, and when
### Dashboards
The **Dashboards** section helps you understand what dashboards people are looking at, and to make sure they're having a smooth experience. If you notice that a popular dashboard has a high average question loading time, you can investigate further using the Questions section outlined above.
![Items](./images/audit-dashboards.png)
A list of all of your dashboards is available as well, and you can see various data points about each dashboard at a glance, such as:
- Number of views
- Average question execution time(ms)
- Number of cards
- Saved by
- Cache duration
- Public link
- Saved on
- Last edited on
### Downloads
Use the **Downloads** section to understand which people are downloading (or exporting) data, and the size (number of rows) of the downloads they're performing. This section contains some visualizations, as well as a list of all downloads.
### Subscriptions and Alerts
Here admins can get an overview of all of the [dashboard subscriptions][dashboard-subscriptions] and [alerts][alerts] that are currently active for that Metabase.
- Dashboard name (or Question name for Alerts)
- Recipients
- Type (e.g., email or Slack)
- Collection
- Frequency
- Created By
- Created At
- Filters
Admins can add and remove people from a subscription or alert by clicking on the item's **Recipients** number. Admins can also delete the subscription or alert entirely by clicking on the **X** on the relevant line.
Everyone can view all of their subscriptions and alerts by clicking the **profile or grid icon** in the upper right, then clicking their **profile name and email** to open **Account settings**, and navigating to **Notifications**.
For more, see [how permissions work with alerts and subscriptions](../permissions/notifications.md).
[alerts]: ../questions/alerts.md
[dashboard-subscriptions]: ../dashboards/subscriptions.md

View File

@@ -1,8 +1,5 @@
---
title: Usage analytics
redirect-from:
- /docs/latest/enterprise-guide/audit
- /docs/latest/usage-and-performance-tools/audit
---
# Usage analytics

View File

@@ -1,6 +0,0 @@
# Each test app under this directory commits its `src/` and its `data_app.yaml`
# manifest. Everything else — the SDK/template base pulled in at test setup and
# the built `dist/` bundle (Vite API) — is generated, so it's ignored here.
/*/*
!/*/src
!/*/data_app.yaml

View File

@@ -1,2 +0,0 @@
name: Custom Error Component
path: ./dist/index.js

View File

@@ -1,34 +0,0 @@
import {
type SdkErrorComponentProps,
StaticQuestion,
} from "@metabase/embedding-sdk-react";
import type { DataAppFactory } from "@metabase/embedding-sdk-react/data-app";
// A question id that does not exist, so the SDK takes its error path — which this
// app overrides with its own `errorComponent`, asserting an app-supplied component
// wins over the host's default neutral error state.
const MISSING_QUESTION_ID = 999999999;
function CustomError({ message }: SdkErrorComponentProps) {
return (
<div data-testid="custom-error-component">Custom app error: {message}</div>
);
}
function App() {
return (
<div data-testid="custom-error-app" style={{ padding: 24 }}>
<h1>Custom error app</h1>
<div style={{ height: 300 }}>
<StaticQuestion questionId={MISSING_QUESTION_ID} />
</div>
</div>
);
}
const factory: DataAppFactory = () => ({
component: App,
providerProps: { errorComponent: CustomError },
});
export default factory;

View File

@@ -1,2 +0,0 @@
name: Kitchen Sink
path: ./dist/index.js

View File

@@ -1,436 +0,0 @@
import {
InteractiveQuestion,
StaticQuestion,
useAction,
} from "@metabase/embedding-sdk-react";
import {
aggregations,
breakout,
copy,
DataAppLink,
DataAppRouter,
filter,
orderBy,
useDataAppLocation,
useMetabaseQuery,
useMetabaseQueryObject,
} from "@metabase/embedding-sdk-react/data-app";
import { type ComponentType, useEffect, useState } from "react";
import { getTestEnv } from "./test-env";
// The sandbox throws host-realm errors, whose `instanceof Error` can be false in
// the guest; read `.message` defensively.
const describeError = (err: unknown): string =>
(err as { message?: string })?.message ?? String(err);
const PROBE_COLOR = "rgb(0, 128, 0)";
function Overview() {
const { scalarQuery, questionQuery } = getTestEnv();
const ordersCount = useMetabaseQuery(scalarQuery);
const totalOrders = ordersCount.data?.rawRows?.[0]?.[0];
const ordersQuery = useMetabaseQueryObject(questionQuery);
return (
<div data-testid="data-app-content" style={{ padding: 24 }}>
<h1 style={{ margin: "0 0 16px" }}>Orders overview</h1>
<section
style={{
padding: 16,
border: "1px solid #e0e0e0",
borderRadius: 8,
marginBottom: 24,
maxWidth: 240,
}}
>
<div
style={{
color: "#6b7280",
fontSize: 12,
textTransform: "uppercase",
letterSpacing: 0.5,
}}
>
Total orders
</div>
<div
data-testid="orders-count"
style={{ fontSize: 32, fontWeight: 700 }}
>
{ordersCount.isLoading ? "…" : String(totalOrders ?? "—")}
</div>
</section>
<h2 style={{ margin: "0 0 8px" }}>All orders</h2>
<div style={{ height: 400 }}>
{ordersQuery.query ? (
<InteractiveQuestion card={{ query: ordersQuery.query }} />
) : (
<div>…</div>
)}
</div>
</div>
);
}
function Details() {
return (
<div data-testid="data-app-details" style={{ padding: 24 }}>
<h1 style={{ margin: "0 0 16px" }}>Order details</h1>
<p>A nested page reached through the data-app router.</p>
</div>
);
}
function QueryStates() {
const { scalarQuery, errorQuery } = getTestEnv();
const broken = useMetabaseQuery(errorQuery!);
const working = useMetabaseQuery(scalarQuery);
const count = working.data?.rawRows?.[0]?.[0];
return (
<div data-testid="data-app-query-states" style={{ padding: 24 }}>
<h1>Query states</h1>
<div data-testid="query-error">{broken.error ? "error" : "no-error"}</div>
<div data-testid="query-loading">
{working.isLoading ? "loading" : "done"}
</div>
<div data-testid="query-value">
{working.isLoading ? "…" : String(count ?? "—")}
</div>
<button
type="button"
data-testid="query-refetch"
onClick={() => {
working.refetch();
}}
>
refetch
</button>
</div>
);
}
function StaticQuestionPage() {
const { questionQuery } = getTestEnv();
const q = useMetabaseQueryObject(questionQuery);
return (
<div data-testid="data-app-static-question" style={{ padding: 24 }}>
<h1>Static question</h1>
<div style={{ height: 360 }}>
{q.query ? (
<StaticQuestion card={{ query: q.query }} />
) : (
<div data-testid="static-question-loading">…</div>
)}
</div>
</div>
);
}
function Combinators() {
const combinators = getTestEnv().combinators!;
const countAgg = aggregations.count();
const q = useMetabaseQuery({
source: combinators.source,
filters: [filter(combinators.filterField, ">", combinators.filterValue)],
aggregations: [countAgg],
breakouts: [breakout(combinators.breakoutField)],
orderBys: [orderBy(countAgg, "desc")],
});
return (
<div data-testid="data-app-combinators" style={{ padding: 24 }}>
<h1>Combinators</h1>
<div data-testid="combinators-loading">
{q.isLoading ? "loading" : "done"}
</div>
<div data-testid="combinators-error">
{q.error ? "error" : "no-error"}
</div>
<div data-testid="combinators-rowcount">
{String(q.data?.rows?.length ?? 0)}
</div>
</div>
);
}
function Actions() {
const { actionId, actionParams } = getTestEnv();
const action = useAction(actionId ?? null);
const [output, setOutput] = useState("idle");
const onExecute = async () => {
try {
const res = await action.execute(actionParams ?? {});
setOutput(res ? "returned-result" : "returned-null");
} catch {
setOutput("threw");
}
};
return (
<div data-testid="data-app-actions" style={{ padding: 24 }}>
<h1>Actions</h1>
<button type="button" data-testid="action-execute" onClick={onExecute}>
execute
</button>
<button
type="button"
data-testid="action-reset"
onClick={() => action.reset()}
>
reset
</button>
<div data-testid="action-executing">
{action.isExecuting ? "executing" : "idle"}
</div>
<div data-testid="action-result">
{action.result ? "has-result" : "no-result"}
</div>
<div data-testid="action-error">
{action.error ? "has-error" : "no-error"}
</div>
<div data-testid="action-output">{output}</div>
</div>
);
}
function Clipboard() {
const [status, setStatus] = useState("idle");
const onCopy = async () => {
try {
await copy("data-app-clipboard-payload");
setStatus("copied");
} catch (err) {
setStatus(`failed: ${describeError(err)}`);
}
};
return (
<div data-testid="data-app-clipboard" style={{ padding: 24 }}>
<h1>Clipboard</h1>
<button type="button" data-testid="clipboard-copy" onClick={onCopy}>
copy
</button>
<div data-testid="clipboard-status">{status}</div>
</div>
);
}
// With no app-supplied `errorComponent`, the host's default neutral error state
// renders here.
function MissingQuestion() {
return (
<div
data-testid="data-app-missing-question"
style={{ padding: 24, height: 300 }}
>
<StaticQuestion questionId={999999999} />
</div>
);
}
// Throws during render so the host's BundleErrorBoundary reports the failure to
// the parent, which renders its themed error screen.
function ThrowingPage(): JSX.Element {
throw new Error("intentional data-app bundle render error");
}
type Probe = { id: string; run: () => void };
const BLOCKED_PROBES: Probe[] = [
{ id: "script", run: () => document.createElement("script") },
{ id: "window-open", run: () => window.open("https://blocked.test") },
{ id: "alert", run: () => window.alert("x") },
{ id: "history", run: () => window.history.pushState({}, "", "/x") },
{
id: "keydown-listener",
run: () => document.addEventListener("keydown", () => {}),
},
{ id: "websocket", run: () => new WebSocket("wss://blocked.test") },
{
id: "sendbeacon",
run: () => navigator.sendBeacon("https://blocked.test"),
},
];
function Sandbox() {
const { sandbox } = getTestEnv();
const [probeResults, setProbeResults] = useState<Record<string, string>>({});
const [innerHtml, setInnerHtml] = useState("pending");
const [blockedFetch, setBlockedFetch] = useState("pending");
const [allowedFetch, setAllowedFetch] = useState("pending");
const [blockedXhr, setBlockedXhr] = useState("pending");
const [allowedXhr, setAllowedXhr] = useState("pending");
useEffect(() => {
const results: Record<string, string> = {};
for (const probe of BLOCKED_PROBES) {
try {
probe.run();
results[probe.id] = "not blocked";
} catch {
results[probe.id] = "blocked";
}
}
setProbeResults(results);
// `innerHTML` is distorted through DOMPurify, which strips <script>.
try {
const el = document.createElement("div");
el.innerHTML = "<script>window.x=1</script><b>ok</b>";
setInnerHtml(el.querySelector("script") ? "not stripped" : "stripped");
} catch (err) {
setInnerHtml(`threw: ${describeError(err)}`);
}
if (sandbox) {
fetch(sandbox.blockedUrl)
.then((res) => setBlockedFetch(`ok: ${res.status}`))
.catch((err) => setBlockedFetch(`blocked: ${describeError(err)}`));
fetch(sandbox.allowedUrl)
.then((res) => setAllowedFetch(`ok: ${res.status}`))
.catch((err) => setAllowedFetch(`blocked: ${describeError(err)}`));
if (sandbox.xhrBlockedUrl) {
try {
const xhr = new XMLHttpRequest();
xhr.open("GET", sandbox.xhrBlockedUrl);
setBlockedXhr("not blocked");
} catch (err) {
setBlockedXhr(`blocked: ${describeError(err)}`);
}
}
if (sandbox.xhrAllowedUrl) {
const xhr = new XMLHttpRequest();
xhr.addEventListener("load", () => setAllowedXhr(`ok: ${xhr.status}`));
xhr.addEventListener("error", () => setAllowedXhr("blocked: error"));
try {
xhr.open("GET", sandbox.xhrAllowedUrl);
xhr.send();
} catch (err) {
setAllowedXhr(`blocked: ${describeError(err)}`);
}
}
}
}, [sandbox]);
return (
<div data-testid="data-app-sandbox" style={{ padding: 24 }}>
<h1 style={{ margin: "0 0 16px" }}>Sandbox</h1>
{BLOCKED_PROBES.map((probe) => (
<div key={probe.id} data-testid={`probe-${probe.id}`}>
{probeResults[probe.id] ?? "pending"}
</div>
))}
<div data-testid="probe-innerhtml">{innerHtml}</div>
<div data-testid="blocked-fetch-result">{blockedFetch}</div>
<div data-testid="allowed-fetch-result">{allowedFetch}</div>
<div data-testid="blocked-xhr-result">{blockedXhr}</div>
<div data-testid="allowed-xhr-result">{allowedXhr}</div>
</div>
);
}
function Isolation() {
const [cssInjected, setCssInjected] = useState("pending");
const [jsMarker, setJsMarker] = useState("pending");
useEffect(() => {
// `createElement("style")` is the one tag the data-app sandbox permits, so
// apps can style their own sandboxed document.
try {
const style = document.createElement("style");
style.textContent = `.isolation-css-probe { color: ${PROBE_COLOR}; } body { background-color: ${PROBE_COLOR}; }`;
document.head.appendChild(style);
setCssInjected("ok");
} catch (err) {
setCssInjected(`failed: ${describeError(err)}`);
}
try {
const globals = globalThis as Record<string, unknown>;
globals.__DATA_APP_ISOLATION_MARKER__ = "in-app";
setJsMarker(String(globals.__DATA_APP_ISOLATION_MARKER__));
} catch (err) {
setJsMarker(`failed: ${describeError(err)}`);
}
}, []);
return (
<div data-testid="data-app-isolation" style={{ padding: 24 }}>
<h1 style={{ margin: "0 0 16px" }}>Isolation</h1>
<div data-testid="css-injected">{cssInjected}</div>
<div data-testid="isolation-css-probe" className="isolation-css-probe">
css probe
</div>
<div data-testid="js-marker">{jsMarker}</div>
</div>
);
}
const ROUTES: Record<string, ComponentType> = {
"/details": Details,
"/sandboxing": Sandbox,
"/isolation": Isolation,
"/query-states": QueryStates,
"/static-question": StaticQuestionPage,
"/combinators": Combinators,
"/actions": Actions,
"/clipboard": Clipboard,
"/missing-question": MissingQuestion,
"/throw": ThrowingPage,
};
function Shell() {
const { pathname, navigate } = useDataAppLocation();
const Page = ROUTES[pathname] ?? Overview;
return (
<div style={{ fontFamily: "sans-serif" }}>
<nav
style={{
padding: 16,
borderBottom: "1px solid #e0e0e0",
display: "flex",
gap: 16,
flexWrap: "wrap",
}}
>
<DataAppLink to="/">Overview</DataAppLink>
<DataAppLink to="/details">Details</DataAppLink>
<DataAppLink to="/sandboxing">Sandbox</DataAppLink>
<DataAppLink to="/isolation">Isolation</DataAppLink>
<button
type="button"
data-testid="navigate-to-details"
onClick={() => navigate("/details")}
>
navigate to details
</button>
</nav>
<div data-testid="current-pathname" style={{ padding: "0 16px" }}>
{pathname}
</div>
<Page />
</div>
);
}
export default function App() {
return (
<DataAppRouter>
<Shell />
</DataAppRouter>
);
}

View File

@@ -1,7 +0,0 @@
import type { DataAppFactory } from "@metabase/embedding-sdk-react/data-app";
import App from "./App";
const factory: DataAppFactory = () => ({ component: App });
export default factory;

View File

@@ -1,20 +0,0 @@
// The type is the tests' contract and lives with them; this file is only how the
// app reads it back inside the sandbox realm.
import type { DataAppTestEnv } from "../../../../helpers/data-app-test-env";
declare global {
// eslint-disable-next-line no-var -- `var` (not `const`) so `globalThis.x` typechecks
var __METABASE_DATA_APP_TEST_ENV__: DataAppTestEnv | undefined;
}
export function getTestEnv(): DataAppTestEnv {
const env = globalThis.__METABASE_DATA_APP_TEST_ENV__;
if (!env) {
throw new Error(
"data-app test env was not injected — pass `testEnv` to H.mockDataApp",
);
}
return env;
}

View File

@@ -1,16 +0,0 @@
{
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"noEmit": true,
"incremental": false,
"rootDir": "../../../.."
},
"include": [
"../../../../frontend/src/types/**/*.d.ts",
"../../../../frontend/src/embedding-sdk-shared/types/*.d.ts",
"../../../../frontend/src/metabase/utils/dayjs.ts",
"*/src/**/*.ts",
"*/src/**/*.tsx"
],
"exclude": [".build"]
}

View File

@@ -1,2 +0,0 @@
// Present, but unreachable: its data_app.yaml can't be parsed, so the app is skipped.
export default () => ({ component: null });

View File

@@ -1,3 +0,0 @@
name: Broken Bundle
# Points at a file that isn't in the repo — a valid config, an un-loadable app.
path: ./missing.js

View File

@@ -1,2 +0,0 @@
name: Good App
path: ./index.js

View File

@@ -1,3 +0,0 @@
// A placeholder bundle — this fixture exercises sync/materialization + the admin
// listing + the bundle endpoint, not rendering, so the bytes are served opaquely.
export default () => ({ component: null });

View File

@@ -1,3 +0,0 @@
// A bundle with no data_app.yaml beside it — nothing declares this directory an
// app, so sync doesn't discover it.
export default () => ({ component: null });

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