Compare commits
154 Commits
python-sec
...
test-dev-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f958779a56 | ||
|
|
ccf9960b97 | ||
|
|
1c2e735ab0 | ||
|
|
bb6977debd | ||
|
|
a08f7ff918 | ||
|
|
fdace56d22 | ||
|
|
8b52336966 | ||
|
|
0795d60b21 | ||
|
|
5fe045c2ab | ||
|
|
1ba3a7c050 | ||
|
|
36fa06a4bd | ||
|
|
a323e1c883 | ||
|
|
a9731f6e65 | ||
|
|
a3e82fb537 | ||
|
|
14a65406f1 | ||
|
|
1591e74409 | ||
|
|
dee31666b9 | ||
|
|
bd7af8355e | ||
|
|
24220fe191 | ||
|
|
f35f7450a6 | ||
|
|
089b82338c | ||
|
|
f63af2b18c | ||
|
|
a37f29e737 | ||
|
|
8112036b95 | ||
|
|
dec394cf5e | ||
|
|
020b02932d | ||
|
|
9fc9f53a1f | ||
|
|
9ccac4efed | ||
|
|
7b60d66387 | ||
|
|
3360008053 | ||
|
|
f24e4bed1f | ||
|
|
07b9ec8ffa | ||
|
|
8ee3e5213c | ||
|
|
a2d673e0fe | ||
|
|
4ea4064511 | ||
|
|
55598bf0bd | ||
|
|
c5b6a38acb | ||
|
|
0669d80f2b | ||
|
|
b7d519320b | ||
|
|
26c003f858 | ||
|
|
be2a5e4ea1 | ||
|
|
528ae0a381 | ||
|
|
4323943627 | ||
|
|
b5849c9944 | ||
|
|
d3456cc4d1 | ||
|
|
32e4c39378 | ||
|
|
3bef91fb42 | ||
|
|
5461b53d55 | ||
|
|
dbeb963735 | ||
|
|
8f92d99055 | ||
|
|
3ee16f6e79 | ||
|
|
8ff4bd113d | ||
|
|
6af601cba0 | ||
|
|
cdac81f0cc | ||
|
|
628eee6db4 | ||
|
|
6482737957 | ||
|
|
30021f1abd | ||
|
|
76c36ef709 | ||
|
|
70abeae733 | ||
|
|
6b1809dca1 | ||
|
|
35444ee8a5 | ||
|
|
1152608228 | ||
|
|
226175c492 | ||
|
|
7519d07ec5 | ||
|
|
8adef47af6 | ||
|
|
075482040b | ||
|
|
fad1ba4fd7 | ||
|
|
ce14a4a511 | ||
|
|
001485bb4b | ||
|
|
73c6fa1778 | ||
|
|
5cfbe5423c | ||
|
|
074f2a6e93 | ||
|
|
3e2c3661be | ||
|
|
428c6be070 | ||
|
|
d188001d3a | ||
|
|
c8f693f5da | ||
|
|
f8ae33f741 | ||
|
|
830717374c | ||
|
|
ccfc4fe280 | ||
|
|
bf0d55aac5 | ||
|
|
916a3b8231 | ||
|
|
8e45662740 | ||
|
|
40e185b47f | ||
|
|
e3a24b8db9 | ||
|
|
07909c841f | ||
|
|
a3f5a5bf8a | ||
|
|
ec0a9ceaea | ||
|
|
07cb2f0a6c | ||
|
|
3e9ff6e040 | ||
|
|
6569bc1271 | ||
|
|
a492c1091b | ||
|
|
cda8f7d301 | ||
|
|
2e2b4e1298 | ||
|
|
7413348d68 | ||
|
|
71d66af5a2 | ||
|
|
0de6d6ee2b | ||
|
|
c2decd2881 | ||
|
|
5672dd99c6 | ||
|
|
f8230a5b56 | ||
|
|
bfec5d9f26 | ||
|
|
096ce217b5 | ||
|
|
a5ee670889 | ||
|
|
628f06a22c | ||
|
|
cbaff9c03e | ||
|
|
e1a1e69911 | ||
|
|
b041c02f5a | ||
|
|
e93496629b | ||
|
|
9c2758cc0d | ||
|
|
3246bebfe4 | ||
|
|
b0ba83e74a | ||
|
|
55554106ae | ||
|
|
64f32ddea7 | ||
|
|
67de65ca3b | ||
|
|
e270803b78 | ||
|
|
d518c62a90 | ||
|
|
35cd24c2fe | ||
|
|
7801008876 | ||
|
|
6ab02a5d6d | ||
|
|
1a6755d898 | ||
|
|
7c3311ba35 | ||
|
|
9cb27d3986 | ||
|
|
57e0ad66ea | ||
|
|
74ec111ffe | ||
|
|
ccd7e61184 | ||
|
|
184c415f6e | ||
|
|
53c503ff0f | ||
|
|
a9396323f5 | ||
|
|
04000cef07 | ||
|
|
638f7e7f86 | ||
|
|
8610bfc613 | ||
|
|
8caa13a827 | ||
|
|
806e4b756c | ||
|
|
8dd86422fe | ||
|
|
f051eb94a5 | ||
|
|
3df3238697 | ||
|
|
3a00f09bfd | ||
|
|
f5bbb2bfde | ||
|
|
898f75a2b1 | ||
|
|
05fa507ed7 | ||
|
|
d1f282210c | ||
|
|
f0ce91f74c | ||
|
|
a8234bd97c | ||
|
|
55e1aa315d | ||
|
|
8c87c3489a | ||
|
|
7297aaaa8a | ||
|
|
eb8d806ffe | ||
|
|
f08dbabf1b | ||
|
|
1ec6f1d2d3 | ||
|
|
8fd7008e90 | ||
|
|
144a139152 | ||
|
|
d7a87295df | ||
|
|
cd9d166077 | ||
|
|
95267311e5 | ||
|
|
74af0a368d |
@@ -26,6 +26,7 @@ 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
|
||||
|
||||
|
||||
@@ -179,6 +179,8 @@
|
||||
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!
|
||||
@@ -524,6 +526,7 @@
|
||||
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
|
||||
@@ -736,6 +739,7 @@
|
||||
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!
|
||||
|
||||
@@ -1048,6 +1048,7 @@
|
||||
health-inspector
|
||||
{:team "Querying Platform"
|
||||
:api #{metabase.health-inspector.api
|
||||
metabase.health-inspector.core
|
||||
metabase.health-inspector.init}
|
||||
:uses #{api
|
||||
lib
|
||||
@@ -1303,7 +1304,8 @@
|
||||
metabase.metabot.core
|
||||
metabase.metabot.init
|
||||
metabase.metabot.self
|
||||
metabase.metabot.settings}
|
||||
metabase.metabot.settings
|
||||
metabase.metabot.tools.entity-details}
|
||||
:friends #{agent-api
|
||||
enterprise/metabot
|
||||
enterprise/metabot-analytics
|
||||
@@ -1323,6 +1325,7 @@
|
||||
documents
|
||||
driver
|
||||
entity-retrieval
|
||||
events
|
||||
interestingness
|
||||
legacy-mbql
|
||||
lib
|
||||
@@ -1335,6 +1338,8 @@
|
||||
permissions
|
||||
premium-features
|
||||
pulse
|
||||
queries
|
||||
query-permissions
|
||||
query-processor
|
||||
request
|
||||
search
|
||||
@@ -1360,6 +1365,7 @@
|
||||
:model/DashboardCard
|
||||
:model/DashboardTab
|
||||
:model/Database
|
||||
:model/Document
|
||||
:model/Field
|
||||
:model/Glossary
|
||||
:model/Measure
|
||||
@@ -2011,6 +2017,7 @@
|
||||
metabase.search.core
|
||||
metabase.search.engine
|
||||
metabase.search.in-place.scoring
|
||||
metabase.search.index-health
|
||||
metabase.search.ingestion
|
||||
metabase.search.init
|
||||
metabase.search.scoring
|
||||
@@ -2025,6 +2032,7 @@
|
||||
collections
|
||||
config
|
||||
events
|
||||
health-inspector
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
@@ -2442,6 +2450,7 @@
|
||||
:friends #{models
|
||||
enterprise/transforms-python}
|
||||
:model-exports #{:model/Transform
|
||||
:model/TransformDagRun
|
||||
:model/TransformJob
|
||||
:model/TransformJobRun
|
||||
:model/TransformJobTransformTag
|
||||
@@ -2499,12 +2508,35 @@
|
||||
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
|
||||
@@ -2687,6 +2719,7 @@
|
||||
warehouses}
|
||||
:model-exports #{:model/Dimension
|
||||
:model/Field
|
||||
:model/FieldUserSettings
|
||||
:model/FieldValues
|
||||
:model/Table}
|
||||
:model-imports #{:model/Card
|
||||
@@ -2991,20 +3024,6 @@
|
||||
: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}
|
||||
@@ -3072,6 +3091,17 @@
|
||||
: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
|
||||
@@ -3149,8 +3179,7 @@
|
||||
|
||||
enterprise/dependencies
|
||||
{:team "Graphy"
|
||||
:api #{metabase-enterprise.dependencies.analysis
|
||||
metabase-enterprise.dependencies.api
|
||||
:api #{metabase-enterprise.dependencies.api
|
||||
metabase-enterprise.dependencies.core
|
||||
metabase-enterprise.dependencies.init
|
||||
metabase-enterprise.dependencies.models.dependency}
|
||||
@@ -3233,7 +3262,9 @@
|
||||
:uses #{analytics-interface
|
||||
collections
|
||||
entity-retrieval
|
||||
health-inspector
|
||||
premium-features
|
||||
search
|
||||
task
|
||||
util
|
||||
enterprise/semantic-search}
|
||||
@@ -3420,6 +3451,7 @@
|
||||
task
|
||||
task-history
|
||||
util
|
||||
enterprise/data-apps
|
||||
enterprise/serialization
|
||||
enterprise/transforms-python}
|
||||
:model-exports #{:model/RemoteSyncTask}
|
||||
@@ -3428,6 +3460,7 @@
|
||||
:model/Dashboard
|
||||
:model/Document
|
||||
:model/Field
|
||||
:model/FieldUserSettings
|
||||
:model/Measure
|
||||
:model/NativeQuerySnippet
|
||||
:model/PythonLibrary
|
||||
@@ -3512,8 +3545,7 @@
|
||||
settings
|
||||
system
|
||||
users
|
||||
util
|
||||
enterprise/serialization}
|
||||
util}
|
||||
:model-imports #{:model/ApiKey
|
||||
:model/PermissionsGroup
|
||||
:model/PermissionsGroupMembership
|
||||
@@ -3554,6 +3586,7 @@
|
||||
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
|
||||
@@ -3564,6 +3597,7 @@
|
||||
config
|
||||
connection-pool
|
||||
events
|
||||
health-inspector
|
||||
lib-be
|
||||
llm
|
||||
metabot
|
||||
@@ -3581,12 +3615,7 @@
|
||||
|
||||
enterprise/serialization
|
||||
{:team "Graphy"
|
||||
: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}
|
||||
:api #{metabase-enterprise.serialization.api metabase-enterprise.serialization.cmd metabase-enterprise.serialization.core}
|
||||
:friends #{enterprise/memoize-monitor}
|
||||
:uses #{analytics
|
||||
api
|
||||
@@ -3602,9 +3631,7 @@
|
||||
search
|
||||
server
|
||||
setup
|
||||
sync
|
||||
util
|
||||
warehouse-schema}
|
||||
util}
|
||||
:model-imports :bypass}
|
||||
|
||||
enterprise/snippet-collections
|
||||
@@ -3765,12 +3792,10 @@
|
||||
premium-features
|
||||
query-processor
|
||||
sample-data
|
||||
server
|
||||
settings
|
||||
sql-tools
|
||||
util
|
||||
workspaces
|
||||
enterprise/serialization}
|
||||
workspaces}
|
||||
:model-exports #{:model/Workspace}
|
||||
:model-imports #{:model/Database
|
||||
:model/Table
|
||||
|
||||
@@ -3,3 +3,8 @@
|
||||
(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))
|
||||
|
||||
47
.clj-kondo/ratchets.edn
Normal file
@@ -0,0 +1,47 @@
|
||||
;; 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}}
|
||||
@@ -153,6 +153,7 @@
|
||||
"build."
|
||||
"i18n." ; bin/i18n
|
||||
"lint-migrations-file-test"
|
||||
"mage."
|
||||
"main-test" ; bin/release-list
|
||||
"metabase.deps-edn-test"
|
||||
"metabase.driver."
|
||||
|
||||
3
.github/CODEOWNERS
vendored
@@ -10,6 +10,7 @@ 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
|
||||
|
||||
@@ -49,5 +50,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
|
||||
|
||||
14
.github/actions/create-backport/action.yml
vendored
@@ -38,8 +38,9 @@ runs:
|
||||
CONFLICT_LIST=$(git ls-files -u)
|
||||
|
||||
CONFLICTS=$(git ls-files -u | wc -l)
|
||||
CONFLICTS=1 # TEMP(test branch): force the conflict path — revert before merge
|
||||
if [ "$CONFLICTS" -gt 0 ]; then
|
||||
git cherry-pick --abort
|
||||
git cherry-pick --abort || true # TEMP(test branch): abort fails when the cherry-pick was clean
|
||||
|
||||
echo "Could not cherry pick because of a conflict"
|
||||
echo "has-conflicts=true" >> $GITHUB_OUTPUT
|
||||
@@ -57,14 +58,12 @@ runs:
|
||||
#${ORIGINAL_PULL_REQUEST_NUMBER}
|
||||
> [!IMPORTANT]
|
||||
> Manual conflict resolution is required.
|
||||
> Checkout the branch and run \`./backport.sh\` script. Force push your changes after cherry-picking. Check the box below when done.
|
||||
> Checkout the branch and run \`./backport.sh\` script. Force push your changes after cherry-picking. Mark the PR as ready for review once conflicts have been addressed.
|
||||
|
||||
Conflicts:
|
||||
\`\`\`shell
|
||||
${CONFLICT_LIST}
|
||||
\`\`\`
|
||||
|
||||
- [ ] Conflicts resolved
|
||||
END
|
||||
)
|
||||
else
|
||||
@@ -74,7 +73,12 @@ runs:
|
||||
|
||||
git push -u origin ${BACKPORT_BRANCH}
|
||||
|
||||
BACKPORT_PR_URL=$(gh pr create --base "${TARGET_BRANCH}" --head "${BACKPORT_BRANCH}" --label "was-backported" --assignee "${{ github.event.pull_request.user.login }}" --title "🤖 backported \"${ORIGINAL_TITLE}\"" --body "${PR_BODY}")
|
||||
PR_ARGS=(--base "${TARGET_BRANCH}" --head "${BACKPORT_BRANCH}" --label "was-backported" --assignee "${{ github.event.pull_request.user.login }}" --title "🤖 backported \"${ORIGINAL_TITLE}\"" --body "${PR_BODY}")
|
||||
if [ "$CONFLICTS" -gt 0 ]; then
|
||||
PR_ARGS+=(--draft)
|
||||
fi
|
||||
|
||||
BACKPORT_PR_URL=$(gh pr create "${PR_ARGS[@]}")
|
||||
BACKPORT_PR_NUMBER=${BACKPORT_PR_URL##*/}
|
||||
|
||||
echo "backport_pr_number=$BACKPORT_PR_NUMBER" >> $GITHUB_OUTPUT
|
||||
|
||||
4
.github/file-paths.yaml
vendored
@@ -8,8 +8,8 @@ default: &default
|
||||
- "bin/**/!(*.md)"
|
||||
|
||||
actionlint:
|
||||
- ".github/workflows/*.yml"
|
||||
- ".github/workflows/*.yaml"
|
||||
- added|modified: ".github/workflows/*.yml"
|
||||
- added|modified: ".github/workflows/*.yaml"
|
||||
|
||||
shared_sources: &shared_sources
|
||||
- "src/**/*.cljc"
|
||||
|
||||
3
.github/scripts/setup-python-runner.sh
vendored
@@ -19,8 +19,7 @@ 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 --branch seccomp python-runner
|
||||
|
||||
git clone --depth 1 https://${METABASE_AUTOMATION_USER_TOKEN}@github.com/metabase/python-runner-container.git python-runner
|
||||
|
||||
if [ "$SKIP_START" = false ]; then
|
||||
echo "Starting python-runner..."
|
||||
|
||||
35
.github/workflows/TEMP-test-create-backport.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: TEMP test create-backport
|
||||
|
||||
# TEMPORARY — delete before merging, along with the TEMP lines in
|
||||
# .github/actions/create-backport/action.yml that force CONFLICTS=1.
|
||||
#
|
||||
# Opens a real backport PR against release-x.<CURRENT_VERSION>.x. Because the
|
||||
# action is forced down its conflict path, the PR is created as a draft and the
|
||||
# auto-approve / auto-merge steps are skipped — nothing can merge.
|
||||
#
|
||||
# Label-gated: this opens a real PR, so it must not fire on every push.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: github.event.label.name == 'test-backport'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
|
||||
- name: Pick a commit to backport
|
||||
id: commit
|
||||
run: echo "sha=$(git rev-parse origin/master)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: ./.github/actions/create-backport
|
||||
with:
|
||||
target-version: ${{ vars.CURRENT_VERSION }}
|
||||
backport-commit: ${{ steps.commit.outputs.sha }}
|
||||
github-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
github-token-2: ${{ secrets.GITHUB_TOKEN }}
|
||||
83
.github/workflows/cache-cleanup-codeql.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
# 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"
|
||||
3
.github/workflows/cut-release-branch.yml
vendored
@@ -250,8 +250,7 @@ jobs:
|
||||
|
||||
const message = [
|
||||
`:announcement-animated: ${eng} The v${next} release branch has been cut.`,
|
||||
`- \`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`,
|
||||
`Add the \`backport\` label to backport changes to all supported release branches.`,
|
||||
].join('\n');
|
||||
|
||||
await sendSlackMessage({
|
||||
|
||||
22
.github/workflows/docs-generate-63.yml
vendored
@@ -1,22 +0,0 @@
|
||||
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 }}
|
||||
4
.github/workflows/docs-release-generate.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
target_branch:
|
||||
description: "Target branch for documentation update"
|
||||
required: false
|
||||
default: "release-x.62.x"
|
||||
default: "release-x.63.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.62.x' }}
|
||||
target_branch: ${{ inputs.target_branch || 'release-x.63.x' }}
|
||||
secrets:
|
||||
automation_token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
|
||||
18
.github/workflows/e2e-test.yml
vendored
@@ -64,6 +64,11 @@ 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)
|
||||
@@ -123,9 +128,20 @@ jobs:
|
||||
|
||||
- name: Get failed specs
|
||||
if: github.run_attempt != '1'
|
||||
run: echo "specs=$(cat failed-specs)" >> $GITHUB_ENV
|
||||
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
|
||||
|
||||
- name: Compile CLJS
|
||||
if: steps.prebuilt-cljs.outcome != 'success'
|
||||
run: bun run build-pure:cljs
|
||||
|
||||
- name: Run Metabase
|
||||
|
||||
85
.github/workflows/kondo-ratchets-self-heal.yml
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
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"
|
||||
29
.github/workflows/migrations.yml
vendored
@@ -24,15 +24,42 @@ jobs:
|
||||
check-migration-versions:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 10 # the `backport`-label path builds the release scripts
|
||||
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
|
||||
|
||||
35
.github/workflows/module-boundaries-stats.yml
vendored
@@ -19,40 +19,7 @@ jobs:
|
||||
run: bun install --frozen-lockfile
|
||||
- name: get module boundary stats
|
||||
id: get-stats
|
||||
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
|
||||
run: node frontend/lint/module-boundaries-stats.mjs | tee -a "$GITHUB_OUTPUT"
|
||||
- name: Upload stats
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
|
||||
17
.github/workflows/release-auto.yml
vendored
@@ -34,6 +34,12 @@ 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: |
|
||||
@@ -125,7 +131,7 @@ jobs:
|
||||
|
||||
core.setOutput('should_release', 'false');
|
||||
- name: Write job summary
|
||||
if: always()
|
||||
if: always() && github.repository == 'metabase/metabase'
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
@@ -157,3 +163,12 @@ 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 }}
|
||||
|
||||
376
.github/workflows/release-embedding-sdk.yml
vendored
@@ -1,46 +1,103 @@
|
||||
# Step 2 of the SDK release process: merging a version-bump PR (opened by
|
||||
# sdk-version-bump-pr.yml, labeled `sdk-release-trigger`) publishes the version
|
||||
# and dist-tag committed in package.template.json's `sdkRelease` block.
|
||||
#
|
||||
# workflow_dispatch publishes the same way, from the branch chosen in the ref
|
||||
# picker, without a PR. It reads the same committed sdkRelease block and never
|
||||
# bumps the version - so it is not how you cut a release. Use it only to publish
|
||||
# a version that is already committed, when the bump PR cannot do it.
|
||||
#
|
||||
# To retry a failed run, re-run the failed jobs on that run rather than
|
||||
# dispatching a fresh one: if publish-npm already succeeded, a fresh run would
|
||||
# stop at check-git-tag on the existing tag.
|
||||
name: Release Metabase Embedding SDK for React
|
||||
run-name: Release SDK from ${{ inputs.branch }}
|
||||
run-name: Release SDK from ${{ github.event_name == 'workflow_dispatch' && github.ref_name || github.event.pull_request.base.ref }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "Branch we want to release the SDK from (to release from other branches use the workflow from those branches)"
|
||||
read_first:
|
||||
description: |
|
||||
⚠️ To cut a release, use the "SDK Version Bump PR" workflow instead — it bumps the version and opens a PR; merging that PR publishes.
|
||||
This publishes the version already committed in package.template.json. It never bumps. Pick the branch in "Use workflow from" above.
|
||||
Use it only when neither the bump PR nor re-running the failed jobs will do.
|
||||
type: choice
|
||||
required: true
|
||||
default: "master"
|
||||
required: false
|
||||
options:
|
||||
- master
|
||||
- Publish the committed version from the branch picked in "Use workflow from" above
|
||||
# Step 2 of the new release process: merging a version-bump PR (opened by
|
||||
# sdk-version-bump-pr.yml, labeled `sdk-release-trigger`) triggers a release
|
||||
# of whatever branch that PR targeted. See `prepare-release` below.
|
||||
#
|
||||
# The `paths` filter keeps this from creating a run for every PR closed
|
||||
# anywhere in the repo - version-bump PRs always touch package.template.json,
|
||||
# so this is a reliable pre-filter ahead of the label/merge check below.
|
||||
pull_request:
|
||||
types: [closed]
|
||||
paths:
|
||||
- enterprise/frontend/src/embedding-sdk-package/package.template.json
|
||||
|
||||
concurrency:
|
||||
# We want to ensure only one job is running at a time per branch because
|
||||
# there could be a conflict when updating the readme file.
|
||||
group: ${{ github.workflow }}-${{ inputs.branch }}
|
||||
cancel-in-progress: true
|
||||
# One release at a time per branch; base.ref is empty on workflow_dispatch, so
|
||||
# it falls back to the ref picker's branch. Never cancel in progress - a queued
|
||||
# release that turns out stale fails loudly at check-git-tag.
|
||||
#
|
||||
# Non-release closed PRs queue here too and skip in seconds. GitHub keeps only
|
||||
# one pending run per group and cancels any earlier one, so one could in theory
|
||||
# evict a release waiting to start; that needs two same-branch releases at once.
|
||||
# `queue: max` would fix it, but actionlint cannot parse it (rhysd/actionlint#654).
|
||||
group: sdk-release-${{ github.event.pull_request.base.ref || github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Gates the whole workflow: either a manual workflow_dispatch, or a merged
|
||||
# PR carrying the sdk-release-trigger label. Every other job depends
|
||||
# (directly or transitively) on this one, so if it's skipped the rest of
|
||||
# the run is skipped too - no separate `if:` needed on downstream jobs.
|
||||
prepare-release:
|
||||
name: Prepare release trigger
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'sdk-release-trigger'))
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
branch: ${{ steps.resolve.outputs.branch }}
|
||||
steps:
|
||||
# base.ref is only set on a pull_request event; on a dispatch it is empty,
|
||||
# so this falls through to the branch picked in the ref picker.
|
||||
- name: Resolve target branch
|
||||
id: resolve
|
||||
run: echo "branch=${{ github.event.pull_request.base.ref || github.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
workflow-summary:
|
||||
name: Log inputs
|
||||
needs: prepare-release
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Generate workflow summary
|
||||
env:
|
||||
BRANCH: ${{ needs.prepare-release.outputs.branch }}
|
||||
run: |
|
||||
tee -a $GITHUB_STEP_SUMMARY << EOF
|
||||
tee -a "$GITHUB_STEP_SUMMARY" << EOF
|
||||
**Inputs:**
|
||||
|
||||
- \`branch\`: ${{ inputs.branch }}
|
||||
- \`branch\`: $BRANCH
|
||||
- \`trigger\`: ${{ github.event_name }}
|
||||
|
||||
triggered by: @${{ github.event.sender.login }}
|
||||
EOF
|
||||
|
||||
send-slack-notification-start:
|
||||
name: "Send Slack notification: SDK release started"
|
||||
needs: determine-sdk-version
|
||||
# Builds release/dist/index.cjs once, for every Slack job to download. It hangs
|
||||
# off prepare-release only - not off the metadata read or anything downstream -
|
||||
# so the artifact exists even when the release fails at its very first step,
|
||||
# which is exactly when send-slack-notification-failure needs it.
|
||||
build-release-scripts:
|
||||
name: Build release scripts
|
||||
needs: prepare-release
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
slackMessageId: ${{ steps.send-slack-message-start.outputs.result }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -51,12 +108,28 @@ jobs:
|
||||
- name: Prepare release scripts
|
||||
uses: ./.github/actions/prepare-release-scripts
|
||||
|
||||
- name: Cache build scripts
|
||||
- name: Upload release scripts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: build-scripts
|
||||
path: release/dist/index.cjs
|
||||
|
||||
send-slack-notification-start:
|
||||
name: "Send Slack notification: SDK release started"
|
||||
needs:
|
||||
- build-release-scripts
|
||||
- read-sdk-release-metadata
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
slackMessageId: ${{ steps.send-slack-message-start.outputs.result }}
|
||||
steps:
|
||||
- name: Retrieve release scripts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: build-scripts
|
||||
path: release/dist/
|
||||
|
||||
- name: Send build start message
|
||||
id: send-slack-message-start
|
||||
uses: actions/github-script@v9
|
||||
@@ -66,7 +139,7 @@ jobs:
|
||||
result-encoding: string
|
||||
script: | # js
|
||||
const { mentionUserByGithubLogin, sendSlackMessage, githubRunLink } = require('${{ github.workspace }}/release/dist/index.cjs');
|
||||
const sdkVersion = "${{ needs.determine-sdk-version.outputs.sdk_version }}";
|
||||
const sdkVersion = "${{ needs.read-sdk-release-metadata.outputs.sdk_version }}";
|
||||
|
||||
const title = `:rocket: *SDK release: ${sdkVersion}* :rocket:`
|
||||
const runLink = githubRunLink(
|
||||
@@ -87,23 +160,22 @@ jobs:
|
||||
send-slack-notification-failure:
|
||||
name: "Send Slack notification: SDK release failed"
|
||||
needs:
|
||||
[
|
||||
"determine-sdk-version",
|
||||
"send-slack-notification-start",
|
||||
"check-git-tag",
|
||||
"test",
|
||||
"build-sdk",
|
||||
"publish-npm",
|
||||
"tag-latest",
|
||||
"git-tag",
|
||||
]
|
||||
- build-release-scripts
|
||||
- read-sdk-release-metadata
|
||||
- send-slack-notification-start
|
||||
- check-git-tag
|
||||
- test
|
||||
- build-sdk
|
||||
- publish-npm
|
||||
- tag-latest
|
||||
- git-tag
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
slackMessageId: ${{ needs.send-slack-notification-start.outputs.slackMessageId }}
|
||||
if: ${{ always() && contains(needs.*.result, 'failure') }}
|
||||
steps:
|
||||
- name: Retrieve build scripts
|
||||
- name: Retrieve release scripts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: build-scripts
|
||||
@@ -115,8 +187,9 @@ jobs:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
with:
|
||||
script: | # js
|
||||
const { githubRunLink, sendSlackReply } = require('${{ github.workspace }}/release/dist/index.cjs');
|
||||
const sdkVersion = "${{ needs.determine-sdk-version.outputs.sdk_version }}";
|
||||
const { githubRunLink, sendSlackReply, sendSlackMessage } = require('${{ github.workspace }}/release/dist/index.cjs');
|
||||
const sdkVersion = "${{ needs.read-sdk-release-metadata.outputs.sdk_version }}";
|
||||
const messageId = "${{ env.slackMessageId }}";
|
||||
|
||||
const runLink = githubRunLink(
|
||||
`:x: ${sdkVersion} Release has failed`,
|
||||
@@ -124,23 +197,34 @@ jobs:
|
||||
context.repo.owner,
|
||||
context.repo.repo,
|
||||
);
|
||||
sendSlackReply({
|
||||
channelName: 'team-embedding-releases',
|
||||
message: runLink,
|
||||
messageId: ${{ env.slackMessageId}},
|
||||
broadcast: true
|
||||
});
|
||||
|
||||
if (messageId) {
|
||||
sendSlackReply({
|
||||
channelName: 'team-embedding-releases',
|
||||
message: runLink,
|
||||
messageId,
|
||||
broadcast: true,
|
||||
});
|
||||
} else {
|
||||
sendSlackMessage({
|
||||
channelName: 'team-embedding-releases',
|
||||
message: runLink,
|
||||
});
|
||||
}
|
||||
|
||||
send-slack-notification-successful:
|
||||
name: "Send Slack notification: SDK release successful"
|
||||
needs:
|
||||
["determine-sdk-version", "send-slack-notification-start", "publish-npm"]
|
||||
- build-release-scripts
|
||||
- read-sdk-release-metadata
|
||||
- send-slack-notification-start
|
||||
- publish-npm
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
slackMessageId: ${{ needs.send-slack-notification-start.outputs.slackMessageId }}
|
||||
steps:
|
||||
- name: Retrieve build scripts
|
||||
- name: Retrieve release scripts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: build-scripts
|
||||
@@ -153,7 +237,7 @@ jobs:
|
||||
with:
|
||||
script: | # js
|
||||
const { githubRunLink, sendSlackReply } = require('${{ github.workspace }}/release/dist/index.cjs');
|
||||
const sdkVersion = "${{ needs.determine-sdk-version.outputs.sdk_version }}";
|
||||
const sdkVersion = "${{ needs.read-sdk-release-metadata.outputs.sdk_version }}";
|
||||
|
||||
const runLink = githubRunLink(
|
||||
`:partydeploy: ${sdkVersion} Release is complete :partydeploy:`,
|
||||
@@ -164,93 +248,104 @@ jobs:
|
||||
sendSlackReply({
|
||||
channelName: 'team-embedding-releases',
|
||||
message: runLink,
|
||||
messageId: ${{ env.slackMessageId}},
|
||||
messageId: "${{ env.slackMessageId }}",
|
||||
broadcast: true
|
||||
});
|
||||
|
||||
determine-sdk-version:
|
||||
# Reads the version + release metadata straight from package.template.json
|
||||
# instead of computing them (the version and sdkRelease.distTag/tagAsLatest
|
||||
# are already correct on this branch, because sdk-version-bump-pr.yml wrote
|
||||
# them there and a human reviewed them before merging).
|
||||
read-sdk-release-metadata:
|
||||
name: Read SDK release metadata
|
||||
needs: prepare-release
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
sdk_version: ${{ fromJson(steps.new-sdk-version.outputs.result).version }}
|
||||
major_version: ${{ fromJson(steps.new-sdk-version.outputs.result).majorVersion }}
|
||||
pre_release_label: ${{ fromJson(steps.new-sdk-version.outputs.result).preReleaseLabel }}
|
||||
sdk_version: ${{ steps.metadata.outputs.sdk_version }}
|
||||
major_version: ${{ steps.metadata.outputs.major_version }}
|
||||
dist_tag: ${{ steps.metadata.outputs.dist_tag }}
|
||||
tag_as_latest: ${{ steps.metadata.outputs.tag_as_latest }}
|
||||
steps:
|
||||
- name: Check out the code using the provided branch
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.branch }}
|
||||
ref: ${{ needs.prepare-release.outputs.branch }}
|
||||
|
||||
- name: Prepare release scripts
|
||||
uses: ./.github/actions/prepare-release-scripts
|
||||
|
||||
- name: Get current SDK version
|
||||
id: current-sdk-version
|
||||
run: |
|
||||
VERSION=$(jq -r .version ./enterprise/frontend/src/embedding-sdk-package/package.template.json)
|
||||
echo "sdk_current_version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get next SDK patch or pre-release version
|
||||
id: new-sdk-version
|
||||
- name: Read version and sdkRelease metadata
|
||||
id: metadata
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: | #js
|
||||
const { getNextSdkVersion } = require('${{ github.workspace }}/release/dist/index.cjs');
|
||||
script: | # js
|
||||
const { readSdkReleaseMetadata } = require('${{ github.workspace }}/release/dist/index.cjs');
|
||||
|
||||
const branch = "${{ inputs.branch }}";
|
||||
const currentVersion = "${{ steps.current-sdk-version.outputs.sdk_current_version }}";
|
||||
let metadata;
|
||||
try {
|
||||
metadata = readSdkReleaseMetadata({
|
||||
packageTemplatePath:
|
||||
'enterprise/frontend/src/embedding-sdk-package/package.template.json',
|
||||
});
|
||||
} catch (error) {
|
||||
core.setFailed(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = getNextSdkVersion(branch, currentVersion);
|
||||
|
||||
return {
|
||||
version: result.version,
|
||||
preReleaseLabel: result.preReleaseLabel,
|
||||
majorVersion: result.majorVersion
|
||||
};
|
||||
core.setOutput('sdk_version', metadata.version);
|
||||
core.setOutput('major_version', metadata.majorVersion);
|
||||
core.setOutput('dist_tag', metadata.distTag);
|
||||
core.setOutput('tag_as_latest', String(metadata.tagAsLatest));
|
||||
|
||||
- name: Append workflow summary
|
||||
run: |
|
||||
tee -a $GITHUB_STEP_SUMMARY << EOF
|
||||
**Derived inputs:**
|
||||
tee -a "$GITHUB_STEP_SUMMARY" << EOF
|
||||
**SDK release metadata (from package.template.json):**
|
||||
|
||||
- \`current_version\`: "${{ steps.current-sdk-version.outputs.sdk_current_version }}"
|
||||
- \`next_version\`: "${{ fromJson(steps.new-sdk-version.outputs.result).version }}"
|
||||
- \`major_version\`: "${{ fromJson(steps.new-sdk-version.outputs.result).majorVersion }}"
|
||||
- \`version\`: "${{ steps.metadata.outputs.sdk_version }}"
|
||||
- \`major_version\`: "${{ steps.metadata.outputs.major_version }}"
|
||||
- \`distTag\`: "${{ steps.metadata.outputs.dist_tag }}"
|
||||
- \`tagAsLatest\`: "${{ steps.metadata.outputs.tag_as_latest }}"
|
||||
EOF
|
||||
|
||||
check-git-tag:
|
||||
needs: determine-sdk-version
|
||||
needs:
|
||||
- prepare-release
|
||||
- read-sdk-release-metadata
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
tag: embedding-sdk-${{ needs.determine-sdk-version.outputs.sdk_version }}
|
||||
tag: embedding-sdk-${{ needs.read-sdk-release-metadata.outputs.sdk_version }}
|
||||
steps:
|
||||
- name: Check out the code using the provided branch
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.branch }}
|
||||
ref: ${{ needs.prepare-release.outputs.branch }}
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: check tags
|
||||
run: |
|
||||
if [ $(git tag -l | grep $tag$) ]
|
||||
if git tag -l | grep -q "${tag}$"
|
||||
then
|
||||
echo "::error::Tag '${{ env.tag }}' already exists. If you expect to run this workflow with the same tag, please remove the tag first and rerun this workflow again."
|
||||
echo "::error::Tag '${{ env.tag }}' already exists. If you expect to run this workflow with the same tag, please remove the tag first and rerun this workflow."
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
|
||||
test:
|
||||
needs: check-git-tag
|
||||
needs:
|
||||
- prepare-release
|
||||
- check-git-tag
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Check out the code using the provided branch
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.branch }}
|
||||
ref: ${{ needs.prepare-release.outputs.branch }}
|
||||
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
@@ -262,14 +357,16 @@ jobs:
|
||||
run: bun run embedding-sdk:test-unit
|
||||
|
||||
build-sdk:
|
||||
needs: [test, determine-sdk-version]
|
||||
needs:
|
||||
- prepare-release
|
||||
- test
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Check out the code using the provided branch
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.branch }}
|
||||
ref: ${{ needs.prepare-release.outputs.branch }}
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
@@ -279,11 +376,9 @@ jobs:
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
|
||||
- name: Bump published npm package version
|
||||
# NOTE: this should happen before "Build SDK bundle" as we inject SDK version into the code during build step
|
||||
run: |
|
||||
./bin/embedding-sdk/release_utils.bash update_package_json_template ${{ needs.determine-sdk-version.outputs.sdk_version }}
|
||||
|
||||
# No version bump here anymore: package.template.json's version is
|
||||
# already correct on this branch (bumped by sdk-version-bump-pr.yml
|
||||
# before merge), so we build with it as-is.
|
||||
- name: Build SDK package
|
||||
run: bun run build-embedding-sdk-package
|
||||
|
||||
@@ -297,94 +392,22 @@ jobs:
|
||||
path: ./resources/embedding-sdk
|
||||
|
||||
publish-npm:
|
||||
needs: [build-sdk, determine-sdk-version]
|
||||
needs:
|
||||
- prepare-release
|
||||
- build-sdk
|
||||
- read-sdk-release-metadata
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
id-token: write # OIDC handshake for npm trusted publishing
|
||||
actions: read # actions/download-artifact reads the Actions API via GITHUB_TOKEN
|
||||
pull-requests: write # gh pr review --approve step uses GITHUB_TOKEN
|
||||
env:
|
||||
sdk_version: ${{ needs.determine-sdk-version.outputs.sdk_version }}
|
||||
steps:
|
||||
- name: Check out branch to prepare for a SDK version bump PR
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
# when we created version update PR, it would only have diff from 2 files SDK readme and SDK package.json template
|
||||
ref: ${{ inputs.branch}}
|
||||
|
||||
- name: Setup git user
|
||||
run: |
|
||||
git config --global user.email "github-automation@metabase.com"
|
||||
git config --global user.name "Metabase Automation"
|
||||
|
||||
- name: Update readme
|
||||
if: inputs.branch != 'master'
|
||||
run: |
|
||||
bash ./bin/embedding-sdk/release_utils.bash update_readme ${{ env.sdk_version }}
|
||||
|
||||
- name: Bump published npm package version
|
||||
run: |
|
||||
bash ./bin/embedding-sdk/release_utils.bash update_package_json_template ${{ env.sdk_version }}
|
||||
|
||||
- name: Create a PR updating readme + published version (using GitHub Metabase Automation account)
|
||||
run: |
|
||||
git checkout -b update-sdk-version-${{ env.sdk_version }}
|
||||
|
||||
if [ "${{ inputs.branch }}" = "master" ]; then
|
||||
COMMIT_MSG="chore(sdk): Update published npm version to ${{ env.sdk_version }}"
|
||||
PR_TITLE="chore(sdk): Update SDK version to ${{ env.sdk_version }}"
|
||||
PR_BODY="Update published npm package version to ${{ env.sdk_version }}"
|
||||
else
|
||||
COMMIT_MSG="docs(sdk): Update Readme version references and published npm version to ${{ env.sdk_version }}"
|
||||
PR_TITLE="docs(sdk): Update SDK version to ${{ env.sdk_version }}"
|
||||
PR_BODY="Update Readme version references and published npm package version to ${{ env.sdk_version }}"
|
||||
fi
|
||||
|
||||
git commit -a -m "$COMMIT_MSG"
|
||||
git push origin HEAD
|
||||
gh pr create --base "${{ inputs.branch }}" \
|
||||
--assignee "${GITHUB_ACTOR}" \
|
||||
--title "$PR_TITLE" \
|
||||
--body "$PR_BODY"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
|
||||
- name: Edit PR adding useful information (using Metabase bot app)
|
||||
# metabase/embedding team is added automatically as code owner of updated `package.template.json` file
|
||||
run: |
|
||||
gh pr edit --add-reviewer albertoperdomo --add-label no-backport
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
|
||||
- name: Auto approve PR (using GitHub Actions account)
|
||||
run: |
|
||||
gh pr review --approve
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Pull request auto-merge (using GitHub Metabase Automation account)
|
||||
run: |
|
||||
gh pr merge --auto --squash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
|
||||
- name: Retrieve build SDK package artifact
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: metabase-sdk
|
||||
path: sdk
|
||||
|
||||
- name: Compute master tag
|
||||
id: compute-tag
|
||||
run: |
|
||||
if [ -n "${{ needs.determine-sdk-version.outputs.pre_release_label }}" ]; then
|
||||
echo "masterTag=${{ needs.determine-sdk-version.outputs.pre_release_label }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "masterTag=nightly" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Trusted publishing requires npm >= 11.5.1 and Node >= 22.14.0.
|
||||
# Node 22.x line bundles npm 10.x, so pin Node 24.x which ships npm 11.x.
|
||||
- name: Setup Node.js for npm publish with trusted publishing
|
||||
@@ -396,34 +419,41 @@ jobs:
|
||||
- name: Publish to NPM
|
||||
working-directory: sdk
|
||||
run: |
|
||||
TAG="${{ steps.compute-tag.outputs.masterTag }}"
|
||||
TAG="${{ needs.read-sdk-release-metadata.outputs.dist_tag }}"
|
||||
echo "Resolved npm tag: $TAG"
|
||||
npm publish --tag "$TAG"
|
||||
|
||||
# When a newer release branch goes gold, remove this entire job from this branch.
|
||||
tag-latest:
|
||||
name: Tag npm release as latest
|
||||
# Pre-gold: remove `if: false` once this branch becomes gold.
|
||||
if: false
|
||||
needs: [publish-npm, determine-sdk-version]
|
||||
# Driven by package.template.json's sdkRelease.tagAsLatest instead of a
|
||||
# hardcoded `if: false` that had to be hand-edited (uncommented) once a
|
||||
# branch went gold - see sdk-version-bump-pr.yml, which is where a human
|
||||
# sets this to true when appropriate.
|
||||
if: ${{ needs.read-sdk-release-metadata.outputs.tag_as_latest == 'true' }}
|
||||
needs:
|
||||
- publish-npm
|
||||
- read-sdk-release-metadata
|
||||
uses: ./.github/workflows/embedding-sdk-tag-latest.yml
|
||||
with:
|
||||
version: ${{ needs.determine-sdk-version.outputs.sdk_version }}
|
||||
version: ${{ needs.read-sdk-release-metadata.outputs.sdk_version }}
|
||||
secrets: inherit
|
||||
|
||||
git-tag:
|
||||
needs: [publish-npm, determine-sdk-version]
|
||||
needs:
|
||||
- prepare-release
|
||||
- publish-npm
|
||||
- read-sdk-release-metadata
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
tag: embedding-sdk-${{ needs.determine-sdk-version.outputs.sdk_version }}
|
||||
stable_tag: embedding-sdk-${{ needs.determine-sdk-version.outputs.major_version }}-stable
|
||||
tag: embedding-sdk-${{ needs.read-sdk-release-metadata.outputs.sdk_version }}
|
||||
stable_tag: embedding-sdk-${{ needs.read-sdk-release-metadata.outputs.major_version }}-stable
|
||||
steps:
|
||||
- name: Check out the code using the provided branch
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
ref: ${{ inputs.branch }}
|
||||
ref: ${{ needs.prepare-release.outputs.branch }}
|
||||
|
||||
- name: Setup git user
|
||||
run: |
|
||||
@@ -436,7 +466,7 @@ jobs:
|
||||
git push origin ${{ env.tag }}
|
||||
|
||||
- name: Create and push SDK stable version tag
|
||||
if: ${{ startsWith(inputs.branch, 'release-x.') }}
|
||||
if: ${{ startsWith(needs.prepare-release.outputs.branch, 'release-x.') }}
|
||||
run: |
|
||||
git tag -a -f ${{ env.stable_tag }} -m "Tagging SDK stable version ${{ env.stable_tag }}"
|
||||
git push origin -f ${{ env.stable_tag }}
|
||||
|
||||
36
.github/workflows/rerun-workflows.yml
vendored
@@ -15,20 +15,30 @@ 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:" >> $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
|
||||
{
|
||||
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"
|
||||
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
node-version: lts/Jod # 22.x.x
|
||||
- run: npm install @slack/web-api
|
||||
sparse-checkout: |
|
||||
.github
|
||||
release
|
||||
|
||||
- name: Prepare release scripts
|
||||
uses: ./.github/actions/prepare-release-scripts
|
||||
|
||||
- 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;
|
||||
@@ -37,6 +47,7 @@ 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) {
|
||||
@@ -55,8 +66,7 @@ jobs:
|
||||
}
|
||||
} else if (!BRANCH.includes('backport-')) {
|
||||
// notify slack of repeated failure
|
||||
const { WebClient } = require('@slack/web-api');
|
||||
const slack = new WebClient('${{ secrets.SLACK_BOT_TOKEN }}');
|
||||
const { mentionUserByGithubLogin, sendSlackMessage } = require('${{ github.workspace }}/release/dist/index.cjs');
|
||||
|
||||
// find out which jobs failed
|
||||
const { owner, repo } = context.repo;
|
||||
@@ -87,9 +97,9 @@ jobs:
|
||||
]
|
||||
}));
|
||||
|
||||
await slack.chat.postMessage({
|
||||
channel: 'engineering-ci',
|
||||
text: 'Failing tests',
|
||||
await sendSlackMessage({
|
||||
channelName: 'engineering-ci',
|
||||
message: 'Failing tests',
|
||||
blocks: [
|
||||
{
|
||||
"type": "header",
|
||||
@@ -107,7 +117,7 @@ jobs:
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"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}\`>`
|
||||
"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}\`>`
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
54
.github/workflows/run-tests.yml
vendored
@@ -93,13 +93,63 @@ 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]
|
||||
needs: [create-test-plan, static-viz-files-changed]
|
||||
uses: ./.github/workflows/backend.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
skip: ${{ needs.create-test-plan.outputs.backend_all != 'true' }}
|
||||
skip: ${{ needs.create-test-plan.outputs.backend_all != 'true' && needs.static-viz-files-changed.outputs.static_viz != 'true' }}
|
||||
|
||||
semantic-search-tests:
|
||||
needs: create-test-plan
|
||||
|
||||
180
.github/workflows/sdk-version-bump-pr.yml
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
# Step 1 of the SDK release process: bump the version (and the sdkRelease
|
||||
# metadata that controls the npm dist-tag / `latest` promotion) in
|
||||
# package.template.json, then open a PR labeled `sdk-release-trigger`.
|
||||
# Merging that PR is what actually triggers a release, via the
|
||||
# `pull_request` trigger on release-embedding-sdk.yml.
|
||||
name: SDK Version Bump PR
|
||||
run-name: Bump Embedding SDK version (${{ inputs.release_type }}) on ${{ github.ref_name }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
read_first:
|
||||
description: "Bumps the version on the branch chosen in 'Use workflow from' above."
|
||||
type: choice
|
||||
required: false
|
||||
options:
|
||||
- Pick the branch in "Use workflow from" above
|
||||
release_type:
|
||||
description: |
|
||||
Before gold:
|
||||
• beta — release branch: publish a beta
|
||||
|
||||
At gold:
|
||||
• patch — release branch: graduate the beta to 0.N.0
|
||||
• preminor — master: move master to the next major
|
||||
|
||||
After gold:
|
||||
• patch — release branch: ship a patch (0.N.1, 0.N.2, ...)
|
||||
|
||||
Any time:
|
||||
• alpha — master: bump the development version
|
||||
• custom — one-off branch (esbuild, data-apps): publish under its own prerelease id
|
||||
type: choice
|
||||
required: true
|
||||
options:
|
||||
- alpha
|
||||
- beta
|
||||
- preminor
|
||||
- patch
|
||||
- custom
|
||||
prerelease_id:
|
||||
description: "release_type=custom only, and only on the FIRST cut of a one-off branch (e.g. data-apps). Becomes the prerelease id in the version (0.62.5-data-apps.0) and the npm dist-tag (62-data-apps). Leave empty on later bumps - the id is reused from the current version."
|
||||
type: string
|
||||
required: false
|
||||
|
||||
concurrency:
|
||||
# one version-bump in flight per branch at a time, to avoid racing PRs
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
bump-version:
|
||||
name: Bump SDK version and open review PR
|
||||
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
env:
|
||||
package_template: enterprise/frontend/src/embedding-sdk-package/package.template.json
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
|
||||
- name: Setup git user
|
||||
run: |
|
||||
git config --global user.email "github-automation@metabase.com"
|
||||
git config --global user.name "Metabase Automation"
|
||||
|
||||
- name: Prepare release scripts
|
||||
uses: ./.github/actions/prepare-release-scripts
|
||||
|
||||
- name: Compute version and sdkRelease metadata
|
||||
id: metadata
|
||||
uses: actions/github-script@v9
|
||||
# Pass inputs via env, not inlined `${{ }}` in the script: github-script
|
||||
# interpolates expressions into the JS source before running it, so a
|
||||
# free-form input like prerelease_id could inject code. env values are
|
||||
# read at runtime and can't escape into the code.
|
||||
env:
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
RELEASE_TYPE: ${{ inputs.release_type }}
|
||||
PRERELEASE_ID: ${{ inputs.prerelease_id }}
|
||||
LATEST_MAJOR_VERSION: ${{ vars.LATEST_MAJOR_VERSION }}
|
||||
PACKAGE_TEMPLATE: ${{ env.package_template }}
|
||||
with:
|
||||
script: | # js
|
||||
const { applySdkVersionBump } = require('${{ github.workspace }}/release/dist/index.cjs');
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = applySdkVersionBump({
|
||||
packageTemplatePath: process.env.PACKAGE_TEMPLATE,
|
||||
branch: process.env.BRANCH,
|
||||
releaseType: process.env.RELEASE_TYPE,
|
||||
prereleaseId: process.env.PRERELEASE_ID,
|
||||
latestMajorVersion: process.env.LATEST_MAJOR_VERSION,
|
||||
});
|
||||
} catch (error) {
|
||||
core.setFailed(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput("previous_version", result.previousVersion);
|
||||
core.setOutput("new_version", result.newVersion);
|
||||
core.setOutput("major_version", result.majorVersion);
|
||||
core.setOutput("dist_tag", result.distTag);
|
||||
core.setOutput("tag_as_latest", String(result.tagAsLatest));
|
||||
|
||||
- name: Append workflow summary
|
||||
env:
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
run: |
|
||||
tee -a "$GITHUB_STEP_SUMMARY" << EOF
|
||||
**SDK version bump:**
|
||||
|
||||
- \`branch\`: $BRANCH
|
||||
- \`release_type\`: ${{ inputs.release_type }}
|
||||
- \`previous_version\`: ${{ steps.metadata.outputs.previous_version }}
|
||||
- \`new_version\`: ${{ steps.metadata.outputs.new_version }}
|
||||
- \`distTag\` (default, review before merging): ${{ steps.metadata.outputs.dist_tag }}
|
||||
- \`tagAsLatest\` (auto-computed from \`vars.LATEST_MAJOR_VERSION\`, review before merging): ${{ steps.metadata.outputs.tag_as_latest }}
|
||||
|
||||
triggered by: @${{ github.event.sender.login }}
|
||||
EOF
|
||||
|
||||
- name: Commit version bump
|
||||
env:
|
||||
BUMP_BRANCH: sdk-version-bump-${{ steps.metadata.outputs.new_version }}
|
||||
run: |
|
||||
# A leftover branch means an earlier bump to this same version was
|
||||
# abandoned. Say so, rather than pushing a near-duplicate alongside it.
|
||||
if git ls-remote --exit-code --heads origin "$BUMP_BRANCH" >/dev/null 2>&1; then
|
||||
echo "::error::'$BUMP_BRANCH' already exists on origin - an earlier bump to this version was never merged. Close its PR, delete the branch, then re-run."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git checkout -b "$BUMP_BRANCH"
|
||||
git add "$package_template"
|
||||
git commit -m "chore(sdk): bump SDK version to ${{ steps.metadata.outputs.new_version }} (${{ inputs.release_type }})"
|
||||
git push origin HEAD
|
||||
|
||||
- name: Open version-bump PR (using GitHub Metabase Automation account)
|
||||
id: open-pr
|
||||
env:
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
run: |
|
||||
# TODO (EMB-2095) auto-approve and auto-merge this PR once the computed
|
||||
# version/distTag/tagAsLatest have proven out over a few real releases.
|
||||
# Nothing gates on review today - merging is only manual as a seatbelt.
|
||||
#
|
||||
# --reviewer tags the team so a release is visible, not because anything
|
||||
# waits on a review. It needs the automation PAT: GITHUB_TOKEN cannot
|
||||
# resolve an org team.
|
||||
gh pr create --base "$BRANCH" \
|
||||
--assignee "${GITHUB_ACTOR}" \
|
||||
--reviewer metabase/embedding \
|
||||
--label sdk-release-trigger \
|
||||
--title "chore(sdk): bump SDK version on $BRANCH to ${{ steps.metadata.outputs.new_version }}" \
|
||||
--body "$(cat <<'BODY'
|
||||
Bumps the Embedding SDK version in `package.template.json` and updates its `sdkRelease` metadata.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Branch | `${{ github.ref_name }}` |
|
||||
| Release type | `${{ inputs.release_type }}` |
|
||||
| Version | `${{ steps.metadata.outputs.previous_version }}` → `${{ steps.metadata.outputs.new_version }}` |
|
||||
| npm dist-tag | `${{ steps.metadata.outputs.dist_tag }}` |
|
||||
| Tag as `latest` | `${{ steps.metadata.outputs.tag_as_latest }}` |
|
||||
|
||||
`distTag` and `tagAsLatest` are computed from the branch and release type. `tagAsLatest` is `true` only when patching the current gold major (`vars.LATEST_MAJOR_VERSION`).
|
||||
|
||||
Merging this PR (with the `sdk-release-trigger` label) triggers [release-embedding-sdk.yml](../actions/workflows/release-embedding-sdk.yml), which builds, publishes to npm, and tags the release.
|
||||
|
||||
_Opened by the [SDK Version Bump PR workflow](../actions/workflows/sdk-version-bump-pr.yml), triggered by @${{ github.event.sender.login }}._
|
||||
BODY
|
||||
)"
|
||||
19
.github/workflows/tag-for-release.yml
vendored
@@ -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
|
||||
echo "\n\nNew version.properties:"
|
||||
printf "\n\nNew version.properties:"
|
||||
cat version.properties
|
||||
echo "\n\nUpdating metabase.jar with new version.properties"
|
||||
printf "\n\nUpdating metabase.jar with new version.properties"
|
||||
jar uf metabase.jar version.properties
|
||||
|
||||
- name: Store commit's SHA-1 hash
|
||||
@@ -280,12 +280,3 @@ 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
@@ -89,6 +89,7 @@ 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
|
||||
@@ -150,6 +151,7 @@ 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/
|
||||
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 382 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.8 KiB |
27
.typedoc/typedoc-plugin-not-exported.js
Normal file
@@ -0,0 +1,27 @@
|
||||
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]);
|
||||
});
|
||||
}
|
||||
@@ -10,6 +10,7 @@ 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",
|
||||
|
||||
30
CLAUDE.md
@@ -77,6 +77,36 @@ 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
@@ -1,7 +1,8 @@
|
||||
{: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.
|
||||
:paths ["mage/src" "mage/test" "bin/lint-migrations-file/src"]
|
||||
;; 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"]
|
||||
;; 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
|
||||
@@ -12,7 +13,7 @@
|
||||
org.babashka/json {:mvn/version "0.1.6"} ;; json parsing
|
||||
table/table {:mvn/version "0.5.0"} ;; table printing
|
||||
}
|
||||
:pods {org.babashka/postgresql {:version "0.1.0"}}
|
||||
;; No :pods key on purpose: tasks lazily load only what they need.
|
||||
:tasks
|
||||
{:requires [[mage.cli :as cli]
|
||||
[mage.color :as c]]
|
||||
@@ -274,6 +275,32 @@
|
||||
: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"]]
|
||||
|
||||
@@ -169,7 +169,6 @@
|
||||
(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))
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
(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
|
||||
|
||||
@@ -3,6 +3,7 @@ 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
|
||||
@@ -39,30 +40,53 @@ elif [[ "$BASE_REF" == "master" ]]; then
|
||||
fi
|
||||
echo "CURRENT_VERSION: ${CURRENT_VERSION}"
|
||||
|
||||
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))
|
||||
# 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
|
||||
fi
|
||||
done
|
||||
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)))
|
||||
|
||||
constraint="="
|
||||
version=$((CURRENT_VERSION + 1))
|
||||
# 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))
|
||||
# ${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
|
||||
|
||||
[[ $matches -gt 1 ]] && echo "NOTE: Multiple backport labels detected; using the earliest version (v${version})." >&2
|
||||
[[ ${#candidates[@]} -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")
|
||||
echo "Expected directory: ${constraint} ${expected_dir}"
|
||||
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 ""
|
||||
|
||||
# Check that new migration files are in the correct directory
|
||||
@@ -84,9 +108,16 @@ while IFS= read -r file; do
|
||||
dir_version=$((10#$dir))
|
||||
dir=$(printf "%03d" "$dir_version")
|
||||
|
||||
if [[ "$constraint" == "=" && "$dir_version" -ne "$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" ]] ||
|
||||
[[ "$constraint" == "<=" && "$dir_version" -gt "$version" ]]; then
|
||||
echo "ERROR: ${file} is in ${dir} but expected ${constraint} ${expected_dir}" >&2
|
||||
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
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
done <<<"$changed_files"
|
||||
@@ -98,19 +129,24 @@ 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 [[ "$constraint" == "<=" ]]; then
|
||||
echo "To fix: move the file(s) to resources/migrations/${expected_dir}/ or any earlier version directory." >&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
|
||||
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 "(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 "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 "" >&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
|
||||
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
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-env node */
|
||||
/* eslint-disable import/no-commonjs, import/order, no-console */
|
||||
/* eslint-disable import/order */
|
||||
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
|
||||
@@ -26,10 +29,52 @@ const REPLACES_MAP = {
|
||||
cljs: "target/cljs_release",
|
||||
};
|
||||
|
||||
const API_EXTRACTOR_CONFIG_PATH = path.join(
|
||||
__dirname,
|
||||
"../../enterprise/frontend/src/embedding-sdk-package/embedding-sdk-api-extractor.json",
|
||||
);
|
||||
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 getLogger = (prefix) => {
|
||||
const verbose = process.env.SDK_FIXUP_VERBOSE_LOGS === "true";
|
||||
@@ -110,7 +155,7 @@ const removeUnresolvedReexports = (filePath) => {
|
||||
}
|
||||
};
|
||||
|
||||
const fixupTypesAfterCompilation = ({ isWatchMode }) => {
|
||||
const fixupTypesAfterCompilation = async ({ isWatchMode }) => {
|
||||
log("Fixing SDK d.ts files...");
|
||||
|
||||
const dtsFilePaths = glob.sync(`${SDK_DIST_DIR_PATH}/**/*.d.ts`);
|
||||
@@ -123,45 +168,97 @@ const fixupTypesAfterCompilation = ({ isWatchMode }) => {
|
||||
console.log("[dts fixup] Done!");
|
||||
|
||||
if (!isWatchMode) {
|
||||
generateDtsRollup();
|
||||
await generateDtsRollup();
|
||||
}
|
||||
};
|
||||
|
||||
const generateDtsRollup = () => {
|
||||
const generateDtsRollup = async () => {
|
||||
// Dts rollup logger
|
||||
const { log, error } = getLogger("dts rollup");
|
||||
|
||||
log("Generate dts rollup...");
|
||||
|
||||
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);
|
||||
const existingEntryPointCount = DTS_ROLLUPS.filter(({ entryPointPath }) =>
|
||||
fs.existsSync(path.resolve(entryPointPath)),
|
||||
).length;
|
||||
|
||||
if (!dtsRollupEntryPointPathExist) {
|
||||
if (existingEntryPointCount === 0) {
|
||||
log("It looks like dts rollup is already generated, skipping...");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const apiExtractorConfig = ExtractorConfig.loadFileAndPrepare(
|
||||
API_EXTRACTOR_CONFIG_PATH,
|
||||
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 extractorResult = Extractor.invoke(apiExtractorConfig, {
|
||||
localBuild: true,
|
||||
showVerboseMessages: true,
|
||||
});
|
||||
for (const { name, status, signal, output } of results) {
|
||||
log(`--- api-extractor: ${name} ---`);
|
||||
process.stdout.write(output);
|
||||
|
||||
if (!extractorResult.succeeded) {
|
||||
error(
|
||||
`API Extractor completed with ${extractorResult.errorCount} errors` +
|
||||
` and ${extractorResult.warningCount} warnings`,
|
||||
);
|
||||
if (status !== 0 || signal) {
|
||||
error(
|
||||
`API Extractor failed for ${name}` +
|
||||
(signal ? ` with signal ${signal}` : ""),
|
||||
);
|
||||
hasExtractorFailure = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasExtractorFailure) {
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
log("API Extractor completed successfully");
|
||||
@@ -220,7 +317,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);
|
||||
fixupTypesAfterCompilation({ isWatchMode });
|
||||
await fixupTypesAfterCompilation({ isWatchMode });
|
||||
|
||||
if (isWatchMode) {
|
||||
console.log("\n\n\n");
|
||||
|
||||
@@ -42,10 +42,16 @@ function generateSdkPackage() {
|
||||
);
|
||||
const sdkPackageTemplateJsonContent = JSON.parse(sdkPackageTemplateJson);
|
||||
|
||||
// `sdkRelease` is release-process metadata (distTag/tagAsLatest) read by
|
||||
// .github/workflows/release-embedding-sdk.yml — it must never ship inside
|
||||
// the published npm package.json.
|
||||
const { sdkRelease: _sdkRelease, ...publishableTemplateJsonContent } =
|
||||
sdkPackageTemplateJsonContent;
|
||||
|
||||
const todayDate = new Date().toJSON().slice(0, 10).replaceAll("-", "");
|
||||
|
||||
const mergedContent = {
|
||||
...sdkPackageTemplateJsonContent,
|
||||
...publishableTemplateJsonContent,
|
||||
version: maybeCommitHash
|
||||
? `${sdkPackageTemplateJsonContent.version}-${todayDate}-${maybeCommitHash}`
|
||||
: sdkPackageTemplateJsonContent.version,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
function update_readme() {
|
||||
sed -i -E 's|(embedding-sdk-)[0-9.]+|\1'"$1"'|' enterprise/frontend/src/embedding-sdk-package/README.md
|
||||
sed -i -E 's|(http://downloads.metabase.com/sdk/v)[0-9.]+|\1'"$1"'|' enterprise/frontend/src/embedding-sdk-package/README.md
|
||||
}
|
||||
|
||||
function update_package_json_template() {
|
||||
sed -i -E 's/("version": ").*"/\1'"$1"'"/' enterprise/frontend/src/embedding-sdk-package/package.template.json
|
||||
}
|
||||
|
||||
$1 $2
|
||||
@@ -207,16 +207,42 @@
|
||||
(str/join ", " bad-ids))
|
||||
{:invalid-ids (vec bad-ids)}))))))
|
||||
|
||||
(defn require-primary-key-exists-has-table-name
|
||||
"Ensures that all primaryKeyExists preconditions specify a tableName."
|
||||
(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."
|
||||
[change-log]
|
||||
(doseq [{:keys [changeSet]} change-log
|
||||
:when changeSet
|
||||
:let [s (pr-str (:preConditions changeSet))]
|
||||
:when (and (str/includes? s ":primaryKeyExists")
|
||||
(not (str/includes? s ":tableName")))]
|
||||
:let [[precondition required] (first (unscoped-preconditions (:preConditions changeSet)))]
|
||||
:when precondition]
|
||||
(throw (validation-error
|
||||
(format "Migration '%s' has primaryKeyExists precondition without tableName" (:id changeSet))
|
||||
(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))
|
||||
{:id (:id changeSet)}))))
|
||||
|
||||
(s/def ::changeSet
|
||||
@@ -292,7 +318,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-primary-key-exists-has-table-name change-log)
|
||||
(require-table-scoped-existence-preconditions change-log)
|
||||
(let [{:keys [changeSet]
|
||||
:as all} (group-by only-key change-log)]
|
||||
(when-not (set/subset? (set (keys all)) #{:property :objectQuotingStrategy :changeSet :logicalFilePath})
|
||||
@@ -368,17 +394,14 @@
|
||||
(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))))
|
||||
|
||||
@@ -398,3 +398,40 @@
|
||||
: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
@@ -67,7 +67,6 @@
|
||||
"@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",
|
||||
@@ -102,7 +101,6 @@
|
||||
"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",
|
||||
@@ -118,6 +116,7 @@
|
||||
"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",
|
||||
@@ -145,7 +144,6 @@
|
||||
"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",
|
||||
@@ -153,6 +151,7 @@
|
||||
"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",
|
||||
@@ -277,6 +276,7 @@
|
||||
"@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-v7": "npm:react-router@7.18.1",
|
||||
"react-router": "7.18.1",
|
||||
"reg-cli": "^0.17.7",
|
||||
"semver": "^7.6.3",
|
||||
"shadow-cljs": "^3.3.5",
|
||||
@@ -402,6 +402,9 @@
|
||||
"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",
|
||||
@@ -1275,6 +1278,8 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -1439,6 +1444,40 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -1969,8 +2008,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -2157,6 +2194,8 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -2681,8 +2720,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -2905,7 +2942,7 @@
|
||||
|
||||
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
|
||||
|
||||
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="],
|
||||
|
||||
@@ -3119,7 +3156,7 @@
|
||||
|
||||
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
|
||||
|
||||
"estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
|
||||
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
@@ -3401,8 +3438,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -3853,6 +3888,30 @@
|
||||
|
||||
"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="],
|
||||
@@ -3953,7 +4012,7 @@
|
||||
|
||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||
|
||||
"marked": ["marked@4.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="],
|
||||
"marked": ["marked@18.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-MrV5puXBfuiy6wl6DLaq3BtIJQAJToAd5zt/ZKhRfGRAuFPALE7/4Y7jnxRQoEgK/pBgurGqLyAuRgZ2xOjr6w=="],
|
||||
|
||||
"marky": ["marky@1.2.5", "", {}, "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q=="],
|
||||
|
||||
@@ -4629,8 +4688,6 @@
|
||||
|
||||
"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="],
|
||||
@@ -4713,9 +4770,7 @@
|
||||
|
||||
"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@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-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-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" } }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||
|
||||
@@ -4811,6 +4866,8 @@
|
||||
|
||||
"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=="],
|
||||
@@ -4869,6 +4926,8 @@
|
||||
|
||||
"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=="],
|
||||
@@ -5093,8 +5152,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -5461,6 +5518,12 @@
|
||||
|
||||
"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=="],
|
||||
@@ -5473,8 +5536,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -5815,6 +5876,8 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -5835,6 +5898,16 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -6055,12 +6128,8 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -6953,9 +7022,7 @@
|
||||
|
||||
"react-remove-scroll-bar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"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-router/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"react-style-singleton/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
@@ -6985,6 +7052,8 @@
|
||||
|
||||
"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=="],
|
||||
@@ -7007,8 +7076,12 @@
|
||||
|
||||
"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=="],
|
||||
@@ -7197,6 +7270,12 @@
|
||||
|
||||
"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=="],
|
||||
@@ -7463,6 +7542,14 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -8331,6 +8418,8 @@
|
||||
|
||||
"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=="],
|
||||
@@ -8517,6 +8606,8 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
2
deps.edn
@@ -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.5"} ; PDF generation (backend dashboard PDF prototype)
|
||||
org.apache.pdfbox/pdfbox {:mvn/version "3.0.8"} ; 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
|
||||
|
||||
@@ -158,7 +158,6 @@
|
||||
(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))
|
||||
|
||||
252
dev/src/dev/kondo_ratchet.clj
Normal file
@@ -0,0 +1,252 @@
|
||||
(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)))))))
|
||||
@@ -182,6 +182,7 @@ 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)
|
||||
|
||||
@@ -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 in the Sample Database:
|
||||
You could write an action that would update the `plan` column for a record in the `invoices` table:
|
||||
|
||||
```sql
|
||||
{% raw %}
|
||||
|
||||
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 61 KiB |
@@ -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](https://www.metabase.com/learn/metabase-basics/querying-and-dashboards/dashboards/markdown).
|
||||
Like text cards on dashboards, prompts support [Markdown](../dashboards/markdown.md).
|
||||
|
||||
## Prompt tips
|
||||
|
||||
|
||||
2413
docs/api.json
generated
@@ -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 and H2), 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), because there's currently no file storage available.
|
||||
|
||||
## Limited custom certificate support
|
||||
|
||||
|
||||
@@ -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 named `config.yml`. 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. The config file should be located at:
|
||||
|
||||
- The current directory (the directory where the running Metabase JAR is located).
|
||||
- A file named `config.yml` or `config.yaml` in 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:
|
||||
|
||||
@@ -1881,8 +1881,7 @@ 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`
|
||||
|
||||
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`.
|
||||
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.
|
||||
|
||||
### `MB_SAML_IDENTITY_PROVIDER_URI`
|
||||
|
||||
@@ -1933,7 +1932,7 @@ Absolute path to the Keystore file to use for signing SAML requests.
|
||||
- Default: `false`
|
||||
- [Configuration file name](./config-file.md): `saml-slo-enabled`
|
||||
|
||||
Is SAML Single Log Out enabled?
|
||||
If enabled, Metabase will redirect users to your configured SAML Single Logout endpoint when they log out of Metabase.
|
||||
|
||||
### `MB_SAML_USER_PROVISIONING_ENABLED`
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 43 KiB |
@@ -82,7 +82,7 @@ You can use [Markdown](https://commonmark.org/help/) to format the text in your
|
||||
|
||||

|
||||
|
||||
To learn more, see [Fun with Markdown in your dashboards](https://www.metabase.com/learn/metabase-basics/querying-and-dashboards/dashboards/markdown).
|
||||
To learn more, see [Format text with Markdown](./markdown.md).
|
||||
|
||||
### 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 **sharing** 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 **Share** icon to find its public links.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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.metabase.com)
|
||||
[the internet's finest webpage](https://www.example.com)
|
||||
```
|
||||
|
||||
This renders as a clickable link: [the internet's finest webpage](https://www.metabase.com).
|
||||
|
||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 64 KiB |
@@ -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, 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, visibility layer, 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".
|
||||
|
||||
|
||||
@@ -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 well as to provide some sample data out of the box.
|
||||
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.
|
||||
|
||||
## Connecting to databases hosted by a cloud provider
|
||||
|
||||
|
||||
@@ -36,8 +36,6 @@ 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)
|
||||
|
||||
@@ -8,6 +8,16 @@ 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
|
||||
|
||||
@@ -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,13 +417,15 @@ 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.
|
||||
|
||||
### Custom visualizations don't render in embeds or public links
|
||||
### When embedding, custom visualizations are only available in the SDK
|
||||
|
||||
[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).
|
||||
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.
|
||||
|
||||
### Custom visualizations don't render in exports and subscriptions
|
||||
|
||||
Static renders—like charts in alerts or dashboard subscriptions—fall back to a table for any card that uses a custom visualization.
|
||||
Static renders—like charts in alerts or dashboard subscriptions—fall back to a default visualization for the card's data shape.
|
||||
|
||||
## Example plugins
|
||||
|
||||
|
||||
@@ -6,6 +6,17 @@ 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`.
|
||||
|
||||
@@ -9,7 +9,7 @@ redirect_from:
|
||||
|
||||
You can style your embedded [Metabase components](./components.md) with a **theme**.
|
||||
|
||||

|
||||

|
||||
|
||||
On OSS and Starter plans, embeds come with two theme presets: [light and dark](#dark-mode-and-light-mode).
|
||||
|
||||
|
||||
@@ -24,12 +24,12 @@ Toggle **Enable guest embeds**.
|
||||
|
||||
## Creating a guest embed
|
||||
|
||||

|
||||

|
||||
|
||||
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 **sharing icon**.
|
||||
2. Click the **Share** 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 **sharing icon** and select **Embed**.
|
||||
2. Click the **Share** 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 **sharing icon** (square with an arrow pointing to the top right).
|
||||
2. Click the **Share** icon (square with an arrow pointing to the top right).
|
||||
3. Select **Embed**.
|
||||
4. Select **Guest embedding**
|
||||
5. Click **Unpublish**.
|
||||
|
||||
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 47 KiB |
@@ -60,23 +60,24 @@ 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) | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| 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) | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| [Custom visualizations](../questions/visualizations/custom.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.
|
||||
|
||||
@@ -103,7 +104,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)).
|
||||
- Embeds don't render [custom visualizations](../questions/visualizations/custom.md): any card that uses one falls back to the default visualization (a table).
|
||||
- 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.
|
||||
|
||||
## Further reading
|
||||
|
||||
|
||||
@@ -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 sharing icon.
|
||||
2. Click the **Share** icon.
|
||||
3. Select **Embed**.
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Admins can create and share public links (URLs) for questions, dashboards, and d
|
||||
|
||||

|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## 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 **Sharing** icon for the question.
|
||||
1. Click on the **Share** 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 **Sharing** button in the top right menu.
|
||||
To share a dashboard via a public link, admins can click on the **Share** button in the top right menu.
|
||||
|
||||

|
||||
|
||||
@@ -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 **Sharing** 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 **Share** 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 **Sharing** icon for your question or dashboard.
|
||||
1. Click on the **Share** 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 **Sharing** icon.
|
||||
2. Click on the **Share** icon.
|
||||
3. Select **Public link** or **Embed**.
|
||||
4. Click **Remove public link**.
|
||||
|
||||
|
||||
@@ -32,6 +32,21 @@ 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`.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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>]
|
||||
);
|
||||
@@ -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 **sharing icon** > **Embed this item in an application**.
|
||||
2. Click on the **Share** 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 **sharing icon** > **Embed this item in an application**.
|
||||
2. Click on the **Share** 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.
|
||||
|
||||
@@ -41,12 +41,12 @@ If you want to embed charts with additional interactive features, like [drill-do
|
||||
|
||||
## Making a question or dashboard embeddable
|
||||
|
||||

|
||||

|
||||
|
||||
To create a static embed:
|
||||
|
||||
1. Go to the question or dashboard that you want to embed in your website.
|
||||
2. Click on the **sharing icon**.
|
||||
2. Click the **Share** 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 > **sharing icon** > **Embed**.
|
||||
1. Go to the question or dashboard > **Share** 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 > **sharing icon** > **Embed this item in an application**.
|
||||
1. Go to the question or dashboard > **Share** icon > **Embed**.
|
||||
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 on the **sharing icon** (square with an arrow pointing to the top right).
|
||||
2. Click the **Share** icon (square with an arrow pointing to the top right).
|
||||
3. Select **Embed**.
|
||||
4. Select **Static embedding**
|
||||
5. Click **Unpublish**.
|
||||
|
||||
@@ -34,10 +34,6 @@ 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.
|
||||
@@ -103,10 +99,6 @@ 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.
|
||||
|
||||
@@ -1,47 +1,80 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
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.
|
||||
|
||||
@@ -25,12 +25,14 @@ 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)
|
||||
|
||||
52
docs/people-and-groups/two-factor-authentication.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
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)
|
||||
@@ -103,7 +103,7 @@ You can export the results of a dashboard and its cards in different ways:
|
||||
|
||||
### Export dashboard as PDF
|
||||
|
||||
You can export a dashboard as a PDF. Click on the **Sharing** button, then select **Export 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.
|
||||
|
||||

|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 52 KiB |
@@ -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, H2, MariaDB, MySQL, MongoDB, SQL Server, SQLite, Vertica. Presto only provides approximate results.
|
||||
Databases that don't support `percentile`: Druid, 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`: H2, SQL Server, SQLite.
|
||||
Databases that don't support `regexExtract`: SQL Server, SQLite.
|
||||
|
||||
Related: [contains](#contains), [doesNotContain](#doesnotcontain), [substring](#substring).
|
||||
|
||||
@@ -1048,8 +1048,6 @@ 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`.
|
||||
@@ -1070,7 +1068,7 @@ Limitations are noted for each aggregation and function above, and here they are
|
||||
|
||||
**SQL Server**: `Median`, `Percentile`, `regexExtract`, `datetime`, `float`, `splitPart`.
|
||||
|
||||
**SQLite**: `exp`, `log`, `Median`, `Percentile`, `power`, `regexExtract`, `StandardDeviation`, `sqrt`, `Variance`, `datetime`, `float`, `splitPart`.
|
||||
**SQLite** (including Metabase Sample Database): `exp`, `log`, `Median`, `Percentile`, `power`, `regexExtract`, `StandardDeviation`, `sqrt`, `Variance`, `datetime`, `float`, `splitPart`.
|
||||
|
||||
**Vertica**: `Median`, `Percentile`, `datetime`, `float`, `splitPart`.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Custom visualizations
|
||||
summary: Add your own chart types to Metabase by uploading visualization plugins built with the Custom Visualizations SDK.
|
||||
summary: Add your own chart types to Metabase by uploading custom visualizations 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 plugin could leak data through outbound asset requests. See [Only add plugins you trust](#only-add-plugins-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 custom visualization could leak data through outbound asset requests. See [Only add visualizations you trust](#only-add-visualizations-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 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.
|
||||
- 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.
|
||||
|
||||
## Using a custom visualization
|
||||
|
||||
@@ -47,42 +47,44 @@ On a question, dashboard or document card, open the visualization sidebar (the *
|
||||
|
||||

|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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 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.
|
||||
- **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.
|
||||
- **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 don't render in embeds
|
||||
### Custom visualizations in embeds only work when using the SDK
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
## 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 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.
|
||||
- **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.
|
||||
- **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 plugin. PNG export is off by default.
|
||||
- **Custom visualizations can support PNG export**, but only if its developer turned on PNG export for that visualization. PNG export is off by default.
|
||||
|
||||
## Only add plugins you trust
|
||||
## Only add visualizations you trust
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
Metabase runs custom visualizations in a sandbox to limit what a plugin can do:
|
||||
Metabase runs custom visualizations in a sandbox to limit what a custom visualization can do:
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
While this sandboxing limits the damage a plugin can do, you still need to review the code.
|
||||
While this sandboxing limits the damage a custom visualization can do, you still need to review the code.
|
||||
|
||||
## Further reading
|
||||
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
---
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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
|
||||
@@ -344,7 +344,7 @@ Columns:
|
||||
- Conversation ID
|
||||
- Created At
|
||||
- User ID
|
||||
- Summary
|
||||
- Title
|
||||
- User Display Name
|
||||
- Message Count
|
||||
- User Message Count
|
||||
@@ -427,6 +427,7 @@ Columns:
|
||||
- Action Qualified ID
|
||||
- Query
|
||||
- Embedding Client
|
||||
- Embedding Client Identifier
|
||||
- Is Preview
|
||||
- Embedding SDK Package Version
|
||||
- Metabase Version
|
||||
@@ -530,6 +531,7 @@ Columns:
|
||||
- Entity ID
|
||||
- Entity Qualified ID
|
||||
- Embedding Client
|
||||
- Embedding Client Identifier
|
||||
- Auth Method
|
||||
- Is Preview
|
||||
- Metabase Version
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
title: Usage analytics
|
||||
redirect-from:
|
||||
- /docs/latest/enterprise-guide/audit
|
||||
- /docs/latest/usage-and-performance-tools/audit
|
||||
---
|
||||
|
||||
# Usage analytics
|
||||
|
||||
6
e2e/support/assets/data-apps/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# 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
|
||||
@@ -0,0 +1,2 @@
|
||||
name: Custom Error Component
|
||||
path: ./dist/index.js
|
||||
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
2
e2e/support/assets/data-apps/kitchen-sink/data_app.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
name: Kitchen Sink
|
||||
path: ./dist/index.js
|
||||
436
e2e/support/assets/data-apps/kitchen-sink/src/App.tsx
Normal file
@@ -0,0 +1,436 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
7
e2e/support/assets/data-apps/kitchen-sink/src/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { DataAppFactory } from "@metabase/embedding-sdk-react/data-app";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
const factory: DataAppFactory = () => ({ component: App });
|
||||
|
||||
export default factory;
|
||||
20
e2e/support/assets/data-apps/kitchen-sink/src/test-env.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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;
|
||||
}
|
||||