Compare commits
239 Commits
bot-679/ex
...
python-run
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2076d909d9 | ||
|
|
b2c2816d86 | ||
|
|
d8104c911d | ||
|
|
5d5930ab64 | ||
|
|
2738ec81b7 | ||
|
|
cd77eb5140 | ||
|
|
2a4046736a | ||
|
|
660c1ddfe6 | ||
|
|
6845e78a5d | ||
|
|
eea9a750d6 | ||
|
|
123a4f966d | ||
|
|
659d0a37b1 | ||
|
|
59a52b2631 | ||
|
|
a83196324b | ||
|
|
9ab2bfbae8 | ||
|
|
c261122999 | ||
|
|
0b65ccce3a | ||
|
|
bb2adc5f46 | ||
|
|
c5dcdc3238 | ||
|
|
5537f0f258 | ||
|
|
bfdd8a0b23 | ||
|
|
3c05a30b8e | ||
|
|
106a707fcc | ||
|
|
21c8136e7e | ||
|
|
7319599ceb | ||
|
|
13bc693357 | ||
|
|
2b11a3bed2 | ||
|
|
c910ca3fd3 | ||
|
|
d8b40292d1 | ||
|
|
b773f68f23 | ||
|
|
647861b624 | ||
|
|
e741639713 | ||
|
|
5765b0df15 | ||
|
|
40d00b8e7f | ||
|
|
58cde65c4e | ||
|
|
5995621da8 | ||
|
|
70d4a78914 | ||
|
|
cf6e1c032a | ||
|
|
e70f22e0eb | ||
|
|
4fd189509f | ||
|
|
f7fd7fe5c5 | ||
|
|
a0e28bc775 | ||
|
|
a5b4f10c10 | ||
|
|
57502d5ef1 | ||
|
|
b2f981e967 | ||
|
|
fb8f72b7bd | ||
|
|
502b66528c | ||
|
|
2838b6501c | ||
|
|
7603550836 | ||
|
|
ba8074bb87 | ||
|
|
90097715d2 | ||
|
|
12839276af | ||
|
|
2ef536a0fe | ||
|
|
9ad40ab1c3 | ||
|
|
6351e5bc0e | ||
|
|
18d8dadf1b | ||
|
|
d5f606e8f2 | ||
|
|
acbf476d8b | ||
|
|
21af2827f2 | ||
|
|
04b141b975 | ||
|
|
ff9d9acdac | ||
|
|
d7848ee998 | ||
|
|
47c037a3f8 | ||
|
|
ab1ccbf8ec | ||
|
|
8662aaaa87 | ||
|
|
81b10ad959 | ||
|
|
0a67d24d79 | ||
|
|
258305b914 | ||
|
|
21b67b2892 | ||
|
|
3dcbfef5c5 | ||
|
|
1ccf800dee | ||
|
|
d4d83d2f8e | ||
|
|
3e655a0bb2 | ||
|
|
0409c4a20b | ||
|
|
b51c0c9efa | ||
|
|
51f8fb9af1 | ||
|
|
bfb0b42b42 | ||
|
|
09544d950d | ||
|
|
03dd34ec4a | ||
|
|
d08e561420 | ||
|
|
214c38b3f2 | ||
|
|
52ae8a14db | ||
|
|
e153a3290b | ||
|
|
2830b3c94a | ||
|
|
2fa8abc247 | ||
|
|
be2a29d138 | ||
|
|
ab089eb84d | ||
|
|
b40d76f5b4 | ||
|
|
b12dfa4fd9 | ||
|
|
5e2e510811 | ||
|
|
c6a2880b31 | ||
|
|
5cd8e165fa | ||
|
|
05c26a4363 | ||
|
|
8ceeb1fce4 | ||
|
|
49a3037d2a | ||
|
|
fa959aaab4 | ||
|
|
eadbd4b00d | ||
|
|
1da8e5751a | ||
|
|
d47651a2cf | ||
|
|
92001b00de | ||
|
|
186c9dca65 | ||
|
|
42aebbf137 | ||
|
|
df7d834e2e | ||
|
|
bc7ffe094f | ||
|
|
205205174c | ||
|
|
05567456f2 | ||
|
|
9bfb9ed7a4 | ||
|
|
b161e19442 | ||
|
|
c28beea344 | ||
|
|
b22ab2010d | ||
|
|
6f4d71c279 | ||
|
|
c8d5e94ffa | ||
|
|
22a1f275f0 | ||
|
|
ce8660f161 | ||
|
|
dcf260f4c9 | ||
|
|
4897452a08 | ||
|
|
c87ad18e21 | ||
|
|
78a8c59d71 | ||
|
|
336c2a8b1a | ||
|
|
567bbd68bb | ||
|
|
ba6895665f | ||
|
|
09c04d91bb | ||
|
|
8977542307 | ||
|
|
ec678a17fc | ||
|
|
6eda44fc53 | ||
|
|
4219500b7a | ||
|
|
2f2ab17ebb | ||
|
|
89c1712267 | ||
|
|
ba03f8d368 | ||
|
|
6d84d80c06 | ||
|
|
b130d0d812 | ||
|
|
caafceff99 | ||
|
|
5fe95dbbae | ||
|
|
65d9029a55 | ||
|
|
e4ec44c4af | ||
|
|
0e9fd4c48c | ||
|
|
cd32f3b89c | ||
|
|
6d79bbc173 | ||
|
|
7c8125d4bb | ||
|
|
54439261a4 | ||
|
|
18706c2456 | ||
|
|
3d5b673c9a | ||
|
|
0e1325ba1e | ||
|
|
9c801dfbd9 | ||
|
|
c27eee3f51 | ||
|
|
1cfc71fed7 | ||
|
|
619f2b2be6 | ||
|
|
64978428b8 | ||
|
|
58d2cd37b6 | ||
|
|
6901847ed2 | ||
|
|
10286754ee | ||
|
|
8f1606adc5 | ||
|
|
5b6e3071ac | ||
|
|
76e96af834 | ||
|
|
92017bab3f | ||
|
|
c86caa092e | ||
|
|
29590b8e09 | ||
|
|
e822288497 | ||
|
|
a6c90cc7ff | ||
|
|
1571e58188 | ||
|
|
ae1e0d6311 | ||
|
|
3f547462c9 | ||
|
|
05aaad4f3a | ||
|
|
92ec0df7b1 | ||
|
|
35b542b651 | ||
|
|
ab7e4712b1 | ||
|
|
47471f9518 | ||
|
|
86868639b4 | ||
|
|
c61c93e825 | ||
|
|
6dfde80586 | ||
|
|
009911d60a | ||
|
|
237bbe2919 | ||
|
|
c8b1767e66 | ||
|
|
18e327042f | ||
|
|
f02d26aa14 | ||
|
|
8056c1835a | ||
|
|
04055423f0 | ||
|
|
4e7d009a53 | ||
|
|
a41303a180 | ||
|
|
8474a8f7f5 | ||
|
|
2b61cce77b | ||
|
|
0a0625d163 | ||
|
|
bb42762372 | ||
|
|
420c4e27e0 | ||
|
|
7d8f3849bf | ||
|
|
def46a7c0c | ||
|
|
3a69ad8f49 | ||
|
|
3a62030c09 | ||
|
|
8a93fb4fcd | ||
|
|
483117981c | ||
|
|
88f48ce996 | ||
|
|
7eb9d27107 | ||
|
|
c5b8eef4d9 | ||
|
|
9c44b33887 | ||
|
|
3b030bcd58 | ||
|
|
4b9c2e7720 | ||
|
|
57586f2427 | ||
|
|
d813be6b09 | ||
|
|
ccd3e61ff0 | ||
|
|
5e647df5be | ||
|
|
04a5c90dab | ||
|
|
bbc00a8677 | ||
|
|
a43d62a505 | ||
|
|
693ac2a9ce | ||
|
|
d4a888c2ec | ||
|
|
73a53fd72d | ||
|
|
7a88dfba69 | ||
|
|
b6b44f3cd6 | ||
|
|
e586b78513 | ||
|
|
330fb2727e | ||
|
|
ea89a862ec | ||
|
|
0e6915287b | ||
|
|
d0ffa5115d | ||
|
|
86dcec8883 | ||
|
|
b2c736e4ca | ||
|
|
4a3e155ac9 | ||
|
|
0f687a4a6b | ||
|
|
5274ca8dcb | ||
|
|
e0123c6246 | ||
|
|
4301a00c1b | ||
|
|
7dc0929cc1 | ||
|
|
cf40c24b36 | ||
|
|
5c4c8753d7 | ||
|
|
d6540d2d62 | ||
|
|
d7f7d8285f | ||
|
|
cd68964a13 | ||
|
|
5e35a33837 | ||
|
|
1734750c21 | ||
|
|
bb7604b53e | ||
|
|
ea43ff2816 | ||
|
|
e8c624b479 | ||
|
|
97d10fee1c | ||
|
|
5c6250fe4d | ||
|
|
21a620c6e8 | ||
|
|
1e3b836a7b | ||
|
|
a6ae573321 | ||
|
|
14740891cd | ||
|
|
908ca1a1d6 | ||
|
|
94309cd070 |
@@ -12,14 +12,7 @@
|
||||
|
||||
## Testing
|
||||
|
||||
- **Run a test:** `./bin/mage run-tests namespace/test-name`
|
||||
- **Run all tests in a namespace:** `./bin/mage run-tests namespace`
|
||||
- **Run all tests for a module:** `./bin/mage run-tests test/metabase/notification` Because the module lives in that directory.
|
||||
|
||||
Note: the `./bin/mage run-tests` command accepts multiple args, so you can pass
|
||||
`./bin/mage run-tests namespace/test-name namespace/other-test namespace/third-test`
|
||||
to run 3 tests, or
|
||||
`./bin/mage run-tests test/metabase/module1 test/metabase/module2` to run 2 modules.
|
||||
Use `./bin/test-agent` to run backend tests. See the top-level CLAUDE.md for usage.
|
||||
|
||||
## Code Readability
|
||||
|
||||
|
||||
357
.claude/skills/add-tracing/SKILL.md
Normal file
@@ -0,0 +1,357 @@
|
||||
--b-
|
||||
name: add-tracing
|
||||
description: Add OpenTelemetry tracing spans to Clojure code following Metabase tracing conventions. Use when instrumenting backend code with trace coverage.
|
||||
---
|
||||
|
||||
# Add Tracing Spans to Clojure Code
|
||||
|
||||
This skill helps you add OpenTelemetry (OTel) tracing spans to the Metabase backend codebase using the custom `tracing/with-span` macro.
|
||||
|
||||
## Reference Files
|
||||
|
||||
- `src/metabase/tracing/core.clj` - `with-span` macro, group registry, SDK lifecycle, `best-effort-sanitize-sql`, Pyroscope integration
|
||||
- `src/metabase/task/impl.clj` - `defjob` macro that wraps Quartz jobs with root spans
|
||||
- `.clj-kondo/config/modules/config.edn` - Module boundary configuration
|
||||
|
||||
## Module Architecture
|
||||
|
||||
The tracing module has a deliberately minimal API surface. **Only 2 namespaces are public** (listed in `:api` in the module config):
|
||||
|
||||
| Namespace | Role | Status |
|
||||
|---|---|---|
|
||||
| `tracing.core` | Primary API: `with-span`, groups, SDK lifecycle, Pyroscope, MDC, `best-effort-sanitize-sql` | **Public API** |
|
||||
| `tracing.init` | Side-effect loader for `quartz` and `settings` | **Public API** (init convention) |
|
||||
| `tracing.attributes` | `best-effort-sanitize-sql` implementation (re-exported via `tracing.core`) | Internal |
|
||||
| `tracing.settings` | Setting definitions (`MB_TRACING_*` env vars) | Internal |
|
||||
| `tracing.quartz` | Quartz JDBC proxy + JobListener | Internal |
|
||||
|
||||
**Rules:**
|
||||
- Only require `[metabase.tracing.core :as tracing]` from outside the module. `tracing/best-effort-sanitize-sql` and all other public functions are available from this single namespace.
|
||||
- Do not add new API namespaces. Add new public functions to `tracing.core` instead.
|
||||
- Do not require internal namespaces (`tracing.attributes`, `tracing.settings`, `tracing.quartz`) from outside the module.
|
||||
- `:uses :any` on the `core` module does NOT bypass the target module's `:api` check — internal namespaces are still enforced.
|
||||
|
||||
### Cyclic Dependency Avoidance
|
||||
|
||||
`tracing/core.clj` is required by many modules across the codebase. It **must NOT** compile-time require `tracing.settings`, as this creates transitive cyclic load dependencies (e.g., `settings/core -> tracing/settings -> tracing/core -> events/impl -> events/core`).
|
||||
|
||||
Instead, `tracing/core.clj` uses `requiring-resolve` for settings access:
|
||||
|
||||
```clojure
|
||||
;; CORRECT — lazy runtime resolution, no compile-time dependency
|
||||
((requiring-resolve 'metabase.tracing.settings/tracing-enabled))
|
||||
|
||||
;; WRONG — creates cyclic load dependency
|
||||
(require '[metabase.tracing.settings :as settings])
|
||||
(settings/tracing-enabled)
|
||||
```
|
||||
|
||||
External library namespaces (clj-otel API, SDK, exporters) are safe to require normally — they don't participate in Metabase namespace cycles.
|
||||
|
||||
**Important:** `requiring-resolve` must use **literal quoted symbols**. Kondo hooks validate that `required-namespaces` are all simple symbols, so dynamic construction fails:
|
||||
|
||||
```clojure
|
||||
;; CORRECT — literal quoted symbol
|
||||
(requiring-resolve 'metabase.tracing.settings/tracing-endpoint)
|
||||
|
||||
;; WRONG — kondo hook rejects this: "Assert failed: (every? simple-symbol? required-namespaces)"
|
||||
(requiring-resolve (symbol "metabase.tracing.settings" "tracing-endpoint"))
|
||||
```
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
When adding tracing spans:
|
||||
|
||||
- [ ] Module has `tracing` in its `:uses` set in `.clj-kondo/config/modules/config.edn`
|
||||
- [ ] Added `[metabase.tracing.core :as tracing]` to ns requires (alphabetically sorted)
|
||||
- [ ] Span wraps a meaningful I/O boundary (not pure computation)
|
||||
- [ ] Group matches the domain (check `src/metabase/tracing/core.clj` for registered groups; add a new one if none fit)
|
||||
- [ ] Span name follows dot-notation convention (`"domain.subsystem.operation"`)
|
||||
- [ ] Attributes use namespaced keywords (`:search/query-length`, `:db/id`)
|
||||
- [ ] No sensitive data in attributes (use `best-effort-sanitize-sql` for HoneySQL, never raw SQL)
|
||||
- [ ] No new tracing namespaces created (add to `tracing.core` instead)
|
||||
- [ ] No `DO_NOT_ADD_NEW_FILES_HERE.txt` violations in the target directory
|
||||
- [ ] Run `clj-kondo --lint <files>` to verify 0 errors, 0 warnings
|
||||
- [ ] Add or update tests in the corresponding `test/` path (see Testing section below)
|
||||
- [ ] Run tests: `clojure -X:dev:test :only <test-ns>`
|
||||
|
||||
## The `with-span` Macro
|
||||
|
||||
```clojure
|
||||
(tracing/with-span group span-name attrs & body)
|
||||
```
|
||||
|
||||
- **group** - A keyword selecting which trace group this span belongs to (e.g., `:tasks`, `:sync`)
|
||||
- **span-name** - A string identifying the span in traces (e.g., `"search.execute"`)
|
||||
- **attrs** - A map of span attributes (e.g., `{:db/id 42}`)
|
||||
- **body** - The code to execute inside the span
|
||||
|
||||
**When disabled:** zero overhead -- single atom deref + boolean check, body runs directly.
|
||||
**When enabled:** creates OTel span AND injects `trace_id`/`span_id` into Log4j2 MDC for log-to-trace correlation.
|
||||
|
||||
## Trace Groups
|
||||
|
||||
Groups are registered in `src/metabase/tracing/core.clj`. Check that file for the current list. The general rule: **match the group to the domain, not the call site.** If code runs inside a Quartz job but is logically search work, use `:search`, not `:tasks`.
|
||||
|
||||
To add a new group:
|
||||
|
||||
```clojure
|
||||
;; In src/metabase/tracing/core.clj
|
||||
(register-group! :my-domain "Description of what this covers")
|
||||
```
|
||||
|
||||
Users enable groups via `MB_TRACING_GROUPS=tasks,search,sync` (comma-separated, or `"all"`).
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### Span Names
|
||||
|
||||
Use dot-separated hierarchical names: `"domain.subsystem.operation"`. The domain prefix should match the group name:
|
||||
|
||||
```
|
||||
search.execute -- `:search` group
|
||||
sync.fingerprint.table -- `:sync` group
|
||||
task.session-cleanup.delete -- `:tasks` group
|
||||
db-app.collection-items -- `:db-app` group
|
||||
```
|
||||
|
||||
### Attributes
|
||||
|
||||
Use namespaced keywords. The namespace groups related attributes:
|
||||
|
||||
```clojure
|
||||
:db/id -- Database ID (integer)
|
||||
:db/engine -- Database engine name (string)
|
||||
:db/statement -- Sanitized SQL (string, via best-effort-sanitize-sql)
|
||||
:search/engine -- Search engine name (string)
|
||||
:search/query-length -- Query string length (integer)
|
||||
:sync/table -- Table name (string)
|
||||
:sync/step -- Sync step name (string)
|
||||
:task/name -- Task name (string)
|
||||
:http/method -- HTTP method (string)
|
||||
:http/url -- Request URL (string)
|
||||
```
|
||||
|
||||
Invent new namespaced attributes as needed (e.g., `:pulse/id`, `:transform/count`). Keep values as primitives (strings, numbers, booleans) -- no maps or collections.
|
||||
|
||||
## Step-by-Step: Adding a Span
|
||||
|
||||
### 1. Check module boundaries
|
||||
|
||||
Look up the module for your namespace in `.clj-kondo/config/modules/config.edn`. If `tracing` is not in the module's `:uses` set, add it (keep alphabetically sorted):
|
||||
|
||||
```edn
|
||||
my-module
|
||||
{:team "MyTeam"
|
||||
:uses #{analytics config tracing util}}
|
||||
```
|
||||
|
||||
### 2. Add the require
|
||||
|
||||
```clojure
|
||||
(ns metabase.my-module.thing
|
||||
(:require
|
||||
[metabase.tracing.core :as tracing]
|
||||
[metabase.util :as u]))
|
||||
```
|
||||
|
||||
`best-effort-sanitize-sql` is available from `tracing.core` — no additional require needed.
|
||||
|
||||
### 3. Identify the I/O boundary
|
||||
|
||||
Only wrap code at **meaningful I/O boundaries**:
|
||||
|
||||
**DO trace:**
|
||||
- External API calls (embedding APIs, metabot, webhooks)
|
||||
- Database queries (both app DB and user DB)
|
||||
- Network requests (HTTP calls to external services)
|
||||
- Heavy batch processing (batch indexing, batch embedding)
|
||||
- Top-level orchestration functions that coordinate multiple sub-operations
|
||||
|
||||
**DO NOT trace:**
|
||||
- Pure computation (sorting, filtering, mapping)
|
||||
- Simple single-row lookups (`t2/select-one :model/Setting :key k`)
|
||||
- Every function in a call chain (only boundaries matter)
|
||||
- Trivial operations (string formatting, hash calculations)
|
||||
|
||||
### 4. Wrap with `with-span`
|
||||
|
||||
```clojure
|
||||
;; Simple span (no attributes needed)
|
||||
(tracing/with-span :search "search.init-index" {}
|
||||
(do-expensive-thing))
|
||||
|
||||
;; Span with static attributes
|
||||
(tracing/with-span :sync "sync.fingerprint.table"
|
||||
{:db/id (:db_id table)
|
||||
:sync/table (:name table)}
|
||||
(fingerprint-fields! table fields))
|
||||
|
||||
;; Span with computed attributes
|
||||
(tracing/with-span :search "search.execute"
|
||||
{:search/engine (name (:search-engine ctx))
|
||||
:search/query-length (count (:search-string ctx))}
|
||||
(search.engine/results ctx))
|
||||
|
||||
;; Span with sanitized SQL (for dynamic HoneySQL queries)
|
||||
(let [hsql {:delete-from [(t2/table-name :model/Session)]
|
||||
:where [:< :created_at oldest-allowed]}]
|
||||
(tracing/with-span :tasks "task.session-cleanup.delete"
|
||||
{:db/statement (tracing/best-effort-sanitize-sql hsql)}
|
||||
(t2/query-one hsql)))
|
||||
|
||||
;; Sub-spans breaking a function into I/O phases
|
||||
(let [embedding (tracing/with-span :search "search.semantic.embedding"
|
||||
{:search.semantic/provider (:provider model)}
|
||||
(get-embedding model search-string))
|
||||
results (tracing/with-span :search "search.semantic.db-query" {}
|
||||
(into [] xform reducible))]
|
||||
(process results))
|
||||
|
||||
;; Per-item iteration spans
|
||||
(doseq [e (search.engine/active-engines)]
|
||||
(tracing/with-span :search "search.ingestion.update" {:search/engine (name e)}
|
||||
(search.engine/update! e batch)))
|
||||
```
|
||||
|
||||
### 5. Add tests
|
||||
|
||||
Create or update tests in the corresponding `test/` path. Follow the patterns in existing tracing tests:
|
||||
|
||||
- **Reference tests:** `test/metabase/tracing/quartz_test.clj`, `test/metabase/server/middleware/trace_test.clj`
|
||||
- Use `tracing/init-enabled-groups!` / `tracing/shutdown-groups!` with `try`/`finally` to manage group lifecycle
|
||||
- Test both enabled and disabled paths (verify zero overhead when group is off)
|
||||
- Use `reify` mocks for Java interfaces (Connection, PreparedStatement, JobListener, etc.)
|
||||
- Add `(set! *warn-on-reflection* true)` and type-hint proxy/reify calls to avoid reflection warnings
|
||||
|
||||
```clojure
|
||||
(deftest my-span-enabled-test
|
||||
(testing "when group is enabled, span is created"
|
||||
(try
|
||||
(tracing/init-enabled-groups! "my-group" "INFO")
|
||||
;; ... test that span behavior occurs ...
|
||||
(finally
|
||||
(tracing/shutdown-groups!)))))
|
||||
|
||||
(deftest my-span-disabled-test
|
||||
(testing "when group is disabled, code runs without tracing"
|
||||
(tracing/shutdown-groups!)
|
||||
;; ... test that code still works, no wrapping applied ...
|
||||
))
|
||||
```
|
||||
|
||||
### 6. Lint and run tests
|
||||
|
||||
```bash
|
||||
# Lint modified source and test files — expect 0 errors, 0 warnings
|
||||
clj-kondo --lint path/to/modified/file.clj path/to/test/file.clj
|
||||
|
||||
# Run tests (requires Java 21+)
|
||||
clojure -X:dev:test :only my-ns.test-ns
|
||||
```
|
||||
|
||||
Expect: all tests pass, 0 failures, 0 errors, no reflection warnings from your files.
|
||||
|
||||
## Sanitizing SQL for Attributes
|
||||
|
||||
When including SQL in span attributes, **always** use `tracing/best-effort-sanitize-sql`. This converts HoneySQL maps to parameterized SQL strings where values become `?` placeholders -- no data leaks.
|
||||
|
||||
```clojure
|
||||
(let [hsql {:delete-from [:core_session]
|
||||
:where [:< :created_at some-timestamp]}]
|
||||
(tracing/with-span :tasks "task.cleanup.delete"
|
||||
{:db/statement (tracing/best-effort-sanitize-sql hsql)}
|
||||
(t2/query-one hsql)))
|
||||
;; Trace attribute: db/statement = "DELETE FROM core_session WHERE created_at < ?"
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Never put raw SQL strings or user-provided values in attributes
|
||||
- Use `best-effort-sanitize-sql` only for app DB (HoneySQL) queries
|
||||
- For external/user DB queries, trace only timing and counts, not SQL content
|
||||
|
||||
## Defjob and Root Spans
|
||||
|
||||
The `defjob` macro in `metabase.task.impl` automatically wraps every Quartz job with a `:tasks` root span:
|
||||
|
||||
```clojure
|
||||
(task/defjob ^{DisallowConcurrentExecution true} SessionCleanup [_]
|
||||
(cleanup-sessions!))
|
||||
;; Automatically creates span: "task.SessionCleanup" {:task/name "SessionCleanup"}
|
||||
```
|
||||
|
||||
You do NOT need a root span inside `defjob` bodies. Add **child spans** for I/O inside the job.
|
||||
|
||||
For code on plain `Thread`s (not Quartz), add the root span manually:
|
||||
|
||||
```clojure
|
||||
(defn init! []
|
||||
(tracing/with-span :search "search.task.init" {}
|
||||
(search/init-index!)))
|
||||
```
|
||||
|
||||
## What NOT to Do
|
||||
|
||||
### Span Usage Mistakes
|
||||
|
||||
```clojure
|
||||
;; WRONG - pure computation, no I/O
|
||||
(tracing/with-span :search "search.format-results" {}
|
||||
(map format-result results))
|
||||
|
||||
;; WRONG - trivial single-row lookup
|
||||
(tracing/with-span :db-app "db-app.get-setting" {}
|
||||
(t2/select-one :model/Setting :key "my-setting"))
|
||||
|
||||
;; WRONG - raw SQL in attributes (data leak)
|
||||
(tracing/with-span :tasks "task.cleanup" {:db/statement raw-sql-string}
|
||||
(execute! raw-sql-string))
|
||||
|
||||
;; WRONG - wrong group (search work should use :search, not :tasks)
|
||||
(tracing/with-span :tasks "search.execute" {} ...)
|
||||
|
||||
;; WRONG - redundant nesting (do-search already has a span)
|
||||
(tracing/with-span :search "search.process" {}
|
||||
(let [results (do-search ctx)]
|
||||
(tracing/with-span :search "search.format" {}
|
||||
(format-results results))))
|
||||
```
|
||||
|
||||
### Architecture Mistakes
|
||||
|
||||
```clojure
|
||||
;; WRONG - creating a new tracing namespace
|
||||
(ns metabase.tracing.my-feature ...)
|
||||
|
||||
;; WRONG - requiring internal tracing namespaces from outside the module
|
||||
(ns metabase.my-module.thing
|
||||
(:require [metabase.tracing.attributes :as trace-attrs] ;; internal!
|
||||
[metabase.tracing.settings :as tracing.settings] ;; internal!
|
||||
[metabase.tracing.quartz :as tracing.quartz])) ;; internal!
|
||||
|
||||
;; WRONG - adding compile-time requires to tracing/core.clj for settings or SDK
|
||||
;; This creates cyclic load dependencies because tracing/core is widely required
|
||||
(ns metabase.tracing.core
|
||||
(:require [metabase.tracing.settings :as settings])) ;; causes cycle!
|
||||
|
||||
;; WRONG - dynamic symbol construction with requiring-resolve (kondo rejects it)
|
||||
(requiring-resolve (symbol "metabase.tracing.settings" "tracing-enabled"))
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
All settings are env-var-only (defined in `src/metabase/tracing/settings.clj`):
|
||||
|
||||
```bash
|
||||
# Core
|
||||
MB_TRACING_ENABLED=true # Enable tracing (default: false)
|
||||
MB_TRACING_ENDPOINT=host:4317 # OTLP collector endpoint (default: http://localhost:4317)
|
||||
MB_TRACING_GROUPS=tasks,search,sync # Comma-separated groups or "all" (default: all)
|
||||
MB_TRACING_SERVICE_NAME=metabase # Service name in traces (default: hostname)
|
||||
MB_TRACING_LOG_LEVEL=DEBUG # Log threshold for traced threads: TRACE/DEBUG/INFO (default: INFO)
|
||||
|
||||
# Batch span processor tuning
|
||||
MB_TRACING_MAX_QUEUE_SIZE=2048 # Max spans queued for export; drops when full (default: 2048)
|
||||
MB_TRACING_EXPORT_TIMEOUT_MS=10000 # Max wait for batch export to complete (default: 10000)
|
||||
MB_TRACING_SCHEDULE_DELAY_MS=5000 # Delay between consecutive batch exports (default: 5000)
|
||||
```
|
||||
260
.claude/skills/e2e-test-create/SKILL.md
Normal file
@@ -0,0 +1,260 @@
|
||||
---
|
||||
name: e2e-test-create
|
||||
description: >
|
||||
Analyze React component source code to understand UI structure,
|
||||
then generate idiomatic Cypress E2E tests following Metabase conventions.
|
||||
Falls back to Playwright MCP browser exploration only when code reading
|
||||
and screenshot debugging are insufficient.
|
||||
disable-model-invocation: true
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Grep
|
||||
- Glob
|
||||
- Skill
|
||||
- "mcp__playwright__*"
|
||||
---
|
||||
|
||||
# Code-Reading-First → Generate Cypress Tests (Metabase)
|
||||
|
||||
You are writing Cypress E2E tests for the **Metabase** codebase.
|
||||
Before generating ANY test code, you MUST analyze React component source code
|
||||
to understand DOM structure, selectors, and user flows.
|
||||
|
||||
## Phase 0 — Research
|
||||
|
||||
1. Read existing helpers before writing anything:
|
||||
- `e2e/support/helpers/` — all shared helpers (restore, signInAs, openOrdersTable, etc.)
|
||||
- `e2e/support/cypress_sample_database.ts` — table/field schema constants (ORDERS, PRODUCTS, etc.)
|
||||
- `e2e/support/cypress_sample_instance_data.ts` — instance-specific IDs (ORDERS_DASHBOARD_ID, NORMAL_USER_ID, etc.)
|
||||
2. Glob `e2e/test/scenarios/` to find the closest existing spec to the area under test.
|
||||
Study its patterns — match them exactly.
|
||||
3. Glob `frontend/src/metabase/` to find React components for the feature area.
|
||||
|
||||
## Phase 1 — Code Analysis
|
||||
|
||||
Read React component source to understand DOM structure. No browser needed — source code has everything.
|
||||
|
||||
1. **Find relevant components**: Glob and grep `frontend/src/metabase/` for the feature area.
|
||||
2. **Extract selectors**: Grep for `data-testid` in relevant components.
|
||||
3. **Note visible text**: Read component JSX for button labels, headings, placeholders.
|
||||
4. **Note aria attributes**: Grep for `aria-label` in relevant components.
|
||||
5. **Understand user flows**: Read event handlers (onClick, onSubmit, onChange) to understand interactions.
|
||||
6. **Find API calls**: Grep for `Api.use`, `fetch`, `useQuery`, endpoint definitions to identify API calls to intercept.
|
||||
7. **Cross-reference with existing specs**: Find specs in the same area and reuse their proven selectors and `cy.intercept` patterns.
|
||||
|
||||
## Phase 2 — Start Backend
|
||||
|
||||
Use `MB_EDITION=oss` by default. Only use `MB_EDITION=ee` when the user explicitly asks to write an enterprise test.
|
||||
|
||||
Start the backend using `run_in_background: true` (NOT `&`).
|
||||
`bin/e2e-backend` automatically detects if a backend is already running and reuses it.
|
||||
```bash
|
||||
MB_EDITION=oss bin/e2e-backend
|
||||
```
|
||||
|
||||
Do NOT manually generate snapshots by running unrelated test specs.
|
||||
The `bun test-cypress` runner has `GENERATE_SNAPSHOTS: true` by default and automatically
|
||||
generates snapshots before running any spec. When running tests in Phase 4 via the `/e2e-test` skill,
|
||||
snapshots will be generated on the first run if they don't already exist.
|
||||
|
||||
Restore clean test data:
|
||||
```bash
|
||||
curl -sf -X POST http://localhost:4000/api/testing/restore/default
|
||||
```
|
||||
|
||||
## Phase 3 — Generate Cypress Spec
|
||||
|
||||
### File location & naming
|
||||
|
||||
Place specs in `e2e/test/scenarios/<area>/` mirroring the URL structure.
|
||||
Name: `<feature>.cy.spec.js` (NOT `.cy.spec.ts`).
|
||||
|
||||
### Metabase conventions (mandatory)
|
||||
|
||||
- **Helpers via `cy.H`**: All helpers are accessed via `const { H } = cy;` — NOT via direct imports from `e2e/support/helpers`.
|
||||
|
||||
```js
|
||||
const { H } = cy;
|
||||
|
||||
describe("feature name", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- **Sample Database schema** (table/field definitions): Import from `cypress_sample_database`.
|
||||
|
||||
```js
|
||||
import { ORDERS, ORDERS_ID, PRODUCTS } from "e2e/support/cypress_sample_database";
|
||||
```
|
||||
|
||||
- **Instance data** (dashboard IDs, user IDs, etc.): Import from `cypress_sample_instance_data`.
|
||||
|
||||
```js
|
||||
import { ORDERS_DASHBOARD_ID } from "e2e/support/cypress_sample_instance_data";
|
||||
```
|
||||
|
||||
- **Selectors** (priority order):
|
||||
1. `cy.findByText()` / `cy.findByLabelText()` / `cy.findByRole()` — from `@testing-library/cypress`
|
||||
2. `cy.findByTestId()` — for `data-testid` attributes
|
||||
3. `cy.get("[data-testid='...']")` — fallback
|
||||
4. NEVER use positional selectors, CSS class names, or XPaths.
|
||||
|
||||
- **Navigation helpers**: Use existing helpers like `H.openOrdersTable()`, `H.openNativeEditor()`,
|
||||
`H.visitDashboard(id)`, `H.visitQuestion(id)` instead of raw `cy.visit()` chains.
|
||||
Grep `e2e/support/helpers/` to discover what's available for your area.
|
||||
|
||||
- **API setup over UI setup**: Use `cy.request()` or existing API helpers to set up state.
|
||||
Only use the UI for the flow you're actually testing.
|
||||
|
||||
- **Assertions**: Assert on visible text, URL, aria state — not DOM structure.
|
||||
|
||||
- **Waits**: Never use `cy.wait(ms)`. Use `cy.intercept()` + `cy.wait("@alias")` for API calls,
|
||||
or `cy.findByText().should("be.visible")` for DOM readiness.
|
||||
|
||||
- **Isolation**: Each `it()` block must be independently runnable.
|
||||
Don't depend on state from a previous `it()`.
|
||||
|
||||
### Spec structure template
|
||||
|
||||
```js
|
||||
const { H } = cy;
|
||||
import { ORDERS_DASHBOARD_ID } from "e2e/support/cypress_sample_instance_data";
|
||||
import { ORDERS, ORDERS_ID } from "e2e/support/cypress_sample_database";
|
||||
|
||||
describe("area > sub-area > feature (#issue-number)", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
it("should do the primary happy-path thing", () => {
|
||||
// test
|
||||
});
|
||||
|
||||
it("should handle the edge case", () => {
|
||||
// test
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Intercepts
|
||||
|
||||
When you identified API calls during code analysis, stub or wait on them:
|
||||
|
||||
```js
|
||||
cy.intercept("POST", "/api/dataset").as("dataset");
|
||||
// ... trigger action ...
|
||||
cy.wait("@dataset");
|
||||
```
|
||||
|
||||
## Phase 4 — Validate
|
||||
|
||||
After generating specs:
|
||||
1. Check that all imported helpers exist (Grep `e2e/support/helpers/`).
|
||||
2. **You MUST use the `/e2e-test` skill** to run tests — do NOT run `bun test-cypress` directly.
|
||||
The `/e2e-test` skill handles edition selection, snapshot management, and correct env vars.
|
||||
`/e2e-test GREP="should do the thing" --spec e2e/test/scenarios/<path>`
|
||||
If you created multiple `it()` blocks, run each one individually to isolate failures.
|
||||
|
||||
## Phase 5 — Fix Failures (up to 2 attempts)
|
||||
|
||||
When a test fails, **try to fix it from Cypress output first**:
|
||||
1. Read the failure screenshot (path printed under `(Screenshots)`).
|
||||
2. Read the error message and code frame from the console output.
|
||||
3. Fix the test and re-run (back to Phase 4, step 2).
|
||||
|
||||
If you cannot diagnose the issue after 2 attempts, proceed to Phase 6.
|
||||
|
||||
## Phase 6 — Playwright Fallback
|
||||
|
||||
Only reach this phase after 2 failed fix attempts from Phase 5. The backend is already running.
|
||||
|
||||
Restore clean test data:
|
||||
```bash
|
||||
curl -sf -X POST http://localhost:4000/api/testing/restore/default
|
||||
```
|
||||
|
||||
**Bypass CSP** headers before navigating (Metabase serves strict CSP that blocks dev server scripts).
|
||||
Use `browser_run_code` to set this up:
|
||||
|
||||
```js
|
||||
async (page) => {
|
||||
// Strip CSP headers so the page loads (mirrors Cypress chromeWebSecurity: false)
|
||||
await page.context().route('**/*', async (route) => {
|
||||
const response = await route.fetch();
|
||||
const headers = { ...response.headers() };
|
||||
delete headers['content-security-policy'];
|
||||
delete headers['content-security-policy-report-only'];
|
||||
await route.fulfill({ response, headers });
|
||||
});
|
||||
|
||||
// Sign in via API
|
||||
const response = await page.request.post('http://localhost:4000/api/session', {
|
||||
data: { username: 'admin@metabase.test', password: '12341234' }
|
||||
});
|
||||
const session = await response.json();
|
||||
await page.context().addCookies([{
|
||||
name: 'metabase.DEVICE',
|
||||
value: session.id,
|
||||
domain: 'localhost',
|
||||
path: '/'
|
||||
}]);
|
||||
|
||||
await page.goto('http://localhost:4000');
|
||||
await page.waitForLoadState('networkidle');
|
||||
return 'signed in';
|
||||
}
|
||||
```
|
||||
|
||||
**Maintain an observation log incrementally.** After EVERY significant Playwright interaction,
|
||||
IMMEDIATELY append what you observed to the scratch file BEFORE performing the next interaction:
|
||||
|
||||
```bash
|
||||
cat >> /tmp/e2e-observations.md << 'OBSERVATION'
|
||||
## [Page/Flow name]
|
||||
- URL: /question/notebook#...
|
||||
- Clicked: "Box plot" button → visible text "Box plot", role: radio
|
||||
- Selectors: data-testid="viz-type-button", findByText("Box plot")
|
||||
- API call: POST /api/dataset (triggered on viz change)
|
||||
- Key state: after selecting viz type, summary sidebar shows metric picker
|
||||
OBSERVATION
|
||||
```
|
||||
|
||||
For each page/flow:
|
||||
- Take an accessibility snapshot (`browser_snapshot`).
|
||||
- Click through interactive elements, fill forms, trigger modals.
|
||||
- **Append to the observation log immediately after each step**: URLs, visible text, aria labels, `data-testid` attrs, API calls.
|
||||
- Screenshot key states.
|
||||
|
||||
After exploration:
|
||||
1. Read back your observation log: `cat /tmp/e2e-observations.md`
|
||||
2. Fix the test using observed selectors and behavior.
|
||||
3. Re-run the test (back to Phase 4).
|
||||
4. Clean up: `rm -f /tmp/e2e-observations.md`
|
||||
|
||||
## Phase 7 — Cleanup
|
||||
|
||||
After all tests pass (or after giving up on fixing failures), **always kill the backend on port 4000**:
|
||||
```bash
|
||||
lsof -ti:4000 | xargs kill 2>/dev/null || true
|
||||
```
|
||||
|
||||
Do NOT use broad `pkill` patterns — there may be other Metabase instances on different ports.
|
||||
The backend process started in Phase 2 will NOT be killed automatically when the Claude session ends.
|
||||
Leaving it running wastes resources and can interfere with future sessions. Always clean up.
|
||||
|
||||
## What NOT to do
|
||||
|
||||
- Do NOT use Playwright as the first step — always analyze source code first.
|
||||
- Do NOT kill the backend between phases — it stays running throughout.
|
||||
- Do NOT invent selectors you didn't find in source code or observe in the browser.
|
||||
- Do NOT hardcode database IDs — import from `cypress_sample_database` or `cypress_sample_instance_data`.
|
||||
- Do NOT use `cy.wait(1000)` or any numeric wait.
|
||||
- Do NOT create setup flows through the UI when an API helper exists.
|
||||
- Do NOT put tests in `cypress/e2e/` — Metabase uses `e2e/test/scenarios/`.
|
||||
- Do NOT import helpers directly from `e2e/support/helpers` — use `const { H } = cy;`.
|
||||
78
.claude/skills/e2e-test/SKILL.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: e2e-test
|
||||
description: Run Cypress E2E tests, analyze failures including screenshots, and stress test for flakiness
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
Run Cypress E2E test: $ARGUMENTS
|
||||
|
||||
## Edition selection
|
||||
|
||||
**Default: `MB_EDITION=oss`** — runs without enterprise tokens, simpler and faster.
|
||||
|
||||
Only use `MB_EDITION=ee` when the user explicitly asks to write or run an enterprise test.
|
||||
|
||||
**For `MB_EDITION=ee`:**
|
||||
These tokens must be exported in the shell before starting Claude Code:
|
||||
- `CYPRESS_MB_ALL_FEATURES_TOKEN` — all features (most EE tests need this)
|
||||
- `CYPRESS_MB_PRO_SELF_HOSTED_TOKEN` — pro self-hosted (permissions, SSO, sandboxing)
|
||||
- `CYPRESS_MB_STARTER_CLOUD_TOKEN` — starter plan (rarely needed)
|
||||
- `CYPRESS_MB_PRO_CLOUD_TOKEN` — pro cloud (rarely needed)
|
||||
|
||||
**NEVER echo, print, or log token values. Only check if they are set:**
|
||||
```bash
|
||||
echo "ALL_FEATURES: ${CYPRESS_MB_ALL_FEATURES_TOKEN:+set}" && echo "PRO_SELF_HOSTED: ${CYPRESS_MB_PRO_SELF_HOSTED_TOKEN:+set}" && echo "STARTER: ${CYPRESS_MB_STARTER_CLOUD_TOKEN:+set}" && echo "PRO_CLOUD: ${CYPRESS_MB_PRO_CLOUD_TOKEN:+set}"
|
||||
```
|
||||
|
||||
If tokens are missing, tell the user: "EE tokens are not set. Either export them and restart Claude Code, or add `MB_EDITION=oss` to run OSS-only tests."
|
||||
|
||||
## Running
|
||||
|
||||
First check if snapshots exist:
|
||||
```bash
|
||||
ls e2e/snapshots/default.sql 2>/dev/null && echo "snapshots exist" || echo "snapshots missing"
|
||||
```
|
||||
|
||||
If snapshots exist, skip regeneration for speed:
|
||||
```bash
|
||||
MB_EDITION=oss CYPRESS_VIDEO=false CYPRESS_RETRIES=0 CYPRESS_GUI=false GENERATE_SNAPSHOTS=false bun test-cypress $ARGUMENTS
|
||||
```
|
||||
|
||||
If snapshots are missing (first run), let the runner generate them:
|
||||
```bash
|
||||
MB_EDITION=oss CYPRESS_VIDEO=false CYPRESS_RETRIES=0 CYPRESS_GUI=false bun test-cypress $ARGUMENTS
|
||||
```
|
||||
|
||||
When running enterprise tests, replace `MB_EDITION=oss` with `MB_EDITION=ee` in the commands above.
|
||||
|
||||
If `$ARGUMENTS` is empty, ask the user which spec to run.
|
||||
|
||||
If the user provides a test name or fuzzy name instead of a spec path, find the spec file:
|
||||
```bash
|
||||
find e2e/test/scenarios -name "*FUZZY_NAME*.cy.spec.*" -type f
|
||||
```
|
||||
Then run with `--spec "path/to/matched.cy.spec.js"`.
|
||||
|
||||
To filter by test name, use the `GREP` env var (not `--env grep=`, which breaks on commas):
|
||||
```bash
|
||||
GREP="test name here" MB_EDITION=oss bun test-cypress --spec path/to/spec.js
|
||||
```
|
||||
|
||||
## Stress test (flaky detection)
|
||||
|
||||
When the user asks to verify a test is not flaky:
|
||||
|
||||
```bash
|
||||
MB_EDITION=oss bin/e2e-stress-test $ARGUMENTS
|
||||
```
|
||||
|
||||
Set `E2E_STRESS_RUNS=N` for more iterations (default: 5). Stops on first failure and prints screenshot paths. Read those screenshots to analyze the failure.
|
||||
|
||||
## Analyzing failures
|
||||
|
||||
When tests fail:
|
||||
|
||||
1. Read the **screenshot** for each failure — paths are printed in Cypress output under `(Screenshots)`, typically at `cypress/screenshots/<spec-name>/<test-name> (failed).png`
|
||||
2. Read the error message and code frame from the console output
|
||||
3. Identify the root cause: is it a test bug, a product bug, or a timing issue?
|
||||
4. Suggest a fix with specific code changes
|
||||
@@ -422,6 +422,8 @@
|
||||
ring.util.codec codec
|
||||
ring.util.response response
|
||||
saml20-clj.core saml
|
||||
metabase.transforms-base.util transforms-base.u
|
||||
metabase.transforms.util transforms.u
|
||||
toucan2.core t2}}}
|
||||
|
||||
:lint-as
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
version}}
|
||||
|
||||
analyze
|
||||
{:team "Querying"
|
||||
{:team "Graphy"
|
||||
:api #{metabase.analyze.core
|
||||
metabase.analyze.query-results}
|
||||
:uses #{config
|
||||
@@ -152,6 +152,7 @@
|
||||
metabase.api.docs
|
||||
metabase.api.init
|
||||
metabase.api.macros
|
||||
metabase.api.macros.scope
|
||||
metabase.api.open-api
|
||||
metabase.api.response
|
||||
metabase.api.routes.common
|
||||
@@ -214,6 +215,7 @@
|
||||
settings
|
||||
task
|
||||
task-history
|
||||
tracing
|
||||
util}}
|
||||
|
||||
auth-identity
|
||||
@@ -275,6 +277,7 @@
|
||||
config
|
||||
events
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
request
|
||||
settings
|
||||
@@ -395,6 +398,7 @@
|
||||
request
|
||||
revisions
|
||||
settings
|
||||
tracing
|
||||
transforms
|
||||
upload
|
||||
util}}
|
||||
@@ -548,7 +552,7 @@
|
||||
metabase.driver.common.parameters
|
||||
metabase.driver.common.parameters.parse
|
||||
metabase.driver.connection
|
||||
metabase.driver.connection.workspaces
|
||||
metabase.driver.connection.workspaces
|
||||
metabase.driver.ddl.interface
|
||||
metabase.driver.h2
|
||||
metabase.driver.impl
|
||||
@@ -587,6 +591,7 @@
|
||||
settings
|
||||
sql-tools
|
||||
system
|
||||
tracing
|
||||
util
|
||||
warehouse-schema
|
||||
warehouses}}
|
||||
@@ -665,7 +670,8 @@
|
||||
:api #{metabase.events.core
|
||||
metabase.events.init}
|
||||
:uses #{models
|
||||
util}}
|
||||
util}
|
||||
:friends #{tracing}}
|
||||
|
||||
formatter
|
||||
{:team "UX West"
|
||||
@@ -694,7 +700,7 @@
|
||||
util}}
|
||||
|
||||
graph
|
||||
{:team "Querying"
|
||||
{:team "Graphy"
|
||||
:api #{metabase.graph.core}
|
||||
:uses #{util}}
|
||||
|
||||
@@ -741,13 +747,13 @@
|
||||
metabase.lib.init
|
||||
metabase.lib.metadata
|
||||
metabase.lib.metadata.protocols
|
||||
metabase.lib.options
|
||||
metabase.lib.schema
|
||||
metabase.lib.schema.actions
|
||||
metabase.lib.schema.aggregation
|
||||
metabase.lib.schema.common
|
||||
metabase.lib.schema.expression
|
||||
metabase.lib.schema.expression.temporal
|
||||
metabase.lib.schema.filter
|
||||
metabase.lib.schema.id
|
||||
metabase.lib.schema.info
|
||||
metabase.lib.schema.join
|
||||
@@ -774,6 +780,7 @@
|
||||
:friends #{legacy-mbql ;; Reuses internal schemas and a few helper functions. Deeply coupled to MBQL.
|
||||
lib-be ;; BE-specific extension of lib; tight coupling is acceptable.
|
||||
query-processor ;; Necessarily manipulates MBQL; better to pull most logic into a new module.
|
||||
source-swap ;; Manipulates MBQL queries; tight coupling with lib is expected.
|
||||
enterprise/sandbox ;; Really part of the QP, though its lives in the enterprise/ tree.
|
||||
}}
|
||||
|
||||
@@ -786,6 +793,16 @@
|
||||
settings
|
||||
util}}
|
||||
|
||||
lib-metric
|
||||
{:team "UX West"
|
||||
:api #{metabase.lib-metric.core
|
||||
metabase.lib-metric.schema}
|
||||
:uses #{lib
|
||||
lib-be
|
||||
settings
|
||||
types
|
||||
util}}
|
||||
|
||||
llm
|
||||
{:team "Metabot"
|
||||
:api #{metabase.llm.api
|
||||
@@ -835,12 +852,29 @@
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
metrics
|
||||
models
|
||||
permissions
|
||||
remote-sync
|
||||
search
|
||||
util}}
|
||||
|
||||
metrics
|
||||
{:team "UX West"
|
||||
:api #{metabase.metrics.api
|
||||
metabase.metrics.core}
|
||||
:uses #{api
|
||||
collections
|
||||
lib
|
||||
lib-be
|
||||
lib-metric
|
||||
models
|
||||
parameters
|
||||
query-processor
|
||||
request
|
||||
server
|
||||
util}}
|
||||
|
||||
model-persistence
|
||||
{:team "Querying"
|
||||
:api #{metabase.model-persistence.api
|
||||
@@ -861,6 +895,7 @@
|
||||
system
|
||||
task
|
||||
task-history
|
||||
tracing
|
||||
util}}
|
||||
|
||||
models
|
||||
@@ -870,6 +905,7 @@
|
||||
metabase.models.interface
|
||||
metabase.models.resolution
|
||||
metabase.models.serialization
|
||||
metabase.models.transforms.transform
|
||||
metabase.models.util.spec-update
|
||||
metabase.models.visualization-settings}
|
||||
:uses #{api
|
||||
@@ -886,6 +922,7 @@
|
||||
search
|
||||
settings
|
||||
transforms
|
||||
transforms-base
|
||||
util}
|
||||
:friends #{transforms}}
|
||||
|
||||
@@ -932,6 +969,7 @@
|
||||
system
|
||||
task
|
||||
task-history
|
||||
tracing
|
||||
users
|
||||
util}}
|
||||
|
||||
@@ -975,6 +1013,7 @@
|
||||
premium-features
|
||||
remote-sync
|
||||
settings
|
||||
tracing
|
||||
util
|
||||
enterprise/advanced-permissions}}
|
||||
|
||||
@@ -1020,6 +1059,7 @@
|
||||
settings
|
||||
startup
|
||||
task
|
||||
tracing
|
||||
util}}
|
||||
|
||||
product-feedback
|
||||
@@ -1087,6 +1127,7 @@
|
||||
request
|
||||
task
|
||||
task-history
|
||||
tracing
|
||||
util
|
||||
enterprise/advanced-permissions
|
||||
enterprise/sandbox}}
|
||||
@@ -1112,6 +1153,7 @@
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
metrics
|
||||
models
|
||||
parameters
|
||||
permissions
|
||||
@@ -1229,6 +1271,7 @@
|
||||
server
|
||||
settings
|
||||
system
|
||||
tracing
|
||||
users
|
||||
util}}
|
||||
|
||||
@@ -1310,6 +1353,7 @@
|
||||
settings
|
||||
startup
|
||||
task
|
||||
tracing
|
||||
transforms
|
||||
util}}
|
||||
|
||||
@@ -1356,6 +1400,7 @@
|
||||
session
|
||||
settings
|
||||
system
|
||||
tracing
|
||||
users
|
||||
util
|
||||
enterprise/sso}}
|
||||
@@ -1377,6 +1422,7 @@
|
||||
sso
|
||||
system
|
||||
task
|
||||
tracing
|
||||
util}}
|
||||
|
||||
settings
|
||||
@@ -1423,6 +1469,15 @@
|
||||
system
|
||||
util}}
|
||||
|
||||
source-swap
|
||||
{:team "Graphy"
|
||||
:api #{metabase.source-swap.core
|
||||
metabase.source-swap.schema}
|
||||
:uses #{driver
|
||||
lib
|
||||
sql-tools
|
||||
util}}
|
||||
|
||||
sql-parsing
|
||||
{:team "DevEx"
|
||||
:api #{metabase.sql-parsing.core}
|
||||
@@ -1468,7 +1523,7 @@
|
||||
util}}
|
||||
|
||||
sync
|
||||
{:team "Querying"
|
||||
{:team "Graphy"
|
||||
:api #{metabase.sync.api
|
||||
metabase.sync.core
|
||||
metabase.sync.init
|
||||
@@ -1490,6 +1545,7 @@
|
||||
settings
|
||||
task
|
||||
task-history
|
||||
tracing
|
||||
util
|
||||
warehouse-schema
|
||||
warehouses}}
|
||||
@@ -1506,6 +1562,7 @@
|
||||
metabase.task.core}
|
||||
:uses #{app-db
|
||||
classloader
|
||||
tracing
|
||||
util}}
|
||||
|
||||
task-history
|
||||
@@ -1522,6 +1579,7 @@
|
||||
query-processor
|
||||
request
|
||||
task
|
||||
tracing
|
||||
util}}
|
||||
|
||||
tenants
|
||||
@@ -1569,12 +1627,20 @@
|
||||
models
|
||||
util}}
|
||||
|
||||
tracing
|
||||
{:team "DevEx"
|
||||
:api #{metabase.tracing.core
|
||||
metabase.tracing.init}
|
||||
:uses #{events
|
||||
settings
|
||||
task
|
||||
util}}
|
||||
|
||||
transforms
|
||||
{:team "Querying"
|
||||
:api #{metabase.transforms.core
|
||||
metabase.transforms.feature-gating
|
||||
metabase.transforms.init
|
||||
metabase.transforms.interface
|
||||
metabase.transforms.schema
|
||||
metabase.transforms.util}
|
||||
:uses #{analytics
|
||||
@@ -1584,21 +1650,37 @@
|
||||
driver
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
premium-features
|
||||
queries
|
||||
query-processor
|
||||
revisions
|
||||
settings
|
||||
sync
|
||||
task
|
||||
task-history
|
||||
tracing
|
||||
transforms-base
|
||||
util}
|
||||
:friends #{models
|
||||
enterprise/transforms-python
|
||||
enterprise/workspaces}}
|
||||
|
||||
transforms-base
|
||||
{:team "Querying"
|
||||
:api #{metabase.transforms-base.init
|
||||
metabase.transforms-base.interface
|
||||
metabase.transforms-base.util}
|
||||
:uses #{driver
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
query-processor
|
||||
sync
|
||||
util}
|
||||
:friends #{transforms
|
||||
enterprise/transforms-python
|
||||
enterprise/workspaces}}
|
||||
|
||||
transforms-inspector
|
||||
{:team "Gadget"
|
||||
:api #{metabase.transforms-inspector.core
|
||||
@@ -1608,7 +1690,7 @@
|
||||
lib-be
|
||||
query-processor
|
||||
sql-tools
|
||||
transforms
|
||||
transforms-base
|
||||
util}}
|
||||
|
||||
transforms-rest
|
||||
@@ -1625,6 +1707,7 @@
|
||||
server
|
||||
sql-tools
|
||||
transforms
|
||||
transforms-base
|
||||
transforms-inspector
|
||||
util}}
|
||||
|
||||
@@ -1921,7 +2004,9 @@
|
||||
request
|
||||
server
|
||||
util
|
||||
enterprise/metabot-v3}}
|
||||
enterprise/api
|
||||
enterprise/metabot-v3
|
||||
enterprise/workspaces}}
|
||||
|
||||
enterprise/ai-entity-analysis
|
||||
{:team "Metabot"
|
||||
@@ -2110,10 +2195,11 @@
|
||||
warehouses}}
|
||||
|
||||
enterprise/dependencies
|
||||
{:team "Querying"
|
||||
{:team "Graphy"
|
||||
:api #{metabase-enterprise.dependencies.api
|
||||
metabase-enterprise.dependencies.core
|
||||
metabase-enterprise.dependencies.init}
|
||||
metabase-enterprise.dependencies.init
|
||||
metabase-enterprise.dependencies.models.dependency}
|
||||
:uses #{analyze
|
||||
api
|
||||
app-db
|
||||
@@ -2134,8 +2220,10 @@
|
||||
request
|
||||
revisions
|
||||
settings
|
||||
sql-tools
|
||||
task
|
||||
transforms
|
||||
transforms-base
|
||||
util}}
|
||||
|
||||
enterprise/email
|
||||
@@ -2253,6 +2341,7 @@
|
||||
system
|
||||
task
|
||||
transforms
|
||||
transforms-base
|
||||
users
|
||||
util
|
||||
warehouses
|
||||
@@ -2294,6 +2383,21 @@
|
||||
enterprise/serialization
|
||||
enterprise/transforms-python}}
|
||||
|
||||
enterprise/replacement
|
||||
{:team "Graphy"
|
||||
:api #{metabase-enterprise.replacement.api}
|
||||
:uses #{api
|
||||
app-db
|
||||
events
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
queries
|
||||
source-swap
|
||||
task
|
||||
util
|
||||
enterprise/dependencies}}
|
||||
|
||||
enterprise/sandbox
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.sandbox.api.routes
|
||||
@@ -2357,6 +2461,7 @@
|
||||
search
|
||||
settings
|
||||
task
|
||||
tracing
|
||||
util
|
||||
enterprise/llm
|
||||
enterprise/metabot-v3}}
|
||||
@@ -2476,6 +2581,7 @@
|
||||
query-processor
|
||||
settings
|
||||
transforms
|
||||
transforms-base
|
||||
util}}
|
||||
|
||||
enterprise/upload-management
|
||||
@@ -2491,8 +2597,8 @@
|
||||
{:team "Metabot"
|
||||
:api #{metabase-enterprise.workspaces.api
|
||||
metabase-enterprise.workspaces.init}
|
||||
:friends #{enterprise/agent-api}
|
||||
:uses #{api
|
||||
api-keys
|
||||
app-db
|
||||
config
|
||||
database-routing
|
||||
@@ -2501,6 +2607,7 @@
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
premium-features
|
||||
queries
|
||||
;; NOTE: we will remove this once we get "pure transforms" module.
|
||||
query-processor
|
||||
@@ -2509,8 +2616,14 @@
|
||||
sql-tools
|
||||
system
|
||||
transforms
|
||||
transforms-base
|
||||
util
|
||||
enterprise/transforms-python}}}
|
||||
enterprise/transforms-python}}
|
||||
|
||||
enterprise/writable-connection
|
||||
{:team "Querying"
|
||||
:api #{}
|
||||
:uses #{premium-features}}}
|
||||
|
||||
;; namespaces matching these patterns (with `re-find`) are excluded from the module linter. Since regex literals
|
||||
;; aren't allowed in EDN just used the `(str <regex>)` version i.e. two slashes instead of one.
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
help-link-custom-destination
|
||||
instance-creation
|
||||
is-hosted?
|
||||
is-metabot-enabled
|
||||
jdbc-data-warehouse-max-connection-pool-size
|
||||
jdbc-data-warehouse-unreturned-connection-timeout-seconds
|
||||
jwt-attribute-email
|
||||
|
||||
8
.gitattributes
vendored
@@ -1,7 +1,9 @@
|
||||
# Custom merge driver for YAML migration files
|
||||
# Treats each changeset as an atomic unit to avoid conflicts within changesets
|
||||
# Custom merge driver for per-release YAML migration files.
|
||||
# Treats each changeset as an atomic unit to avoid conflicts within changesets.
|
||||
# Only applies to per-release *_update_migrations.yaml files — directory-based
|
||||
# migrations (e.g. 060/*.yaml) have one file per migration and don't need this.
|
||||
# Execute this in your shell to enable custom merge driver:
|
||||
# echo '\n[include]\npath=../.gitconfig' >> .git/config
|
||||
#
|
||||
# TODO (Bryan 12/12/25) -- reenable this when we can completely verify correctness
|
||||
# resources/migrations/*.yaml merge=yaml-migrations
|
||||
# resources/migrations/*_update_migrations.yaml merge=yaml-migrations
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# execute this in your shell to enable custom merge driver:
|
||||
# echo '\n[include]\npath=../.gitconfig' >> .git/config
|
||||
[merge "yaml-migrations"]
|
||||
name = YAML migration changeset-aware merge
|
||||
name = Per-release YAML migration changeset-aware merge
|
||||
driver = bin/mage -merge-yaml-migrations %O %A %B %L %P
|
||||
|
||||
268
.github/CODEOWNERS
vendored
@@ -1,266 +1,30 @@
|
||||
# Before changing this file, make sure you have read how we use it:
|
||||
# https://www.notion.so/metabase/Pull-Request-Code-Review-Guide-374f05df889748b69d67af753f9bc9b8?pvs=4#36550c04ceab40b5a8b8bee6264b090c
|
||||
|
||||
# @metabase/core-backend-querying-platform
|
||||
src/metabase/lib @metabase/core-backend-querying-platform
|
||||
src/metabase/lib_be @metabase/core-backend-querying-platform
|
||||
src/metabase/query_processor @metabase/core-backend-querying-platform
|
||||
|
||||
# @metabase/graphy
|
||||
src/metabase/graph @metabase/graphy
|
||||
enterprise/backend/src/metabase_enterprise/dependencies @metabase/graphy
|
||||
enterprise/frontend/src/metabase-enterprise/dependencies @metabase/graphy
|
||||
|
||||
# @metabase/embedding
|
||||
enterprise/frontend/src/embedding-sdk-package @metabase/embedding
|
||||
enterprise/frontend/src/embedding-sdk-bundle @metabase/embedding
|
||||
enterprise/frontend/src/embedding-sdk-shared @metabase/embedding
|
||||
frontend/src/metabase/embedding-sdk @metabase/embedding
|
||||
frontend/src/metabase/public/containers/PublicOrEmbeddedQuestion @metabase/embedding
|
||||
frontend/src/metabase/public/containers/PublicOrEmbeddedDashboard @metabase/embedding
|
||||
frontend/src/metabase/ui @metabase/core-frontend-ux-west
|
||||
# snowplow/* @metabase/data
|
||||
src/metabase/analytics/prometheus.clj @metabase/cloud-ops
|
||||
.github @metabase/devexp
|
||||
.clj-kondo @metabase/devexp
|
||||
deps.edn @metabase/backend-developers
|
||||
|
||||
# @metabase/tech-writers
|
||||
docs @metabase/tech-writers
|
||||
docs/developers-guide # exclude from tech-writer ownership
|
||||
|
||||
#
|
||||
# Backend modules <=> teams that own them
|
||||
#
|
||||
# @metabase/devexp
|
||||
.github @metabase/devexp
|
||||
|
||||
# modules/drivers @metabase/drivers
|
||||
src/metabase/driver/sql_jdbc/actions.clj @metabase/core-ux-east
|
||||
test/metabase/driver/sql_jdbc/actions_test.clj @metabase/core-ux-east
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/advanced_config @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/advanced_config @metabase/core-backend-admin-webapp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/advanced_permissions @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/advanced_permissions @metabase/core-backend-admin-webapp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/airgap @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/airgap @metabase/core-backend-admin-webapp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/api @metabase/devexp
|
||||
# enterprise/backend/test/metabase_enterprise/api @metabase/devexp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/audit_app @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/audit_app @metabase/core-backend-admin-webapp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/auth_provider @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/auth_provider @metabase/core-backend-admin-webapp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/billing @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/billing @metabase/core-backend-admin-webapp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/cache @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/cache @metabase/core-backend-admin-webapp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/content_verification @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/content_verification @metabase/core-backend-admin-webapp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/core @metabase/devexp
|
||||
# enterprise/backend/test/metabase_enterprise/core @metabase/devexp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/dashboard_subscription_filters @metabase/core-ux-east
|
||||
# enterprise/backend/test/metabase_enterprise/dashboard_subscription_filters @metabase/core-ux-east
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/enhancements @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/enhancements @metabase/core-backend-admin-webapp
|
||||
|
||||
# # enterprise/backend/src/metabase_enterprise/llm
|
||||
# # enterprise/backend/test/metabase_enterprise/llm
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/query_reference_validation @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/query_reference_validation @metabase/core-backend-admin-webapp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/sandbox @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/sandbox @metabase/core-backend-admin-webapp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/scim @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/scim @metabase/core-backend-admin-webapp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/search ...
|
||||
# enterprise/backend/test/metabase_enterprise/search ...
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/serialization @metabase/core-ux-east
|
||||
# enterprise/backend/test/metabase_enterprise/serialization @metabase/core-ux-east
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/snippet_collections @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/snippet_collections @metabase/core-backend-admin-webapp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/sso @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/sso @metabase/core-backend-admin-webapp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/stale @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/stale @metabase/core-backend-admin-webapp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/stats @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/stats @metabase/core-backend-admin-webapp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/task @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/task @metabase/core-backend-admin-webapp
|
||||
|
||||
# enterprise/backend/src/metabase_enterprise/upload_management @metabase/core-backend-admin-webapp
|
||||
# enterprise/backend/test/metabase_enterprise/upload_management @metabase/core-backend-admin-webapp
|
||||
|
||||
src/metabase/actions @metabase/core-ux-east
|
||||
test/metabase/actions @metabase/core-ux-east
|
||||
|
||||
# src/metabase/activity_feed @metabase/core-backend-admin-webapp
|
||||
# test/metabase/activity_feed @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/analytics @metabase/core-backend-admin-webapp
|
||||
# test/metabase/analytics @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/analyze @metabase/drivers
|
||||
# test/metabase/analyze @metabase/drivers
|
||||
|
||||
# src/metabase/api @metabase/devexp
|
||||
# test/metabase/api @metabase/devexp
|
||||
|
||||
# src/metabase/audit @metabase/core-backend-admin-webapp
|
||||
# test/metabase/audit @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/auth_provider @metabase/core-backend-admin-webapp
|
||||
# test/metabase/auth_provider @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/bookmarks @metabase/core-backend-admin-webapp
|
||||
# test/metabase/bookmarks @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/cmd @metabase/devexp
|
||||
# test/metabase/cmd @metabase/devexp
|
||||
|
||||
# src/metabase/channel @metabase/core-ux-east
|
||||
# test/metabase/channel @metabase/core-ux-east
|
||||
|
||||
# src/metabase/cloud_migration ...
|
||||
# test/metabase/cloud_migration ...
|
||||
|
||||
# src/metabase/cmd @metabase/devexp
|
||||
# test/metabase/cmd @metabase/devexp
|
||||
|
||||
# src/metabase/config @metabase/devexp
|
||||
# test/metabase/config @metabase/devexp
|
||||
|
||||
# src/metabase/core @metabase/devexp
|
||||
# test/metabase/core @metabase/devexp
|
||||
|
||||
# src/metabase/db @metabase/core-backend-admin-webapp
|
||||
# test/metabase/db @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/driver @metabase/drivers
|
||||
# test/metabase/driver @metabase/drivers
|
||||
|
||||
# src/metabase/eid_translation @metabase/core-backend-admin-webapp
|
||||
# test/metabase/eid_translation @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/embed @metabase/embedding
|
||||
# test/metabase/embed @metabase/embedding
|
||||
|
||||
# src/metabase/events @metabase/devexp
|
||||
# test/metabase/events @metabase/devexp
|
||||
|
||||
# src/metabase/formatter @metabase/core-backend-querying
|
||||
# test/metabase/formatter @metabase/core-backend-querying
|
||||
|
||||
# src/metabase/indexed_entities @metabase/core-backend-querying
|
||||
# test/metabase/indexed_entities @metabase/core-backend-querying
|
||||
|
||||
# src/metabase/integrations ...
|
||||
# test/metabase/integrations ...
|
||||
|
||||
# src/metabase/legacy_mbql @metabase/core-backend-querying
|
||||
# test/metabase/legacy_mbql @metabase/core-backend-querying
|
||||
|
||||
# src/metabase/lib @metabase/core-backend-querying
|
||||
# test/metabase/lib @metabase/core-backend-querying
|
||||
|
||||
# src/metabase/logger @metabase/devexp
|
||||
# test/metabase/logger @metabase/devexp
|
||||
|
||||
# src/metabase/login_history @metabase/core-backend-admin-webapp
|
||||
# test/metabase/login_history @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/model_persistence @metabase/core-backend-querying
|
||||
# test/metabase/model_persistence @metabase/core-backend-querying
|
||||
|
||||
# src/metabase/models @metabase/devexp
|
||||
# test/metabase/models @metabase/devexp
|
||||
|
||||
# src/metabase/moderation @metabase/core-backend-admin-webapp
|
||||
# test/metabase/moderation @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/notification @metabase/core-ux-east
|
||||
# test/metabase/notification @metabase/core-ux-east
|
||||
|
||||
# src/metabase/permissions @metabase/core-backend-admin-webapp
|
||||
# test/metabase/permissions @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/plugins @metabase/devexp
|
||||
# test/metabase/plugins @metabase/devexp
|
||||
|
||||
# src/metabase/premium_features @metabase/core-backend-admin-webapp
|
||||
# test/metabase/premium_features @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/public_settings @metabase/core-backend-admin-webapp
|
||||
# test/metabase/public_settings @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/public_sharing @metabase/core-backend-admin-webapp
|
||||
# test/metabase/public_sharing @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/pulse @metabase/core-ux-east
|
||||
# test/metabase/pulse @metabase/core-ux-east
|
||||
|
||||
# src/metabase/query_analysis @metabase/core-backend-querying
|
||||
# test/metabase/query_analysis @metabase/core-backend-querying
|
||||
|
||||
# src/metabase/query_processor @metabase/core-backend-querying
|
||||
# test/metabase/query_processor @metabase/core-backend-querying
|
||||
|
||||
# src/metabase/request @metabase/devexp
|
||||
# test/metabase/request @metabase/devexp
|
||||
|
||||
# src/metabase/revisions @metabase/core-backend-admin-webapp
|
||||
# test/metabase/revisions @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/sample_data @metabase/core-backend-admin-webapp
|
||||
# test/metabase/sample_data @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/search ...
|
||||
# test/metabase/search ...
|
||||
|
||||
# src/metabase/segments @metabase/core-backend-querying
|
||||
# test/metabase/segments @metabase/core-backend-querying
|
||||
|
||||
# src/metabase/server @metabase/devexp
|
||||
# test/metabase/server @metabase/devexp
|
||||
|
||||
# src/metabase/session @metabase/core-backend-admin-webapp
|
||||
# test/metabase/session @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/setup @metabase/core-backend-admin-webapp
|
||||
# test/metabase/setup @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/sso @metabase/core-backend-admin-webapp
|
||||
# test/metabase/sso @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/sync @metabase/drivers
|
||||
# test/metabase/sync @metabase/drivers
|
||||
|
||||
# src/metabase/task @metabase/devexp
|
||||
# test/metabase/task @metabase/devexp
|
||||
|
||||
# src/metabase/tiles @metabase/core-backend-dashviz
|
||||
# test/metabase/tiles @metabase/core-backend-dashviz
|
||||
|
||||
# src/metabase/timeline @metabase/core-backend-dashviz
|
||||
# test/metabase/timeline @metabase/core-backend-dashviz
|
||||
|
||||
# src/metabase/types @metabase/core-backend-querying
|
||||
# test/metabase/types @metabase/core-backend-querying
|
||||
|
||||
# src/metabase/upload @metabase/core-backend-admin-webapp
|
||||
# test/metabase/upload @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/user_key_value @metabase/core-backend-admin-webapp
|
||||
# test/metabase/user_key_value @metabase/core-backend-admin-webapp
|
||||
|
||||
# src/metabase/util @metabase/devexp
|
||||
# test/metabase/util @metabase/devexp
|
||||
|
||||
# src/metabase/xrays @metabase/core-backend-querying
|
||||
# test/metabase/xrays @metabase/core-backend-querying
|
||||
# @metabase/cloud-ops
|
||||
src/metabase/analytics/prometheus.clj @metabase/cloud-ops
|
||||
|
||||
25
.github/actions/fetch-artifact/action.yml
vendored
@@ -51,12 +51,25 @@ runs:
|
||||
|
||||
const artifact_id = artifacts.data.artifacts[0].id;
|
||||
|
||||
const download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: artifact_id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
let download;
|
||||
const maxAttempts = 3;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: artifact_id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
break;
|
||||
} catch (err) {
|
||||
console.log(`Attempt ${attempt}/${maxAttempts} failed: ${err.message}`);
|
||||
if (attempt === maxAttempts) throw err;
|
||||
const delay = attempt * 15000; // 15s, 30s
|
||||
console.log(`Retrying in ${delay / 1000}s...`);
|
||||
await new Promise(r => setTimeout(r, delay));
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/mb.zip`, Buffer.from(download.data));
|
||||
|
||||
|
||||
8
.github/actions/get-cache-keys/action.yml
vendored
@@ -7,9 +7,15 @@ outputs:
|
||||
m2-cache-key:
|
||||
description: "The cache key for the Maven cache."
|
||||
value: ${{ steps.keys-factory.outputs.m2-cache-key }}
|
||||
m2-restore-key:
|
||||
description: "The fallback restore key prefix for the Maven cache."
|
||||
value: ${{ steps.keys-factory.outputs.m2-restore-key }}
|
||||
node-cache-key:
|
||||
description: "The cache key for the Node.js cache."
|
||||
value: ${{ steps.keys-factory.outputs.node-cache-key }}
|
||||
node-restore-key:
|
||||
description: "The fallback restore key prefix for the Node.js cache."
|
||||
value: ${{ steps.keys-factory.outputs.node-restore-key }}
|
||||
eslint-cache-key:
|
||||
description: "The cache key for Eslint cache"
|
||||
value: ${{ steps.keys-factory.outputs.eslint-cache-key }}
|
||||
@@ -31,7 +37,9 @@ runs:
|
||||
CACHE_SUFFIX="${{ runner.os }}-$WEEK_OF_YEAR"
|
||||
|
||||
echo "m2-cache-key=$M2_CACHE_PREFIX-$CACHE_SUFFIX" >> $GITHUB_OUTPUT
|
||||
echo "m2-restore-key=$M2_CACHE_PREFIX-" >> $GITHUB_OUTPUT
|
||||
echo "node-cache-key=$NODE_CACHE_PREFIX-$CACHE_SUFFIX" >> $GITHUB_OUTPUT
|
||||
echo "node-restore-key=$NODE_CACHE_PREFIX-" >> $GITHUB_OUTPUT
|
||||
echo "eslint-cache-key=$ESLINT_CACHE_PREFIX-$CACHE_SUFFIX" >> $GITHUB_OUTPUT
|
||||
echo "cypress-cache-key=$CYPRESS_CACHE_PREFIX-$CACHE_SUFFIX" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
16
.github/actions/prepare-backend/action.yml
vendored
@@ -19,10 +19,18 @@ runs:
|
||||
distribution: "temurin"
|
||||
- name: Install Clojure CLI
|
||||
shell: bash
|
||||
continue-on-error: true
|
||||
run: |
|
||||
curl -O https://download.clojure.org/install/linux-install-${{ inputs.clojure-version }}.sh &&
|
||||
sudo bash ./linux-install-${{ inputs.clojure-version }}.sh
|
||||
for attempt in 1 2 3; do
|
||||
curl --retry 3 --retry-delay 5 --fail -O https://download.clojure.org/install/linux-install-${{ inputs.clojure-version }}.sh \
|
||||
&& sudo bash ./linux-install-${{ inputs.clojure-version }}.sh \
|
||||
&& break
|
||||
echo "::warning::Clojure install attempt $attempt failed, retrying in 10s..."
|
||||
sleep 10
|
||||
done
|
||||
if ! clojure --version; then
|
||||
echo "::error::Clojure CLI installation failed after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Get M2 cache key
|
||||
uses: ./.github/actions/get-cache-keys
|
||||
@@ -42,3 +50,5 @@ runs:
|
||||
~/.m2
|
||||
~/.gitlibs
|
||||
key: ${{ steps.get-cache-keys.outputs.m2-cache-key }}
|
||||
restore-keys: |
|
||||
${{ steps.get-cache-keys.outputs.m2-restore-key }}
|
||||
|
||||
17
.github/actions/prepare-frontend/action.yml
vendored
@@ -11,17 +11,10 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Prepare Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Prepare Node + Bun environment
|
||||
uses: ./.github/actions/prepare-node-bun
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }} # this overrides, if present
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.7"
|
||||
token: ${{ github.token }}
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Get cache keys
|
||||
uses: ./.github/actions/get-cache-keys
|
||||
@@ -34,6 +27,8 @@ runs:
|
||||
~/.m2
|
||||
~/.gitlibs
|
||||
key: ${{ steps.get-cache-keys.outputs.m2-cache-key }}
|
||||
restore-keys: |
|
||||
${{ steps.get-cache-keys.outputs.m2-restore-key }}
|
||||
|
||||
- name: Get bun cache directory
|
||||
id: bun-cache
|
||||
@@ -51,6 +46,8 @@ runs:
|
||||
${{ steps.bun-cache.outputs.dir }}
|
||||
node_modules
|
||||
key: ${{ steps.get-cache-keys.outputs.node-cache-key }}
|
||||
restore-keys: |
|
||||
${{ steps.get-cache-keys.outputs.node-restore-key }}
|
||||
|
||||
# postinstall lifecycle hook can be dangerous in CI so we only run it locally (see package.json)
|
||||
# If a job wants to patch packages, it does so explicitly in the CI. This setting is opt-out.
|
||||
|
||||
20
.github/actions/prepare-node-bun/action.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Prepare Node + Bun environment
|
||||
description: "Set up Node.js and Bun for CI jobs"
|
||||
inputs:
|
||||
node-version:
|
||||
description: "The version of Node.js to use, overriding .nvmrc"
|
||||
required: false
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Prepare Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.7"
|
||||
token: ${{ github.token }}
|
||||
39
.github/actions/sdk-bundle-size-check/action.yml
vendored
@@ -35,9 +35,22 @@ runs:
|
||||
with:
|
||||
name: metabase-${{ inputs.edition }}-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
|
||||
path: temp/current-uberjar
|
||||
- name: Extract embedding-sdk.js from current uberjar
|
||||
- name: Extract embedding-sdk bundles from current uberjar
|
||||
shell: bash
|
||||
run: unzip -j temp/current-uberjar/target/uberjar/metabase.jar frontend_client/app/embedding-sdk.js -d temp/current-bundle
|
||||
run: |
|
||||
# Try new layout first (app/embedding-sdk/legacy/), fall back to old layout (app/embedding-sdk.js)
|
||||
unzip -j temp/current-uberjar/target/uberjar/metabase.jar \
|
||||
"frontend_client/app/embedding-sdk/legacy/embedding-sdk.js" \
|
||||
-d temp/current-bundle 2>/dev/null || \
|
||||
unzip -j temp/current-uberjar/target/uberjar/metabase.jar \
|
||||
"frontend_client/app/embedding-sdk.js" \
|
||||
-d temp/current-bundle
|
||||
|
||||
# Extract chunks directory if it exists (new layout only)
|
||||
mkdir -p temp/current-chunks
|
||||
unzip -j temp/current-uberjar/target/uberjar/metabase.jar \
|
||||
"frontend_client/app/embedding-sdk/chunks/*" \
|
||||
-d temp/current-chunks 2>/dev/null || true
|
||||
- name: Checkout base ref
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -53,13 +66,21 @@ runs:
|
||||
id: compare
|
||||
shell: bash
|
||||
run: |
|
||||
# --- Legacy (monolithic) bundle comparison ---
|
||||
current_size=$(wc -c < temp/current-bundle/embedding-sdk.js) || { echo "::error::Failed to read current bundle"; exit 1; }
|
||||
base_ref_size=$(wc -c < temp/base-ref-src/resources/frontend_client/app/embedding-sdk.js) || { echo "::error::Failed to read base-ref bundle"; exit 1; }
|
||||
|
||||
# Base ref: try new layout first, fall back to old
|
||||
if [ -f "temp/base-ref-src/resources/frontend_client/app/embedding-sdk/legacy/embedding-sdk.js" ]; then
|
||||
base_ref_size=$(wc -c < "temp/base-ref-src/resources/frontend_client/app/embedding-sdk/legacy/embedding-sdk.js")
|
||||
else
|
||||
base_ref_size=$(wc -c < "temp/base-ref-src/resources/frontend_client/app/embedding-sdk.js")
|
||||
fi
|
||||
[ "$current_size" -gt 0 ] && [ "$base_ref_size" -gt 0 ] || { echo "::error::Bundle file is empty"; exit 1; }
|
||||
|
||||
diff=$((current_size - base_ref_size))
|
||||
percent=$(( diff * 100 / base_ref_size ))
|
||||
|
||||
echo "=== Legacy (monolithic) bundle ==="
|
||||
echo "Current size: $current_size bytes"
|
||||
echo "Base ref size: $base_ref_size bytes"
|
||||
echo "Difference: $diff bytes ($percent%)"
|
||||
@@ -75,3 +96,15 @@ runs:
|
||||
|
||||
echo "status=$status" >> $GITHUB_OUTPUT
|
||||
echo "size_change_percent=$percent" >> $GITHUB_OUTPUT
|
||||
|
||||
# --- Chunked bundle (informational) ---
|
||||
if ls temp/current-chunks/*.js 1>/dev/null 2>&1; then
|
||||
chunks_size=$(cat temp/current-chunks/*.js | wc -c)
|
||||
echo ""
|
||||
echo "=== Chunked bundle (bootstrap + chunks) ==="
|
||||
echo "Total chunked size: $chunks_size bytes"
|
||||
echo "chunks_size=$chunks_size" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo ""
|
||||
echo "No chunked bundle found (expected on master)"
|
||||
fi
|
||||
|
||||
2
.github/file-paths.yaml
vendored
@@ -30,7 +30,7 @@ frontend_configs: &frontend_configs
|
||||
- "**/tsconfig*.json"
|
||||
- "package.json"
|
||||
- "babel.config.json"
|
||||
- ".eslintrc.js"
|
||||
- "eslint.config.mjs"
|
||||
- "postcss.config.js"
|
||||
- "rspack.config.js"
|
||||
- "rspack.main.config.js"
|
||||
|
||||
52
.github/scripts/build-cross-version-matrix.js
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
// This script is used in .github/workflows/cross-version.yml
|
||||
// to generate a matrix for cross-version migration testing
|
||||
|
||||
/**
|
||||
* Generate a CI matrix for cross-version migration testing (EE only).
|
||||
*
|
||||
* Creates pairs of [source, target] for testing HEAD against the last N
|
||||
* minor versions in both upgrade and downgrade directions.
|
||||
*
|
||||
* @param {number} currentVersion - The current major version number (e.g., 59)
|
||||
* @param {number} count - Number of previous minor versions to test against (default: 3)
|
||||
* @returns {{ config: Array<{ source: string, target: string }> }}
|
||||
*
|
||||
* @example
|
||||
* buildMatrix(59)
|
||||
* // Returns:
|
||||
* // {
|
||||
* // config: [
|
||||
* // { source: "HEAD", target: "v1.59.x" },
|
||||
* // { source: "v1.59.x", target: "HEAD" },
|
||||
* // { source: "HEAD", target: "v1.58.x" },
|
||||
* // { source: "v1.58.x", target: "HEAD" },
|
||||
* // { source: "HEAD", target: "v1.57.x" },
|
||||
* // { source: "v1.57.x", target: "HEAD" },
|
||||
* // ]
|
||||
* // }
|
||||
*/
|
||||
function buildCrossVersionMatrix(currentVersion, count = 3) {
|
||||
if (!Number.isInteger(currentVersion) || currentVersion < 1) {
|
||||
throw new Error(
|
||||
`Invalid currentVersion: ${currentVersion}. Must be a positive integer.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!Number.isInteger(count) || count < 1) {
|
||||
throw new Error(`Invalid count: ${count}. Must be a positive integer.`);
|
||||
}
|
||||
|
||||
const config = Array.from({ length: count }, (_, i) => currentVersion - i)
|
||||
.filter((majorVersion) => majorVersion >= 1)
|
||||
.flatMap((majorVersion) => {
|
||||
const version = `v1.${majorVersion}.x`;
|
||||
return [
|
||||
{ source: "HEAD", target: version },
|
||||
{ source: version, target: "HEAD" },
|
||||
];
|
||||
});
|
||||
|
||||
return { config };
|
||||
}
|
||||
|
||||
module.exports = { buildCrossVersionMatrix };
|
||||
242
.github/scripts/build-cross-version-matrix.unit.spec.js
vendored
Normal file
@@ -0,0 +1,242 @@
|
||||
const { buildCrossVersionMatrix } = require("./build-cross-version-matrix");
|
||||
|
||||
describe("buildCrossVersionMatrix", () => {
|
||||
describe("default behavior", () => {
|
||||
it("should generate matrix with 3 versions by default", () => {
|
||||
const result = buildCrossVersionMatrix(59);
|
||||
expect(result).toEqual({
|
||||
config: [
|
||||
{ source: "HEAD", target: "v1.59.x" },
|
||||
{ source: "v1.59.x", target: "HEAD" },
|
||||
{ source: "HEAD", target: "v1.58.x" },
|
||||
{ source: "v1.58.x", target: "HEAD" },
|
||||
{ source: "HEAD", target: "v1.57.x" },
|
||||
{ source: "v1.57.x", target: "HEAD" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should use EE prefix (v1)", () => {
|
||||
const result = buildCrossVersionMatrix(50);
|
||||
expect(result.config[0].target).toBe("v1.50.x");
|
||||
expect(result.config[1].source).toBe("v1.50.x");
|
||||
});
|
||||
|
||||
it("should create pairs in both directions (upgrade and downgrade)", () => {
|
||||
const result = buildCrossVersionMatrix(59);
|
||||
const entries = result.config;
|
||||
|
||||
// Check HEAD -> version (upgrade test)
|
||||
expect(entries.filter((e) => e.source === "HEAD")).toHaveLength(3);
|
||||
// Check version -> HEAD (downgrade test)
|
||||
expect(entries.filter((e) => e.target === "HEAD")).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("custom count", () => {
|
||||
it("should generate matrix with custom count", () => {
|
||||
const result = buildCrossVersionMatrix(59, 5);
|
||||
expect(result.config).toHaveLength(10); // 5 versions × 2 directions
|
||||
expect(result.config).toEqual([
|
||||
{ source: "HEAD", target: "v1.59.x" },
|
||||
{ source: "v1.59.x", target: "HEAD" },
|
||||
{ source: "HEAD", target: "v1.58.x" },
|
||||
{ source: "v1.58.x", target: "HEAD" },
|
||||
{ source: "HEAD", target: "v1.57.x" },
|
||||
{ source: "v1.57.x", target: "HEAD" },
|
||||
{ source: "HEAD", target: "v1.56.x" },
|
||||
{ source: "v1.56.x", target: "HEAD" },
|
||||
{ source: "HEAD", target: "v1.55.x" },
|
||||
{ source: "v1.55.x", target: "HEAD" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should generate matrix with count=1", () => {
|
||||
const result = buildCrossVersionMatrix(59, 1);
|
||||
expect(result.config).toHaveLength(2);
|
||||
expect(result.config).toEqual([
|
||||
{ source: "HEAD", target: "v1.59.x" },
|
||||
{ source: "v1.59.x", target: "HEAD" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should generate matrix with large count", () => {
|
||||
const result = buildCrossVersionMatrix(59, 10);
|
||||
expect(result.config).toHaveLength(20);
|
||||
|
||||
// Check first and last entries
|
||||
expect(result.config[0]).toEqual({
|
||||
source: "HEAD",
|
||||
target: "v1.59.x",
|
||||
});
|
||||
expect(result.config[18]).toEqual({
|
||||
source: "HEAD",
|
||||
target: "v1.50.x",
|
||||
});
|
||||
expect(result.config[19]).toEqual({
|
||||
source: "v1.50.x",
|
||||
target: "HEAD",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle version 1", () => {
|
||||
const result = buildCrossVersionMatrix(1);
|
||||
expect(result).toEqual({
|
||||
config: [
|
||||
{ source: "HEAD", target: "v1.1.x" },
|
||||
{ source: "v1.1.x", target: "HEAD" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should stop when version would go below 1", () => {
|
||||
const result = buildCrossVersionMatrix(2, 5);
|
||||
// Can only generate v1.2.x and v1.1.x (2 versions, not 5)
|
||||
expect(result.config).toHaveLength(4);
|
||||
expect(result).toEqual({
|
||||
config: [
|
||||
{ source: "HEAD", target: "v1.2.x" },
|
||||
{ source: "v1.2.x", target: "HEAD" },
|
||||
{ source: "HEAD", target: "v1.1.x" },
|
||||
{ source: "v1.1.x", target: "HEAD" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle high version numbers", () => {
|
||||
const result = buildCrossVersionMatrix(100, 2);
|
||||
expect(result).toEqual({
|
||||
config: [
|
||||
{ source: "HEAD", target: "v1.100.x" },
|
||||
{ source: "v1.100.x", target: "HEAD" },
|
||||
{ source: "HEAD", target: "v1.99.x" },
|
||||
{ source: "v1.99.x", target: "HEAD" },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should throw for undefined currentVersion", () => {
|
||||
expect(() => buildCrossVersionMatrix()).toThrow(
|
||||
"Invalid currentVersion: undefined. Must be a positive integer.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw for non-integer currentVersion", () => {
|
||||
expect(() => buildCrossVersionMatrix(59.5)).toThrow(
|
||||
"Invalid currentVersion: 59.5. Must be a positive integer.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw for zero currentVersion", () => {
|
||||
expect(() => buildCrossVersionMatrix(0)).toThrow(
|
||||
"Invalid currentVersion: 0. Must be a positive integer.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw for negative currentVersion", () => {
|
||||
expect(() => buildCrossVersionMatrix(-1)).toThrow(
|
||||
"Invalid currentVersion: -1. Must be a positive integer.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw for NaN currentVersion", () => {
|
||||
expect(() => buildCrossVersionMatrix(NaN)).toThrow(
|
||||
"Invalid currentVersion: NaN. Must be a positive integer.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw for non-integer count", () => {
|
||||
expect(() => buildCrossVersionMatrix(59, 2.5)).toThrow(
|
||||
"Invalid count: 2.5. Must be a positive integer.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw for zero count", () => {
|
||||
expect(() => buildCrossVersionMatrix(59, 0)).toThrow(
|
||||
"Invalid count: 0. Must be a positive integer.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw for negative count", () => {
|
||||
expect(() => buildCrossVersionMatrix(59, -1)).toThrow(
|
||||
"Invalid count: -1. Must be a positive integer.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("matrix structure for GitHub Actions", () => {
|
||||
it("should return object with config array", () => {
|
||||
const result = buildCrossVersionMatrix(59);
|
||||
expect(result).toHaveProperty("config");
|
||||
expect(Array.isArray(result.config)).toBe(true);
|
||||
});
|
||||
|
||||
it("should produce valid JSON for GitHub Actions", () => {
|
||||
const result = buildCrossVersionMatrix(59);
|
||||
const json = JSON.stringify(result);
|
||||
expect(() => JSON.parse(json)).not.toThrow();
|
||||
|
||||
const parsed = JSON.parse(json);
|
||||
expect(parsed.config).toHaveLength(6);
|
||||
expect(parsed.config[0]).toEqual({
|
||||
source: "HEAD",
|
||||
target: "v1.59.x",
|
||||
});
|
||||
});
|
||||
|
||||
it("should have consistent entry structure", () => {
|
||||
const result = buildCrossVersionMatrix(59, 5);
|
||||
|
||||
result.config.forEach((entry) => {
|
||||
expect(entry).toHaveProperty("source");
|
||||
expect(entry).toHaveProperty("target");
|
||||
expect(typeof entry.source).toBe("string");
|
||||
expect(typeof entry.target).toBe("string");
|
||||
// Either source or target should be HEAD
|
||||
expect(entry.source === "HEAD" || entry.target === "HEAD").toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("version ordering", () => {
|
||||
it("should generate versions in descending order", () => {
|
||||
const result = buildCrossVersionMatrix(59, 5);
|
||||
const versions = result.config
|
||||
.filter((e) => e.source === "HEAD")
|
||||
.map((e) => e.target);
|
||||
|
||||
expect(versions).toEqual([
|
||||
"v1.59.x",
|
||||
"v1.58.x",
|
||||
"v1.57.x",
|
||||
"v1.56.x",
|
||||
"v1.55.x",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should interleave upgrade and downgrade tests", () => {
|
||||
const result = buildCrossVersionMatrix(59, 2);
|
||||
|
||||
expect(result.config[0]).toEqual({
|
||||
source: "HEAD",
|
||||
target: "v1.59.x",
|
||||
});
|
||||
expect(result.config[1]).toEqual({
|
||||
source: "v1.59.x",
|
||||
target: "HEAD",
|
||||
});
|
||||
expect(result.config[2]).toEqual({
|
||||
source: "HEAD",
|
||||
target: "v1.58.x",
|
||||
});
|
||||
expect(result.config[3]).toEqual({
|
||||
source: "v1.58.x",
|
||||
target: "HEAD",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
2
.github/scripts/setup-python-runner.sh
vendored
@@ -19,7 +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 python-runner
|
||||
git clone --depth 1 -b extensible-resource-schema https://${METABASE_AUTOMATION_USER_TOKEN}@github.com/metabase/python-runner-container.git python-runner
|
||||
|
||||
if [ "$SKIP_START" = false ]; then
|
||||
echo "Starting python-runner..."
|
||||
|
||||
20
.github/team.json
vendored
@@ -4,14 +4,23 @@
|
||||
"name": "Querying",
|
||||
"label": ".Team/Querying",
|
||||
"members": [
|
||||
"ranquild",
|
||||
"romeovs",
|
||||
"bshepherdson",
|
||||
"metamben",
|
||||
"wzimrin",
|
||||
"rileythomp",
|
||||
"camsual"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Graphy",
|
||||
"label": ".Team/Graphy",
|
||||
"members": [
|
||||
"ranquild",
|
||||
"romeovs",
|
||||
"ericnormand",
|
||||
"galdre",
|
||||
"rileythomp"
|
||||
"dpsutton",
|
||||
"escherize",
|
||||
"bgrabow"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -36,9 +45,6 @@
|
||||
"name": "DevEx",
|
||||
"label": ".Team/DevEx",
|
||||
"members": [
|
||||
"camsaul",
|
||||
"dpsutton",
|
||||
"escherize",
|
||||
"fraserdrops",
|
||||
"nemanjaglumac"
|
||||
]
|
||||
|
||||
20
.github/workflows/app-db.yml
vendored
@@ -20,9 +20,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version:
|
||||
- name: MariaDB 10.2
|
||||
junit-name: be-tests-mariadb-10-2-ee
|
||||
image: circleci/mariadb:10.2.23
|
||||
- name: MariaDB 10.6
|
||||
junit-name: be-tests-mariadb-10-6-ee
|
||||
image: circleci/mariadb:10.6
|
||||
env:
|
||||
enable-ssl-tests: 'false'
|
||||
- name: MariaDB Latest
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
MB_MYSQL_SSL_TEST_USER: metabase
|
||||
MB_MYSQL_SSL_TEST_PASSWORD: ${{ secrets.MYSQL_RDS_SSL_INSTANCE_PASSWORD }}
|
||||
# for historic reasons (I don't want to mess around with required jobs) the job name should be something like
|
||||
# "be-tests-mariadb-10-2-ee (0)"
|
||||
# "be-tests-mariadb-10-6-ee (0)"
|
||||
name: "${{ matrix.version.name }} ${{ matrix.job.name }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -134,9 +134,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version:
|
||||
- name: MySQL 8.0
|
||||
junit-name: be-tests-mysql-8-0-ee
|
||||
image: cimg/mysql:8.0
|
||||
- name: MySQL 8.4
|
||||
junit-name: be-tests-mysql-8-4-ee
|
||||
image: cimg/mysql:8.4
|
||||
env:
|
||||
enable-ssl-tests: 'false'
|
||||
enable-aws-iam-tests: 'false'
|
||||
@@ -224,7 +224,7 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_AURORA_IAM_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_AURORA_IAM_SECRET_ACCESS_KEY }}
|
||||
# for historic reasons (I don't want to mess around with required jobs) the job name should be something like
|
||||
# "be-tests-mariadb-10-2-ee (0)"
|
||||
# "be-tests-mariadb-10-6-ee (0)"
|
||||
name: "${{ matrix.version.name }} ${{ matrix.job.name }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -258,9 +258,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version:
|
||||
- name: Postgres 12.x
|
||||
- name: Postgres 14.x
|
||||
junit-name: postgres-ee
|
||||
docker-image: postgres:12-alpine
|
||||
docker-image: postgres:14-alpine
|
||||
env:
|
||||
enable-ssl-tests: 'false'
|
||||
enable-aws-iam-tests: 'false'
|
||||
|
||||
5
.github/workflows/backend-cloverage.yml
vendored
@@ -11,8 +11,9 @@ name: Backend-Cloverage
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 10 * * 1-5' # every weekday at 5am US eastern time
|
||||
# re-enable once we figure out the correctness of the reports in DEV-1104
|
||||
# schedule:
|
||||
# - cron: '0 10 * * 1-5' # every weekday at 5am US eastern time
|
||||
|
||||
jobs:
|
||||
setup-oss:
|
||||
|
||||
13
.github/workflows/backend.yml
vendored
@@ -36,6 +36,18 @@ jobs:
|
||||
- run: clojure -X:dev:ee:ee-dev:drivers:drivers-dev:test:eastwood
|
||||
name: Run Eastwood linter
|
||||
|
||||
be-linter-eastwood-test:
|
||||
if: ${{ !inputs.skip }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
name: Eastwood (test)
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
- run: clojure -X:dev:ee:ee-dev:drivers:drivers-dev:test:eastwood/test
|
||||
name: Run Eastwood linter on tests
|
||||
|
||||
be-tests:
|
||||
if: ${{ !inputs.skip }}
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -184,6 +196,7 @@ jobs:
|
||||
be-tests-result:
|
||||
needs:
|
||||
- be-linter-eastwood
|
||||
- be-linter-eastwood-test
|
||||
- be-linter-clj-kondo
|
||||
- be-cljfmt
|
||||
- be-check
|
||||
|
||||
38
.github/workflows/codespell.yml
vendored
@@ -43,27 +43,35 @@ jobs:
|
||||
app-id: ${{ secrets.METABASE_BOT_APP_ID }}
|
||||
private-key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Build Claude prompt
|
||||
if: steps.codespell_check.outcome == 'failure'
|
||||
run: |
|
||||
cat > /tmp/claude_prompt.txt << 'PROMPT_HEADER'
|
||||
You are tasked with fixing spelling errors detected by codespell in this repository.
|
||||
|
||||
## Codespell Output
|
||||
The following spelling errors were detected:
|
||||
```
|
||||
PROMPT_HEADER
|
||||
cat codespell_output.txt >> /tmp/claude_prompt.txt
|
||||
cat >> /tmp/claude_prompt.txt << 'PROMPT_FOOTER'
|
||||
```
|
||||
|
||||
## Instructions
|
||||
1. **Review the codespell output and identify reported spelling errors** - The usual format codespell uses is `typo ==> correction`
|
||||
2. **Fix every spelling error** - Use the Edit tool to fix each misspelling with the suggested correction
|
||||
- If codespell suggests multiple corrections, choose the most appropriate one based on context
|
||||
|
||||
Human reviewers will verify the changes before merging, so fix all errors reported by codespell.
|
||||
PROMPT_FOOTER
|
||||
|
||||
- name: Fix spelling errors with Claude Code
|
||||
if: steps.codespell_check.outcome == 'failure'
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ steps.claude-app-token.outputs.token }}
|
||||
prompt: |
|
||||
You are tasked with fixing spelling errors detected by codespell in this repository.
|
||||
|
||||
## Codespell Output
|
||||
The following spelling errors were detected:
|
||||
```
|
||||
$(cat codespell_output.txt)
|
||||
```
|
||||
|
||||
## Instructions
|
||||
1. **Review the codespell output and identify reported spelling errors** - The usual format codespell uses is `typo ==> correction`
|
||||
2. **Fix every spelling error** - Use the Edit tool to fix each misspelling with the suggested correction
|
||||
- If codespell suggests multiple corrections, choose the most appropriate one based on context
|
||||
|
||||
Human reviewers will verify the changes before merging, so fix all errors reported by codespell.
|
||||
prompt_file: /tmp/claude_prompt.txt
|
||||
claude_args: |
|
||||
--allowedTools "Write,Read,Glob,Grep,Edit,MultiEdit,Bash(cat*),Bash(ls*)"
|
||||
--disallowedTools "Bash(git commit*),Bash(git push*),Bash(gh*)"
|
||||
|
||||
12
.github/workflows/cross-branch-migrations.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
base-ref: ${{ env.BASE_REF }}
|
||||
head-ref: ${{ env.HEAD_REF }}
|
||||
|
||||
migrations-postgres-12-ee:
|
||||
migrations-postgres-14-ee:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
MB_DB_USER: circle_test
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12-alpine
|
||||
image: postgres:14-alpine
|
||||
ports:
|
||||
- "5432:5432"
|
||||
env:
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
base-ref: ${{ env.BASE_REF }}
|
||||
head-ref: ${{ env.HEAD_REF }}
|
||||
|
||||
migrations-mysql-8-0-ee:
|
||||
migrations-mysql-8-4-ee:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
MB_DB_USER: root
|
||||
services:
|
||||
mysql:
|
||||
image: cimg/mysql:8.0
|
||||
image: cimg/mysql:8.4
|
||||
ports:
|
||||
- "3306:3306"
|
||||
steps:
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
base-ref: ${{ env.BASE_REF }}
|
||||
head-ref: ${{ env.HEAD_REF }}
|
||||
|
||||
migrations-mariadb-10-2-ee:
|
||||
migrations-mariadb-10-6-ee:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
MB_DB_USER: root
|
||||
services:
|
||||
mariadb:
|
||||
image: circleci/mariadb:10.2.23
|
||||
image: circleci/mariadb:10.6
|
||||
ports:
|
||||
- "3306:3306"
|
||||
steps:
|
||||
|
||||
102
.github/workflows/cross-version.yml
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
name: Cross-Version Migration Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
source_version:
|
||||
description: "Source version (e.g., v1.58.6 or HEAD)"
|
||||
required: true
|
||||
type: string
|
||||
target_version:
|
||||
description: "Target version (e.g., v1.58.7 or HEAD)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "cross-version/**"
|
||||
- ".github/workflows/cross-version.yml"
|
||||
- ".github/scripts/build-cross-version-matrix.js"
|
||||
|
||||
schedule:
|
||||
# Run nightly at 3 AM UTC
|
||||
- cron: "0 3 * * *"
|
||||
|
||||
jobs:
|
||||
# Generate matrix for scheduled/PR runs
|
||||
generate-matrix:
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github
|
||||
|
||||
- name: Generate version matrix
|
||||
id: set-matrix
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { buildCrossVersionMatrix } = require('${{ github.workspace }}/.github/scripts/build-cross-version-matrix');
|
||||
const { config } = buildCrossVersionMatrix(${{ vars.CURRENT_VERSION }});
|
||||
const matrix = JSON.stringify({ include: config });
|
||||
core.setOutput("matrix", matrix);
|
||||
console.log("matrix:", matrix);
|
||||
|
||||
# Matrix-based tests for scheduled/PR runs
|
||||
test-matrix:
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
needs: generate-matrix
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare release scripts
|
||||
uses: ./.github/actions/prepare-release-scripts
|
||||
|
||||
- name: Run cross-version test
|
||||
env:
|
||||
CURRENT_VERSION: ${{ vars.CURRENT_VERSION }}
|
||||
run: |
|
||||
cd cross-version
|
||||
./test.sh --source "${{ matrix.source }}" --target "${{ matrix.target }}"
|
||||
|
||||
- name: Show logs on failure
|
||||
if: failure()
|
||||
run: |
|
||||
cd cross-version
|
||||
docker compose logs metabase || true
|
||||
docker compose logs postgres || true
|
||||
|
||||
# Manual test for workflow_dispatch
|
||||
test-manual:
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare release scripts
|
||||
uses: ./.github/actions/prepare-release-scripts
|
||||
|
||||
- name: Run cross-version test
|
||||
env:
|
||||
CURRENT_VERSION: ${{ vars.CURRENT_VERSION }}
|
||||
run: |
|
||||
cd cross-version
|
||||
./test.sh --source "${{ inputs.source_version }}" --target "${{ inputs.target_version }}"
|
||||
|
||||
- name: Show logs on failure
|
||||
if: failure()
|
||||
run: |
|
||||
cd cross-version
|
||||
docker compose logs metabase || true
|
||||
docker compose logs postgres || true
|
||||
79
.github/workflows/disabled-drivers-slack-notification.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
name: Disabled Drivers Slack Notification
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 9 * * 1" # every Monday at 9:00 UTC
|
||||
|
||||
jobs:
|
||||
notify-disabled-drivers:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/Jod
|
||||
|
||||
- name: Install Slack SDK
|
||||
run: npm install @slack/web-api
|
||||
|
||||
- name: Check disabled drivers and notify Slack
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
with:
|
||||
script: |
|
||||
const { WebClient } = require('@slack/web-api');
|
||||
|
||||
// Fetch the config
|
||||
const response = await fetch('https://raw.githubusercontent.com/metabase/ci-test-config/refs/heads/master/ci-test-config.json');
|
||||
const config = await response.json();
|
||||
|
||||
const drivers = config.ignored?.drivers || [];
|
||||
const lastUpdated = config.metadata?.last_updated || 'unknown';
|
||||
const reason = config.metadata?.reason || 'No reason provided';
|
||||
|
||||
if (drivers.length === 0) {
|
||||
console.log('No drivers are currently disabled. Skipping Slack notification.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!process.env.SLACK_BOT_TOKEN) {
|
||||
throw new Error('SLACK_BOT_TOKEN is required');
|
||||
}
|
||||
|
||||
const slack = new WebClient(process.env.SLACK_BOT_TOKEN);
|
||||
|
||||
await slack.chat.postMessage({
|
||||
channel: 'engineering-ci',
|
||||
text: 'CI Disabled Drivers Report',
|
||||
blocks: [
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: `:no_entry: *CI Disabled Drivers*\n${drivers.map(d => `\`${d}\``).join(' ')}`
|
||||
},
|
||||
accessory: {
|
||||
type: 'button',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'View Config',
|
||||
emoji: true
|
||||
},
|
||||
url: 'https://github.com/metabase/ci-test-config/blob/master/ci-test-config.json'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'context',
|
||||
elements: [
|
||||
{
|
||||
type: 'mrkdwn',
|
||||
text: `${reason} · :clock1: *${lastUpdated}*`
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
console.log('Slack notification sent successfully');
|
||||
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.58.x"
|
||||
default: "release-x.59.x"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
@@ -16,6 +16,6 @@ jobs:
|
||||
name: Call documentation update for release branch
|
||||
uses: ./.github/workflows/docs-generate-base.yml
|
||||
with:
|
||||
target_branch: ${{ inputs.target_branch || 'release-x.58.x' }}
|
||||
target_branch: ${{ inputs.target_branch || 'release-x.59.x' }}
|
||||
secrets:
|
||||
automation_token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
|
||||
20
.github/workflows/drivers-stress-test.yml
vendored
@@ -109,7 +109,7 @@ jobs:
|
||||
junit-name: 'be-tests-druid-ee'
|
||||
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
|
||||
|
||||
be-tests-mariadb-10-2-ee:
|
||||
be-tests-mariadb-10-6-ee:
|
||||
if: inputs.driver == 'Maria DB'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
@@ -124,15 +124,15 @@ jobs:
|
||||
MB_MYSQL_TEST_USER: root
|
||||
services:
|
||||
mariadb:
|
||||
image: circleci/mariadb:10.2.23
|
||||
image: circleci/mariadb:10.6
|
||||
ports:
|
||||
- "3306:3306"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test MariaDB driver (10.2)
|
||||
- name: Test MariaDB driver (10.6)
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-mariadb-10-2-ee'
|
||||
junit-name: 'be-tests-mariadb-10-6-ee'
|
||||
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
|
||||
|
||||
be-tests-mariadb-latest-ee:
|
||||
@@ -298,7 +298,7 @@ jobs:
|
||||
junit-name: 'be-tests-mongo-latest-ee'
|
||||
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
|
||||
|
||||
be-tests-mysql-8-0-ee:
|
||||
be-tests-mysql-8-4-ee:
|
||||
if: inputs.driver == 'MySQL'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 90
|
||||
@@ -313,15 +313,15 @@ jobs:
|
||||
MB_MYSQL_TEST_USER: root
|
||||
services:
|
||||
mysql:
|
||||
image: cimg/mysql:8.0
|
||||
image: cimg/mysql:8.4
|
||||
ports:
|
||||
- "3306:3306"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test MySQL driver (8.0)
|
||||
- name: Test MySQL driver (8.4)
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-mysql-8-0-ee'
|
||||
junit-name: 'be-tests-mysql-8-4-ee'
|
||||
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
|
||||
|
||||
be-tests-mysql-latest-ee:
|
||||
@@ -438,7 +438,7 @@ jobs:
|
||||
MB_POSTGRESQL_TEST_USER: mb_test
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12-alpine
|
||||
image: postgres:14-alpine
|
||||
ports:
|
||||
- "5432:5432"
|
||||
env:
|
||||
@@ -447,7 +447,7 @@ jobs:
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test Postgres driver (12)
|
||||
- name: Test Postgres driver (14)
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-postgres-ee'
|
||||
|
||||
35
.github/workflows/drivers.yml
vendored
@@ -151,11 +151,10 @@ jobs:
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
:fail-fast? true
|
||||
# DS 2025-09-26 temporarily disabled while we diagnose flakes
|
||||
# - name: Transforms Python Tests
|
||||
# test-args: >-
|
||||
# :only-tags [:mb/transforms-python-test]
|
||||
# needs-python-runner: true
|
||||
- name: Transforms Python Tests
|
||||
test-args: >-
|
||||
:only-tags [:mb/transforms-python-test]
|
||||
needs-python-runner: true
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: bigquery-cloud-sdk
|
||||
@@ -562,9 +561,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version:
|
||||
- name: MariaDB 10.2
|
||||
junit-name: be-tests-mariadb-10-2-ee
|
||||
image: circleci/mariadb:10.2.23
|
||||
- name: MariaDB 10.6
|
||||
junit-name: be-tests-mariadb-10-6-ee
|
||||
image: circleci/mariadb:10.6
|
||||
env:
|
||||
enable-ssl-tests: 'false'
|
||||
enable-aws-iam-tests: 'false'
|
||||
@@ -574,9 +573,9 @@ jobs:
|
||||
env:
|
||||
enable-ssl-tests: 'false'
|
||||
enable-aws-iam-tests: 'false'
|
||||
- name: MySQL 8.0
|
||||
junit-name: be-tests-mysql-8-0-ee
|
||||
image: mysql:8.0
|
||||
- name: MySQL 8.4
|
||||
junit-name: be-tests-mysql-8-4-ee
|
||||
image: mysql:8.4
|
||||
env:
|
||||
enable-ssl-tests: 'false'
|
||||
enable-aws-iam-tests: 'false'
|
||||
@@ -989,9 +988,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version:
|
||||
- name: Postgres 12.x
|
||||
- name: Postgres 14.x
|
||||
junit-name: postgres-ee
|
||||
docker-image: postgres:12-alpine
|
||||
docker-image: postgres:14-alpine
|
||||
env:
|
||||
enable-ssl-tests: 'false'
|
||||
enable-aws-iam-tests: 'false'
|
||||
@@ -1278,12 +1277,10 @@ jobs:
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
:fail-fast? true
|
||||
|
||||
# DS 2025-09-26 temporarily disabled while we diagnose flakes
|
||||
# - name: Transforms Python Tests
|
||||
# test-args: >-
|
||||
# :only-tags [:mb/transforms-python-test]
|
||||
# needs-python-runner: true
|
||||
- name: Transforms Python Tests
|
||||
test-args: >-
|
||||
:only-tags [:mb/transforms-python-test]
|
||||
needs-python-runner: true
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: snowflake
|
||||
|
||||
@@ -88,6 +88,7 @@ jobs:
|
||||
e2e-sample-apps-tests:
|
||||
needs: [get-sample-apps-data]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
test_suite: ${{ fromJSON(needs.get-sample-apps-data.outputs.matrix) }}
|
||||
environment: ["production", "development"]
|
||||
@@ -194,6 +195,7 @@ jobs:
|
||||
e2e-shoppy-tests:
|
||||
needs: [get-sample-apps-data]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
environment: ["production", "development"]
|
||||
if: |
|
||||
@@ -342,6 +344,7 @@ jobs:
|
||||
# For more details see `Embedding SDK integration tests with Host Apps` in `enterprise/frontend/src/embedding-sdk-package/dev.md`
|
||||
e2e-host-apps-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
test_suite:
|
||||
[
|
||||
|
||||
13
.github/workflows/embedding-sdk.yml
vendored
@@ -156,11 +156,18 @@ jobs:
|
||||
- name: Send notification to Slack via Zapier
|
||||
if: success() && steps.compare.outputs.status == 'increased'
|
||||
shell: bash
|
||||
env:
|
||||
ZAPIER_WEBHOOK_URL: ${{ secrets.EMBEDDING_ZAPIER_WEBHOOK_URL }}
|
||||
SIZE_CHANGE_PERCENT: ${{ steps.compare.outputs.size_change_percent }}
|
||||
BUNDLE_SIZE_THRESHOLD_PERCENT: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
run: |
|
||||
curl -s -X POST "${{ secrets.EMBEDDING_ZAPIER_WEBHOOK_URL }}" \
|
||||
-d '⚠️ Embedding SDK bundle size increased by ${{ steps.compare.outputs.size_change_percent }}% (threshold: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}%)
|
||||
curl -s -X POST "$ZAPIER_WEBHOOK_URL" \
|
||||
-d "⚠️ Embedding SDK bundle size increased by ${SIZE_CHANGE_PERCENT}% (threshold: ${BUNDLE_SIZE_THRESHOLD_PERCENT}%)
|
||||
|
||||
PR #${{ github.event.pull_request.number }} ${{ github.event.pull_request.title }} (${{ github.event.pull_request.html_url }})'
|
||||
PR #${PR_NUMBER} ${PR_TITLE} (${PR_URL})"
|
||||
|
||||
- name: Comment on PR if bundle size has decreased
|
||||
uses: ./.github/actions/notify-pull-request
|
||||
|
||||
3
.github/workflows/mage.yml
vendored
@@ -2,9 +2,6 @@ name: Mage Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "mage/**"
|
||||
- "bb.edn"
|
||||
|
||||
jobs:
|
||||
# run mage tests even if the rest of be isnt needed
|
||||
|
||||
78
.github/workflows/module-boundaries-stats.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: Module Boundaries Stats
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: 40 1 * * 5 # every Friday at 1:40
|
||||
|
||||
jobs:
|
||||
module-boundaries-stats:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- name: Install dependencies
|
||||
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));
|
||||
")
|
||||
|
||||
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]);
|
||||
}
|
||||
")
|
||||
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, $UNMODULED unmoduled folders, $VIOLATIONS violations"
|
||||
|
||||
echo "NAMED_MODULES=$NAMED_MODULES" >> $GITHUB_OUTPUT
|
||||
echo "UNMODULED=$UNMODULED" >> $GITHUB_OUTPUT
|
||||
echo "VIOLATIONS=$VIOLATIONS" >> $GITHUB_OUTPUT
|
||||
- name: Upload stats
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
NAMED_MODULES: ${{ steps.get-stats.outputs.NAMED_MODULES }}
|
||||
UNMODULED: ${{ steps.get-stats.outputs.UNMODULED }}
|
||||
VIOLATIONS: ${{ steps.get-stats.outputs.VIOLATIONS }}
|
||||
API_KEY: ${{ secrets.STATS_API_KEY }}
|
||||
with:
|
||||
script: | # js
|
||||
const { uploadCsvToMb } = require('${{ github.workspace }}/.github/scripts/csv-to-mb.js');
|
||||
|
||||
console.log("NAMED_MODULES", Number(process.env.NAMED_MODULES));
|
||||
console.log("UNMODULED", Number(process.env.UNMODULED));
|
||||
console.log("VIOLATIONS", Number(process.env.VIOLATIONS));
|
||||
|
||||
const data = [
|
||||
{
|
||||
'Date': new Date().toISOString().slice(0, 10), // YYYY-MM-DD
|
||||
'Modules': Number(process.env.NAMED_MODULES),
|
||||
'Unmoduled folders': Number(process.env.UNMODULED),
|
||||
'Violations': Number(process.env.VIOLATIONS),
|
||||
}
|
||||
];
|
||||
|
||||
uploadCsvToMb({
|
||||
baseUrl: 'https://stats.metabase.com',
|
||||
tableId: 75146,
|
||||
jsonData: data,
|
||||
mode: 'append',
|
||||
}).then(() => {
|
||||
console.log('Data uploaded successfully');
|
||||
}).catch((error) => {
|
||||
console.error('Error uploading data:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
6
.github/workflows/openapi-check.yml
vendored
@@ -22,10 +22,8 @@ jobs:
|
||||
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
# fetch full git history to allow fetching the latest base branch commit and comparing changes
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js 22
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.x
|
||||
- name: Prepare Node + Bun environment
|
||||
uses: ./.github/actions/prepare-node-bun
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
- name: Generate version.properties
|
||||
|
||||
266
.github/workflows/repro-bot.yml
vendored
Normal file
@@ -0,0 +1,266 @@
|
||||
name: "Repro Bot"
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- labeled
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: "Issue number to reproduce"
|
||||
required: true
|
||||
type: string
|
||||
repro_bot_branch:
|
||||
description: "Branch of repro-bot to use"
|
||||
required: false
|
||||
default: "main"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
repro-bot:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
id-token: write
|
||||
if: |
|
||||
(github.event_name == 'push') ||
|
||||
(github.event_name == 'issues' && github.event.label.name == '.Run Repro Bot' && github.event.issue.state == 'open') ||
|
||||
(github.event_name == 'workflow_dispatch')
|
||||
|
||||
steps:
|
||||
- name: Get issue number
|
||||
id: issue
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "number=${{ github.event.inputs.issue_number }}" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ github.event_name }}" = "push" ]; then
|
||||
echo "number=70292" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/prepare-backend
|
||||
- uses: ./.github/actions/prepare-frontend
|
||||
|
||||
- name: Checkout repro-bot
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: metabase/repro-bot
|
||||
ref: ${{ github.event.inputs.repro_bot_branch || 'main' }}
|
||||
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
path: repro-bot
|
||||
|
||||
- name: Pre-clone metabase bare git repo
|
||||
run: git clone --bare .git repro-bot/git/metabase
|
||||
|
||||
- name: Capture Node.js path for MCP subprocesses
|
||||
run: echo "NODE_BIN_DIR=$(dirname $(which node))" >> $GITHUB_ENV
|
||||
|
||||
- name: Install playwright-cli
|
||||
run: |
|
||||
npm install -g @playwright/cli@latest
|
||||
playwright-cli install --skills
|
||||
|
||||
- name: Install bbin and clj-nrepl-eval
|
||||
run: |
|
||||
# Install babashka
|
||||
curl -sLO https://raw.githubusercontent.com/babashka/babashka/master/install
|
||||
chmod +x install
|
||||
sudo ./install
|
||||
rm install
|
||||
|
||||
# Install bbin
|
||||
mkdir -p ~/.bbin/bin
|
||||
curl -sL https://raw.githubusercontent.com/babashka/bbin/main/bbin > ~/.bbin/bin/bbin
|
||||
chmod +x ~/.bbin/bin/bbin
|
||||
export PATH="$HOME/.bbin/bin:$PATH"
|
||||
export BBIN_HOME="$HOME/.bbin"
|
||||
|
||||
# Install clj-nrepl-eval via bbin
|
||||
bbin install https://github.com/bhauman/clojure-mcp-light.git \
|
||||
--tag v0.2.1 \
|
||||
--as clj-nrepl-eval \
|
||||
--main-opts '["-m" "clojure-mcp-light.nrepl-eval"]'
|
||||
|
||||
# Add to PATH for the rest of the workflow
|
||||
echo "$HOME/.bbin/bin" >> $GITHUB_PATH
|
||||
|
||||
# Verify
|
||||
clj-nrepl-eval --help | head -3
|
||||
|
||||
- name: Register MCP servers
|
||||
env:
|
||||
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY_REPRO_BOT }}
|
||||
run: |
|
||||
UV_PATH="$(which uv)"
|
||||
jq -n \
|
||||
--arg uv "$UV_PATH" \
|
||||
--arg key "$LINEAR_API_KEY" \
|
||||
'{
|
||||
mcpServers: {
|
||||
"metabase-repro": {
|
||||
command: $uv,
|
||||
args: ["run", "--directory", "mcp", "--with", "fastmcp", "python", "-m", "metabase_repro"]
|
||||
},
|
||||
"linear-server": {
|
||||
url: "https://mcp.linear.app/mcp",
|
||||
type: "http",
|
||||
headers: {
|
||||
Authorization: ("Bearer " + $key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > ~/.claude.json
|
||||
|
||||
- name: Generate GitHub App Token
|
||||
uses: actions/create-github-app-token@v2
|
||||
id: claude-app-token
|
||||
with:
|
||||
app-id: ${{ secrets.METABASE_BOT_APP_ID }}
|
||||
private-key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Run repro-bot with Claude Code
|
||||
id: claude
|
||||
continue-on-error: true
|
||||
uses: anthropics/claude-code-action@v1
|
||||
env:
|
||||
CLAUDE_CODE_DEBUG_LOGS_DIR: /tmp/claude-debug-logs
|
||||
CLAUDE_WORKING_DIR: ${{ github.workspace }}/repro-bot
|
||||
MB_PREMIUM_EMBEDDING_TOKEN: ${{ secrets.PR_ENV_MB_PREMIUM_EMBEDDING_TOKEN }}
|
||||
GITHUB_ACTIONS_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
timeout-minutes: 30
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_REPRO_BOT }}
|
||||
github_token: ${{ steps.claude-app-token.outputs.token }}
|
||||
prompt: |
|
||||
Read and follow the instructions in .claude/commands/repro.md to investigate this GitHub issue:
|
||||
https://github.com/${{ github.repository }}/issues/${{ steps.issue.outputs.number }}
|
||||
|
||||
## CI Environment Notes
|
||||
- The bare git repo has been pre-cloned at git/metabase. The master worktree still needs setup via setup_master_worktree().
|
||||
- The GITHUB_ACTIONS_RUN_URL environment variable is set. If any phase fails catastrophically, follow the CI error handling in repro.md to post the error to Linear with the action run URL.
|
||||
- Docker is available for appdb (Postgres/MySQL/MariaDB) if needed.
|
||||
- Java and Python/uv are pre-installed.
|
||||
claude_args: >-
|
||||
--allowedTools
|
||||
"Glob,
|
||||
Grep,
|
||||
Read,
|
||||
ToolSearch,
|
||||
Skill(*),
|
||||
WebSearch,
|
||||
WebFetch(domain:docs.metabase.com),
|
||||
WebFetch(domain:metabase.com),
|
||||
WebFetch(domain:www.metabase.com),
|
||||
Edit(//./**),
|
||||
Edit(repro/**),
|
||||
Edit(/tmp/**),
|
||||
Write(//./**),
|
||||
Write(repro/**),
|
||||
Write(/tmp/**),
|
||||
Bash(base64 *),
|
||||
Bash(cat *),
|
||||
Bash(cd *),
|
||||
Bash(clj-nrepl-eval *),
|
||||
Bash(clojure *),
|
||||
Bash(cp *),
|
||||
Bash(diff *),
|
||||
Bash(docker *),
|
||||
Bash(echo *),
|
||||
Bash(find *),
|
||||
Bash(gh api repos/metabase/metabase/*),
|
||||
Bash(gh issue list * --repo metabase/metabase *),
|
||||
Bash(gh issue view * --repo metabase/metabase *),
|
||||
Bash(gh release *),
|
||||
Bash(gh search *),
|
||||
Bash(git *),
|
||||
Bash(grep *),
|
||||
Bash(head *),
|
||||
Bash(jar *),
|
||||
Bash(jq *),
|
||||
Bash(kill *),
|
||||
Bash(ls *),
|
||||
Bash(mkdir *),
|
||||
Bash(mv *),
|
||||
Bash(npx playwright-cli:*),
|
||||
Bash(pgrep *),
|
||||
Bash(playwright-cli:*),
|
||||
Bash(printf *),
|
||||
Bash(ps *),
|
||||
Bash(psql *),
|
||||
Bash(PGPASSWORD=*),
|
||||
Bash(python -m mcp.metabase_repro *),
|
||||
Bash(python3 *),
|
||||
Bash(sed *),
|
||||
Bash(sort *),
|
||||
Bash(strings *),
|
||||
Bash(tail *),
|
||||
Bash(tee *),
|
||||
Bash(touch *),
|
||||
Bash(unzip *),
|
||||
Bash(wc *),
|
||||
Bash(which *),
|
||||
Bash(xargs *),
|
||||
mcp__metabase-repro__api_call,
|
||||
mcp__metabase-repro__browser_snapshot,
|
||||
mcp__metabase-repro__check_setup,
|
||||
mcp__metabase-repro__check_setup_token,
|
||||
mcp__metabase-repro__cleanup,
|
||||
mcp__metabase-repro__create_env,
|
||||
mcp__metabase-repro__get_session_token,
|
||||
mcp__metabase-repro__get_status,
|
||||
mcp__metabase-repro__jar_download,
|
||||
mcp__metabase-repro__repl_eval,
|
||||
mcp__metabase-repro__reset_metabase,
|
||||
mcp__metabase-repro__run_test,
|
||||
mcp__metabase-repro__save_test_patch,
|
||||
mcp__metabase-repro__setup_master_worktree,
|
||||
mcp__metabase-repro__setup_worktree,
|
||||
mcp__metabase-repro__start_appdb,
|
||||
mcp__metabase-repro__start_metabase,
|
||||
mcp__metabase-repro__stop_appdb_tool,
|
||||
mcp__metabase-repro__stop_metabase,
|
||||
mcp__linear-server__create_attachment,
|
||||
mcp__linear-server__get_issue,
|
||||
mcp__linear-server__list_comments,
|
||||
mcp__linear-server__list_issues,
|
||||
mcp__linear-server__list_issue_labels,
|
||||
mcp__linear-server__save_comment,
|
||||
mcp__linear-server__save_issue"
|
||||
--disallowedTools "Bash(git push*),Bash(rm -rf /*)"
|
||||
|
||||
- name: Verify repro-bot completed successfully
|
||||
run: |
|
||||
if [ "${{ steps.claude.outcome }}" = "failure" ]; then
|
||||
echo "::error::Claude Code crashed or exited with an error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EXEC_FILE="${{ steps.claude.outputs.execution_file }}"
|
||||
if [ -z "$EXEC_FILE" ] || [ ! -f "$EXEC_FILE" ]; then
|
||||
echo "::error::No execution file found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q "mcp__linear-server__" "$EXEC_FILE"; then
|
||||
echo "::error::Claude did not post a comment on Linear"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Repro-bot completed successfully and posted to Linear"
|
||||
|
||||
- name: Upload repro-bot artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: repro-bot-${{ steps.issue.outputs.number }}
|
||||
if-no-files-found: ignore
|
||||
path: |
|
||||
repro/*/SUGGESTIONS.md
|
||||
repro/*/*.log
|
||||
repro/*/*.patch
|
||||
repro/*/playwright/**
|
||||
${{ steps.claude.outputs.execution_file }}
|
||||
18
.github/workflows/rerun-workflows.yml
vendored
@@ -40,11 +40,19 @@ jobs:
|
||||
const BRANCH = process.env.BRANCH_NAME;
|
||||
|
||||
if (ATTEMPT <= MAX_ATTEMPTS) {
|
||||
github.rest.actions.reRunWorkflowFailedJobs({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.event.workflow_run.id }},
|
||||
});
|
||||
try {
|
||||
await github.rest.actions.reRunWorkflowFailedJobs({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.event.workflow_run.id }},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status === 403) {
|
||||
console.log(`Skipping re-run: ${error.message}`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else if (!BRANCH.includes('backport-')) {
|
||||
// notify slack of repeated failure
|
||||
const { WebClient } = require('@slack/web-api');
|
||||
|
||||
63
.github/workflows/reset-cloud-test-dataset.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Reset Cloud Test Dataset
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
driver:
|
||||
description: 'Driver to reset dataset for'
|
||||
required: true
|
||||
default: 'snowflake'
|
||||
type: choice
|
||||
options:
|
||||
- snowflake
|
||||
- bigquery-cloud-sdk
|
||||
- redshift
|
||||
- databricks
|
||||
dataset-name:
|
||||
description: 'Dataset name to drop (e.g., test-data)'
|
||||
required: true
|
||||
default: 'test-data'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
reset-dataset:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: ${{ inputs.driver }}
|
||||
# Snowflake
|
||||
MB_SNOWFLAKE_TEST_USER: METABASE CI
|
||||
MB_SNOWFLAKE_TEST_ACCOUNT: ${{ secrets.MB_SNOWFLAKE_TEST_ACCOUNT }}
|
||||
MB_SNOWFLAKE_TEST_PASSWORD: ${{ secrets.MB_SNOWFLAKE_TEST_PASSWORD }}
|
||||
MB_SNOWFLAKE_TEST_PRIVATE_KEY: ${{ secrets.MB_SNOWFLAKE_TEST_PRIVATE_KEY }}
|
||||
MB_SNOWFLAKE_TEST_WAREHOUSE: ${{ secrets.MB_SNOWFLAKE_TEST_WAREHOUSE }}
|
||||
# BigQuery
|
||||
MB_BIGQUERY_TEST_PROJECT_ID: ${{ secrets.BIGQUERY_TEST_PROJECT_ID }}
|
||||
MB_BIGQUERY_TEST_CLIENT_ID: ${{ secrets.MB_BIGQUERY_TEST_CLIENT_ID }}
|
||||
MB_BIGQUERY_TEST_CLIENT_SECRET: ${{ secrets.MB_BIGQUERY_TEST_CLIENT_SECRET }}
|
||||
MB_BIGQUERY_TEST_ACCESS_TOKEN: ${{ secrets.MB_BIGQUERY_TEST_ACCESS_TOKEN }}
|
||||
MB_BIGQUERY_TEST_REFRESH_TOKEN: ${{ secrets.MB_BIGQUERY_TEST_REFRESH_TOKEN }}
|
||||
MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON: ${{ secrets.MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON }}
|
||||
MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON_ROUTING: ${{ secrets.MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON_ROUTING }}
|
||||
# Redshift
|
||||
MB_REDSHIFT_TEST_USER: metabase_ci
|
||||
MB_REDSHIFT_TEST_DB: testdb
|
||||
MB_REDSHIFT_TEST_HOST: ${{ secrets.MB_REDSHIFT_TEST_HOST }}
|
||||
MB_REDSHIFT_TEST_PASSWORD: ${{ secrets.MB_REDSHIFT_TEST_PASSWORD }}
|
||||
# Databricks
|
||||
MB_DATABRICKS_TEST_HOST: ${{ secrets.MB_DATABRICKS_JDBC_TEST_HOST }}
|
||||
MB_DATABRICKS_TEST_HTTP_PATH: ${{ secrets.MB_DATABRICKS_JDBC_TEST_HTTP_PATH }}
|
||||
MB_DATABRICKS_TEST_TOKEN: ${{ secrets.MB_DATABRICKS_JDBC_TEST_TOKEN }}
|
||||
MB_DATABRICKS_TEST_CATALOG: metabase_ci
|
||||
MB_DATABRICKS_TEST_CLIENT_ID: ${{ secrets.MB_DATABRICKS_JDBC_TEST_CLIENT_ID }}
|
||||
MB_DATABRICKS_TEST_OAUTH_SECRET: ${{ secrets.MB_DATABRICKS_JDBC_TEST_OAUTH_SECRET }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/prepare-backend
|
||||
- name: Drop dataset
|
||||
run: >-
|
||||
clojure -X:dev:drivers:drivers-dev:test
|
||||
metabase.test.data.impl/drop-dataset!
|
||||
:driver '"${{ inputs.driver }}"'
|
||||
:dataset-name '"${{ inputs.dataset-name }}"'
|
||||
2
.github/workflows/run-tests.yml
vendored
@@ -81,7 +81,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
skip: ${{ needs.files-changed.outputs.frontend_all != 'true' }}
|
||||
skip-lint: ${{ needs.files-changed.outputs.frontend_all != 'true' && needs.files-changed.outputs.e2e_specs != 'true' }}
|
||||
skip-lint: ${{ needs.files-changed.outputs.frontend_all != 'true' && needs.files-changed.outputs.e2e_all != 'true' }}
|
||||
|
||||
e2e-tests:
|
||||
needs: [files-changed, uberjar]
|
||||
|
||||
18
.github/workflows/semantic-search.yml
vendored
@@ -75,9 +75,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version:
|
||||
- name: MariaDB 10.2 with pgvector pg17
|
||||
junit-name: semantic-search-tests-mariadb-10-2
|
||||
mariadb-image: circleci/mariadb:10.2.23
|
||||
- name: MariaDB 10.6 with pgvector pg17
|
||||
junit-name: semantic-search-tests-mariadb-10-6
|
||||
mariadb-image: circleci/mariadb:10.6
|
||||
pgvector-image: pgvector/pgvector:pg17
|
||||
- name: MariaDB Latest with pgvector pg17
|
||||
junit-name: semantic-search-tests-mariadb-latest
|
||||
@@ -149,9 +149,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version:
|
||||
- name: MySQL 8.0 with pgvector pg17
|
||||
junit-name: semantic-search-tests-mysql-8-0
|
||||
mysql-image: cimg/mysql:8.0
|
||||
- name: MySQL 8.4 with pgvector pg17
|
||||
junit-name: semantic-search-tests-mysql-8-4
|
||||
mysql-image: cimg/mysql:8.4
|
||||
pgvector-image: pgvector/pgvector:pg17
|
||||
- name: MySQL Latest with pgvector pg17
|
||||
junit-name: semantic-search-tests-mysql-latest
|
||||
@@ -223,9 +223,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version:
|
||||
- name: Postgres 12.x with pgvector pg17
|
||||
junit-name: semantic-search-tests-postgres-12
|
||||
postgres-image: postgres:12-alpine
|
||||
- name: Postgres 14.x with pgvector pg17
|
||||
junit-name: semantic-search-tests-postgres-14
|
||||
postgres-image: postgres:14-alpine
|
||||
pgvector-image: pgvector/pgvector:pg17
|
||||
- name: Postgres Latest with pgvector pg17
|
||||
junit-name: semantic-search-tests-postgres-latest
|
||||
|
||||
18
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
*.class
|
||||
*.*.rej
|
||||
*.d.css
|
||||
*.iml
|
||||
*.jar
|
||||
*.log
|
||||
@@ -25,6 +26,7 @@
|
||||
/.env*
|
||||
!/.env.example
|
||||
/.envrc
|
||||
/.workmux.yaml
|
||||
/.lein-env
|
||||
/.lein-failures
|
||||
/.lein-plugins
|
||||
@@ -65,6 +67,7 @@ cypress.env.json
|
||||
/resources/frontend_client/app/embed.js
|
||||
/resources/frontend_client/app/embedding-sdk.js
|
||||
/resources/frontend_client/app/embedding-sdk.js.map
|
||||
/resources/frontend_client/app/embedding-sdk/
|
||||
/resources/frontend_client/tmp-*/
|
||||
/resources/frontend_client/embed.html
|
||||
/resources/frontend_client/embed-sdk.html
|
||||
@@ -75,6 +78,7 @@ cypress.env.json
|
||||
/resources/instance_analytics.zip
|
||||
/resources/license-backend-third-party.txt
|
||||
/resources/license-frontend-third-party.txt
|
||||
/resources/metabase/config/modules.edn
|
||||
/resources/namespaces.edn
|
||||
/resources/sample-dataset.db.trace.db
|
||||
/resources/version.properties
|
||||
@@ -123,13 +127,13 @@ modules/drivers/*/.lsp/*
|
||||
**/.clj-kondo/.cache
|
||||
|
||||
# clj-kondo: ignore all except our defined config
|
||||
.clj-kondo/*
|
||||
!.clj-kondo/README.md
|
||||
!.clj-kondo/config.edn
|
||||
!.clj-kondo/config/
|
||||
!.clj-kondo/src/
|
||||
!.clj-kondo/test/
|
||||
!.clj-kondo/macros/
|
||||
**/.clj-kondo/*
|
||||
!**/.clj-kondo/README.md
|
||||
!**/.clj-kondo/config.edn
|
||||
!**/.clj-kondo/config/
|
||||
!**/.clj-kondo/src/
|
||||
!**/.clj-kondo/test/
|
||||
!**/.clj-kondo/macros/
|
||||
|
||||
# Editor- or environment-specific local files
|
||||
*.code-workspace
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
3
.vscode/extensions.json
vendored
@@ -9,6 +9,7 @@
|
||||
"github.vscode-pull-request-github",
|
||||
"limejuly.vscode-yaml-block-highlight",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"styled-components.vscode-styled-components"
|
||||
"styled-components.vscode-styled-components",
|
||||
"vunguyentuan.vscode-css-variables"
|
||||
]
|
||||
}
|
||||
|
||||
3
.vscode/settings.json
vendored
@@ -4,5 +4,6 @@
|
||||
"jake.autoDetect": "off",
|
||||
"gulp.autoDetect": "off",
|
||||
"npm.autoDetect": "off",
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"cssVariables.lookupFiles": ["**/*.d.css", "node_modules/@mantine/core/*.css"]
|
||||
}
|
||||
|
||||
12
CLAUDE.md
@@ -25,6 +25,18 @@ For detailed guidance on writing and reviewing code and documentation, see the s
|
||||
|
||||
**Important**: When working with frontend code, read [frontend/CLAUDE.md](frontend/CLAUDE.md) for project-specific guidelines on component preferences, styling, TypeScript migration, testing requirements, and available scripts.
|
||||
|
||||
## Running Backend Tests
|
||||
|
||||
Use `./bin/test-agent` to run Clojure tests. It produces clean, plain-text output with no progress bars or ANSI codes.
|
||||
|
||||
```bash
|
||||
./bin/test-agent :only '[metabase.foo-test]' # run a namespace
|
||||
./bin/test-agent :only '[metabase.foo-test/some-test]' # run a single test
|
||||
./bin/test-agent :only '[metabase.foo-test metabase.bar-test]' # multiple namespaces
|
||||
```
|
||||
|
||||
Do not use `clj -X:dev:test` directly — its progress-bar output is hard to parse.
|
||||
|
||||
## Tool Preferences
|
||||
|
||||
If `clojure-mcp` tools are available, prefer them over shell-based alternatives for Clojure development.
|
||||
|
||||
71
README.md
@@ -10,20 +10,24 @@
|
||||
|
||||
## Get started
|
||||
|
||||
The easiest way to get started with Metabase is to sign up for a free trial of [Metabase Cloud](https://store.metabase.com/checkout). You get support, backups, upgrades, an SMTP server, SSL certificate, SoC2 Type 2 security auditing, and more (plus your money goes toward improving Metabase). Check out our quick overview of [cloud vs self-hosting](https://www.metabase.com/docs/latest/cloud/cloud-vs-self-hosting). If you need to, you can always switch to [self-hosting](https://www.metabase.com/docs/latest/installation-and-operation/installing-metabase) Metabase at any time (or vice versa).
|
||||
The easiest way to get started with Metabase is to sign up for a free trial of [Metabase Cloud](https://store.metabase.com/checkout).
|
||||
|
||||
You get expert support, backups, upgrades, an SMTP server, SSL certificate, SoC2 Type 2 security auditing, and more (plus your money goes toward improving a major open-source project). Check out our quick overview of [cloud vs self-hosting](https://www.metabase.com/docs/latest/cloud/cloud-vs-self-hosting). If you need to, you can always switch to [self-hosting](https://www.metabase.com/docs/latest/installation-and-operation/installing-metabase) Metabase at any time (or vice versa).
|
||||
|
||||
## Key Features
|
||||
|
||||
- [Set up in five minutes](https://www.metabase.com/docs/latest/configuring-metabase/setting-up-metabase) (we're not kidding).
|
||||
- [Set up in five minutes](https://www.metabase.com/docs/latest/configuring-metabase/setting-up-metabase) (we're not kidding), or have us [host Metabase for you](https://www.metabase.com/cloud/) so you don't even need to think about it.
|
||||
- Let anyone on your team [ask questions](https://www.metabase.com/docs/latest/questions/introduction) without knowing SQL.
|
||||
- Use the [SQL editor](https://www.metabase.com/docs/latest/questions/native-editor/writing-sql) for more complex queries.
|
||||
- Build handsome, interactive [dashboards](https://www.metabase.com/docs/latest/dashboards/introduction) with filters, auto-refresh, fullscreen, and custom click behavior.
|
||||
- Create [models](https://www.metabase.com/learn/metabase-basics/getting-started/models) that clean up, annotate, and/or combine raw tables.
|
||||
- Define canonical [segments and metrics](https://www.metabase.com/docs/latest/data-modeling/metrics) for your team to use.
|
||||
- Send data to Slack or email on a schedule with [dashboard subscriptions](https://www.metabase.com/docs/latest/dashboards/subscriptions).
|
||||
- Set up [alerts](https://www.metabase.com/docs/latest/questions/alerts) to have Metabase notify you when your data changes.
|
||||
- [Embed charts and dashboards](https://www.metabase.com/docs/latest/embedding/introduction) in your app, or even [your entire Metabase](https://www.metabase.com/docs/latest/embedding/interactive-embedding).
|
||||
- Build with the [Embedded Analytics SDK for React](https://www.metabase.com/docs/latest/embedding/sdk/introduction) to embed standalone components with custom styling that matches your app's design.
|
||||
- Ask AI: [Metabot](https://www.metabase.com/docs/latest/ai/metabot) gives you answers you can trust, helps you write queries, and more. Or build your own [AI agent](https://www.metabase.com/docs/latest/ai/agent-api) to query your data.
|
||||
- Build handsome, interactive [dashboards](https://www.metabase.com/docs/latest/dashboards/introduction) with filters, auto-refresh, fullscreen, custom click behavior, and more.
|
||||
- Use [documents](https://www.metabase.com/docs/latest/documents/introduction) for long-form data analysis, and invite people to comment.
|
||||
- [Transform](https://www.metabase.com/docs/latest/data-studio/transforms/transforms-overview) raw data into analytics-ready tables, track down broken dependencies, and define canonical metrics in Metabase's [Data Studio](https://www.metabase.com/docs/latest/data-studio/overview).
|
||||
- Set up [alerts on your data](https://www.metabase.com/docs/latest/questions/alerts), or schedule [dashboard subscriptions](https://www.metabase.com/docs/latest/dashboards/subscriptions) to email, Slack, or even a webhook.
|
||||
- Curate content in a [Library](https://www.metabase.com/docs/latest/data-studio/library), and [version your work with Git](https://www.metabase.com/docs/latest/installation-and-operation/remote-sync).
|
||||
- [Embed Metabase in your app](https://www.metabase.com/docs/latest/embedding/introduction), with components for charts, dashboards, data browser, AI chat, and more. You can even put [an entire Metabase](https://www.metabase.com/docs/latest/embedding/interactive-embedding) in your app.
|
||||
- Set granular [permissions](https://www.metabase.com/docs/latest/permissions/introduction) that work for both internal teams and embedded analytics, whether you co-locate your customer data, or give each customer their own database.
|
||||
- Dark mode, content translations, and way more stuff than we can list here.
|
||||
|
||||
Take a [tour of Metabase](https://www.metabase.com/learn/metabase-basics/overview/tour-of-metabase).
|
||||
|
||||
@@ -36,57 +40,22 @@ Take a [tour of Metabase](https://www.metabase.com/learn/metabase-basics/overvie
|
||||
|
||||
Metabase can be run just about anywhere. Check out our [Installation Guides](https://www.metabase.com/docs/latest/installation-and-operation/installing-metabase).
|
||||
|
||||
## Documentation
|
||||
|
||||
The [Metabase handbook](https://www.metabase.com/docs/latest/).
|
||||
|
||||
## Contributing
|
||||
|
||||
## Quick Setup: Dev environment
|
||||
To contribute to Metabase, see our [Developer docs](./docs/developers-guide/start.md).
|
||||
|
||||
In order to spin up a development environment, you need to start the front end and the backend as follows:
|
||||
## Extending Metabase
|
||||
|
||||
### Frontend quick setup
|
||||
|
||||
The following command will install the JavaScript dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To build and run without watching changes:
|
||||
|
||||
```bash
|
||||
bun run build
|
||||
```
|
||||
|
||||
To build and run with hot-reload:
|
||||
|
||||
```bash
|
||||
bun run build-hot
|
||||
```
|
||||
|
||||
### Backend quick setup
|
||||
|
||||
To run the backend, you'll need to build the drivers first, and then start the backend:
|
||||
|
||||
```bash
|
||||
./bin/build-drivers.sh
|
||||
clojure -M:run
|
||||
```
|
||||
|
||||
For a more detailed setup of a dev environment for Metabase, check out our [Developers Guide](./docs/developers-guide/start.md).
|
||||
Hit our API to integrate analytics. Check out our guide, [Working with the Metabase API](https://www.metabase.com/learn/metabase-basics/administration/administration-and-operation/metabase-api).
|
||||
|
||||
## Internationalization
|
||||
|
||||
We want Metabase to be available in as many languages as possible. See which translations are available and help contribute to internationalization using our project over at [Crowdin](https://crowdin.com/project/metabase-i18n). You can also check out our [policies on translations](https://www.metabase.com/docs/latest/administration-guide/localization.html).
|
||||
|
||||
## Extending Metabase
|
||||
|
||||
Hit our Query API from JavaScript to integrate analytics. Metabase enables your application to:
|
||||
|
||||
- Build moderation interfaces.
|
||||
- Export subsets of your users to third party marketing automation software.
|
||||
- Provide a custom customer lookup application for the people in your company.
|
||||
|
||||
Check out our guide, [Working with the Metabase API](https://www.metabase.com/learn/metabase-basics/administration/administration-and-operation/metabase-api).
|
||||
|
||||
## Security Disclosure
|
||||
|
||||
See [SECURITY.md](./SECURITY.md) for details.
|
||||
|
||||
65
bb.edn
@@ -162,6 +162,15 @@
|
||||
(p/shell "bin/pre-lint-migrations-file.sh")
|
||||
(lint-migrations-file/-main))}
|
||||
|
||||
new-migration
|
||||
{:doc "Create a new directory-based migration file (v60+)"
|
||||
:requires [[mage.new-migration :as new-migration]]
|
||||
:examples [["./bin/mage new-migration 60 mq_indexes" "Create resources/migrations/060/20260905_mq_indexes.yaml"]]
|
||||
:arg-schema [:tuple
|
||||
[:int {:name "version" :description "Major version number (e.g., 60)"}]
|
||||
[:string {:name "description" :description "Lowercase alphanumeric/underscore name (e.g., mq_indexes)"}]]
|
||||
:task (task! (new-migration/new-migration parsed))}
|
||||
|
||||
jar-download
|
||||
{:doc "Download (and optionally run) jar for a metabase version or branch"
|
||||
:examples [["./bin/mage jar-download 50" "Download the latest enterprise version of release 50 to ~/path/to/my/metabase/jars"]
|
||||
@@ -247,6 +256,14 @@
|
||||
:requires [[mage.util :as u]]
|
||||
:task (task! (u/print-tasks parsed))}
|
||||
|
||||
generate-cursive-resolve-as
|
||||
{:doc "Generate XML configuration for Cursive's symbol resolution based on the current clj-kondo lint-as configuration."
|
||||
:examples [["./bin/mage generate-cursive-resolve-as" "Generate resolve-as XML configuration"]
|
||||
["./bin/mage generate-cursive-resolve-as > /Users/bgrabow/Library/Application\\ Support/JetBrains/IntelliJIdea2025.3/options/ClojureResolveSettings.xml"
|
||||
"Generate resolve-as XML configuration and install it in your global editor configuration"]]
|
||||
:requires [[mage.cursive-resolve-as :as cursive-resolve-as]]
|
||||
:task (task! (cursive-resolve-as/do-it))}
|
||||
|
||||
analytics:setup
|
||||
{:doc "Set up usage analytics development mode"
|
||||
:examples [["./bin/mage analytics:setup" "Enable usage analytics dev mode and import content"]]
|
||||
@@ -302,7 +319,7 @@
|
||||
"If you see 'gh: command not found' or auth errors, run: gh auth login"))
|
||||
:requires [[mage.ci-report :as ci-report]]
|
||||
:task (task! (ci-report/generate-report! parsed))}
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Private Tasks:
|
||||
;; These tasks are more for internal tooling, e.g. for use from git hooks etc.
|
||||
@@ -334,12 +351,13 @@
|
||||
"RUN"
|
||||
(str "any of " (pr-str driver-triggers) " affected by cloud-driver module changes")]
|
||||
["7" "Cloud driver + query-processor updated" "RUN" "query-processor module updated"]
|
||||
["8" "Cloud driver, no relevant changes" "SKIP" "no relevant changes for cloud driver"]
|
||||
["9"
|
||||
["8" "Cloud driver + driver deps affected" "RUN" "driver module affected by shared code changes"]
|
||||
["9" "Cloud driver, no relevant changes" "SKIP" "no relevant changes for cloud driver"]
|
||||
["10"
|
||||
(str (pr-str driver-triggers) " module deps affected")
|
||||
"RUN"
|
||||
(str "any of " (pr-str driver-triggers) " affected by module changes")]
|
||||
["10" "Self-hosted driver, not affected" "SKIP" "driver module not affected"]])))))
|
||||
["11" "Self-hosted driver, not affected" "SKIP" "driver module not affected"]])))))
|
||||
:options [[nil "--git-ref REF" "The git ref to compute changed modules against"]
|
||||
[nil "--is-master-or-release MASTER_OR_RELEASE"]
|
||||
[nil "--pr-labels PR_LABELS" "Comma delimited labels on the pr"]
|
||||
@@ -525,4 +543,41 @@
|
||||
:requires [[mage.escherize.fix-whitespace :as fix-whitespace]]
|
||||
:examples [["mage -escherize-fix-whitespace '**/*.clj{,c,s}'" "Removes all lines like ' ' from all clj files"]]
|
||||
:arg-schema [:sequential [:string {:name "glob-pattern" :description "Glob pattern to match files"}]]
|
||||
:task (task! (fix-whitespace/run-clean parsed))}}}
|
||||
:task (task! (fix-whitespace/run-clean parsed))}
|
||||
|
||||
-edpaget-dev-env
|
||||
{:doc "Stand up (or tear down) an isolated dev environment for this worktree"
|
||||
:examples [["./bin/mage -edpaget-dev-env --app-db postgres" "Postgres app-db with auto ports"]
|
||||
["./bin/mage -edpaget-dev-env --app-db postgres --with mysql --with mongo" "Full stack"]
|
||||
["./bin/mage -edpaget-dev-env" "Minimal: just port config (H2 app-db)"]
|
||||
["./bin/mage -edpaget-dev-env --down" "Tear down containers for this worktree"]
|
||||
["./bin/mage -edpaget-dev-env --status" "Show running containers and ports"]]
|
||||
:requires [[mage.edpaget.dev-env :as dev-env]]
|
||||
:options [[nil "--app-db DB" "App database: h2 (default), postgres, mysql" :default "h2"]
|
||||
[nil "--with SERVICE" "Warehouse service (repeatable): postgres, mysql, mariadb, mongo, clickhouse, ldap, maildev"
|
||||
:multi true :default [] :update-fn conj]
|
||||
[nil "--down" "Tear down all containers for this worktree"]
|
||||
[nil "--status" "Show current env status"]
|
||||
[nil "--slot SLOT" "Override port slot (0-99)" :parse-fn Integer/parseInt]]
|
||||
:task (task! (dev-env/dev-env! parsed))}
|
||||
|
||||
-johnswanson-dev-env
|
||||
{:doc "Stand up (or tear down) an isolated dev environment for this worktree. Interactive prompts when flags omitted."
|
||||
:examples [["./bin/mage -johnswanson-dev-env" "Interactive TUI to configure infra (saves config)"]
|
||||
["./bin/mage -johnswanson-dev-env --run" "Configure + launch backend/frontend"]
|
||||
["./bin/mage -johnswanson-dev-env --run --no-frontend" "Configure + backend only"]
|
||||
["./bin/mage -johnswanson-dev-env --down" "Stop everything for this worktree"]
|
||||
["./bin/mage -johnswanson-dev-env --status" "Show running containers and processes"]]
|
||||
:requires [[mage.johnswanson.dev-env :as jds-dev-env]]
|
||||
:options [["-e" "--edition EDITION" "ee or oss (default: prompt)"]
|
||||
["-t" "--token TOKEN" "Token type: all-features, starter-cloud, pro-cloud, pro-self-hosted, none"]
|
||||
[nil "--app-db DB" "App database: h2, postgres, mysql, mariadb (default: prompt)"]
|
||||
[nil "--with SERVICE" "Warehouse service (repeatable): postgres, mysql, mariadb, mongo, clickhouse, ldap, maildev"
|
||||
:multi true :default [] :update-fn conj]
|
||||
[nil "--run" "Also launch backend + frontend in background"]
|
||||
[nil "--no-frontend" "Skip frontend dev server (only with --run)"]
|
||||
[nil "--down" "Stop everything for this worktree"]
|
||||
[nil "--status" "Show current env status"]
|
||||
[nil "--slot SLOT" "Override port slot (0-99)" :parse-fn Integer/parseInt]
|
||||
[nil "--fresh" "Recreate containers from scratch"]]
|
||||
:task (task! (jds-dev-env/dev-env! parsed))}}}
|
||||
|
||||
96
bin/claude-dangerous
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs Claude Code in a custom Docker container with --dangerously-skip-permissions.
|
||||
# The image includes Clojure, Java, Babashka, and all tools from mise.toml.
|
||||
# DB connection info is forwarded from mise.local.toml.
|
||||
#
|
||||
# Usage: workmux add --agent ./bin/claude-dangerous feat/my-branch
|
||||
#
|
||||
# Pass --build as the first arg to force a rebuild of the image.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
IMAGE_NAME="metabase-claude-code"
|
||||
DOCKERFILE="$PROJECT_ROOT/dev/docker/claude-code/Dockerfile"
|
||||
|
||||
# Build image if --build passed or image doesn't exist
|
||||
if [[ "${1:-}" == "--build" ]]; then
|
||||
shift
|
||||
echo "Building $IMAGE_NAME image..."
|
||||
docker build -t "$IMAGE_NAME" -f "$DOCKERFILE" "$PROJECT_ROOT"
|
||||
elif ! docker image inspect "$IMAGE_NAME" &>/dev/null; then
|
||||
echo "Image $IMAGE_NAME not found, building..."
|
||||
docker build -t "$IMAGE_NAME" -f "$DOCKERFILE" "$PROJECT_ROOT"
|
||||
fi
|
||||
|
||||
# On macOS, extract Claude OAuth credentials from the system keychain
|
||||
# and prepare a minimal .claude.json for the container.
|
||||
AUTH_TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$AUTH_TMPDIR"' EXIT
|
||||
|
||||
AUTH_MOUNT_ARGS=()
|
||||
if [[ "$OSTYPE" == darwin* ]]; then
|
||||
KEYCHAIN_CREDS=$(security find-generic-password -s "Claude Code-credentials" -a "$USER" -w 2>/dev/null || true)
|
||||
if [[ -n "$KEYCHAIN_CREDS" ]]; then
|
||||
mkdir -p "$AUTH_TMPDIR/.claude"
|
||||
echo "$KEYCHAIN_CREDS" > "$AUTH_TMPDIR/.claude/.credentials.json"
|
||||
AUTH_MOUNT_ARGS+=(-v "$AUTH_TMPDIR/.claude/.credentials.json:/home/claude/.claude/.credentials.json")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build a minimal .claude.json with onboarding complete and project trust.
|
||||
# Mounting the host's full ~/.claude.json won't work (host-specific paths).
|
||||
python3 -c "
|
||||
import json, os
|
||||
host_config = '$HOME/.claude.json'
|
||||
config = {}
|
||||
if os.path.exists(host_config):
|
||||
with open(host_config) as f:
|
||||
full = json.load(f)
|
||||
# Copy only the auth-related keys
|
||||
for key in ('oauthAccount', 'customApiKeyResponses'):
|
||||
if key in full:
|
||||
config[key] = full[key]
|
||||
config['hasCompletedOnboarding'] = True
|
||||
config['bypassPermissionsModeAccepted'] = True
|
||||
config['dangerouslySkipPermissions'] = True
|
||||
config['autoAcceptPermissions'] = True
|
||||
config['numStartups'] = 1
|
||||
config['projects'] = {
|
||||
'/app': {
|
||||
'allowedTools': [],
|
||||
'hasTrustDialogAccepted': True,
|
||||
'skipConfirmationDialogs': True,
|
||||
}
|
||||
}
|
||||
with open('$AUTH_TMPDIR/claude.json', 'w') as f:
|
||||
json.dump(config, f, indent=2)
|
||||
"
|
||||
AUTH_MOUNT_ARGS+=(-v "$AUTH_TMPDIR/claude.json:/home/claude/.claude.json")
|
||||
|
||||
# Parse mise.local.toml for env vars (DB connection, ports, etc.)
|
||||
ENV_ARGS=()
|
||||
if [[ -f "$PROJECT_ROOT/mise.local.toml" ]]; then
|
||||
while IFS='=' read -r key value; do
|
||||
key="$(echo "$key" | xargs)"
|
||||
value="$(echo "$value" | sed 's/^[[:space:]]*"//;s/"[[:space:]]*$//')"
|
||||
if [[ -n "$key" && -n "$value" ]]; then
|
||||
# Rewrite localhost to host.docker.internal so container can reach host DBs
|
||||
if [[ "$key" == "MB_DB_CONNECTION_URI" ]]; then
|
||||
value="${value//localhost/host.docker.internal}"
|
||||
fi
|
||||
ENV_ARGS+=(-e "$key=$value")
|
||||
fi
|
||||
done < <(grep -E '^[A-Z_]+ *= *"' "$PROJECT_ROOT/mise.local.toml")
|
||||
fi
|
||||
|
||||
exec docker run -it --rm \
|
||||
-v "$PROJECT_ROOT:/app" \
|
||||
-w /app \
|
||||
-v /dev/null:/app/mise.local.toml:ro \
|
||||
"${AUTH_MOUNT_ARGS[@]}" \
|
||||
-e ANTHROPIC_API_KEY \
|
||||
"${ENV_ARGS[@]}" \
|
||||
--add-host=host.docker.internal:host-gateway \
|
||||
"$IMAGE_NAME" \
|
||||
--dangerously-skip-permissions
|
||||
11
bin/e2e-backend
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PORT="${MB_JETTY_PORT:-4000}"
|
||||
|
||||
if curl -sf "http://localhost:$PORT/api/health" > /dev/null 2>&1; then
|
||||
echo "⚠️ Backend already running on :$PORT — reusing it. If you need a different MB_EDITION, kill it first: lsof -ti:$PORT | xargs kill"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec node e2e/runner/start-backend.js
|
||||
42
bin/e2e-stress-test
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
RUNS="${E2E_STRESS_RUNS:-5}"
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: bin/e2e-stress-test [cypress args...]"
|
||||
echo " e.g. bin/e2e-stress-test --spec e2e/test/scenarios/organization/bookmarks-dashboard.cy.spec.js"
|
||||
echo ""
|
||||
echo "Set E2E_STRESS_RUNS=N to change iteration count (default: 5)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf cypress/screenshots cypress/videos
|
||||
|
||||
for i in $(seq 1 "$RUNS"); do
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Run $i / $RUNS"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
if ! CYPRESS_VIDEO=false CYPRESS_RETRIES=0 CYPRESS_GUI=false GENERATE_SNAPSHOTS=false bun test-cypress "$@"; then
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " FAILED on run $i / $RUNS"
|
||||
echo "=========================================="
|
||||
|
||||
if ls cypress/screenshots/**/*.png 1>/dev/null 2>&1; then
|
||||
echo ""
|
||||
echo "Screenshots:"
|
||||
find cypress/screenshots -name "*.png" -print
|
||||
fi
|
||||
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " All $RUNS runs passed"
|
||||
echo "=========================================="
|
||||
@@ -18,16 +18,9 @@
|
||||
This format should no longer be used for new migrations, use `id-timestamp-format-re` instead."
|
||||
#"^v\d{2,}\.\d{2}-\d{3}$")
|
||||
|
||||
(s/def ::id (s/and string?
|
||||
(s/or
|
||||
;; new ids should conform this format
|
||||
:id-with-timestamp ;; e.g: v49.2023-12-14T08:54:54
|
||||
(s/and #(re-matches id-timestamp-format-re %))
|
||||
:id-with-old-version-format ;; e.g: v49.00-008
|
||||
(s/and #(re-matches id-number-format-re %)
|
||||
;; the cut off id for this old format is v49.00-060
|
||||
;; see #36787 for context
|
||||
#(neg? (compare % "v49.00-061"))))))
|
||||
;; Basic type check only — format enforcement is file-aware and happens in
|
||||
;; `lint-migrations-file/require-change-set-ids-match-file-format`.
|
||||
(s/def ::id string?)
|
||||
|
||||
(s/def ::author string?)
|
||||
|
||||
@@ -74,54 +67,6 @@
|
||||
[]))) ; provide an empty list if dbms-val is nil
|
||||
changes))))))
|
||||
|
||||
(def change-types-supporting-rollback
|
||||
"This set was generated with a little grep and awk from the docs here:
|
||||
https://docs.liquibase.com/workflows/liquibase-community/liquibase-auto-rollback.html
|
||||
|
||||
If a new change type is introduced that supports automatic rollback, it should be added
|
||||
to this set."
|
||||
#{:addCheckConstraint
|
||||
:addColumn
|
||||
:addDefaultValue
|
||||
:addForeignKeyConstraint
|
||||
:addLookupTable
|
||||
:addNotNullConstraint
|
||||
:addPrimaryKey
|
||||
:addUniqueConstraint
|
||||
:createIndex
|
||||
:createSequence
|
||||
:createSynonym
|
||||
:createTable
|
||||
:createView
|
||||
:disableCheckConstraint
|
||||
:disableTrigger
|
||||
:dropNotNullConstraint
|
||||
:enableCheckConstraint
|
||||
:enableTrigger
|
||||
:renameColumn
|
||||
:renameSequence
|
||||
:renameTable
|
||||
:renameTrigger
|
||||
:renameView
|
||||
;; assumes all custom changes use the `def-migration` or `define-reversible-migration` in
|
||||
;; [[metabase.app-db.custom-migrations]]
|
||||
:customChange})
|
||||
|
||||
(defn- major-version
|
||||
"Returns major version from id string, e.g. 44 from \"v44.00-034\""
|
||||
[id-str]
|
||||
(when (string? id-str)
|
||||
(some-> (re-find #"\d+" id-str) Integer/parseInt)))
|
||||
|
||||
(defn- rollback-present-when-required?
|
||||
"Ensures rollback key is present when change type doesn't support auto rollback"
|
||||
[{:keys [id changes] :as change-set}]
|
||||
(or
|
||||
(int? id)
|
||||
(< (major-version id) 45)
|
||||
(some change-types-supporting-rollback (mapcat keys changes))
|
||||
(contains? change-set :rollback)))
|
||||
|
||||
(defn- disallow-delete-cascade-with-add-column
|
||||
"Returns false if addColumn changeSet uses deleteCascade. See Metabase issue #14321"
|
||||
[{:keys [changes]}]
|
||||
@@ -135,7 +80,6 @@
|
||||
|
||||
(s/def ::change-set
|
||||
(s/and
|
||||
rollback-present-when-required?
|
||||
disallow-delete-cascade-with-add-column
|
||||
(s/keys :req-un [::id ::author ::changes ::comment]
|
||||
:opt-un [::preConditions])))
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.string :as str])
|
||||
(:import
|
||||
[clojure.lang ExceptionInfo]))
|
||||
(clojure.lang ExceptionInfo)
|
||||
(java.lang Integer)))
|
||||
|
||||
#_:clj-kondo/ignore
|
||||
(set! *warn-on-reflection* true)
|
||||
@@ -52,26 +53,58 @@
|
||||
(when (seq duplicates)
|
||||
(throw (validation-error "Change set IDs are not distinct." {:duplicates duplicates})))))
|
||||
|
||||
(defn- require-change-set-ids-in-order [change-log]
|
||||
(let [ids (change-set-ids change-log)
|
||||
out-of-order-ids (->> ids
|
||||
(partition 2 1)
|
||||
(filter (fn [[id1 id2]]
|
||||
(pos? (compare id1 id2)))))]
|
||||
(defn- directory-based-migration-file?
|
||||
"Returns true if the file is a directory-based migration file (e.g., `060/20260905_mq_indexes.yaml`)."
|
||||
[file]
|
||||
(boolean (re-matches #".*\d{3}/\d{8}_[a-z0-9_]+\.yaml$" (str file))))
|
||||
|
||||
(when (seq out-of-order-ids)
|
||||
(throw (validation-error "Change set IDs are not in order"
|
||||
{:out-of-order-ids out-of-order-ids})))))
|
||||
(defn- file-version
|
||||
"Extracts the migration version number from a file.
|
||||
For per-release files like `059_update_migrations.yaml`, parses from the filename.
|
||||
For directory-based files like `060/20260106_125531.yaml`, parses from the parent directory name."
|
||||
[^java.io.File file]
|
||||
(if (directory-based-migration-file? file)
|
||||
(parse-long (re-find #"\d+" (.getName (.getParentFile file))))
|
||||
(parse-long (re-find #"\d+" (.getName file)))))
|
||||
|
||||
(defn- changeset-version+id
|
||||
"Returns [version local-id] for a changeset.
|
||||
Parses from the ID itself, which should be of the form 'v{version}.{local-id}'."
|
||||
[_file changeset-id]
|
||||
(let [id-str (str changeset-id)]
|
||||
(when-let [[_ version local-id] (re-matches #"^v(\d+)\.(.+)$" id-str)]
|
||||
[(parse-long version) local-id])))
|
||||
|
||||
(defn- changeset-at-or-after?
|
||||
"Returns true if the changeset (identified by file context and changeset ID) is at or after
|
||||
the given [threshold-version threshold-id]."
|
||||
[file changeset-id threshold-version threshold-id]
|
||||
(when-let [[v local-id] (changeset-version+id file changeset-id)]
|
||||
(or (> v threshold-version)
|
||||
(and (= v threshold-version)
|
||||
(not (neg? (compare local-id threshold-id)))))))
|
||||
|
||||
(defn- require-change-set-ids-in-order [change-log file]
|
||||
(when-not (directory-based-migration-file? file)
|
||||
(let [ids (change-set-ids change-log)
|
||||
out-of-order-ids (->> ids
|
||||
(partition 2 1)
|
||||
(filter (fn [[id1 id2]]
|
||||
(pos? (compare id1 id2)))))]
|
||||
(when (seq out-of-order-ids)
|
||||
(throw (validation-error "Change set IDs are not in order"
|
||||
{:out-of-order-ids out-of-order-ids}))))))
|
||||
|
||||
(defn- require-change-set-ids-in-correct-file [change-log file]
|
||||
(let [file-version (parse-long (re-find #"\d+" (.getName file)))
|
||||
(let [fv (file-version file)
|
||||
ids (change-set-ids change-log)
|
||||
wrong-file-ids (->> ids
|
||||
(filter (fn [id]
|
||||
(let [id-version (parse-long (re-find #"\d+" id))]
|
||||
(if (= file-version 1)
|
||||
(> id-version 55)
|
||||
(not= file-version id-version))))))]
|
||||
wrong-file-ids
|
||||
(->> ids
|
||||
(filter (fn [id]
|
||||
(let [id-version (parse-long (re-find #"\d+" id))]
|
||||
(if (= fv 1)
|
||||
(> id-version 55)
|
||||
(not= fv id-version))))))]
|
||||
(when (seq wrong-file-ids)
|
||||
(throw (validation-error "Change set IDs are in the wrong file"
|
||||
{:wrong-file-ids wrong-file-ids})))))
|
||||
@@ -101,18 +134,19 @@
|
||||
(some #(check-change-use-types? target-types %) (get-in change-set [:changeSet :changes])))
|
||||
|
||||
(defn- require-no-types-in-change-log!
|
||||
"Returns true if none of the changes in the change-log contain usage of any type specified in `target-types`.
|
||||
|
||||
`id-filter-fn` is a function that takes an ID and return true if the changeset should be checked."
|
||||
"Throws if any changeset at or after [version id] uses a type in target-types.
|
||||
When version and id are not provided, checks all changesets."
|
||||
([target-types change-log]
|
||||
(require-no-types-in-change-log! target-types change-log (constantly true)))
|
||||
([target-types change-log id-filter-fn]
|
||||
(require-no-types-in-change-log! target-types change-log nil nil nil))
|
||||
([target-types change-log file version id]
|
||||
{:pre [(set? target-types)]}
|
||||
(when-let [using-types? (->> change-log
|
||||
(filter (fn [change-set]
|
||||
(let [id (get-in change-set [:changeSet :id])]
|
||||
(and (string? id)
|
||||
(id-filter-fn id)))))
|
||||
(let [cs-id (get-in change-set [:changeSet :id])]
|
||||
(and cs-id
|
||||
(if version
|
||||
(changeset-at-or-after? file cs-id version id)
|
||||
true)))))
|
||||
(filter #(check-change-set-use-types? target-types %))
|
||||
(map #(get-in % [:changeSet :id]))
|
||||
seq)]
|
||||
@@ -132,17 +166,46 @@
|
||||
(defn require-no-bare-boolean-types
|
||||
"Ensures that no \"boolean\" type columns are added in changesets with id later than v49.00-032. From that point on,
|
||||
\"${boolean.type}\" should be used instead, so that we can consistently use `BIT(1)` for Boolean columns on MySQL."
|
||||
[change-log]
|
||||
(require-no-types-in-change-log! #{"boolean"} change-log #(pos? (compare % "v49.00-032"))))
|
||||
[change-log file]
|
||||
(require-no-types-in-change-log! #{"boolean"} change-log file 49 "00-032"))
|
||||
|
||||
(defn require-no-datetime-type
|
||||
"Ensures that no \"datetime\" or \"timestamp without time zone\".
|
||||
From that point on, \"${timestamp_type}\" should be used instead, so that all of our time related columns are tz-aware."
|
||||
[change-log]
|
||||
[change-log file]
|
||||
(require-no-types-in-change-log!
|
||||
#{"datetime" "timestamp" "timestamp without time zone"}
|
||||
change-log
|
||||
#(pos? (compare % "v49.00-000"))))
|
||||
change-log file 49 "00-000"))
|
||||
|
||||
(def ^:private directory-based-id-format-re
|
||||
"For directory-based migration files (v60+), IDs must be prefixed with the correct version.
|
||||
E.g., `v60.my-migration-name` or `v60.1`"
|
||||
#"^v\d{2,}\..+$")
|
||||
|
||||
(defn- require-change-set-ids-match-file-format
|
||||
"Enforces that changeset IDs use the correct format for the file type:
|
||||
- Directory-based files (v60+): IDs must be prefixed with v{version}. (e.g., v60.anything)
|
||||
- 001_update_migrations.yaml: any string ID is allowed (legacy file)
|
||||
- Other per-release files: IDs must match the timestamp format"
|
||||
[change-log file]
|
||||
(cond
|
||||
(directory-based-migration-file? file)
|
||||
(let [ids (change-set-ids change-log)
|
||||
bad-ids (remove #(re-matches directory-based-id-format-re %) ids)]
|
||||
(when (seq bad-ids)
|
||||
(throw (validation-error
|
||||
(format "Directory-based migration file contains IDs without v{version}. prefix: %s"
|
||||
(str/join ", " bad-ids))
|
||||
{:invalid-ids (vec bad-ids)}))))
|
||||
|
||||
(not= (file-version file) 1)
|
||||
(let [ids (change-set-ids change-log)
|
||||
bad-ids (remove #(re-matches change-set.strict/id-timestamp-format-re %) ids)]
|
||||
(when (seq bad-ids)
|
||||
(throw (validation-error
|
||||
(format "Per-release migration file contains non-timestamp ID formats: %s"
|
||||
(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."
|
||||
@@ -165,25 +228,82 @@
|
||||
(throw (validation-error "Expected exactly one key." {:keys keys})))
|
||||
(first keys)))
|
||||
|
||||
(defn- major-version
|
||||
"Returns major version from id string, e.g. 44 from \"v44.00-034\".
|
||||
For directory-based migrations (file path matches `NNN/`), extracts
|
||||
version from the file path. Otherwise parses from the id string."
|
||||
[id-str file]
|
||||
(if-let [[_ file-version] (when file (re-find #"(\d{3})[/\\]" (str file)))]
|
||||
(Integer/parseInt file-version)
|
||||
(when (string? id-str)
|
||||
(some-> (re-find #"\d+" id-str) Integer/parseInt))))
|
||||
|
||||
(def change-types-supporting-rollback
|
||||
"This set was generated with a little grep and awk from the docs here:
|
||||
https://docs.liquibase.com/workflows/liquibase-community/liquibase-auto-rollback.html
|
||||
|
||||
If a new change type is introduced that supports automatic rollback, it should be added
|
||||
to this set."
|
||||
#{:addCheckConstraint
|
||||
:addColumn
|
||||
:addDefaultValue
|
||||
:addForeignKeyConstraint
|
||||
:addLookupTable
|
||||
:addNotNullConstraint
|
||||
:addPrimaryKey
|
||||
:addUniqueConstraint
|
||||
:createIndex
|
||||
:createSequence
|
||||
:createSynonym
|
||||
:createTable
|
||||
:createView
|
||||
:disableCheckConstraint
|
||||
:disableTrigger
|
||||
:dropNotNullConstraint
|
||||
:enableCheckConstraint
|
||||
:enableTrigger
|
||||
:renameColumn
|
||||
:renameSequence
|
||||
:renameTable
|
||||
:renameTrigger
|
||||
:renameView
|
||||
;; assumes all custom changes use the `def-migration` or `define-reversible-migration` in
|
||||
;; [[metabase.app-db.custom-migrations]]
|
||||
:customChange})
|
||||
|
||||
(defn- rollback-present-when-required?
|
||||
"Ensures rollback key is present when change type doesn't support auto rollback.
|
||||
`file` is used to extract version for directory-based migrations."
|
||||
[{:keys [id changes] :as change-set} file]
|
||||
(or
|
||||
(let [v (major-version (str id) file)]
|
||||
(and v (< v 45)))
|
||||
(some change-types-supporting-rollback (mapcat keys changes))
|
||||
(contains? change-set :rollback)))
|
||||
|
||||
(defn- validate-database-change-log [change-log file]
|
||||
(when (string? change-log)
|
||||
(throw (validation-error "Expected `:databaseChangeLog` to be a map, not a string.")))
|
||||
|
||||
(require-distinct-change-set-ids change-log)
|
||||
(require-change-set-ids-in-correct-file change-log file)
|
||||
(require-change-set-ids-in-order change-log)
|
||||
(require-change-set-ids-in-order change-log file)
|
||||
(require-change-set-ids-match-file-format change-log file)
|
||||
(require-no-bare-blob-or-text-types change-log)
|
||||
(require-no-bare-boolean-types change-log)
|
||||
(require-no-datetime-type 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)
|
||||
(let [{:keys [changeSet]
|
||||
:as all} (group-by only-key change-log)]
|
||||
(when-not (set/subset? (set (keys all)) #{:property :objectQuotingStrategy :changeSet})
|
||||
(throw (validation-error "Expected exactly one of :property, :objectQuotingStrategy, or :changeSet."
|
||||
(when-not (set/subset? (set (keys all)) #{:property :objectQuotingStrategy :changeSet :logicalFilePath})
|
||||
(throw (validation-error "Expected exactly one of :property, :objectQuotingStrategy, :logicalFilePath, or :changeSet."
|
||||
{:keys (keys all)})))
|
||||
(doseq [{:keys [changeSet]} changeSet
|
||||
:when (not (s/valid? ::changeSet changeSet))]
|
||||
(throw (validation-error "Invalid change set." (s/explain-data ::changeSet changeSet))))))
|
||||
(doseq [{:keys [changeSet]} changeSet]
|
||||
(when-not (s/valid? ::changeSet changeSet)
|
||||
(throw (validation-error "Invalid change set." (s/explain-data ::changeSet changeSet))))
|
||||
(when-not (rollback-present-when-required? changeSet file)
|
||||
(throw (validation-error "Rollback is required but not present."
|
||||
{:id (:id changeSet)}))))))
|
||||
|
||||
(defn- validate-migrations [migrations file]
|
||||
(require-database-change-log! migrations)
|
||||
@@ -195,7 +315,14 @@
|
||||
(let [dir-str #?(:bb "resources/migrations" :clj "../../resources/migrations")
|
||||
dir (io/file dir-str)]
|
||||
(->> (file-seq dir)
|
||||
(filter #(and (.isFile %) (str/ends-with? (.getName %) "_update_migrations.yaml"))))))
|
||||
(filter (fn [^java.io.File f]
|
||||
(and (.isFile f)
|
||||
(or
|
||||
;; Per-release files: 059_update_migrations.yaml
|
||||
(str/ends-with? (.getName f) "_update_migrations.yaml")
|
||||
;; Directory-based files: 060/20260106_125531.yaml
|
||||
(directory-based-migration-file? f)))))
|
||||
(sort-by str))))
|
||||
|
||||
(defn- migrations [file]
|
||||
(assert (.exists file) (format "%s does not exist" file))
|
||||
@@ -207,13 +334,22 @@
|
||||
(fix-vals (yaml/parse-string
|
||||
#_:clj-kondo/ignore (slurp file)))))
|
||||
|
||||
(defn- display-name
|
||||
"Returns a human-readable name for a migration file.
|
||||
For per-release files, returns just the filename.
|
||||
For directory-based files, returns `parent/filename`."
|
||||
[^java.io.File file]
|
||||
(if (directory-based-migration-file? file)
|
||||
(str (.getName (.getParentFile file)) "/" (.getName file))
|
||||
(.getName file)))
|
||||
|
||||
(defn- validate-all []
|
||||
(doseq [file (migration-files)]
|
||||
(println "Validating" (.getName file) "...")
|
||||
(println "Validating" (display-name file) "...")
|
||||
(try
|
||||
(validate-migrations (migrations file) file)
|
||||
(catch ExceptionInfo e
|
||||
(throw (ex-info (.getMessage e) (assoc (ex-data e) :file (.getName file)) e))))))
|
||||
(throw (ex-info (.getMessage e) (assoc (ex-data e) :file (display-name file)) e))))))
|
||||
|
||||
(defn -main
|
||||
"Entry point for Clojure CLI task `lint-migrations-file`. Run it with
|
||||
|
||||
@@ -200,17 +200,19 @@
|
||||
(validate-id "v49.2024-01-01T10:30:00"))))
|
||||
|
||||
(testing "invalid date components should throw an error"
|
||||
(are [msg id]
|
||||
(thrown-with-msg?
|
||||
clojure.lang.ExceptionInfo
|
||||
#"Invalid change set\."
|
||||
(validate-id "v49.2024-30-01T10:30:00")
|
||||
msg)
|
||||
"invalid month" "v49.2024-13-01T10:30:00"
|
||||
"invalid day" "v49.2024-01-32T10:30:00"
|
||||
"invalid hour" "v49.2024-01-01T25:30:00"
|
||||
"invalid minute" "v49.2024-01-01T10:60:00"
|
||||
"invalid second" "v49.2024-01-01T10:30:60"))))
|
||||
(let [validate-id-strict (fn [id]
|
||||
(validate-file (io/file "049_update_migrations.yaml")
|
||||
(mock-change-set :id id)))]
|
||||
(are [msg id]
|
||||
(thrown-with-msg?
|
||||
clojure.lang.ExceptionInfo
|
||||
#"non-timestamp ID formats"
|
||||
(validate-id-strict id))
|
||||
"invalid month" "v49.2024-13-01T10:30:00"
|
||||
"invalid day" "v49.2024-01-32T10:30:00"
|
||||
"invalid hour" "v49.2024-01-01T25:30:00"
|
||||
"invalid minute" "v49.2024-01-01T10:60:00"
|
||||
"invalid second" "v49.2024-01-01T10:30:60")))))
|
||||
|
||||
(deftest ^:parallel validate-id-in-file-test
|
||||
(letfn [(validate-id [id file]
|
||||
@@ -285,7 +287,7 @@
|
||||
(testing "missing rollback key fails"
|
||||
(is (thrown-with-msg?
|
||||
clojure.lang.ExceptionInfo
|
||||
#"Invalid change set\."
|
||||
#"Rollback is required but not present\."
|
||||
(validate (update (mock-change-set :id "v49.2024-01-01T10:30:00" :changes [{:sql {:sql "select 1"}}])
|
||||
:changeSet dissoc :rollback)))))
|
||||
(testing "nil rollback is allowed"
|
||||
|
||||
31
bin/test-agent
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Agent-friendly test runner for Metabase backend (Clojure) tests.
|
||||
#
|
||||
# Differences from a normal `clj -X:dev:test` run:
|
||||
# - Uses HAWK_MODE=cli/ci so eftest.report.pretty is used instead of the
|
||||
# progress-bar reporter. Failures print inline as they happen.
|
||||
# - Strips ANSI escape codes from output so results are plain text.
|
||||
# - Filters Maven download noise and reflection warnings.
|
||||
#
|
||||
# Usage:
|
||||
# bin/test-agent :only '[metabase.foo-test]'
|
||||
# bin/test-agent :only '[metabase.foo-test/some-test]'
|
||||
# bin/test-agent # runs all tests (rarely what you want)
|
||||
#
|
||||
# Any extra arguments are forwarded directly to `clojure -X:dev:ee:ee-dev:test`.
|
||||
# The :test alias uses port 0 (OS-assigned random port) so parallel runs never collide.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
export HAWK_MODE=cli/ci
|
||||
|
||||
# Filter noise and strip ANSI escape sequences
|
||||
clean_output() {
|
||||
grep -v '^Downloading: ' \
|
||||
| grep -v '^Reflection warning,' \
|
||||
| grep -v '^Excluding directory' \
|
||||
| grep -v '^Looking for test namespaces in directory' \
|
||||
| sed $'s/\033[\[(][0-9;]*[A-Za-z]//g; s/\033[^[]*//g'
|
||||
}
|
||||
|
||||
exec clojure -X:dev:ee:ee-dev:test "$@" 2>&1 | clean_output
|
||||
215
bun.lock
@@ -102,7 +102,7 @@
|
||||
"hast-util-from-html": "^2.0.1",
|
||||
"hast-util-to-html": "^9.0.0",
|
||||
"history": "3",
|
||||
"html2canvas-pro": "1.5.0",
|
||||
"html2canvas-pro": "^2.0.2",
|
||||
"humanize-plus": "^1.8.1",
|
||||
"icepick": "2.4.0",
|
||||
"iframe-resizer": "~4.3.2",
|
||||
@@ -112,18 +112,16 @@
|
||||
"js-cookie": "^2.1.2",
|
||||
"js-yaml": "^4.1.1",
|
||||
"json5": "2.2.2",
|
||||
"jspdf": "^4.1.0",
|
||||
"jspdf": "^4.2.0",
|
||||
"kbar": "0.1.0-beta.45",
|
||||
"leaflet": "^1.2.0",
|
||||
"leaflet-draw": "^0.4.9",
|
||||
"leaflet.heat": "^0.2.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.orderby": "^4.6.0",
|
||||
"merge-refs": "^1.3.0",
|
||||
"mustache": "^2.3.2",
|
||||
"normalizr": "^3.0.2",
|
||||
"ora": "^8.0.1",
|
||||
"password-generator": "2.3.2",
|
||||
"prettier": "^3.3.3",
|
||||
"process": "^0.11.10",
|
||||
"prop-types": "^15.5.7",
|
||||
@@ -139,12 +137,12 @@
|
||||
"react-dnd-html5-backend": "4",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-grid-layout": "^1.4.4",
|
||||
"react-grid-layout": "^1.5.3",
|
||||
"react-innertext": "^1.1.5",
|
||||
"react-is": "^18.2.0",
|
||||
"react-markdown": "^9.0.0",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-resizable": "^3.1.3",
|
||||
"react-router": "3",
|
||||
"react-router-redux": "^4.0.8",
|
||||
"react-transition-group": "^4.4.5",
|
||||
@@ -153,7 +151,7 @@
|
||||
"redux-actions": "^2.0.1",
|
||||
"redux-auth-wrapper": "2.1.0",
|
||||
"redux-undo": "^1.1.0",
|
||||
"rehype-external-links": "^2.0.1",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"reselect": "^5.1.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
@@ -300,6 +298,7 @@
|
||||
"eslint-import-resolver-node": "^0.3.9",
|
||||
"eslint-import-resolver-webpack": "^0.13.10",
|
||||
"eslint-module-utils": "^2.12.1",
|
||||
"eslint-plugin-boundaries": "5.4.0",
|
||||
"eslint-plugin-chai-friendly": "^1.1.0",
|
||||
"eslint-plugin-cypress": "6.1.0",
|
||||
"eslint-plugin-depend": "1.4.0",
|
||||
@@ -370,7 +369,7 @@
|
||||
"react-refresh": "^0.14.2",
|
||||
"reg-cli": "^0.17.7",
|
||||
"semver": "^7.6.3",
|
||||
"shadow-cljs": "^2.28.20",
|
||||
"shadow-cljs": "^3.3.5",
|
||||
"source-map-loader": "^4.0.1",
|
||||
"storybook": "^8.6.15",
|
||||
"storybook-addon-pseudo-states": "^4.0.2",
|
||||
@@ -687,6 +686,8 @@
|
||||
|
||||
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="],
|
||||
|
||||
"@boundaries/elements": ["@boundaries/elements@1.2.0", "", { "dependencies": { "eslint-import-resolver-node": "0.3.9", "eslint-module-utils": "2.12.1", "handlebars": "4.7.8", "is-core-module": "2.16.1", "micromatch": "4.0.8" } }, "sha512-W65Gum02liMd3hmNrLmDBX1u5BmRMcunouFjLXyhxHnNY4YlK1kTxsgfflZ5XBGSnPnO0MkiUzAcoGzYrlx0RQ=="],
|
||||
|
||||
"@bundled-es-modules/cookie": ["@bundled-es-modules/cookie@2.0.1", "", { "dependencies": { "cookie": "^0.7.2" } }, "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw=="],
|
||||
|
||||
"@bundled-es-modules/statuses": ["@bundled-es-modules/statuses@1.0.1", "", { "dependencies": { "statuses": "^2.0.1" } }, "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg=="],
|
||||
@@ -1929,7 +1930,7 @@
|
||||
|
||||
"@uiw/react-codemirror": ["@uiw/react-codemirror@4.23.8", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@codemirror/commands": "^6.1.0", "@codemirror/state": "^6.1.1", "@codemirror/theme-one-dark": "^6.0.0", "@uiw/codemirror-extensions-basic-setup": "4.23.8", "codemirror": "^6.0.0" } }, "sha512-/NA5Pj4MmXkLSlmlUm4yfEmRLntrNq5TkQKBSINn7TukXQ4fc+C6Bk0U60Qa4rkvCSgwzZdQ2exyP0t0+2GtqA=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.2.0", "", {}, "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="],
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
|
||||
|
||||
@@ -2899,6 +2900,8 @@
|
||||
|
||||
"eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="],
|
||||
|
||||
"eslint-plugin-boundaries": ["eslint-plugin-boundaries@5.4.0", "", { "dependencies": { "@boundaries/elements": "1.2.0", "chalk": "4.1.2", "eslint-import-resolver-node": "0.3.9", "eslint-module-utils": "2.12.1", "micromatch": "4.0.8" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-6SQmEhXCqGrrxm9YiM24SC95CqrVi2MUOm5SDrfquceh/os8MIAvZYsDU69zvtCSb1S6UbNEmdioi1gCDc8+VQ=="],
|
||||
|
||||
"eslint-plugin-chai-friendly": ["eslint-plugin-chai-friendly@1.1.0", "", { "peerDependencies": { "eslint": ">=3.0.0" } }, "sha512-+T1rClpDdXkgBAhC16vRQMI5umiWojVqkj9oUTdpma50+uByCZM/oBfxitZiOkjMRlm725mwFfz/RVgyDRvCKA=="],
|
||||
|
||||
"eslint-plugin-cypress": ["eslint-plugin-cypress@6.1.0", "", { "dependencies": { "globals": "^17.3.0" }, "peerDependencies": { "eslint": ">=9" } }, "sha512-B8sxtNpINDxFkmsu1qKYjg70VsP8SGneEXgLcZMk1bUZcW08S+JyaiMdof1x6dmt02FgOD7YkT4wOaOD5HotJw=="],
|
||||
@@ -3215,6 +3218,8 @@
|
||||
|
||||
"hast-util-from-parse5": ["hast-util-from-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^8.0.0", "property-information": "^6.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ=="],
|
||||
|
||||
"hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="],
|
||||
|
||||
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
|
||||
|
||||
"hast-util-raw": ["hast-util-raw@9.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-PldBy71wO9Uq1kyaMch9AHIghtQvIwxBUkv823pKmkTM3oV1JxtsTNYdevMxvUHqcnOAuO65JKU2+0NOxc2ksA=="],
|
||||
@@ -3265,7 +3270,7 @@
|
||||
|
||||
"html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="],
|
||||
|
||||
"html2canvas-pro": ["html2canvas-pro@1.5.0", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-izxSphcINRwfEVV6eamsPVdhsxSYqX8n/hxzK+niVWdB+onM+aYRoVO+xgS9iMmZoUleZTWg1tJwryikib2hXg=="],
|
||||
"html2canvas-pro": ["html2canvas-pro@2.0.2", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-9G/t0XgCZWonLwL0JwI7su6NdbOPUY7Ur4Ihpp8+XMaW9ibA2nDXF181Jr6tm94k8lX6sthpaXB3XqEnsMd5Cw=="],
|
||||
|
||||
"htmlparser2": ["htmlparser2@6.1.0", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.0.0", "domutils": "^2.5.2", "entities": "^2.0.0" } }, "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A=="],
|
||||
|
||||
@@ -3497,7 +3502,7 @@
|
||||
|
||||
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
"isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
||||
|
||||
"isobject": ["isobject@3.0.1", "", {}, "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="],
|
||||
|
||||
@@ -3635,7 +3640,7 @@
|
||||
|
||||
"jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="],
|
||||
|
||||
"jspdf": ["jspdf@4.1.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { "canvg": "^3.0.11", "core-js": "^3.6.0", "dompurify": "^3.3.1", "html2canvas": "^1.0.0-rc.5" } }, "sha512-xd1d/XRkwqnsq6FP3zH1Q+Ejqn2ULIJeDZ+FTKpaabVpZREjsJKRJwuokTNgdqOU+fl55KgbvgZ1pRTSWCP2kQ=="],
|
||||
"jspdf": ["jspdf@4.2.0", "", { "dependencies": { "@babel/runtime": "^7.28.6", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { "canvg": "^3.0.11", "core-js": "^3.6.0", "dompurify": "^3.3.1", "html2canvas": "^1.0.0-rc.5" } }, "sha512-hR/hnRevAXXlrjeqU5oahOE+Ln9ORJUB5brLHHqH67A+RBQZuFr5GkbI9XQI8OUFSEezKegsi45QRpc4bGj75Q=="],
|
||||
|
||||
"jsprim": ["jsprim@2.0.2", "", { "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.4.0", "verror": "1.10.0" } }, "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ=="],
|
||||
|
||||
@@ -3669,8 +3674,6 @@
|
||||
|
||||
"leaflet-draw": ["leaflet-draw@0.4.14", "", {}, "sha512-O99KSPjyHNMDE+uD/fpdPydvQfTE8QruaG7ylEEtKCIaSSb60mCWoDdGUqGEHU9PGPu2JxbEfkGTXY9eYv7aEw=="],
|
||||
|
||||
"leaflet.heat": ["leaflet.heat@0.2.0", "", {}, "sha1-EJ2M9Ybwre5B8Fr/Ax4np3/swik="],
|
||||
|
||||
"less": ["less@4.2.2", "", { "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", "tslib": "^2.3.0" }, "optionalDependencies": { "errno": "^0.1.1", "graceful-fs": "^4.1.2", "image-size": "~0.5.0", "make-dir": "^2.1.0", "mime": "^1.4.1", "needle": "^3.1.0", "source-map": "~0.6.0" }, "bin": { "lessc": "bin/lessc" } }, "sha512-tkuLHQlvWUTeQ3doAqnHbNn8T6WX1KA8yvbKG9x4VtKtIjHsVKQZCH11zRgAfbDAXC2UNIg/K9BYAAcEzUIrNg=="],
|
||||
|
||||
"leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
|
||||
@@ -3907,7 +3910,7 @@
|
||||
|
||||
"micromark-util-types": ["micromark-util-types@2.0.0", "", {}, "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.5", "", { "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" } }, "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA=="],
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"miller-rabin": ["miller-rabin@4.0.1", "", { "dependencies": { "bn.js": "^4.0.0", "brorand": "^1.0.1" }, "bin": { "miller-rabin": "bin/miller-rabin" } }, "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA=="],
|
||||
|
||||
@@ -4023,8 +4026,6 @@
|
||||
|
||||
"node-int64": ["node-int64@0.4.0", "", {}, "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs="],
|
||||
|
||||
"node-libs-browser": ["node-libs-browser@2.2.1", "", { "dependencies": { "assert": "^1.1.1", "browserify-zlib": "^0.2.0", "buffer": "^4.3.0", "console-browserify": "^1.1.0", "constants-browserify": "^1.0.0", "crypto-browserify": "^3.11.0", "domain-browser": "^1.1.1", "events": "^3.0.0", "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", "path-browserify": "0.0.1", "process": "^0.11.10", "punycode": "^1.2.4", "querystring-es3": "^0.2.0", "readable-stream": "^2.3.3", "stream-browserify": "^2.0.1", "stream-http": "^2.7.2", "string_decoder": "^1.0.0", "timers-browserify": "^2.0.4", "tty-browserify": "0.0.0", "url": "^0.11.0", "util": "^0.11.0", "vm-browserify": "^1.0.1" } }, "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q=="],
|
||||
|
||||
"node-notifier": ["node-notifier@9.0.1", "", { "dependencies": { "growly": "^1.3.0", "is-wsl": "^2.2.0", "semver": "^7.3.2", "shellwords": "^0.1.1", "uuid": "^8.3.0", "which": "^2.0.2" } }, "sha512-fPNFIp2hF/Dq7qLDzSg4vZ0J4e9v60gJR+Qx7RbjbWqzPDdEqeVpEx5CFeDAELIl+A/woaaNn1fQ5nEVerMxJg=="],
|
||||
|
||||
"node-polyfill-webpack-plugin": ["node-polyfill-webpack-plugin@4.0.0", "", { "dependencies": { "assert": "^2.1.0", "browserify-zlib": "^0.2.0", "buffer": "^6.0.3", "console-browserify": "^1.2.0", "constants-browserify": "^1.0.0", "crypto-browserify": "^3.12.0", "domain-browser": "^5.7.0", "events": "^3.3.0", "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", "process": "^0.11.10", "punycode": "^2.3.1", "querystring-es3": "^0.2.1", "readable-stream": "^4.5.2", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "string_decoder": "^1.3.0", "timers-browserify": "^2.0.12", "tty-browserify": "^0.0.1", "type-fest": "^4.18.2", "url": "^0.11.3", "util": "^0.12.5", "vm-browserify": "^1.1.2" } }, "sha512-WLk77vLpbcpmTekRj6s6vYxk30XoyaY5MDZ4+9g8OaKoG3Ij+TjOqhpQjVUlfDZBPBgpNATDltaQkzuXSnnkwg=="],
|
||||
@@ -4167,8 +4168,6 @@
|
||||
|
||||
"pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="],
|
||||
|
||||
"password-generator": ["password-generator@2.3.2", "", { "bin": { "password-generator": "bin/password-generator" } }, "sha512-kJWUrdveSAqHCeWJWnv5vNc89hFHM5au+pvKja5+xCTxlRF3zQaecJlR6hSoOotAJtQ3otQq4/Q4iWc/TxsXhA=="],
|
||||
|
||||
"patch-console": ["patch-console@1.0.0", "", {}, "sha512-nxl9nrnLQmh64iTzMfyylSlRozL7kAXIaxw1fVcLYdyhNkJCRUzirRZTikXGJsg+hc4fqpneTK6iU2H1Q8THSA=="],
|
||||
|
||||
"patch-package": ["patch-package@8.0.0", "", { "dependencies": { "@yarnpkg/lockfile": "^1.1.0", "chalk": "^4.1.2", "ci-info": "^3.7.0", "cross-spawn": "^7.0.3", "find-yarn-workspace-root": "^2.0.0", "fs-extra": "^9.0.0", "json-stable-stringify": "^1.0.2", "klaw-sync": "^6.0.0", "minimist": "^1.2.6", "open": "^7.4.2", "rimraf": "^2.6.3", "semver": "^7.5.3", "slash": "^2.0.0", "tmp": "^0.0.33", "yaml": "^2.2.2" }, "bin": { "patch-package": "index.js" } }, "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA=="],
|
||||
@@ -4505,13 +4504,13 @@
|
||||
|
||||
"react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
||||
|
||||
"react-draggable": ["react-draggable@4.4.6", "", { "dependencies": { "clsx": "^1.1.1", "prop-types": "^15.8.1" } }, "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw=="],
|
||||
"react-draggable": ["react-draggable@4.5.0", "", { "dependencies": { "clsx": "^2.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw=="],
|
||||
|
||||
"react-dropzone": ["react-dropzone@14.2.3", "", { "dependencies": { "attr-accept": "^2.2.2", "file-selector": "^0.6.0", "prop-types": "^15.8.1" } }, "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug=="],
|
||||
|
||||
"react-fast-compare": ["react-fast-compare@2.0.4", "", {}, "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="],
|
||||
|
||||
"react-grid-layout": ["react-grid-layout@1.5.0", "", { "dependencies": { "clsx": "^2.0.0", "fast-equals": "^4.0.3", "prop-types": "^15.8.1", "react-draggable": "^4.4.5", "react-resizable": "^3.0.5", "resize-observer-polyfill": "^1.5.1" } }, "sha512-WBKX7w/LsTfI99WskSu6nX2nbJAUD7GD6nIXcwYLyPpnslojtmql2oD3I2g5C3AK8hrxIarYT8awhuDIp7iQ5w=="],
|
||||
"react-grid-layout": ["react-grid-layout@1.5.3", "", { "dependencies": { "clsx": "^2.1.1", "fast-equals": "^4.0.3", "prop-types": "^15.8.1", "react-draggable": "^4.4.6", "react-resizable": "^3.0.5", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, "sha512-KaG6IbjD6fYhagUtIvOzhftXG+ViKZjCjADe86X1KHl7C/dsBN2z0mi14nbvZKTkp0RKiil9RPcJBgq3LnoA8g=="],
|
||||
|
||||
"react-innertext": ["react-innertext@1.1.5", "", {}, "sha512-PWAqdqhxhHIv80dT9znP2KvS+hfkbRovFp4zFYHFFlOoQLRiawIic81gKb3U1wEyJZgMwgs3JoLtwryASRWP3Q=="],
|
||||
|
||||
@@ -4533,7 +4532,7 @@
|
||||
|
||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" } }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||
|
||||
"react-resizable": ["react-resizable@3.0.5", "", { "dependencies": { "prop-types": "15.x", "react-draggable": "^4.0.3" } }, "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w=="],
|
||||
"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=="],
|
||||
|
||||
@@ -4621,7 +4620,7 @@
|
||||
|
||||
"regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="],
|
||||
|
||||
"rehype-external-links": ["rehype-external-links@2.0.1", "", { "dependencies": { "@types/hast": "^2.0.0", "extend": "^3.0.0", "is-absolute-url": "^4.0.0", "space-separated-tokens": "^2.0.0", "unified": "^10.0.0", "unist-util-visit": "^4.0.0" } }, "sha512-u2dNypma+ps12SJWlS23zvbqwNx0Hl24t0YHXSM/6FCZj/pqWETCO3WyyrvALv4JYvRtuPjhiv2Lpen15ESqbA=="],
|
||||
"rehype-external-links": ["rehype-external-links@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-is-element": "^3.0.0", "is-absolute-url": "^4.0.0", "space-separated-tokens": "^2.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw=="],
|
||||
|
||||
"relateurl": ["relateurl@0.2.7", "", {}, "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk="],
|
||||
|
||||
@@ -4783,7 +4782,7 @@
|
||||
|
||||
"sha1": ["sha1@1.1.1", "", { "dependencies": { "charenc": ">= 0.0.1", "crypt": ">= 0.0.1" } }, "sha1-rdqnqTFo85PxnrKxUJFhjicA+Eg="],
|
||||
|
||||
"shadow-cljs": ["shadow-cljs@2.28.20", "", { "dependencies": { "node-libs-browser": "^2.2.1", "readline-sync": "^1.4.7", "shadow-cljs-jar": "1.3.4", "source-map-support": "^0.4.15", "which": "^1.3.1", "ws": "^7.4.6" }, "bin": { "shadow-cljs": "cli/runner.js" } }, "sha512-Q9N+aw2suSUZ6vmWhZupl0rkyjoYrnpU1WbMrNrbuUiKE6XjvT1vcBkTUrqkMJEPraV7u+H9b5d/RcKnj8nbhg=="],
|
||||
"shadow-cljs": ["shadow-cljs@3.3.6", "", { "dependencies": { "buffer": "^6.0.3", "process": "^0.11.10", "readline-sync": "^1.4.10", "shadow-cljs-jar": "1.3.4", "source-map-support": "^0.5.21", "which": "^5.0.0", "ws": "^8.18.1" }, "bin": { "shadow-cljs": "cli/runner.js" } }, "sha512-t5TBJmy77abwlkAY7UOXa1s9H/28nTHeDVckgzEVS8ONYtDvbNCHEuBYfS7wwV9wVZVybRNb7dILWNXISJL1Nw=="],
|
||||
|
||||
"shadow-cljs-jar": ["shadow-cljs-jar@1.3.4", "", {}, "sha512-cZB2pzVXBnhpJ6PQdsjO+j/MksR28mv4QD/hP/2y1fsIa9Z9RutYgh3N34FZ8Ktl4puAXaIGlct+gMCJ5BmwmA=="],
|
||||
|
||||
@@ -4849,7 +4848,7 @@
|
||||
|
||||
"source-map-loader": ["source-map-loader@4.0.1", "", { "dependencies": { "abab": "^2.0.6", "iconv-lite": "^0.6.3", "source-map-js": "^1.0.2" } }, "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA=="],
|
||||
|
||||
"source-map-support": ["source-map-support@0.4.18", "", { "dependencies": { "source-map": "^0.5.6" } }, "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA=="],
|
||||
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||
|
||||
"sourcemap-codec": ["sourcemap-codec@1.4.8", "", {}, "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="],
|
||||
|
||||
@@ -5077,8 +5076,6 @@
|
||||
|
||||
"tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="],
|
||||
|
||||
"to-arraybuffer": ["to-arraybuffer@1.0.1", "", {}, "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M="],
|
||||
|
||||
"to-buffer": ["to-buffer@1.2.1", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ=="],
|
||||
|
||||
"to-camel-case": ["to-camel-case@1.0.0", "", { "dependencies": { "to-space-case": "^1.0.0" } }, "sha512-nD8pQi5H34kyu1QDMFjzEIYqk0xa9Alt6ZfrdEMuHCFOfTLhDG5pgTu/aAM9Wt9lXILwlXmWP43b8sav0GNE8Q=="],
|
||||
@@ -5345,7 +5342,7 @@
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha1-lmRU6HZUYuN2RNNib2dCzotwll0="],
|
||||
|
||||
"which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="],
|
||||
"which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
|
||||
|
||||
"which-boxed-primitive": ["which-boxed-primitive@1.0.2", "", { "dependencies": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", "is-number-object": "^1.0.4", "is-string": "^1.0.5", "is-symbol": "^1.0.3" } }, "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg=="],
|
||||
|
||||
@@ -5375,7 +5372,7 @@
|
||||
|
||||
"write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="],
|
||||
|
||||
"ws": ["ws@7.5.10", "", {}, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
||||
"ws": ["ws@8.18.3", "", {}, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
||||
|
||||
@@ -5455,6 +5452,8 @@
|
||||
|
||||
"@babel/register/source-map-support": ["source-map-support@0.5.19", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw=="],
|
||||
|
||||
"@boundaries/elements/is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||
|
||||
"@bundled-es-modules/tough-cookie/tough-cookie": ["tough-cookie@4.1.4", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag=="],
|
||||
|
||||
"@cacheable/memory/keyv": ["keyv@5.5.3", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A=="],
|
||||
@@ -5551,8 +5550,6 @@
|
||||
|
||||
"@jest/core/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
|
||||
|
||||
"@jest/core/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"@jest/core/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="],
|
||||
|
||||
"@jest/core/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||
@@ -5575,8 +5572,6 @@
|
||||
|
||||
"@jest/transform/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
||||
|
||||
"@jest/transform/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"@jest/transform/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||
|
||||
"@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
@@ -5609,6 +5604,8 @@
|
||||
|
||||
"@loki/target-native-android-emulator/tempy": ["tempy@1.0.1", "", { "dependencies": { "del": "^6.0.0", "is-stream": "^2.0.0", "temp-dir": "^2.0.0", "type-fest": "^0.16.0", "unique-string": "^2.0.0" } }, "sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w=="],
|
||||
|
||||
"@loki/target-native-core/ws": ["ws@7.5.10", "", {}, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
||||
|
||||
"@loki/target-native-ios-simulator/tempy": ["tempy@1.0.1", "", { "dependencies": { "del": "^6.0.0", "is-stream": "^2.0.0", "temp-dir": "^2.0.0", "type-fest": "^0.16.0", "unique-string": "^2.0.0" } }, "sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w=="],
|
||||
|
||||
"@mantine/core/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
@@ -5623,8 +5620,6 @@
|
||||
|
||||
"@nicolo-ribaudo/eslint-scope-5-internals/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
|
||||
|
||||
"@parcel/watcher/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"@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=="],
|
||||
@@ -5719,8 +5714,6 @@
|
||||
|
||||
"@storybook/react-docgen-typescript-plugin/find-cache-dir": ["find-cache-dir@3.3.1", "", { "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", "pkg-dir": "^4.1.0" } }, "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ=="],
|
||||
|
||||
"@storybook/react-docgen-typescript-plugin/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"@storybook/react-docgen-typescript-plugin/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@storybook/test/@testing-library/dom": ["@testing-library/dom@10.4.0", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" } }, "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ=="],
|
||||
@@ -5985,6 +5978,8 @@
|
||||
|
||||
"chrome-remote-interface/commander": ["commander@2.11.0", "", {}, "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ=="],
|
||||
|
||||
"chrome-remote-interface/ws": ["ws@7.5.10", "", {}, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
||||
|
||||
"cipher-base/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"citty/consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||
@@ -6219,8 +6214,6 @@
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"fast-glob/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"fast-png/pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="],
|
||||
|
||||
"figures/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||
@@ -6239,8 +6232,6 @@
|
||||
|
||||
"find-test-names/acorn-walk": ["acorn-walk@8.2.0", "", {}, "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA=="],
|
||||
|
||||
"find-yarn-workspace-root/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"flat-cache/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
|
||||
|
||||
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
@@ -6275,6 +6266,8 @@
|
||||
|
||||
"global-prefix/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||
|
||||
"global-prefix/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="],
|
||||
|
||||
"globby/ignore": ["ignore@5.1.8", "", {}, "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw=="],
|
||||
|
||||
"globby/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||
@@ -6295,6 +6288,8 @@
|
||||
|
||||
"hast-util-raw/@types/unist": ["@types/unist@3.0.2", "", {}, "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="],
|
||||
|
||||
"hast-util-raw/@ungap/structured-clone": ["@ungap/structured-clone@1.2.0", "", {}, "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="],
|
||||
|
||||
"hast-util-to-html/@types/unist": ["@types/unist@3.0.2", "", {}, "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="],
|
||||
|
||||
"hast-util-to-jsx-runtime/@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
|
||||
@@ -6321,8 +6316,6 @@
|
||||
|
||||
"http-proxy-middleware/is-plain-obj": ["is-plain-obj@3.0.0", "", {}, "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA=="],
|
||||
|
||||
"http-proxy-middleware/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"img-diff-js/@types/node": ["@types/node@14.18.63", "", {}, "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="],
|
||||
|
||||
"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
@@ -6343,6 +6336,8 @@
|
||||
|
||||
"ink/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
||||
|
||||
"ink/ws": ["ws@7.5.10", "", {}, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
||||
|
||||
"inquirer-file-selector/chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="],
|
||||
|
||||
"inquirer-toggle/@inquirer/core": ["@inquirer/core@8.2.4", "", { "dependencies": { "@inquirer/figures": "^1.0.3", "@inquirer/type": "^1.3.3", "@types/mute-stream": "^0.0.4", "@types/node": "^20.14.9", "@types/wrap-ansi": "^3.0.0", "ansi-escapes": "^4.3.2", "cli-spinners": "^2.9.2", "cli-width": "^4.1.0", "mute-stream": "^1.0.0", "picocolors": "^1.0.1", "signal-exit": "^4.1.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0" } }, "sha512-7vsXSfxtrrbwMTirfaKwPcjqJy7pzeuF/bP62yo1NQrRJ5HjmMlrhZml/Ljm9ODc1RnbhJlTeSnCkjtFddKjwA=="],
|
||||
@@ -6395,8 +6390,6 @@
|
||||
|
||||
"jest-config/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||
|
||||
"jest-config/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"jest-config/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
|
||||
|
||||
"jest-config/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="],
|
||||
@@ -6411,16 +6404,12 @@
|
||||
|
||||
"jest-haste-map/jest-worker": ["jest-worker@30.2.0", "", { "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", "jest-util": "30.2.0", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" } }, "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g=="],
|
||||
|
||||
"jest-haste-map/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"jest-junit/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||
|
||||
"jest-leak-detector/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="],
|
||||
|
||||
"jest-matcher-utils/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="],
|
||||
|
||||
"jest-message-util/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"jest-message-util/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="],
|
||||
|
||||
"jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||
@@ -6467,14 +6456,14 @@
|
||||
|
||||
"jsdom/whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
|
||||
|
||||
"jsdom/ws": ["ws@8.18.3", "", {}, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"json-schema-to-ts/@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"json-stable-stringify/call-bind": ["call-bind@1.0.7", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.1" } }, "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w=="],
|
||||
|
||||
"jsonwebtoken/semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="],
|
||||
|
||||
"jspdf/@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
||||
|
||||
"jsx-ast-utils/array-includes": ["array-includes@3.1.3", "", { "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3", "es-abstract": "^1.18.0-next.2", "get-intrinsic": "^1.1.1", "is-string": "^1.0.5" } }, "sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A=="],
|
||||
|
||||
"jwa/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
@@ -6505,6 +6494,8 @@
|
||||
|
||||
"lint-staged/listr2": ["listr2@6.6.1", "", { "dependencies": { "cli-truncate": "^3.1.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^5.0.1", "rfdc": "^1.3.0", "wrap-ansi": "^8.1.0" } }, "sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg=="],
|
||||
|
||||
"lint-staged/micromatch": ["micromatch@4.0.5", "", { "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" } }, "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA=="],
|
||||
|
||||
"listr2/colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
|
||||
|
||||
"load-json-file/graceful-fs": ["graceful-fs@4.2.4", "", {}, "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw=="],
|
||||
@@ -6539,9 +6530,9 @@
|
||||
|
||||
"mdast-util-mdx-jsx/@types/unist": ["@types/unist@3.0.2", "", {}, "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="],
|
||||
|
||||
"mdast-util-to-markdown/@types/unist": ["@types/unist@3.0.2", "", {}, "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="],
|
||||
"mdast-util-to-hast/@ungap/structured-clone": ["@ungap/structured-clone@1.2.0", "", {}, "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
"mdast-util-to-markdown/@types/unist": ["@types/unist@3.0.2", "", {}, "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="],
|
||||
|
||||
"miller-rabin/bn.js": ["bn.js@4.11.9", "", {}, "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="],
|
||||
|
||||
@@ -6597,32 +6588,6 @@
|
||||
|
||||
"node-environment-flags/semver": ["semver@5.7.1", "", { "bin": { "semver": "./bin/semver" } }, "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="],
|
||||
|
||||
"node-libs-browser/assert": ["assert@1.5.0", "", { "dependencies": { "object-assign": "^4.1.1", "util": "0.10.3" } }, "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA=="],
|
||||
|
||||
"node-libs-browser/buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="],
|
||||
|
||||
"node-libs-browser/domain-browser": ["domain-browser@1.2.0", "", {}, "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA=="],
|
||||
|
||||
"node-libs-browser/events": ["events@3.2.0", "", {}, "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg=="],
|
||||
|
||||
"node-libs-browser/path-browserify": ["path-browserify@0.0.1", "", {}, "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ=="],
|
||||
|
||||
"node-libs-browser/punycode": ["punycode@1.4.1", "", {}, "sha1-wNWmOycYgArY4esPpSachN1BhF4="],
|
||||
|
||||
"node-libs-browser/readable-stream": ["readable-stream@2.3.7", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw=="],
|
||||
|
||||
"node-libs-browser/stream-browserify": ["stream-browserify@2.0.2", "", { "dependencies": { "inherits": "~2.0.1", "readable-stream": "^2.0.2" } }, "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg=="],
|
||||
|
||||
"node-libs-browser/stream-http": ["stream-http@2.8.3", "", { "dependencies": { "builtin-status-codes": "^3.0.0", "inherits": "^2.0.1", "readable-stream": "^2.3.6", "to-arraybuffer": "^1.0.0", "xtend": "^4.0.0" } }, "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw=="],
|
||||
|
||||
"node-libs-browser/timers-browserify": ["timers-browserify@2.0.11", "", { "dependencies": { "setimmediate": "^1.0.4" } }, "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ=="],
|
||||
|
||||
"node-libs-browser/tty-browserify": ["tty-browserify@0.0.0", "", {}, "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY="],
|
||||
|
||||
"node-libs-browser/url": ["url@0.11.0", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE="],
|
||||
|
||||
"node-libs-browser/util": ["util@0.11.1", "", { "dependencies": { "inherits": "2.0.3" } }, "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ=="],
|
||||
|
||||
"node-notifier/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
|
||||
"node-notifier/semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="],
|
||||
@@ -6763,6 +6728,8 @@
|
||||
|
||||
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha1-PFMZQukIwml8DsNEhYwobHygpgo="],
|
||||
|
||||
"react-devtools-core/ws": ["ws@7.5.10", "", {}, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
||||
|
||||
"react-dnd/dnd-core": ["dnd-core@4.0.5", "", { "dependencies": { "asap": "^2.0.6", "invariant": "^2.2.4", "lodash": "^4.17.10", "redux": "^4.0.0" } }, "sha512-GSyGmfGom9oyTFJ4Ll/95Dn3ZDvPkrgINwfeOd+gTI0RGIN1TcTGChrHnIHF3A3e1PymyEKZg+3ouN3w2uIJGQ=="],
|
||||
|
||||
"react-dnd/hoist-non-react-statics": ["hoist-non-react-statics@2.5.5", "", {}, "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw=="],
|
||||
@@ -6779,8 +6746,6 @@
|
||||
|
||||
"react-docgen-typescript-plugin/find-cache-dir": ["find-cache-dir@3.3.1", "", { "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", "pkg-dir": "^4.1.0" } }, "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ=="],
|
||||
|
||||
"react-docgen-typescript-plugin/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"react-draggable/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="],
|
||||
|
||||
"react-grid-layout/fast-equals": ["fast-equals@4.0.3", "", {}, "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg=="],
|
||||
@@ -6841,12 +6806,6 @@
|
||||
|
||||
"reg-cli/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"rehype-external-links/@types/hast": ["@types/hast@2.3.2", "", { "dependencies": { "@types/unist": "*" } }, "sha512-Op5W7jYgZI7AWKY5wQ0/QNMzQM7dGQPyW1rXKNiymVCy5iTfdPuGu4HhYNOM2sIv8gUfIuIdcYlXmAepwaowow=="],
|
||||
|
||||
"rehype-external-links/unified": ["unified@10.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "bail": "^2.0.0", "extend": "^3.0.0", "is-buffer": "^2.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^5.0.0" } }, "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q=="],
|
||||
|
||||
"rehype-external-links/unist-util-visit": ["unist-util-visit@4.1.1", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-n9KN3WV9k4h1DxYR1LoajgN93wpEi/7ZplVe02IoB4gH5ctI1AaF2670BLHQYbwj+pY83gFtyeySFiyMHJklrg=="],
|
||||
|
||||
"renderkid/css-select": ["css-select@4.1.3", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^5.0.0", "domhandler": "^4.2.0", "domutils": "^2.6.0", "nth-check": "^2.0.0" } }, "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA=="],
|
||||
|
||||
"resolve/is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||
@@ -6883,10 +6842,14 @@
|
||||
|
||||
"simple-websocket/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"simple-websocket/ws": ["ws@7.5.10", "", {}, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
||||
|
||||
"snake-case/tslib": ["tslib@2.2.0", "", {}, "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="],
|
||||
|
||||
"sockjs/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||
|
||||
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"spdy-transport/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"sql-formatter/get-stdin": ["get-stdin@8.0.0", "", {}, "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg=="],
|
||||
@@ -6933,8 +6896,6 @@
|
||||
|
||||
"stylelint/meow": ["meow@13.2.0", "", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="],
|
||||
|
||||
"stylelint/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"stylelint-scss/known-css-properties": ["known-css-properties@0.36.0", "", {}, "sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA=="],
|
||||
|
||||
"stylelint-scss/mdn-data": ["mdn-data@2.24.0", "", {}, "sha512-i97fklrJl03tL1tdRVw0ZfLLvuDsdb6wxL+TrJ+PKkCbLrp2PCu2+OYdCKychIUm19nSM/35S6qz7pJpnXttoA=="],
|
||||
@@ -7073,6 +7034,8 @@
|
||||
|
||||
"webpack-bundle-analyzer/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||
|
||||
"webpack-bundle-analyzer/ws": ["ws@7.5.10", "", {}, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
||||
|
||||
"webpack-cli/@discoveryjs/json-ext": ["@discoveryjs/json-ext@0.5.3", "", {}, "sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g=="],
|
||||
|
||||
"webpack-cli/colorette": ["colorette@2.0.16", "", {}, "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g=="],
|
||||
@@ -7175,6 +7138,8 @@
|
||||
|
||||
"@ferocia-oss/osnap/execa/strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="],
|
||||
|
||||
"@ferocia-oss/osnap/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"@inquirer/checkbox/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
|
||||
|
||||
"@inquirer/core/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
|
||||
@@ -7189,8 +7154,6 @@
|
||||
|
||||
"@jest/core/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
|
||||
|
||||
"@jest/core/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"@jest/core/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"@jest/core/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
@@ -7207,8 +7170,6 @@
|
||||
|
||||
"@jest/reporters/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"@jest/reporters/jest-worker/@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"@jest/reporters/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
|
||||
"@jest/source-map/@jridgewell/trace-mapping/@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
@@ -7219,8 +7180,6 @@
|
||||
|
||||
"@jest/transform/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.4.15", "", {}, "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="],
|
||||
|
||||
"@jest/transform/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"@jridgewell/gen-mapping/@jridgewell/trace-mapping/@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/gen-mapping/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.4.15", "", {}, "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="],
|
||||
@@ -7271,8 +7230,6 @@
|
||||
|
||||
"@nicolo-ribaudo/eslint-scope-5-internals/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="],
|
||||
|
||||
"@parcel/watcher/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"@redocly/cli/glob/minimatch": ["minimatch@10.2.0", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w=="],
|
||||
|
||||
"@redocly/cli/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
||||
@@ -7281,6 +7238,8 @@
|
||||
|
||||
"@rspack/cli/webpack-bundle-analyzer/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||
|
||||
"@rspack/cli/webpack-bundle-analyzer/ws": ["ws@7.5.10", "", {}, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
||||
|
||||
"@rspack/cli/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"@rspack/cli/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
@@ -7369,8 +7328,6 @@
|
||||
|
||||
"@storybook/react-docgen-typescript-plugin/find-cache-dir/make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="],
|
||||
|
||||
"@storybook/react-docgen-typescript-plugin/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"@storybook/test/@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.13", "", {}, "sha512-R305kwb5CcMDIpSHUnLyIAp7SrSPBx6F0VfQFB3M75xVMHhXJJIdePYgbPPh1o57vCHNu5QztokWUPsLjWzFqw=="],
|
||||
|
||||
"@svgr/core/cosmiconfig/js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||
@@ -7503,6 +7460,8 @@
|
||||
|
||||
"configstore/unique-string/crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"css-loader/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
|
||||
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
||||
@@ -7597,8 +7556,6 @@
|
||||
|
||||
"fast-glob/glob-parent/is-glob": ["is-glob@4.0.1", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg=="],
|
||||
|
||||
"fast-glob/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"file-entry-cache/flat-cache/flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||
|
||||
"filing-cabinet/enhanced-resolve/graceful-fs": ["graceful-fs@4.2.6", "", {}, "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ=="],
|
||||
@@ -7609,8 +7566,6 @@
|
||||
|
||||
"find-cypress-specs/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
|
||||
"find-yarn-workspace-root/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"fork-ts-checker-webpack-plugin/chokidar/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||
|
||||
"fork-ts-checker-webpack-plugin/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
@@ -7629,6 +7584,8 @@
|
||||
|
||||
"fork-ts-checker-webpack-plugin/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
|
||||
"global-prefix/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"hpack.js/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="],
|
||||
|
||||
"hpack.js/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
@@ -7639,8 +7596,6 @@
|
||||
|
||||
"htmlparser2/domutils/domelementtype": ["domelementtype@2.2.0", "", {}, "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A=="],
|
||||
|
||||
"http-proxy-middleware/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"import-jsx/make-dir/semver": ["semver@6.3.0", "", { "bin": { "semver": "./bin/semver.js" } }, "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="],
|
||||
|
||||
"ink/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
|
||||
@@ -7769,8 +7724,6 @@
|
||||
|
||||
"jest-config/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"jest-config/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"jest-config/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"jest-config/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
@@ -7785,12 +7738,8 @@
|
||||
|
||||
"jest-haste-map/anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"jest-haste-map/jest-worker/@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"jest-haste-map/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
|
||||
"jest-haste-map/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"jest-leak-detector/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"jest-leak-detector/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
@@ -7799,14 +7748,10 @@
|
||||
|
||||
"jest-matcher-utils/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
|
||||
"jest-message-util/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"jest-message-util/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"jest-message-util/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
|
||||
"jest-runner/jest-worker/@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"jest-runner/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
|
||||
"jest-runner/source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
@@ -7867,6 +7812,8 @@
|
||||
|
||||
"lint-staged/listr2/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
"lint-staged/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"log-update/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
|
||||
|
||||
"md5.js/hash-base/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
@@ -7913,26 +7860,12 @@
|
||||
|
||||
"nano-css/css-tree/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"node-libs-browser/assert/util": ["util@0.10.3", "", { "dependencies": { "inherits": "2.0.1" } }, "sha1-evsa/lCAUkZInj23/g7TeTNqwPk="],
|
||||
|
||||
"node-libs-browser/buffer/base64-js": ["base64-js@1.3.1", "", {}, "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="],
|
||||
|
||||
"node-libs-browser/buffer/ieee754": ["ieee754@1.1.13", "", {}, "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="],
|
||||
|
||||
"node-libs-browser/buffer/isarray": ["isarray@1.0.0", "", {}, "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="],
|
||||
|
||||
"node-libs-browser/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="],
|
||||
|
||||
"node-libs-browser/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"node-libs-browser/url/punycode": ["punycode@1.3.2", "", {}, "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="],
|
||||
|
||||
"node-libs-browser/util/inherits": ["inherits@2.0.3", "", {}, "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="],
|
||||
|
||||
"node-notifier/is-wsl/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||
|
||||
"node-notifier/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
|
||||
"node-notifier/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"oas-resolver/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"oas-resolver/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
@@ -8063,8 +7996,6 @@
|
||||
|
||||
"react-docgen-typescript-plugin/find-cache-dir/make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="],
|
||||
|
||||
"react-docgen-typescript-plugin/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"react-docgen/@types/babel__core/@types/babel__traverse": ["@types/babel__traverse@7.14.2", "", { "dependencies": { "@babel/types": "^7.3.0" } }, "sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA=="],
|
||||
|
||||
"read-pkg-up/find-up/path-exists": ["path-exists@2.1.0", "", { "dependencies": { "pinkie-promise": "^2.0.0" } }, "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ=="],
|
||||
@@ -8093,16 +8024,6 @@
|
||||
|
||||
"reg-cli/meow/redent": ["redent@1.0.0", "", { "dependencies": { "indent-string": "^2.1.0", "strip-indent": "^1.0.1" } }, "sha512-qtW5hKzGQZqKoh6JNSD+4lfitfPKGz42e6QwiRmPM5mmKtR0N41AbJRYu0xJi7nhOJ4WDgRkKvAk6tw4WIwR4g=="],
|
||||
|
||||
"rehype-external-links/unified/is-buffer": ["is-buffer@2.0.5", "", {}, "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="],
|
||||
|
||||
"rehype-external-links/unified/is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"rehype-external-links/unified/vfile": ["vfile@5.3.6", "", { "dependencies": { "@types/unist": "^2.0.0", "is-buffer": "^2.0.0", "unist-util-stringify-position": "^3.0.0", "vfile-message": "^3.0.0" } }, "sha512-ADBsmerdGBs2WYckrLBEmuETSPyTD4TuLxTrw0DvjirxW1ra4ZwkbzG8ndsv3Q57smvHxo677MHaQrY9yxH8cA=="],
|
||||
|
||||
"rehype-external-links/unist-util-visit/unist-util-is": ["unist-util-is@5.1.1", "", {}, "sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ=="],
|
||||
|
||||
"rehype-external-links/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.1", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-gks4baapT/kNRaWxuGkl5BIhoanZo7sC/cUT/JToSRNL1dYoXRFl75d++NkjYk4TAu2uv2Px+l8guMajogeuiw=="],
|
||||
|
||||
"renderkid/css-select/css-what": ["css-what@5.1.0", "", {}, "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw=="],
|
||||
|
||||
"renderkid/css-select/domhandler": ["domhandler@4.2.2", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w=="],
|
||||
@@ -8149,8 +8070,6 @@
|
||||
|
||||
"stylelint/globby/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||
|
||||
"stylelint/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"stylus/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"svgo/css-tree/mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
|
||||
@@ -8721,8 +8640,6 @@
|
||||
|
||||
"msw/@inquirer/confirm/@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
||||
|
||||
"node-libs-browser/assert/util/inherits": ["inherits@2.0.1", "", {}, "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="],
|
||||
|
||||
"node-notifier/semver/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||
|
||||
"object-is/call-bind/get-intrinsic/has-proto": ["has-proto@1.0.1", "", {}, "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg=="],
|
||||
@@ -8839,10 +8756,6 @@
|
||||
|
||||
"reg-cli/meow/redent/strip-indent": ["strip-indent@1.0.1", "", { "dependencies": { "get-stdin": "^4.0.1" }, "bin": { "strip-indent": "cli.js" } }, "sha512-I5iQq6aFMM62fBEAIB/hXzwJD6EEZ0xEGCX2t7oXqaKPIRgt4WruAQ285BISgdkP+HLGWyeGmNJcpIwFeRYRUA=="],
|
||||
|
||||
"rehype-external-links/unified/vfile/unist-util-stringify-position": ["unist-util-stringify-position@3.0.2", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-7A6eiDCs9UtjcwZOcCpM4aPII3bAAGv13E96IkawkOAW0OhH+yRxtY0lzo8KiHpzEMfH7Q+FizUmwp8Iqy5EWg=="],
|
||||
|
||||
"rehype-external-links/unified/vfile/vfile-message": ["vfile-message@3.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^3.0.0" } }, "sha512-0yaU+rj2gKAyEk12ffdSbBfjnnj+b1zqTBv3OQCTn8yEB02bsPizwdBPrLJjHnK+cU9EMMcUnNv938XcZIkmdA=="],
|
||||
|
||||
"renderkid/css-select/domhandler/domelementtype": ["domelementtype@2.2.0", "", {}, "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A=="],
|
||||
|
||||
"renderkid/css-select/domutils/dom-serializer": ["dom-serializer@1.3.2", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig=="],
|
||||
|
||||
55
cross-version/README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Cross-Version Migration Testing
|
||||
|
||||
Tests Metabase database migrations across version upgrades and downgrades.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Test upgrade (OSS)
|
||||
./test.sh --source v0.58.6 --target v0.58.7
|
||||
|
||||
# Test downgrade (OSS)
|
||||
./test.sh --source v0.58.7 --target v0.58.6
|
||||
|
||||
# Test Enterprise edition (use v1.x.x)
|
||||
./test.sh --source v1.58.6 --target v1.58.7
|
||||
```
|
||||
|
||||
Edition is inferred from version prefix: `v0.x.x` = OSS, `v1.x.x` = EE.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Docker
|
||||
- Docker Compose v2
|
||||
- Bun (for version helpers CLI)
|
||||
|
||||
## How It Works
|
||||
|
||||
### Upgrade Test
|
||||
1. Start PostgreSQL container
|
||||
2. Start SOURCE Metabase version, wait for healthy
|
||||
3. Stop SOURCE container (PostgreSQL persists)
|
||||
4. Start TARGET version - migrations run automatically
|
||||
5. Verify health check passes
|
||||
|
||||
### Downgrade Test
|
||||
1. Start PostgreSQL container
|
||||
2. Start SOURCE (newer) Metabase version, wait for healthy
|
||||
3. Stop SOURCE container
|
||||
4. Start TARGET (older) version - expects refusal
|
||||
5. Run `migrate down` command
|
||||
6. Start TARGET version again
|
||||
7. Verify health check passes
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `METABASE_PORT` | 3000 | Port to expose Metabase |
|
||||
| `HEALTH_TIMEOUT` | 120 | Seconds to wait for health check |
|
||||
|
||||
## CI
|
||||
|
||||
The GitHub workflow (`.github/workflows/cross-version.yml`) supports:
|
||||
- **Manual trigger**: Specify source/target versions
|
||||
- **Nightly schedule**: Runs predefined version matrix
|
||||
19
cross-version/cli.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bun
|
||||
import {
|
||||
compareVersions,
|
||||
getDockerImage,
|
||||
getMajorVersion,
|
||||
} from "../release/src/cross-version-helpers";
|
||||
|
||||
const [cmd, ...args] = process.argv.slice(2);
|
||||
|
||||
if (cmd === "compare") {
|
||||
console.log(compareVersions(args[0], args[1]));
|
||||
} else if (cmd === "image") {
|
||||
console.log(getDockerImage(args[0]));
|
||||
} else if (cmd === "major") {
|
||||
console.log(getMajorVersion(args[0]));
|
||||
} else {
|
||||
console.error("Usage: cli.ts <compare|image|major> [args]");
|
||||
process.exit(1);
|
||||
}
|
||||
32
cross-version/docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
environment:
|
||||
POSTGRES_USER: metabase
|
||||
POSTGRES_PASSWORD: metabase
|
||||
POSTGRES_DB: metabase
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U metabase"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
metabase:
|
||||
image: ${METABASE_IMAGE:-metabase/metabase:latest}
|
||||
ports:
|
||||
- "${METABASE_PORT:-3000}:3000"
|
||||
environment:
|
||||
MB_DB_TYPE: postgres
|
||||
MB_DB_DBNAME: metabase
|
||||
MB_DB_PORT: 5432
|
||||
MB_DB_USER: metabase
|
||||
MB_DB_PASS: metabase
|
||||
MB_DB_HOST: postgres
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
383
cross-version/test.sh
Executable file
@@ -0,0 +1,383 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Cross-version migration testing for Metabase
|
||||
#
|
||||
# Usage:
|
||||
# ./test.sh --source v0.58.6 --target v0.58.7
|
||||
#
|
||||
# Edition (OSS/EE) is inferred from version prefix (v0.x = OSS, v1.x = EE).
|
||||
# Direction (upgrade/downgrade) is inferred from version comparison.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Defaults
|
||||
SOURCE_VERSION=""
|
||||
TARGET_VERSION=""
|
||||
METABASE_PORT="${METABASE_PORT:-3000}"
|
||||
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-120}"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log() { echo -e "${GREEN}[cross-version]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[cross-version]${NC} $*"; }
|
||||
error() { echo -e "${RED}[cross-version]${NC} $*" >&2; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 --source VERSION --target VERSION
|
||||
|
||||
Options:
|
||||
--source VERSION Source version (e.g., v0.58.6 for OSS, v1.58.6 for EE)
|
||||
--target VERSION Target version (e.g., v0.58.7 for OSS, v1.58.7 for EE)
|
||||
--help Show this help
|
||||
|
||||
Examples:
|
||||
$0 --source v0.58.6 --target v0.58.7 # Upgrade test (OSS)
|
||||
$0 --source v0.58.7 --target v0.58.6 # Downgrade test (OSS)
|
||||
$0 --source v1.58.6 --target v1.58.7 # Upgrade test (EE)
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--source) SOURCE_VERSION="$2"; shift 2 ;;
|
||||
--target) TARGET_VERSION="$2"; shift 2 ;;
|
||||
--help) usage ;;
|
||||
*) error "Unknown option: $1"; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$SOURCE_VERSION" || -z "$TARGET_VERSION" ]]; then
|
||||
error "Both --source and --target are required"
|
||||
usage
|
||||
fi
|
||||
|
||||
# CLI wrapper for version helpers (uses tested TypeScript implementation)
|
||||
cli() {
|
||||
bun "$SCRIPT_DIR/cli.ts" "$@"
|
||||
}
|
||||
|
||||
wait_for_health() {
|
||||
local timeout="$1"
|
||||
local start_time=$(date +%s)
|
||||
|
||||
log "Waiting for Metabase to be healthy (timeout: ${timeout}s)..."
|
||||
|
||||
while true; do
|
||||
local current_time=$(date +%s)
|
||||
local elapsed=$((current_time - start_time))
|
||||
|
||||
if (( elapsed >= timeout )); then
|
||||
error "Health check timed out after ${timeout}s"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if curl -sf "http://localhost:${METABASE_PORT}/api/health" > /dev/null 2>&1; then
|
||||
local response=$(curl -sf "http://localhost:${METABASE_PORT}/api/health")
|
||||
if [[ "$response" == '{"status":"ok"}' ]]; then
|
||||
log "Health check passed after ${elapsed}s"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
done
|
||||
}
|
||||
|
||||
# Start Metabase with a specific image
|
||||
start_metabase() {
|
||||
local image="$1"
|
||||
log "Starting Metabase with image: $image"
|
||||
|
||||
METABASE_IMAGE="$image" METABASE_PORT="$METABASE_PORT" \
|
||||
docker compose up -d --quiet-pull
|
||||
}
|
||||
|
||||
# Stop Metabase (but keep postgres)
|
||||
stop_metabase() {
|
||||
log "Stopping Metabase container..."
|
||||
docker compose stop metabase
|
||||
docker compose rm -f metabase
|
||||
}
|
||||
|
||||
# Get edition prefix from version (0 for OSS, 1 for EE)
|
||||
get_edition_prefix() {
|
||||
local version="$1"
|
||||
if [[ "$version" == "HEAD" ]]; then
|
||||
echo "1" # HEAD is EE
|
||||
elif [[ "$version" =~ ^v([01])\. ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
else
|
||||
error "Cannot determine edition from version: $version"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# https://www.metabase.com/docs/latest/installation-and-operation/upgrading-metabase#using-the-migrate-down-command
|
||||
# Run a single migrate down step (rolls back exactly one major version)
|
||||
# This function is idempotent - it takes a version string and handles everything internally.
|
||||
migrate_down_step() {
|
||||
local version="$1"
|
||||
local image is_head="false"
|
||||
|
||||
# Resolve image from version
|
||||
image=$(cli image "$version") || return 1
|
||||
[[ "$version" == "HEAD" ]] && is_head="true"
|
||||
|
||||
log "Running 'migrate down' for version: $version (image: $image)"
|
||||
|
||||
local output
|
||||
local exit_code=0
|
||||
|
||||
# The HEAD Docker image has version "vUNKNOWN" in its version.properties,
|
||||
# which causes `migrate down` to fail with a NullPointerException when trying
|
||||
# to determine the current major version. To work around this, we extract the
|
||||
# JAR, replace version.properties with the upcoming version, and mount the
|
||||
# modified JAR.
|
||||
#
|
||||
# HEAD is the next major version (CURRENT_VERSION + 1).
|
||||
#
|
||||
# See DEV-1636 for a potential backend fix that would make this unnecessary.
|
||||
if [[ "$is_head" == "true" ]]; then
|
||||
if [[ -z "$CURRENT_VERSION" ]]; then
|
||||
error "CURRENT_VERSION must be set when testing HEAD downgrades"
|
||||
return 1
|
||||
fi
|
||||
local next_version=$((CURRENT_VERSION + 1))
|
||||
local upcoming_version="v1.${next_version}.0"
|
||||
local work_dir="/tmp/metabase-head-jar"
|
||||
local modified_jar="$work_dir/metabase.jar"
|
||||
|
||||
log "HEAD JAR needs patching (version.properties says 'vUNKNOWN', setting to $upcoming_version)"
|
||||
rm -rf "$work_dir"
|
||||
mkdir -p "$work_dir"
|
||||
|
||||
# Extract JAR from container (copy to host with proper permissions)
|
||||
docker create --name metabase-jar-extract "$image" >/dev/null
|
||||
docker cp metabase-jar-extract:/app/metabase.jar "$modified_jar"
|
||||
docker rm metabase-jar-extract >/dev/null
|
||||
|
||||
# Create version.properties with upcoming version and update the JAR
|
||||
echo "tag=$upcoming_version" > "$work_dir/version.properties"
|
||||
echo "hash=HEAD" >> "$work_dir/version.properties"
|
||||
echo "date=$(date +%Y-%m-%d)" >> "$work_dir/version.properties"
|
||||
(cd "$work_dir" && zip -u metabase.jar version.properties >/dev/null)
|
||||
|
||||
log "HEAD JAR patched - proceeding with migrate down"
|
||||
|
||||
output=$(METABASE_IMAGE="$image" docker compose run --rm --quiet-pull \
|
||||
-v "$modified_jar:/app/metabase.jar:ro" \
|
||||
metabase "migrate down" 2>&1) || exit_code=$?
|
||||
else
|
||||
output=$(METABASE_IMAGE="$image" docker compose run --rm --quiet-pull metabase "migrate down" 2>&1) || exit_code=$?
|
||||
fi
|
||||
|
||||
# Always print output for visibility
|
||||
echo "$output"
|
||||
|
||||
# Check for errors - Metabase returns 0 even on partial rollback failures
|
||||
if [[ $exit_code -ne 0 ]]; then
|
||||
error "migrate down failed with exit code $exit_code"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$output" | grep -q "ERROR.*liquibase\|RollbackFailedException\|Command failed with exception"; then
|
||||
error "migrate down encountered errors (check logs above)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Cascading migrate down: rolls back multiple major versions one at a time
|
||||
# Metabase only supports rolling back one major version per `migrate down` call,
|
||||
# so we need to iterate through intermediate versions.
|
||||
cascading_migrate_down() {
|
||||
local source_version="$1"
|
||||
local target_version="$2"
|
||||
|
||||
local source_major target_major edition_prefix
|
||||
source_major=$(cli major "$source_version") || return 1
|
||||
target_major=$(cli major "$target_version") || return 1
|
||||
edition_prefix=$(get_edition_prefix "$source_version") || return 1
|
||||
|
||||
local steps=$((source_major - target_major))
|
||||
log "Cascading migrate down: v$source_major → v$target_major ($steps step(s))"
|
||||
|
||||
# Build version sequence: source version first, then intermediate rolling versions
|
||||
local versions=("$source_version")
|
||||
local current=$((source_major - 1))
|
||||
while (( current > target_major )); do
|
||||
versions+=("v${edition_prefix}.${current}.x")
|
||||
((current--))
|
||||
done
|
||||
|
||||
# Simple loop - each step handles its own image resolution
|
||||
local step=1
|
||||
for version in "${versions[@]}"; do
|
||||
log "--------------------------------------------"
|
||||
log "Cascade step $step/$steps: $version"
|
||||
log "--------------------------------------------"
|
||||
if ! migrate_down_step "$version"; then
|
||||
error "Migrate down failed at step $step ($version)"
|
||||
return 1
|
||||
fi
|
||||
((step++))
|
||||
done
|
||||
|
||||
log "--------------------------------------------"
|
||||
log "Cascading migrate down complete"
|
||||
}
|
||||
|
||||
# Check if Metabase refuses to start (downgrade detection)
|
||||
check_downgrade_refused() {
|
||||
local timeout=60
|
||||
local start_time=$(date +%s)
|
||||
|
||||
log "Checking if Metabase detects downgrade and refuses to start..."
|
||||
|
||||
while true; do
|
||||
local current_time=$(date +%s)
|
||||
local elapsed=$((current_time - start_time))
|
||||
|
||||
if (( elapsed >= timeout )); then
|
||||
error "Timed out waiting for downgrade detection"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check container logs for downgrade error
|
||||
local logs=$(docker compose logs metabase 2>&1 || true)
|
||||
|
||||
if echo "$logs" | grep -qi "migrate.*down\|downgrade\|database.*newer"; then
|
||||
log "Metabase correctly detected downgrade scenario"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local status=$(docker compose ps metabase --format '{{.Status}}' 2>/dev/null || echo "")
|
||||
if [[ "$status" =~ [Ee]xit ]]; then
|
||||
log "Metabase container exited (expected for downgrade)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
done
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
log "Cleaning up..."
|
||||
docker compose down -v --remove-orphans 2>/dev/null || true
|
||||
}
|
||||
|
||||
check_image_exists() {
|
||||
local image="$1"
|
||||
if ! docker manifest inspect "$image" > /dev/null 2>&1; then
|
||||
error "Docker image not found: $image"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
local direction source_image target_image
|
||||
|
||||
direction=$(cli compare "$SOURCE_VERSION" "$TARGET_VERSION") || exit 1
|
||||
source_image=$(cli image "$SOURCE_VERSION") || exit 1
|
||||
target_image=$(cli image "$TARGET_VERSION") || exit 1
|
||||
|
||||
if [[ "$direction" == "same" ]]; then
|
||||
error "Source and target versions are the same"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Checking if Docker images exist..."
|
||||
check_image_exists "$source_image"
|
||||
check_image_exists "$target_image"
|
||||
|
||||
log "============================================"
|
||||
log "Cross-Version Migration Test"
|
||||
log "============================================"
|
||||
log "Direction: $direction"
|
||||
log "Source: $source_image"
|
||||
log "Target: $target_image"
|
||||
log "============================================"
|
||||
|
||||
# Ensure clean state
|
||||
cleanup
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
log ""
|
||||
log "Step 1: Starting SOURCE version ($SOURCE_VERSION)..."
|
||||
start_metabase "$source_image"
|
||||
|
||||
if ! wait_for_health "$HEALTH_TIMEOUT"; then
|
||||
error "❌ SOURCE version ($SOURCE_VERSION) failed health check"
|
||||
docker compose logs metabase
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "✅ SOURCE version ($SOURCE_VERSION) is healthy"
|
||||
|
||||
log ""
|
||||
log "Step 2: Stopping SOURCE version ($SOURCE_VERSION)..."
|
||||
stop_metabase
|
||||
|
||||
log ""
|
||||
log "Step 3: Starting TARGET version ($TARGET_VERSION)..."
|
||||
start_metabase "$target_image"
|
||||
|
||||
if [[ "$direction" == "upgrade" ]]; then
|
||||
# Upgrade: should migrate automatically and become healthy
|
||||
if ! wait_for_health "$HEALTH_TIMEOUT"; then
|
||||
error "❌ TARGET version ($TARGET_VERSION) failed health check after upgrade"
|
||||
docker compose logs metabase
|
||||
exit 1
|
||||
fi
|
||||
log "✅ UPGRADE successful - TARGET version ($TARGET_VERSION) is healthy"
|
||||
|
||||
else
|
||||
# Downgrade: should refuse to start, then we run migrate down
|
||||
if ! check_downgrade_refused; then
|
||||
error "❌ TARGET version ($TARGET_VERSION) did not properly detect downgrade"
|
||||
docker compose logs metabase
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stop the failed container
|
||||
stop_metabase
|
||||
|
||||
log ""
|
||||
log "============================================"
|
||||
log "Step 4: Rolling back database ($SOURCE_VERSION → $TARGET_VERSION)..."
|
||||
log "============================================"
|
||||
if ! cascading_migrate_down "$SOURCE_VERSION" "$TARGET_VERSION"; then
|
||||
error "❌ migrate down failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Try starting again
|
||||
log ""
|
||||
log "Step 5: Starting TARGET version ($TARGET_VERSION) after migrate down..."
|
||||
start_metabase "$target_image"
|
||||
|
||||
if ! wait_for_health "$HEALTH_TIMEOUT"; then
|
||||
error "❌ TARGET version ($TARGET_VERSION) failed health check after migrate down"
|
||||
docker compose logs metabase
|
||||
exit 1
|
||||
fi
|
||||
log "✅ DOWNGRADE successful - TARGET version ($TARGET_VERSION) is healthy after migrate down"
|
||||
fi
|
||||
|
||||
log ""
|
||||
log "============================================"
|
||||
log "✅ TEST PASSED"
|
||||
log "============================================"
|
||||
}
|
||||
|
||||
main
|
||||
65
deps.edn
@@ -47,13 +47,15 @@
|
||||
com.github.seancorfield/honeysql {:mvn/version "2.7.1350"} ; Honey SQL 2. SQL generation from Clojure data maps
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.1070"} ; Talk to JDBC DBs
|
||||
com.github.steffan-westcott/clj-otel-api {:mvn/version "0.2.8"} ; Telemetry library
|
||||
com.github.steffan-westcott/clj-otel-exporter-otlp {:mvn/version "0.2.8"} ; OTLP span exporter (gRPC and HTTP)
|
||||
com.github.steffan-westcott/clj-otel-sdk {:mvn/version "0.2.8"} ; OpenTelemetry SDK for programmatic configuration
|
||||
com.github.vertical-blank/sql-formatter {:mvn/version "2.0.5"} ; Java SQL formatting library https://github.com/vertical-blank/sql-formatter
|
||||
com.google.guava/guava {:mvn/version "33.5.0-jre"} ; dep for BigQuery, Spark, and GA. Require here rather than letting different dep versions stomp on each other — see comments on #9697
|
||||
com.h2database/h2 ^:antq/exclude ; https://metaboat.slack.com/archives/CKZEMT1MJ/p1727191010259979
|
||||
{:mvn/version "2.1.214"} ; embedded SQL database
|
||||
com.ibm.icu/icu4j {:mvn/version "78.1"} ; Used to transliterate Unicode characters into Latin characters
|
||||
com.knuddels/jtokkit {:mvn/version "1.1.0"} ; Java tokenizer library for OpenAI models, used for token counting in semantic search
|
||||
com.mchange/c3p0 {:mvn/version "0.11.2"} ; DB connection pool
|
||||
com.mchange/c3p0 {:mvn/version "0.12.0"} ; DB connection pool
|
||||
com.snowplowanalytics/snowplow-java-tracker ^:antq/exclude ; 2+ depend on apache httpclient 5.x, but saml and clj-http depend on 4.x
|
||||
{:mvn/version "1.0.1"} ; Snowplow analytics
|
||||
com.sun.mail/jakarta.mail ^:antq/exclude ; 2+ changes imports from javax.mail to jakarta.mail
|
||||
@@ -80,7 +82,7 @@
|
||||
hiccup/hiccup {:mvn/version "2.0.0"} ; HTML templating
|
||||
inflections/inflections {:mvn/version "0.15.0"} ; Clojure/Script library used for prularizing words
|
||||
instaparse/instaparse {:mvn/version "1.5.0"} ; Make your own parser
|
||||
io.github.camsaul/toucan2 {:mvn/version "1.0.570"}
|
||||
io.github.camsaul/toucan2 {:mvn/version "1.0.571"}
|
||||
io.github.eerohele/pp {#_#_:git/tag "2024-11-13.77" ; super fast pretty-printing library
|
||||
:git/sha "ffc4edc482c057cd9412c62f018a41e9140c618b"
|
||||
:git/url "https://github.com/eerohele/pp"}
|
||||
@@ -111,14 +113,17 @@
|
||||
net.sf.cssbox/cssbox {:mvn/version "5.0.2" ; HTML / CSS rendering
|
||||
:exclusions [org.slf4j/slf4j-api
|
||||
junit/junit]}
|
||||
net.thisptr/jackson-jq {:mvn/version "1.6.0"} ; Java implementation of the JQ json query language
|
||||
org.apache.commons/commons-compress {:mvn/version "1.28.0"} ; compression utils
|
||||
org.apache.commons/commons-lang3 {:mvn/version "3.19.0"} ; helper methods for working with java.lang stuff
|
||||
org.apache.logging.log4j/log4j-1.2-api {:mvn/version "2.25.3"} ; apache logging framework
|
||||
org.apache.logging.log4j/log4j-api {:mvn/version "2.25.3"} ; add compatibility with log4j 1.2
|
||||
org.apache.logging.log4j/log4j-core {:mvn/version "2.25.3"} ; apache logging framework
|
||||
org.apache.logging.log4j/log4j-jcl {:mvn/version "2.25.3"} ; allows the commons-logging API to work with log4j 2
|
||||
org.apache.logging.log4j/log4j-jul {:mvn/version "2.25.3"} ; java.util.logging (JUL) -> Log4j2 adapter
|
||||
net.thisptr/jackson-jq {:mvn/version "1.6.0"} ; Java implementation of the JQ json query language
|
||||
org.apache.commons/commons-compress {:mvn/version "1.28.0"} ; compression utils
|
||||
org.apache.commons/commons-lang3 {:mvn/version "3.19.0"} ; helper methods for working with java.lang stuff
|
||||
org.apache.httpcomponents.client5/httpclient5 {:mvn/version "5.4.4"} ; pinned: ClickHouse JDBC 0.9.7 needs httpcore5 5.3.4+; must pair with httpclient5 5.4.x
|
||||
org.apache.httpcomponents.core5/httpcore5 {:mvn/version "5.3.4"} ; pinned: ClickHouse JDBC 0.9.7 requires URIBuilder.optimize() added in 5.3
|
||||
org.apache.httpcomponents.core5/httpcore5-h2 {:mvn/version "5.3.4"} ; pinned: must match httpcore5 version
|
||||
org.apache.logging.log4j/log4j-1.2-api {:mvn/version "2.25.3"} ; apache logging framework
|
||||
org.apache.logging.log4j/log4j-api {:mvn/version "2.25.3"} ; add compatibility with log4j 1.2
|
||||
org.apache.logging.log4j/log4j-core {:mvn/version "2.25.3"} ; apache logging framework
|
||||
org.apache.logging.log4j/log4j-jcl {:mvn/version "2.25.3"} ; allows the commons-logging API to work with log4j 2
|
||||
org.apache.logging.log4j/log4j-jul {:mvn/version "2.25.3"} ; java.util.logging (JUL) -> Log4j2 adapter
|
||||
org.apache.logging.log4j/log4j-layout-template-json
|
||||
{:mvn/version "2.25.2"} ; allows the custom json logging format
|
||||
org.apache.logging.log4j/log4j-slf4j2-impl
|
||||
@@ -157,10 +162,10 @@
|
||||
org.clojure/tools.namespace {:mvn/version "1.5.0"}
|
||||
org.clojure/tools.reader {:mvn/version "1.5.2"}
|
||||
org.clojure/tools.trace {:mvn/version "0.8.0"} ; function tracing
|
||||
org.eclipse.jetty/jetty-server {:mvn/version "12.1.3"}
|
||||
org.eclipse.jetty/jetty-unixdomain-server {:mvn/version "12.1.3"}
|
||||
org.eclipse.jetty.ee9/jetty-ee9-servlet {:mvn/version "12.1.3"}
|
||||
org.eclipse.jetty.ee9.websocket/jetty-ee9-websocket-jetty-server {:mvn/version "12.1.3"}
|
||||
org.eclipse.jetty/jetty-server {:mvn/version "12.1.7"}
|
||||
org.eclipse.jetty/jetty-unixdomain-server {:mvn/version "12.1.7"}
|
||||
org.eclipse.jetty.ee9/jetty-ee9-servlet {:mvn/version "12.1.7"}
|
||||
org.eclipse.jetty.ee9.websocket/jetty-ee9-websocket-jetty-server {:mvn/version "12.1.7"}
|
||||
org.eclipse.jgit/org.eclipse.jgit {:mvn/version "7.3.0.202506031305-r"}; Java implementation of Git version control system
|
||||
org.flatland/ordered {:mvn/version "1.15.12"} ; ordered maps & sets
|
||||
org.graalvm.polyglot/js-community {:mvn/version "25.0.1" :extension "pom"} ; JavaScript engine
|
||||
@@ -327,9 +332,9 @@
|
||||
"-Dmb.jetty.join=false"
|
||||
"-Dmb.field.filter.operators.enabled=true"
|
||||
"-Dmb.api.key=test-api-key"
|
||||
;; Different port from normal `:dev` so you can run tests on a different server.
|
||||
;; TODO -- figure out how to do a random port like in the old project.clj?
|
||||
"-Dmb.jetty.port=3001"]}
|
||||
;; Port 0 tells the OS to assign a random free port, allowing parallel test runs.
|
||||
;; The actual port is read from the running Jetty server via server.instance/server-port.
|
||||
"-Dmb.jetty.port=0"]}
|
||||
|
||||
;; run the dev server with
|
||||
;; clojure -M:run
|
||||
@@ -445,9 +450,10 @@
|
||||
com.lambdaisland/glogi {:mvn/version "1.3.169"}
|
||||
io.github.metabase/hawk {:mvn/version "1.0.13"}
|
||||
org.clojars.mmb90/cljs-cache {:mvn/version "0.1.4"}
|
||||
org.clojure/clojurescript {:mvn/version "1.12.134"}
|
||||
refactor-nrepl/refactor-nrepl {:mvn/version "3.11.0"}
|
||||
;; keep it in sync with package.json
|
||||
thheller/shadow-cljs {:mvn/version "3.2.1"}}}
|
||||
thheller/shadow-cljs {:mvn/version "3.3.5"}}}
|
||||
|
||||
;; for local dev -- include the drivers locally with :dev:drivers
|
||||
:drivers
|
||||
@@ -631,6 +637,29 @@
|
||||
;; and a simple reproduction repo can be found here https://github.com/metabase/snowplow-eastwood-issue
|
||||
:exclude-namespaces [metabase.analytics.snowplow]}}
|
||||
|
||||
;; Test counterpart of :eastwood. Starts with :reflection only; add more linters as we fix other eastwood
|
||||
;; issues in tests.
|
||||
;;
|
||||
;; clojure -X:dev:ee:ee-dev:drivers:drivers-dev:test:eastwood/test
|
||||
:eastwood/test
|
||||
{:exec-fn metabase.linters.eastwood/eastwood
|
||||
:exec-args {:source-paths ["test"
|
||||
"enterprise/backend/test"]
|
||||
:linters [:reflection]
|
||||
:exclude-namespaces
|
||||
[;; byte/1 dispatch value trips up Eastwood's reader
|
||||
metabase.test.http-client
|
||||
;; Cookie/.getName qualified method trips up Eastwood's analyzer
|
||||
metabase.server.middleware.settings-cache-test
|
||||
;; .cljc syntax error during Eastwood's analyze+eval phase
|
||||
metabase.lib.query-test
|
||||
;; "count not supported on this type: Symbol" in Eastwood's analyzer
|
||||
metabase.query-processor.parameters.temporal-units-test
|
||||
;; Syntax error compiling fn* in Eastwood's analyzer
|
||||
metabase.sql-tools.macaw.references-test
|
||||
;; Method too large for Eastwood's analyzer
|
||||
metabase.sql-tools.sqlglot.references-test]}}
|
||||
|
||||
;; clojure -T:whitespace-linter
|
||||
:whitespace-linter
|
||||
{:deps {com.github.camsaul/whitespace-linter {:sha "e35bc252ccf5cc74f7d543ef95ad8a3e5131f25b"}}
|
||||
|
||||
38
dev/docker/claude-code/Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
||||
FROM ubuntu:noble
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl git rlwrap bash ca-certificates gnupg ripgrep sudo \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create non-root user (--dangerously-skip-permissions refuses to run as root)
|
||||
RUN useradd -m -s /bin/bash claude
|
||||
RUN echo 'claude ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
USER claude
|
||||
|
||||
# Install mise
|
||||
RUN curl https://mise.run | sh
|
||||
ENV PATH="/home/claude/.local/bin:$PATH"
|
||||
|
||||
# Copy project mise.toml to install the same tool versions
|
||||
COPY --chown=claude:claude mise.toml /home/claude/.config/mise/config.toml
|
||||
|
||||
# Trust the mise config files so there's no interactive prompt
|
||||
RUN mise trust /home/claude/.config/mise/config.toml
|
||||
|
||||
# Install all tools defined in mise.toml
|
||||
RUN mise install
|
||||
|
||||
# Put mise shims on PATH
|
||||
RUN echo 'eval "$(mise activate bash)"' >> /home/claude/.bashrc
|
||||
ENV PATH="/home/claude/.local/share/mise/shims:$PATH"
|
||||
|
||||
# Auto-accept mise trust prompts for project configs mounted at runtime
|
||||
ENV MISE_YES=1
|
||||
|
||||
# Create .claude directory so Claude Code can write session data
|
||||
RUN mkdir -p /home/claude/.claude
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENTRYPOINT ["claude"]
|
||||
@@ -133,7 +133,9 @@
|
||||
(let [;; Try different possible file paths
|
||||
possible-paths [(str "src/" file)
|
||||
file
|
||||
(str "test/" file)]
|
||||
(str "test/" file)
|
||||
(str "enterprise/backend/src/" file)
|
||||
(str "enterprise/backend/test/" file)]
|
||||
source-file (first (filter #(and % (.exists (java.io.File. ^String %))) possible-paths))]
|
||||
(when-not source-file
|
||||
(throw (ex-info (str "Could not find source file for: " file)
|
||||
|
||||
@@ -230,8 +230,13 @@
|
||||
(str "mutation-testing-lib-" short-ns "-" fn-name)))
|
||||
|
||||
(defn create-branch!
|
||||
"Create and checkout a new branch from the base branch for mutation testing a function."
|
||||
"Create and checkout a new branch from the base branch for mutation testing a function.
|
||||
Fails early if there are uncommitted changes."
|
||||
[target-ns fn-name]
|
||||
(let [status (:out (sh "git" "status" "--porcelain" "-uno"))]
|
||||
(when-not (str/blank? status)
|
||||
(throw (ex-info (str "Uncommitted changes detected. Commit or stash before running.\n" status)
|
||||
{:status status}))))
|
||||
(let [branch (branch-name target-ns fn-name)]
|
||||
(sh "git" "checkout" (base-branch))
|
||||
(sh "git" "pull")
|
||||
@@ -441,19 +446,22 @@
|
||||
test-path-base (-> test-ns
|
||||
(str/replace "." "/")
|
||||
(str/replace "-" "_"))
|
||||
source-ext (cond
|
||||
(.exists (io/file (str "src/" ns-path ".cljc"))) ".cljc"
|
||||
(.exists (io/file (str "src/" ns-path ".clj"))) ".clj"
|
||||
:else ".cljc")
|
||||
test-ext (cond
|
||||
(.exists (io/file (str "test/" test-path-base ".cljc"))) ".cljc"
|
||||
(.exists (io/file (str "test/" test-path-base ".clj"))) ".clj"
|
||||
:else ".cljc")]
|
||||
source-prefixes ["src/" "enterprise/backend/src/"]
|
||||
test-prefixes ["test/" "enterprise/backend/test/"]
|
||||
find-file (fn [prefixes base exts]
|
||||
(first (for [prefix prefixes, ext exts
|
||||
:let [path (str prefix base ext)]
|
||||
:when (.exists (io/file path))]
|
||||
{:prefix prefix :ext ext :path path})))
|
||||
source-match (or (find-file source-prefixes ns-path [".cljc" ".clj"])
|
||||
{:prefix "src/" :ext ".cljc" :path (str "src/" ns-path ".cljc")})
|
||||
test-match (or (find-file test-prefixes test-path-base [".cljc" ".clj"])
|
||||
{:prefix "test/" :ext ".cljc" :path (str "test/" test-path-base ".cljc")})]
|
||||
{:target-ns (symbol target-ns)
|
||||
:test-ns (symbol test-ns)
|
||||
:short-name short-name
|
||||
:source-path (str "src/" ns-path source-ext)
|
||||
:test-path (str "test/" test-path-base test-ext)
|
||||
:source-path (:path source-match)
|
||||
:test-path (:path test-match)
|
||||
:report-path (str "mutation-testing-report." target-ns ".before.md")}))
|
||||
|
||||
(defn group-functions
|
||||
@@ -717,6 +725,70 @@
|
||||
(println " " (:primary-fn r) "-" (:error r))))
|
||||
(println "========================================")))
|
||||
|
||||
(defn run-one!
|
||||
"Run mutation testing for a single function. No branching or PR creation.
|
||||
Generates a report, invokes Claude to write tests, and verifies mutations are killed.
|
||||
|
||||
Arguments:
|
||||
- fq-fn-sym — fully-qualified function symbol (e.g. 'my.ns/my-fn)
|
||||
- test-ns-sym — test namespace symbol (e.g. 'my.ns-test)
|
||||
|
||||
Options:
|
||||
- :max-retries — max Claude retries for surviving mutations (default 2)"
|
||||
([fq-fn-sym test-ns-sym] (run-one! fq-fn-sym test-ns-sym {}))
|
||||
([fq-fn-sym test-ns-sym opts]
|
||||
(let [fq-fn-name (symbol fq-fn-sym)
|
||||
target-ns (symbol (namespace fq-fn-name))
|
||||
test-ns (symbol (str test-ns-sym))
|
||||
parsed (parse-namespace target-ns)
|
||||
{:keys [source-path test-path report-path]} parsed
|
||||
max-retries (get opts :max-retries 2)]
|
||||
|
||||
;; 1. Generate report
|
||||
(println "Generating mutation testing report for" (str target-ns) "...")
|
||||
(coverage/generate-report target-ns [test-ns] report-path)
|
||||
(println "Report written to" report-path)
|
||||
|
||||
;; 2. Run coverage for the single function
|
||||
(println "Running coverage for" (str fq-fn-name) "...")
|
||||
(let [coverage-results (coverage/test-namespace target-ns [test-ns])
|
||||
fn-data (get coverage-results fq-fn-name)]
|
||||
(when-not fn-data
|
||||
(throw (ex-info (str "Function not found in coverage results: " fq-fn-name)
|
||||
{:available (keys coverage-results)})))
|
||||
(let [mutations (:survived fn-data)]
|
||||
(if (empty? mutations)
|
||||
(do (println "No surviving mutations for" (str fq-fn-name) "— nothing to do.")
|
||||
{:status :success :killed [] :survived []})
|
||||
|
||||
(do
|
||||
(println (count mutations) "surviving mutations")
|
||||
|
||||
;; 3. Invoke Claude
|
||||
(println "Invoking Claude to write tests...")
|
||||
(let [prompt (build-test-prompt {:target-ns target-ns
|
||||
:test-ns test-ns
|
||||
:source-path source-path
|
||||
:test-path test-path
|
||||
:fn-names [fq-fn-name]
|
||||
:mutations mutations})]
|
||||
(invoke-claude! prompt))
|
||||
|
||||
;; 4. Verify and retry
|
||||
(println "Verifying mutations killed...")
|
||||
(let [result (verify-and-retry! {:fn-names [fq-fn-name]
|
||||
:test-ns test-ns
|
||||
:test-path test-path
|
||||
:max-retries max-retries})]
|
||||
(println "\nResult:" (:status result))
|
||||
(println " Killed:" (count (:killed result)))
|
||||
(println " Survived:" (count (:survived result)))
|
||||
(when (seq (:survived result))
|
||||
(println "\n Surviving mutations:")
|
||||
(doseq [{:keys [description]} (:survived result)]
|
||||
(println " -" description)))
|
||||
result))))))))
|
||||
|
||||
(defn run!
|
||||
"Main entry point. Runs the full mutation testing workflow for a namespace.
|
||||
|
||||
@@ -735,6 +807,11 @@
|
||||
(set-config! {:base-branch bb}))
|
||||
(when-let [pid (:project-id opts)]
|
||||
(set-config! {:project-id pid}))
|
||||
;; 0. Fail early if working tree is dirty
|
||||
(let [status (:out (sh "git" "status" "--porcelain" "-uno"))]
|
||||
(when-not (str/blank? status)
|
||||
(throw (ex-info (str "Uncommitted changes detected. Commit or stash before running.\n" status)
|
||||
{:status status}))))
|
||||
(let [parsed (parse-namespace target-ns-sym)
|
||||
{:keys [target-ns test-ns report-path]} parsed]
|
||||
;; 1. Generate report
|
||||
@@ -767,7 +844,15 @@
|
||||
(try
|
||||
(process-group! parsed group)
|
||||
(catch Exception e
|
||||
(println "ERROR:" (.getMessage e))
|
||||
(let [log-file (str "mutation-testing-error-"
|
||||
(name (:primary-fn group))
|
||||
".log")]
|
||||
(with-open [w (io/writer log-file)]
|
||||
(.write w (str "Error processing group: " (:primary-fn group) "\n"))
|
||||
(.write w (str "Message: " (.getMessage e) "\n\n"))
|
||||
(.printStackTrace e (java.io.PrintWriter. w)))
|
||||
(println "ERROR:" (.getMessage e))
|
||||
(println " Full stack trace logged to" log-file))
|
||||
(try (return-to-base!) (catch Exception _))
|
||||
{:error (.getMessage e)
|
||||
:primary-fn (:primary-fn group)})))))]
|
||||
|
||||
@@ -65,7 +65,9 @@
|
||||
(def cli-spec [["-h" "--help" "Show this help text"]
|
||||
["-H" "--hot" "Enable hot reloading"] ;
|
||||
["-p" "--port PORT" "Port to run the nREPL server on"
|
||||
:default 50605
|
||||
:default (if-let [p (env/env :nrepl-port)]
|
||||
(Integer/parseInt p)
|
||||
50605)
|
||||
:parse-fn #(Integer/parseInt %)]])
|
||||
|
||||
(defn -main
|
||||
|
||||
@@ -30,13 +30,36 @@ For now, Metabot is only available for Metabase Cloud. Before you can set up Met
|
||||
|
||||
Once you've added Metabot AI in the Metabase store, you can log into your Metabase and configure it in _Admin > AI_.
|
||||
|
||||
## Metabot options
|
||||
|
||||
You can set options for Metabot, both for your internal Metabase, and (separately) for embedded Metabase contexts. That way you can, say, use Metabot in your Metabase, while not giving access to Metabot in your embedded Metabase.
|
||||
|
||||
- [Enable Metabot](#enable-metabot)
|
||||
- [Verified content](#verified-content)
|
||||
- [Collection Metabot can use](#collection-metabot-can-use)
|
||||
|
||||
## Enable Metabot
|
||||
|
||||
Toggle [Metabot](./metabot.md) on or off for your Metabase or embedded contexts. Both Metabot and Embedded Metabot are enabled by default.
|
||||
|
||||
### Internal Metabot
|
||||
|
||||
When enabled, Metabot is available to help people create questions, analyze data, and answer questions about your data. When disabled, the Metabot icon and keyboard shortcuts are hidden.
|
||||
|
||||
### Embedded Metabot
|
||||
|
||||
The toggle will turn embedded Metabot on or off. The toggle affects both full-app embeds and modular embeds.
|
||||
|
||||
- [Full-app embedding](../embedding/full-app-embedding.md): The Metabot icon and keyboard shortcuts are only available when Metabot is enabled. Turning off Embedded Metabot will hide these icons and disable the keyboard shortcuts.
|
||||
- [Modular embedding](../embedding/modular-embedding.md) The toggle doesn't add Metabot anywhere; you have to explicitly include a chat component (like the SDK's [`MetabotQuestion`](../embedding/sdk/ai-chat.md)) in your application. If, however, you've added a component, and you turn off the Embedded Metabot toggle, your chat component will stop working, so you should also remove or hide the component in your application.
|
||||
|
||||
## Verified content
|
||||
|
||||
Admins on Pro and Enterprise plans can tell Metabot to only work with [models](../data-modeling/models.md) and [metrics](../data-modeling/metrics.md) that have been [verified](../exploration-and-organization/content-verification.md).
|
||||
|
||||
Restricting Metabot to verified models and metrics (and only models and metrics) helps Metabot produce more reliable answers, since you know someone has at least vetted the data Metabot can use.
|
||||
|
||||
## Collection for natural language querying
|
||||
## Collection Metabot can use
|
||||
|
||||
Select a collection (including its subcollections) to limit which collections Metabot searches during [AI exploration](../ai/metabot.md#ai-exploration).
|
||||
|
||||
@@ -44,18 +67,6 @@ This setting only affects conversations started from **+ New → AI exploration*
|
||||
|
||||
People can still @-mention items outside of this collection when prompting in AI exploration. Metabot can also see the person's current context (for example, Metabot will know about the dashboard they're currently viewing, even if the dashboard is outside the selected collection).
|
||||
|
||||
## When embedding Metabot, you can pick a collection for Metabot to have access to
|
||||
|
||||
When embedding Metabot in your app, you can select a collection for Metabot:
|
||||
|
||||
2. Click **Embedded Metabot**.
|
||||
3. In the **Collection Embedded Metabot can use** section, click **Pick a collection**.
|
||||
4. Select the collection that contains the models and metrics you want Metabot to use.
|
||||
|
||||
Metabot will use the models and metrics in that collection to help answer questions and generate queries. You can change this collection at any time. To give Metabot access to all collections, you can set the collection to the root collection, called "Our Analytics" (the default).
|
||||
|
||||
Alternatively (or additionally), you can restrict Metabot to [verified content](#verified-content).
|
||||
|
||||
## Tips for making the most of Metabot
|
||||
|
||||
The best thing you can do to improve Metabot's performance is to prep your data like you would for onboarding a new (human) hire to your data. In practice, this means you should:
|
||||
|
||||
4478
docs/api.json
@@ -7,12 +7,15 @@ redirect_from:
|
||||
- /docs/latest/embedding/whitelabeling
|
||||
- /docs/latest/embedding/fonts
|
||||
- /docs/latest/embedding/customize-embeds
|
||||
summary: Configure colors, fonts, and illustrations in your Metabase.
|
||||
---
|
||||
|
||||
# Appearance
|
||||
|
||||
{% include plans-blockquote.html feature="Custom appearance" %}
|
||||
|
||||
_Admin > Settings > Appearance_
|
||||
|
||||
Appearance settings give admins the option to whitelabel Metabase to match your company’s branding.
|
||||
|
||||
If you're looking for date, time, number, or currency formatting, see [Formatting defaults](../data-modeling/formatting.md).
|
||||
@@ -23,37 +26,10 @@ Hit cmd/ctrl + k to bring up the command palette and search for "Branding" and c
|
||||
|
||||
You can also click the **grid** icon at the bottom of the navigation sidebar and click through **Admin** > **Settings** > **Appearance**.
|
||||
|
||||
Appearance settings are split across different tabs:
|
||||
|
||||
- [Branding](#branding)
|
||||
- [Conceal Metabase](#conceal-metabase)
|
||||
|
||||
## Dark mode
|
||||
|
||||
People can display their Metabase in dark mode in their [account settings](../people-and-groups/account-settings.md#theme). Options are:
|
||||
|
||||
- System default (which is also the default setting). Metabase will switch between light and dark mode when the system switches without having to reload the page.
|
||||
- Always use dark mode.
|
||||
- Always use light mode.
|
||||
|
||||
You can quickly toggle dark mode from anywhere in Metabase by opening the [command palette](../exploration-and-organization/exploration.md#command-palette) and searching for "light" or "dark" or "theme".
|
||||
|
||||
Dark mode is a user-level setting, not an instance-level setting. Currently, there's no way to change the theme to dark mode for the entire instance, but you can edit some [user interface colors](#user-interface-colors).
|
||||
|
||||
## Branding
|
||||
|
||||
In the Branding tab, you can configure your Metabase to match your brand visuals and voice.
|
||||
|
||||
- [Color palette](#color-palette)
|
||||
- [User interface colors](#user-interface-colors)
|
||||
- [Chart colors](#chart-colors)
|
||||
- [Logo](#logo)
|
||||
- [Font](#font)
|
||||
- [Loading message](#loading-message)
|
||||
- [Favicon](#favicon)
|
||||
|
||||
## Color palette
|
||||
|
||||
_Admin > Settings > Appearance > Branding_
|
||||
|
||||
You can customize colors in both the application UI and in the Metabase charts.
|
||||
|
||||
### User interface colors
|
||||
@@ -66,62 +42,93 @@ You can customize the colors that Metabase uses throughout the app:
|
||||
- **Second color:** The color of aggregations and breakouts in the graphical query builder.
|
||||
- **Third color:** Color of filters in the query builder, buttons and links in filter widgets.
|
||||
|
||||

|
||||
|
||||
When choosing interface color, keep in mind that some users of your Metabase can choose to display the interface in [dark mode](../people-and-groups/account-settings.md#theme). Pick colors that work well both in light and dark mode.
|
||||
|
||||
### Chart colors
|
||||
|
||||

|
||||
|
||||
You can choose up to 24 hex values. If you choose fewer than 24 colors, Metabase will auto-generate colors to fill in the rest of the values. The order of colors doesn't apply to the order of colors for series on charts. When building a chart, Metabase will select colors to make each series easy to distinguish.
|
||||
|
||||
Custom colors are unavailable for:
|
||||
You can preview what the chart colors will look like by switching to the **Preview** tab.
|
||||
|
||||

|
||||
|
||||
Custom colors are **unavailable** for:
|
||||
|
||||
- [Number charts](../questions/visualizations/numbers.md)
|
||||
- [Trend charts](../questions/visualizations/trend.md)
|
||||
- [Funnel charts](../questions/visualizations/funnel.md)
|
||||
- Conditional formatting ([tables](../questions/visualizations/table.md) and [pivot tables](../questions/visualizations/pivot-table.md))
|
||||
- [Maps](../questions/visualizations/map.md)
|
||||
- [Pin and grid maps](../questions/visualizations/map.md)
|
||||
|
||||
## Logo
|
||||
Only a **limited subset** of custom colors will be used for:
|
||||
|
||||
- Conditional formatting in [tables](../questions/visualizations/table.md) and [pivot tables](../questions/visualizations/pivot-table.md)
|
||||
- [Maps](../questions/visualizations/map.md)
|
||||
- [Sunburst chart](../questions/visualizations/pie-or-donut-chart.md)
|
||||
|
||||
In most cases, replacing default colors with custom chart colors will not be applied to
|
||||
|
||||
## Icons
|
||||
|
||||
_Admin > Settings > Appearance > Branding_
|
||||
|
||||
### Logo
|
||||
|
||||
You can replace Metabase's familiar, tasteful, inspired-yet-not-threateningly-avant-garde dotted M logo with your very own logo. For things to work best, the logo you upload should be an SVG file that looks good when it's around 60px tall. (In other words, ask the nearest designer for help.)
|
||||
|
||||
### Favicon
|
||||
|
||||
The URL or image that you want to use as the favicon (the logo visible in browser tabs, address bars, bookmark lists, and other places).
|
||||
|
||||
If you use a relative path, that path isn't relative to the Metabase JAR, but to the webserver. So unless you're using a reverse-proxy, the path will be relative to the frontend resources available to the JAR.
|
||||
|
||||
## Dark mode
|
||||
|
||||
People can display their Metabase in dark mode in their [account settings](../people-and-groups/account-settings.md#theme). Options are:
|
||||
|
||||
- System default (which is also the default setting). Metabase will switch between light and dark mode when the system switches without having to reload the page.
|
||||
- Always use dark mode.
|
||||
- Always use light mode.
|
||||
|
||||
You can quickly toggle dark mode from anywhere in Metabase by opening the [command palette](../exploration-and-organization/exploration.md#command-palette) and searching for "light" or "dark" or "theme".
|
||||
|
||||
Dark mode is a user-level setting, not an instance-level setting. Currently, there's no way to change the theme to dark mode for the entire instance, but you can edit some [user interface colors](#user-interface-colors).
|
||||
|
||||
## Font
|
||||
|
||||
_Admin > Settings > Appearance > Branding_
|
||||
|
||||
This is the primary font used in charts and throughout the Metabase application (your "instance font"). See [Fonts](./fonts.md).
|
||||
|
||||
## Loading message
|
||||
|
||||
_Admin > Settings > Appearance > Branding_
|
||||
|
||||
This message is the text Metabase presents when it's loading a query. Options include:
|
||||
|
||||
- "Doing science..." (the default)
|
||||
- "Running query..."
|
||||
- "Loading results..."
|
||||
|
||||
## Favicon
|
||||
## Names
|
||||
|
||||
The URL or image that you want to use as the favicon (the logo visible in browser tabs, address bars, bookmark lists, and other places).
|
||||
_Admin > Settings > Appearance > Conceal Metabase_
|
||||
|
||||
If you use a relative path, that path isn't relative to the Metabase JAR, but to the webserver. So unless you're using a reverse-proxy, the path will be relative to the frontend resources available to the JAR.
|
||||
|
||||
## Conceal Metabase
|
||||
|
||||
Hide or customize pieces of the Metabase product to tailor the experience to your brand and needs.
|
||||
|
||||
- [Application name](#application-name)
|
||||
- [Documentation and references](#documentation-and-references)
|
||||
- [Help link in the settings menu](#help-link-in-the-settings-menu)
|
||||
- [Metabase illustrations](#metabase-illustrations)
|
||||
|
||||
## Application name
|
||||
### Application name
|
||||
|
||||
You can change every place in the app that says “Metabase” to something like “Acme Analytics,” or whatever you want to call your Metabase app.
|
||||
|
||||
## Documentation and references
|
||||
Setting a custom application name will also hide the "How to use Metabase" section in the navigation.
|
||||
|
||||
Control the visibility of links to official Metabase documentation and other references to Metabase in your instance.
|
||||
### Documentation and references
|
||||
|
||||
This setting affects all links in the product experience that point to Metabase.com URLs (with the exception of links and references in the Admin settings).
|
||||
Control the visibility of links to official Metabase documentation and other references to Metabase in your instance outside of Admin settings.
|
||||
|
||||
## Help link in the settings menu
|
||||
Keep in mind that if you hide documentation links, people using your Metabase might not be able to troubleshoot their problems or learn how to use Metabase features like [custom expressions](../questions/query-builder/expressions.md).
|
||||
|
||||
### Help link in the settings menu
|
||||
|
||||
The Settings menu (the "grid" menu in the upper right of your Metabase) includes a **Help** option that links to a [Metabase help page](https://www.metabase.com/help/) by default. You can change this menu Help item by selecting one of the following options:
|
||||
|
||||
@@ -131,6 +138,8 @@ The Settings menu (the "grid" menu in the upper right of your Metabase) includes
|
||||
|
||||
## Metabase illustrations
|
||||
|
||||
_Admin > Settings > Appearance > Conceal Metabase_
|
||||
|
||||

|
||||
|
||||
Customize each of the illustrations in Metabase.
|
||||
|
||||
@@ -109,6 +109,7 @@ config:
|
||||
can-run-adhoc-query-check-threshold: 250
|
||||
check-for-updates: true
|
||||
config-from-file-sync-databases: true
|
||||
csv-field-separator: ','
|
||||
custom-formatting: {}
|
||||
custom-geojson: null
|
||||
custom-geojson-enabled: true
|
||||
@@ -292,6 +293,7 @@ config:
|
||||
sync-leaf-fields-limit: 1000
|
||||
synchronous-batch-updates: false
|
||||
thread-interrupt-escalation-timeout-ms: 0
|
||||
transform-timeout: 240
|
||||
transforms-enabled: false
|
||||
unaggregated-query-row-limit: null
|
||||
uploads-settings: null
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
title: Custom maps
|
||||
redirect_from:
|
||||
- /docs/latest/administration-guide/20-custom-maps
|
||||
summary: Add custom region maps and customize the map tile server.
|
||||
---
|
||||
|
||||
# Custom maps
|
||||
|
||||
By default, Metabase uses OpenStreetMaps for map visualizations, but there are a few customization options.
|
||||
_Admin > Settings > Maps_
|
||||
|
||||
## Map settings
|
||||
By default, Metabase uses OpenStreetMaps tiles for pin and grid map visualizations, and comes with two built-in region maps - world countries and US states. You can change the tiles used for pin and grid maps, and upload additional region maps.
|
||||
|
||||
To find the map settings:
|
||||
|
||||
@@ -22,7 +23,12 @@ To find the map settings:
|
||||
By default, Metabase uses the [OpenStreetMaps](https://www.openstreetmap.org) tile server for pin and grid maps. If your organization requires a different
|
||||
look or level of detail in your map visualizations, you can change the map tile server by adding the map tile server path to the first field on the page.
|
||||
|
||||
The path must either be a URL that starts with `http://` or `https://` or a relative path to a local file in the JVM's classpath.
|
||||
To change the map tile server:
|
||||
|
||||
1. Go to **Admin > Settings > Maps**.
|
||||
2. Under **Map tile server URL**, add the path to the new map tile server.
|
||||
|
||||
The path must either be a URL that starts with `http://` or `https://` or a relative path to a local file in the JVM's classpath.
|
||||
|
||||
Currently, Metabase uses a single tile server per instance. You can't specify different tiles for different maps.
|
||||
|
||||
@@ -32,6 +38,8 @@ Metabase comes with two built-in [region maps](../questions/visualizations/map.m
|
||||
|
||||
If you need a map of other regions - like EU countries or NYC neighborhoods - you can specify a GeoJSON file containing region information. You can often find GeoJSON maps by searching online for "[Your region] + geojson", like "NYC neighborhoods GeoJSON." Many community members and government organizations have already developed map files for common regions. You can also create your own GeoJSON with a tool like [MapShaper](https://mapshaper.org/) or [GeoJSON.io](https://geojson.io/).
|
||||
|
||||
### Custom geoJSON requirements
|
||||
|
||||
Your GeoJSON file should:
|
||||
|
||||
- Be less than 5 MB in size.
|
||||
@@ -39,6 +47,8 @@ Your GeoJSON file should:
|
||||
- Use geographic coordinates (latitude and longitude) to define region polygons. Metabase doesn't support projected coordinates, so you'll need to convert projected coordinates to geographic coordinates.
|
||||
- Accessible by a public URL. Currently, you can't upload a GeoJSON to Metabase.
|
||||
|
||||
### Add a custom map
|
||||
|
||||
To add a custom map:
|
||||
|
||||
1. Click the **grid icon**, then go to **Admin** > **Settings** > **Maps** > **Custom Maps**.
|
||||
@@ -47,11 +57,11 @@ To add a custom map:
|
||||
4. Provide the URL to your GeoJSON file.
|
||||
5. Specify the JSON properties that should serve as region identifier and region name.
|
||||
|
||||

|
||||

|
||||
|
||||
- **Name of the map** will be displayed in the region selector for [custom region maps](../questions/visualizations/map.md#custom-regions)
|
||||
- **Region's identifier** is a GeoJSON field that identifies your region. The values in this field should match how the regions are referenced in your data. The field does not need to match the display name .
|
||||
- **Region's display name** is a GeoJSON field that specifies how your region name will be displayed on a map. This field can be different from region's identifier.
|
||||
- **Name of the map** will be displayed in the region selector for [custom region maps](../questions/visualizations/map.md#custom-regions).
|
||||
- **Region's identifier** is a GeoJSON field that identifies your region. The values in this field should match how the data refers to the regions. The field doesn't need to match the display name .
|
||||
- **Region's display name** is a GeoJSON field that specifies how maps will display region names. This field can be different from region's identifier.
|
||||
|
||||
To pre-load one or more region maps when Metabase starts, you can use the environment variable [`MB_CUSTOM_GEOJSON`](./environment-variables.md#mb_custom_geojson) or a [config file option](./config-file.md) `custom-geojson` .
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Once you connect your database to Metabase, you'll want to configure an email ac
|
||||
|
||||
Both admins and people with [settings access](../permissions/application.md#settings-access) can set up email.
|
||||
|
||||
## Email on Metabase Cloud
|
||||
## Set up email on Metabase Cloud
|
||||
|
||||
_Admin > Settings > Email_
|
||||
|
||||
@@ -57,47 +57,103 @@ When setting up a custom SMTP server on Metabase Cloud, you'll configure these f
|
||||
- **SMTP USERNAME**: Your SMTP account username.
|
||||
- **SMTP PASSWORD**: Your SMTP account password.
|
||||
|
||||
You'll also need to specify:
|
||||
|
||||
- **From address**: The email address you want to use for the sender of emails.
|
||||
- **Reply-to address**: The email address you want replies to go to, if different from the From address.
|
||||
Once you set up your SMTP, you'll be able to [Configure email settings](#configure-email-settings).
|
||||
|
||||
You can edit these settings at any time. You can also toggle between this custom SMTP server and the server managed by Metabase Cloud.
|
||||
|
||||
## Configuring your email account
|
||||
## Set up email on self-hosted Metabases
|
||||
|
||||
_Admin > Settings > Email_
|
||||
|
||||
For Metabase to send messages to people, you'll need to set up an email account to send emails via **SMTP** (simple mail transfer protocol). SMTP is an email standard that, when combined with SSL/TLS, provides security protection for emails.
|
||||
|
||||
To start, go to the Admin Panel from the dropdown menu in the top right of Metabase, then from the Settings page, click on **Email** in the left menu.
|
||||
If you're on Metabase Cloud, see [Set up email on Metabase Cloud](#set-up-email-on-metabase-cloud).
|
||||
|
||||
You should see this form:
|
||||
To set up email on self-hosted Metabase:
|
||||
|
||||

|
||||
1. Go to **Admin > Settings > Email**.
|
||||
2. Next to **Self-Hosted SMTP**, click **Configure**.
|
||||
3. Specify the email configuration:
|
||||
|
||||
Here you'll set:
|
||||

|
||||
|
||||
- **SMTP HOST**: The address of the SMTP server that handles your emails.
|
||||
- **SMTP PORT**: The port your SMTP server uses for outgoing emails.
|
||||
- **SMTP SECURITY**:
|
||||
- **SMTP Host**: The address of the SMTP server that handles your emails.
|
||||
- **SMTP Port**: The port your SMTP server uses for outgoing emails.
|
||||
- **SMTP Security**:
|
||||
- None
|
||||
- SSL
|
||||
- TLS
|
||||
- STARTTLS
|
||||
- **SMTP USERNAME**: Your SMTP account username.
|
||||
- **SMTP PASSWORD**: Your SMTP account password.
|
||||
- **SMTP Username**: Your SMTP account username.
|
||||
- **SMTP Password**: Your SMTP account password.
|
||||
|
||||
You'll also need to specify your:
|
||||
3. Click **Save changes**.
|
||||
|
||||
## Configure email settings
|
||||
|
||||
_Admin > Settings > Email_
|
||||
|
||||

|
||||
|
||||
### Add From and Reply-to addresses
|
||||
|
||||
Once you set up your email, you can configure:
|
||||
|
||||
- **From Name**: The name you want to use for the sender of emails.
|
||||
- **From address**: The email address you want to use for the sender of emails.
|
||||
- **Reply-to address**: The email address you want replies to go to, if different from the From address.
|
||||
|
||||
## Add recipients as CC or BCC
|
||||
### Add recipients as CC or BCC
|
||||
|
||||
By default, Metabase will hide email recipients by including them in the BCC list (Blind Carbon Copy) of the email. But if you're having issues with your email provider blocking emails with BCC recipients, and you don't mind having people see who else has been copied on the email Metabase sends them, you can tell Metabase to CC (Carbon Copy) recipients instead.
|
||||
|
||||
### Recommended email settings
|
||||
To configure CC/BCC:
|
||||
|
||||
1. Go to **Admin > Settings > Email**
|
||||
2. Under **Add recipients as CC or BCC**, select how you want Metabase to handle recipients.
|
||||
|
||||
### Approved domains for notifications
|
||||
|
||||
{% include plans-blockquote.html feature="Approved domains for notifications" %}
|
||||
|
||||
You can configure allowed email address domain(s) for new [dashboard subscriptions](../dashboards/subscriptions.md) and [alerts](../questions/alerts.md).
|
||||
|
||||
Adding approved domains allows you to restrict which email addresses people can send alerts and subscriptions to. This restriction only applies to sending email to people who lack an account with that Metabase. People with Metabase accounts who aren't [restricted by row or column security](../permissions/row-and-column-security.md) will be able to email any other person with an account in that same Metabase.
|
||||
|
||||
To configure allowed domains:
|
||||
|
||||
1. Go to **Admin > Settings > Email**
|
||||
2. Under **Allowed domains for notifications**, add allowed domains:
|
||||
|
||||
- To allow all domains, leave the field empty (allowing all domains is the default).
|
||||
- To specify multiple domains, separate each domain with a comma, with no space in between (e.g., `domain1,domain2`).
|
||||
|
||||
You can also set this property using the environment variable [`MB_SUBSCRIPTION_ALLOWED_DOMAINS`](../configuring-metabase/environment-variables.md#mb_subscription_allowed_domains).
|
||||
|
||||
> Changing this setting won't affect already existing subscriptions and alerts.
|
||||
|
||||
### Suggest recipients on dashboard subscriptions and alerts
|
||||
|
||||
{% include plans-blockquote.html feature="Configuring suggested recipients" %}
|
||||
|
||||
You can control which recipients people can see when they create a new [dashboard subscription](../dashboards/subscriptions.md) or [alert](../questions/alerts.md).
|
||||
|
||||

|
||||
|
||||
For example, you may want to restrict people to viewing potential recipients that belong to the same [groups](../people-and-groups/managing.md#groups) they are a member of.
|
||||
|
||||
To configure suggested recipients for subscriptions and alerts:
|
||||
|
||||
1. Go to **Admin > Settings > Email**.
|
||||
2. Under **Suggest recipients on dashboard subscriptions and alerts**, select one of the options:
|
||||
|
||||
- Suggest all users
|
||||
- Only suggest users in the same groups
|
||||
- Don't show suggestions
|
||||
|
||||
People with [row or column restrictions](../permissions/row-and-column-security.md) won't see suggestions.
|
||||
|
||||
## Email best practices
|
||||
|
||||
- SSL is strongly recommended because it's more secure and gives your account extra protection from threats.
|
||||
- If your email service has a whitelist of email addresses that are allowed to send email, be sure to whitelist the email address that you put in the **From address** field to ensure you and your teammates receive all emails from Metabase.
|
||||
@@ -110,11 +166,11 @@ By default, Metabase will hide email recipients by including them in the BCC lis
|
||||
|
||||
### Google Apps
|
||||
|
||||
1. In the **SMTP HOST** field, enter smtp.gmail.com
|
||||
2. Fill in 465 for the **SMTP PORT** field
|
||||
3. For the **SMTP SECURITY** field, enter **SSL**
|
||||
4. In the **SMTP USERNAME** field, enter your Google Apps email address (e.g. hello@yourdomain.com)
|
||||
5. Enter your Google Apps password in the **SMTP PASSWORD** field
|
||||
1. In the **SMTP Host** field, enter `smtp.gmail.com`.
|
||||
2. Fill in 465 for the **SMTP Port** field.
|
||||
3. For the **SMTP Security**, select **SSL**.
|
||||
4. In the **SMTP UsernameE** field, enter your Google Apps email address (e.g. `hello@yourdomain.com`).
|
||||
5. Enter your Google Apps password in the **SMTP Password** field.
|
||||
6. Enter the email address you would like to be used as the sender of system notifications in the **From address** field.
|
||||
|
||||
### Amazon SES
|
||||
@@ -135,36 +191,6 @@ Check if [email quotas](https://docs.aws.amazon.com/ses/latest/dg/quotas.html) a
|
||||
3. Although Mandrill lists **port 587**, [any port supported by Mandrill](https://mailchimp.com/developer/transactional/docs/smtp-integration/#the-basics) will work for SMTP email.
|
||||
4. Now you can go back to the Metabase Admin Panel form and enter the info there.
|
||||
|
||||
## Approved domains for notifications
|
||||
|
||||
{% include plans-blockquote.html feature="Approved domains for notifications" %}
|
||||
|
||||
Allowed email address domain(s) for new [dashboard subscriptions](../dashboards/subscriptions.md) and [alerts](../questions/alerts.md).
|
||||
|
||||
Adding approved domains allows you to restrict which email addresses people can send alerts and subscriptions to. This restriction only applies to sending email to people who lack an account with that Metabase. People with Metabase accounts who aren't [restricted by row or column security](../permissions/row-and-column-security.md) will be able to email any other person with an account in that same Metabase.
|
||||
|
||||
To allow all domains, leave the field empty (allowing all domains is the default).
|
||||
|
||||
To specify multiple domains, separate each domain with a comma, with no space in between (e.g., "domain1,domain2").
|
||||
|
||||
You can also set this property using the environment variable [`MB_SUBSCRIPTION_ALLOWED_DOMAINS`](../configuring-metabase/environment-variables.md#mb_subscription_allowed_domains).
|
||||
|
||||
> This setting doesn't affect existing subscriptions and alerts.
|
||||
|
||||
## Suggest recipients on dashboard subscriptions and alerts
|
||||
|
||||
{% include plans-blockquote.html feature="Configuring suggested recipients" %}
|
||||
|
||||
Control which recipients people can see when they create a new [dashboard subscription](../dashboards/subscriptions.md) or [alert](../questions/alerts.md). For example, you may want to restrict people to viewing potential recipients that belong to the same [groups](../people-and-groups/managing.md#groups) they are a member of.
|
||||
|
||||
Options include:
|
||||
|
||||
- Suggest all users
|
||||
- Only suggest users in the same groups
|
||||
- Don't show suggestions
|
||||
|
||||
People with [row or column restrictions](../permissions/row-and-column-security.md) won't see suggestions.
|
||||
|
||||
## Further reading
|
||||
|
||||
- [Alerts](../questions/alerts.md)
|
||||
|
||||
@@ -316,6 +316,15 @@ Identify when new versions of Metabase are available.
|
||||
Whether to (asynchronously) sync newly created Databases during config-from-file initialization. By default, true,
|
||||
but you can disable this behavior if you want to sync it manually or use SerDes to populate its data model.
|
||||
|
||||
### `MB_CSV_FIELD_SEPARATOR`
|
||||
|
||||
- Type: string
|
||||
- Default: `,`
|
||||
- [Exported as](../installation-and-operation/serialization.md): `csv-field-separator`.
|
||||
- [Configuration file name](./config-file.md): `csv-field-separator`
|
||||
|
||||
Character to use as field separator in CSV exports. Defaults to comma (,). Common alternatives include semicolon (;) for European locales and tab (\t).
|
||||
|
||||
### `MB_CUSTOM_FORMATTING`
|
||||
|
||||
- Type: json
|
||||
@@ -388,9 +397,12 @@ Timeout in milliseconds for connecting to databases, both Metabase application d
|
||||
|
||||
By default, this is 20 minutes.
|
||||
|
||||
Timeout in minutes for databases query execution, both Metabase application database and data connections.
|
||||
If you have long-running queries, you might consider increasing this value.
|
||||
Adjusting the timeout does not impact Metabase’s frontend.
|
||||
Timeout in minutes for the database's query execution, both for the Metabase application database and any data connections.
|
||||
If you have long-running queries, you might consider increasing this value. Adjusting the timeout does not impact Metabase’s frontend.
|
||||
|
||||
This setting also applies to individual queries executed within transforms, so make sure the duration is long enough
|
||||
that it doesn't timeout any long-running queries in your transforms.
|
||||
|
||||
Please be aware that other services (like Nginx) may still drop long-running queries.
|
||||
|
||||
### `MB_DEFAULT_MAPS_ENABLED`
|
||||
@@ -945,7 +957,9 @@ String used to seed the private key used to validate JWT messages. A hexadecimal
|
||||
- Default: `true`
|
||||
- [Configuration file name](./config-file.md): `jwt-user-provisioning-enabled`
|
||||
|
||||
When a user logs in via JWT, create a Metabase account for them automatically if they don't have one.
|
||||
Determines what happens when a user logs in via JWT and doesn't have a Metabase account.
|
||||
|
||||
When set to `true`, users who log in via JWT will automatically get a Metabase account if they don't have one, or get their existing account reactivated. When set to `false`, only users with active Metabase accounts can log in via JWT.
|
||||
|
||||
### `MB_LANDING_PAGE`
|
||||
|
||||
@@ -1128,8 +1142,9 @@ User lookup filter. The placeholder '{login}' will be replaced by the user suppl
|
||||
- Default: `true`
|
||||
- [Configuration file name](./config-file.md): `ldap-user-provisioning-enabled`
|
||||
|
||||
When we enable LDAP user provisioning, we automatically create a Metabase account on LDAP signin for users who
|
||||
don't have one.
|
||||
Determines what happens when a user logs in via LDAP and doesn't have a Metabase account.
|
||||
|
||||
When set to `true`, users who log in via LDAP will automatically get a Metabase account if they don't have one, or get their existing account reactivated. When set to `false`, only users with active Metabase accounts can log in via LDAP.
|
||||
|
||||
### `MB_LICENSE_TOKEN_MISSING_BANNER_DISMISSAL_TIMESTAMP`
|
||||
|
||||
@@ -1332,7 +1347,9 @@ JSON containing OIDC provider configurations.
|
||||
- Default: `true`
|
||||
- [Configuration file name](./config-file.md): `oidc-user-provisioning-enabled`
|
||||
|
||||
When a user logs in via OIDC, create a Metabase account for them automatically if they don't have one.
|
||||
Determines what happens when a user logs in via OIDC and doesn't have a Metabase account.
|
||||
|
||||
When set to `true`, users who log in via OIDC will automatically get a Metabase account if they don't have one, or get their existing account reactivated. When set to `false`, only users with active Metabase accounts can log in via OIDC.
|
||||
|
||||
### `MB_PERSISTED_MODEL_REFRESH_CRON_SCHEDULE`
|
||||
|
||||
@@ -1661,8 +1678,9 @@ Is SAML Single Log Out enabled?
|
||||
- Default: `true`
|
||||
- [Configuration file name](./config-file.md): `saml-user-provisioning-enabled`
|
||||
|
||||
When we enable SAML user provisioning, we automatically create a Metabase account on SAML signin for users who
|
||||
don't have one.
|
||||
Determines what happens when a user logs in via SAML and doesn't have a Metabase account.
|
||||
|
||||
When set to `true`, users who log in via SAML will automatically get a Metabase account if they don't have one, or get their existing account reactivated. When set to `false`, only users with active Metabase accounts can log in via SAML.
|
||||
|
||||
### `MB_SCIM_ENABLED`
|
||||
|
||||
@@ -2035,6 +2053,18 @@ By default, this is 0 and the thread interrupt escalation does not run.
|
||||
Timeout in milliseconds to wait after query cancellation before escalating to thread interruption.
|
||||
This is used to free up threads that are stuck waiting for a DB response after a query has been cancelled.
|
||||
|
||||
### `MB_TRANSFORM_TIMEOUT`
|
||||
|
||||
> Only available on Metabase [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans.
|
||||
|
||||
- Type: integer
|
||||
- Default: `240`
|
||||
|
||||
The timeout for a transform job, in minutes.
|
||||
|
||||
Each query executed by a transform is also subject to the MB_DB_QUERY_TIMEOUT_MINUTES timeout,
|
||||
so make sure that value isn't lower, or it will timeout your transform.
|
||||
|
||||
### `MB_TRANSFORMS_ENABLED`
|
||||
|
||||
- Type: boolean
|
||||
@@ -2439,13 +2469,26 @@ Default: `""`
|
||||
|
||||
When set, starts a Java Flight Recorder (JFR) recording at startup that can be analyzed with JDK Mission Control or other JFR tools.
|
||||
|
||||
- `"true"` generates a timestamped output file like `metabase-2026_01_15.jfr`
|
||||
- Any other non-empty value is used as the output filename (`.jfr` extension is appended if missing)
|
||||
- `"true"` generates a timestamped output file like `metabase-20260115_143000.jfr`
|
||||
- A value ending in `.jfr` is used as the output filename
|
||||
- Any other non-empty value is treated as a directory path. A new timestamped JFR file is written to that directory every 30 minutes (e.g., `metabase-20260115_143000.jfr`). Data is only written at the end of each 30-minute interval.
|
||||
- `""` or `"false"` disables monitoring (the default)
|
||||
|
||||
The performance recording stores only method signature calls and other code execution metrics.
|
||||
It does not store any sensitive information such as environment variables, system properties, or other machine information.
|
||||
|
||||
### `MB_MONITOR_PERFORMANCE_SAVE_RATE`
|
||||
|
||||
Type: integer<br>
|
||||
Default: mode-specific (5 minutes for single-file mode, 30 minutes for rolling mode)
|
||||
|
||||
Override the interval (in minutes) at which JFR recording data is saved to disk. Only applies when [MB_MONITOR_PERFORMANCE](#mb_monitor_performance) is enabled.
|
||||
|
||||
- In single-file mode, the default is `5` minutes.
|
||||
- In rolling directory mode, the default is `30` minutes.
|
||||
|
||||
Setting this value overrides the default for whichever mode is active.
|
||||
|
||||
### `MB_NO_SURVEYS`
|
||||
|
||||
Type: boolean<br>
|
||||
|
||||
@@ -7,15 +7,21 @@ redirect_from:
|
||||
|
||||
# Fonts
|
||||
|
||||
{% include plans-blockquote.html feature="Customizable font" %}
|
||||
{% include plans-blockquote.html feature="Customizable fonts" is_plural=true%}
|
||||
|
||||
On Pro and Enterprise plans, you can customize the font Metabase uses (the default is [Lato](https://fonts.google.com/specimen/Lato)). You can choose from a curated set of Google Fonts that accommodate the regular, bold, and heavy font weights that Metabase relies on for its various UI elements.
|
||||
|
||||
## Included fonts
|
||||

|
||||
|
||||
To change your Metabase font, click the **grid** icon in the upper right of the screen and select **Admin** > **Settings** > **Appearance**. Under **Font**, select from a list of included fonts.
|
||||
## Configure fonts
|
||||
|
||||
To change your Metabase font:
|
||||
|
||||
1. Click the **grid** icon in the upper right of the screen.
|
||||
2. Select **Admin**.
|
||||
3. In Admin console, go to **Settings** > **Appearance** > **Branding**
|
||||
4. Under **Font**, select from a list of included fonts:
|
||||
|
||||
- [Custom font](#custom-fonts)
|
||||
- [Lato](https://fonts.google.com/specimen/Lato)
|
||||
- [Lora](https://fonts.google.com/specimen/Lora)
|
||||
- [Merriweather](https://fonts.google.com/specimen/Merriweather)
|
||||
@@ -35,16 +41,23 @@ To change your Metabase font, click the **grid** icon in the upper right of the
|
||||
- [Slabo 27px](https://fonts.google.com/specimen/Slabo+27px)
|
||||
- Source Sans Pro
|
||||
- [Ubuntu](https://fonts.google.com/specimen/Ubuntu)
|
||||
- [Custom font](#add-custom-fonts)
|
||||
|
||||
Comic Sans is currently not included.
|
||||
|
||||
## Custom fonts
|
||||
You can also upload your own custom font.
|
||||
|
||||
To set a custom font, click the **grid** icon in the upper right of the screen and select **Admin** > **Settings** > **Appearance**.
|
||||
## Add custom fonts
|
||||
|
||||
For **Font**, select **Custom...** and enter URLs to your font files.
|
||||
To set a custom font:
|
||||
|
||||
## Font style options
|
||||
1. Click the **grid** icon in the upper right of the screen.
|
||||
2. Select **Admin**.
|
||||
3. In the Admin console, go to **Settings** > **Appearance** > **Branding**.
|
||||
4. Under **Fonts**, click the dropdown and select **Custom...**
|
||||
5. Enter URLs to your font files.
|
||||
|
||||
### Custom font style options
|
||||
|
||||
You can set three different font styles with three different URLs, one for each font style (size and weight) that Metabase will use to display its user interface properly. The font weights are:
|
||||
|
||||
@@ -60,7 +73,7 @@ For best results, set at least the 400 and 700 styles. If you have a single font
|
||||
|
||||
For custom fonts, Metabase supports woff, woff2, ttf files. If the URL you provide lacks a specific file extension, Metabase will assume it's a woff2 file.
|
||||
|
||||
## Google Font URLs
|
||||
### Google Font URLs
|
||||
|
||||
To get a URL for a [Google Font](https://fonts.google.com/), visit the Google Font page and select the style you want to use. Then make an HTTP request to get the URL of the tff file. For example, say we want to use Roboto Mono: here's how we could get the font file URLs for both the 400 and 700 styles:
|
||||
|
||||
@@ -95,7 +108,7 @@ For latin text, we'd copy the relevant src URLs for each style, in this case:
|
||||
|
||||
And paste those URLS in the relevant input fields in **Admin** > **Settings** > **Appearance** > **Font** section where it says "Tell us where to find the file for each font weight. You don’t need to include all of them, but it’ll look better if you do."
|
||||
|
||||
## Hosting fonts on GitHub
|
||||
### Hosting fonts on GitHub
|
||||
|
||||
If you host a font on GitHub, the font should be in a public repository, and you'll need to link to the raw font file(s) served from the raw.githubusercontent.com domain.
|
||||
|
||||
@@ -115,13 +128,13 @@ raw.githubusercontent.com/${user}/${repo}/${branch}/${path}
|
||||
|
||||
Note that in the raw link, there is no `/blob/` directory in the URL.
|
||||
|
||||
## Supporting multiple languages
|
||||
### Supporting multiple languages
|
||||
|
||||
To support multiple character sets, for example both Latin and Cyrillic, you'll need to merge font files.
|
||||
|
||||
## Customizing the font for individual embedded items
|
||||
|
||||
When configuring [guest embeds](../embedding/guest-embedding.md), you can select from any of the [included fonts](#included-fonts). If you've set a custom font for your Metabase, you can also select "Use instance font" to use your custom font in the embed.
|
||||
When configuring [guest embeds](../embedding/guest-embedding.md), you can select from any of the [included fonts](#configure-fonts). If you've set a custom font for your Metabase, you can also select "Use instance font" to use your custom font in the embed.
|
||||
|
||||
## Further reading
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 216 KiB |
BIN
docs/configuring-metabase/images/email-settings.png
Normal file
|
After Width: | Height: | Size: 258 KiB |
BIN
docs/configuring-metabase/images/font-settings.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
docs/configuring-metabase/images/map-settings.png
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
docs/configuring-metabase/images/preview.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
docs/configuring-metabase/images/qb-colors.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
docs/configuring-metabase/images/suggested-recipients.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 106 KiB |
@@ -6,15 +6,122 @@ redirect_from:
|
||||
|
||||
# Languages and localization
|
||||
|
||||
_Admin > Settings > Localization_
|
||||
|
||||
Admins can update the localization settings for the instance:
|
||||
|
||||
1. Click the **grid** icon in the upper right.
|
||||
2. Click **Admin**.
|
||||
2. Select **Admin**.
|
||||
3. In the **Settings** tab, click on **Localization** in the left sidebar.
|
||||
|
||||
These localization settings allow you to set global language and formatting defaults for dates, times, numbers, and currencies.
|
||||
|
||||
You can also override these localization options for specific fields or questions. For more info, see [Formatting](../data-modeling/formatting.md).
|
||||
People can also override these localization options for specific fields or questions. For more info, see [Formatting](../data-modeling/formatting.md).
|
||||
|
||||
## Instance settings
|
||||
|
||||
### Set default instance language
|
||||
|
||||
_Admin > Settings > Localization > Instance settings > Instance language_
|
||||
|
||||
You can set the default language (also called the "instance language") across your Metabase UI, system [emails](./email.md), [dashboard subscriptions](../dashboards/subscriptions.md), and [alerts](../questions/alerts.md).
|
||||
|
||||
People can override these settings in their personal [account settings](../people-and-groups/account-settings.md).
|
||||
|
||||
To set the instance-wider language:
|
||||
|
||||
1. Go to **Admin > Settings > Localization**.
|
||||
2. Under **Instance language**, pick the language for your instance.
|
||||
|
||||
See [List of supported languages](#supported-languages). Some translations are created by the Metabase community, and might not be perfect.
|
||||
|
||||
### Set default instance report timezone
|
||||
|
||||
_Admin > Settings > Localization > Instance settings > Report timezone_
|
||||
|
||||
Use **report timezone** to set a default display time zone for dates and times in Metabase. The report timezone setting is a display setting only, so changing the report timezone won't affect the time zone of any data in your database.
|
||||
|
||||
Report timezone doesn't apply to `timestamp without time zone` data types, including the output of [`convertTimezone`](../questions/query-builder/expressions/converttimezone.md) expressions. For example:
|
||||
|
||||
| Raw timestamp in your database | Data type | Report time zone | Displayed as |
|
||||
| ---------------------------------------- | ----------------------------- | ---------------- | ---------------------- |
|
||||
| `2022-12-28T12:00:00 AT TIME ZONE 'CST'` | `timestamp with time zone` | 'Canada/Eastern' | Dec 28, 2022, 7:00 AM |
|
||||
| `2022-12-28T12:00:00-06:00` | `timestamp with offset` | 'Canada/Eastern' | Dec 28, 2022, 7:00 AM |
|
||||
| `2022-12-28T12:00:00` | `timestamp without time zone` | 'Canada/Eastern' | Dec 28, 2022, 12:00 AM |
|
||||
|
||||
Report timezone is only supported for the following databases:
|
||||
|
||||
- BigQuery
|
||||
- Druid
|
||||
- MySQL
|
||||
- Oracle
|
||||
- PostgreSQL
|
||||
- Presto
|
||||
- Redshift
|
||||
- Vertica
|
||||
|
||||
To set report timezone:
|
||||
|
||||
1. Go to **Admin > Settings > Localization**.
|
||||
2. Under **Report timezone**, pick the default report timezone.
|
||||
|
||||
### First day of the week
|
||||
|
||||
_Admin > Settings > Localization > Instance settings > First day of the week_
|
||||
|
||||
If you need to, you can change the first day of the week for your instance (the default is Sunday).
|
||||
|
||||
Setting the first day of the week affects how the [query builder](../questions/query-builder/editor.md) filters or groups by week. People can, however, use the `week` function to override this default when filtering or grouping by week of year. See [using a different first week of the year](../questions/query-builder/expressions/week.md#using-a-different-first-week-of-the-year).
|
||||
|
||||
This setting doesn't affect [SQL queries](../questions/native-editor/writing-sql.md).
|
||||
|
||||
## Dates and times display settings
|
||||
|
||||
You can configure the default formatting for dates and times.
|
||||
|
||||
Note that people can also override these localization options for specific fields or questions. For more info, see [Formatting](../data-modeling/formatting.md).
|
||||
|
||||
To configure default date and time format settings:
|
||||
|
||||
1. Go to **Admin > Settings > Localization**.
|
||||
2. Under **Dates and times**, configure your settings:
|
||||
- **Date style:** the way dates should be displayed in tables, axis labels, and tooltips.
|
||||
- **Date separators:** you can choose between slashes (`2022/12/14`), dashes (`2022-12-14`), and dots (`2022.12.14`).
|
||||
- **Abbreviate names of days and months:** whenever a date is displayed with the day of the week and/or the month written out, turning this setting on will display e.g. "January" as "Jan" or "Monday" as "Mon".
|
||||
- **Time style:** choose to display the time using either a 12 or 24-hour clock (e.g., 3:00 PM or 15:00).
|
||||
|
||||
## Number display settings
|
||||
|
||||
_Admin > Settings > Localization_
|
||||
|
||||
Some people use commas to separate thousands places, and others use periods. You can configure which camp you belong to.
|
||||
|
||||
Note that people can also override these localization options for specific fields or questions. For more info, see [Formatting](../data-modeling/formatting.md).
|
||||
|
||||
To configure default separator settings:
|
||||
|
||||
1. Go to **Admin > Settings > Localization**.
|
||||
2. Under **Number**, configure the **Separator style**.
|
||||
|
||||
## Currency display settings
|
||||
|
||||
_Admin > Settings > Localization_
|
||||
|
||||
You can choose how should currency fields be displayed by default (you can to configure _which_ fields should be displayed as currency in table metadata or in question formatting setting, see [Formatting](../data-modeling/formatting.md) )
|
||||
|
||||
Note that people can also override these localization options for specific fields or questions.
|
||||
|
||||
To configure default currency settings:
|
||||
|
||||
1. Go to **Admin > Settings > Localization**.
|
||||
2. Under **Currency**, configure currency settings:
|
||||
|
||||
- **Unit of currency:** if you do most of your business in a particular currency, you can specify that here.
|
||||
- **Currency label style:** whether you want to have your currencies labeled with a symbol, a code (like "USD"), or its full name.
|
||||
|
||||
## Localizing embedded Metabase
|
||||
|
||||
You can translate both Metabase UI elements (like button labels) and content (like dashboard names) in guest embeds. See [Setting the language for guest embeds](../embedding/guest-embedding.md) and [Translating embedded dashboards and questions](../embedding/translations.md).
|
||||
|
||||
## Supported languages
|
||||
|
||||
@@ -78,65 +185,3 @@ Note that while we only remove languages in major releases, we are happy to add
|
||||
### Contributing to translations for Metabase
|
||||
|
||||
If you'd like to help make Metabase available in a language you're fluent in, we'd love your help! Check out our [Crowdin project](https://crowdin.com/project/metabase-i18n).
|
||||
|
||||
## Instance settings
|
||||
|
||||
### Instance language
|
||||
|
||||
Here you can set the default language (also called the "instance language") across your Metabase UI, system [emails](./email.md), [dashboard subscriptions](../dashboards/subscriptions.md), and [alerts](../questions/alerts.md).
|
||||
|
||||
People can override these settings in their personal [account settings](../people-and-groups/account-settings.md).
|
||||
|
||||
Some translations are created by the Metabase community, and might not be perfect.
|
||||
|
||||
### Report timezone
|
||||
|
||||
Use **report timezone** to set a default display time zone for dates and times in Metabase. The report timezone setting is a display setting only, so changing the report timezone won't affect the time zone of any data in your database.
|
||||
|
||||
Report timezone doesn't apply to `timestamp without time zone` data types, including the output of [`convertTimezone`](../questions/query-builder/expressions/converttimezone.md) expressions. For example:
|
||||
|
||||
| Raw timestamp in your database | Data type | Report time zone | Displayed as |
|
||||
| ---------------------------------------- | ----------------------------- | ---------------- | ---------------------- |
|
||||
| `2022-12-28T12:00:00 AT TIME ZONE 'CST'` | `timestamp with time zone` | 'Canada/Eastern' | Dec 28, 2022, 7:00 AM |
|
||||
| `2022-12-28T12:00:00-06:00` | `timestamp with offset` | 'Canada/Eastern' | Dec 28, 2022, 7:00 AM |
|
||||
| `2022-12-28T12:00:00` | `timestamp without time zone` | 'Canada/Eastern' | Dec 28, 2022, 12:00 AM |
|
||||
|
||||
Report timezone is only supported for the following databases:
|
||||
|
||||
- BigQuery
|
||||
- Druid
|
||||
- MySQL
|
||||
- Oracle
|
||||
- PostgreSQL
|
||||
- Presto
|
||||
- Redshift
|
||||
- Vertica
|
||||
|
||||
### First day of the week
|
||||
|
||||
If you need to, you can change the first day of the week for your instance (the default is Sunday).
|
||||
|
||||
Setting the first day of the week affects how the [query builder](../questions/query-builder/editor.md) filters or groups by week. People can, however, use the `week` function to override this default when filtering or grouping by week of year. See [using a different first week of the year](../questions/query-builder/expressions/week.md#using-a-different-first-week-of-the-year).
|
||||
|
||||
This setting doesn't affect [SQL queries](../questions/native-editor/writing-sql.md).
|
||||
|
||||
## Dates and times
|
||||
|
||||
- **Date style:** the way dates should be displayed in tables, axis labels, and tooltips.
|
||||
- **Date separators:** you can choose between slashes (`2022/12/14`), dashes (`2022-12-14`), and dots (`2022.12.14`).
|
||||
- **Abbreviate names of days and months:** whenever a date is displayed with the day of the week and/or the month written out, turning this setting on will display e.g. "January" as "Jan" or "Monday" as "Mon".
|
||||
- **Time style:** choose to display the time using either a 12 or 24-hour clock (e.g., 3:00 PM or 15:00).
|
||||
|
||||
## Numbers
|
||||
|
||||
- **Separator style:** some people use commas to separate thousands places, and others use periods. Here's where you can indicate which camp you belong to.
|
||||
|
||||
## Currency
|
||||
|
||||
- **Unit of currency:** if you do most of your business in a particular currency, you can specify that here.
|
||||
- **Currency label style:** whether you want to have your currencies labeled with a symbol, a code (like "USD"), or its full name.
|
||||
- **Where to display the unit of currency:** this pertains specifically to tables, and lets you choose whether you want the currency labels to appear only in the column heading, or next to each value in the column.
|
||||
|
||||
## Localizing embedded Metabase
|
||||
|
||||
You can translate both Metabase UI elements (like button labels) and content (like dashboard names) in guest embeds. See [Setting the language for guest embeds](../embedding/guest-embedding.md) and [Translating embedded dashboards and questions](../embedding/translations.md).
|
||||
|
||||
@@ -21,20 +21,25 @@ See [How to read logs](../troubleshooting-guide/server-logs.md).
|
||||
|
||||
## Configuring logging Level
|
||||
|
||||
Metabase uses [log4j](https://logging.apache.org/log4j/2.x/)for logging configuration. Here is Metabase's [default logging configuration](https://github.com/metabase/metabase/blob/master/resources/log4j2.xml). Some troubleshooting tasks might require you to override this logging configuration (for example, to see more details about errors). See Log4j's docs for info on [log levels](https://logging.apache.org/log4j/2.x/manual/customloglevels.html).
|
||||
Metabase uses [Log4j](https://logging.apache.org/log4j/2.x/) for logging configuration. Here is Metabase's [default logging configuration](https://github.com/metabase/metabase/blob/master/resources/log4j2.xml). Some troubleshooting tasks might require you to override this logging configuration (for example, to see more details about errors). See Log4j's docs for info on [log levels](https://logging.apache.org/log4j/2.x/manual/customloglevels.html).
|
||||
|
||||
### Temporary override logging configuration
|
||||
|
||||
To temporarily adjust the logging configuration, go to **Admin** > **Tools** > **Logs** and click on **Customize log levels**.
|
||||
_Admin > Tools > Logs_.
|
||||
|
||||
You can select from log level presets for common troubleshooting tasks (for example, troubleshooting sync issues), or provide your own configuration as a JSON. For example, here's an override configuration that increases logging for troubleshooting linked filters:
|
||||
To temporarily adjust the logging configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"metabase.parameters.chain-filter": "debug",
|
||||
"metabase.parameters.chain-filter.dedupe-joins": "debug"
|
||||
}
|
||||
```
|
||||
1. Go to **Admin** > **Tools** > **Logs**.
|
||||
2. Click on **Customize log levels** above the logs.
|
||||
|
||||
You can select from log-level presets for common troubleshooting tasks (for example, troubleshooting sync issues), or provide your own configuration as JSON. For example, here's an override configuration that increases logging for troubleshooting linked filters:
|
||||
|
||||
```json
|
||||
{
|
||||
"metabase.parameters.chain-filter": "debug",
|
||||
"metabase.parameters.chain-filter.dedupe-joins": "debug"
|
||||
}
|
||||
```
|
||||
|
||||
The override from Admin settings will be temporary. You can select for how long the override should be in place (e.g., 60 minutes). When the override times out, the logging configuration will revert to the default logging configuration (or a custom configuration if you're using a [custom log file](#use-a-custom-log-configuration-file)).
|
||||
|
||||
|
||||
@@ -6,10 +6,12 @@ redirect_from:
|
||||
|
||||
# General settings
|
||||
|
||||
This section contains settings for your whole instance, like its URL, the reporting timezone, and toggles for disabling or enabling some of Metabase's optional features.
|
||||
_Admin > Settings > Feneral_
|
||||
|
||||
This section contains settings for your whole instance, like its URL, the reporting timezone, and toggles for disabling or enabling some of Metabase's optional features.
|
||||
You can configure these settings by clicking the **grid icon** in the upper right, then going to **Admin** > **Settings** > **General**.
|
||||
|
||||
|
||||
## Site name
|
||||
|
||||
How you’d like to refer to this instance of Metabase.
|
||||
@@ -18,13 +20,7 @@ How you’d like to refer to this instance of Metabase.
|
||||
|
||||
The site URL is the web address that people use to access your Metabase instance. Make sure to include `http://` or `https://` to make sure it’s reachable. This feature is only available for self-hosted Metabases. For customers on Metabase Cloud, check out [Custom domains](../cloud/custom-domain.md).
|
||||
|
||||
## Custom homepage
|
||||
|
||||
Admins can pick a dashboard to serve as the homepage. If people lack permissions to view the selected dashboard, Metabase will redirect them to the default homepage.
|
||||
|
||||
If you've set a dashboard as your homepage, and want to revert to the default Metabase homepage, simply turn off the **Enabled** toggle to disable the Custom Homepage feature.
|
||||
|
||||
## Redirect to HTTPS
|
||||
### Redirect to HTTPS
|
||||
|
||||
By default, Metabase is served over HTTP.
|
||||
|
||||
@@ -34,13 +30,19 @@ For example, say you enable HTTPS redirect for a Metabase instance at the site U
|
||||
|
||||
> Note: if you haven't set up HTTPS on your server, Metabase will not let you enable HTTPS redirect. Instead, you'll get a warning saying "It looks like HTTPS is not properly configured."
|
||||
|
||||
## Custom homepage
|
||||
|
||||
Admins can pick a dashboard to serve as the homepage. If people lack permissions to view the selected dashboard, Metabase will redirect them to the default homepage.
|
||||
|
||||
To revert to the default Metabase homepage, simply toggle off Custom homepage.
|
||||
|
||||
## Email address for help requests
|
||||
|
||||
This email address will be displayed in various messages throughout Metabase when users encounter a scenario where they need assistance from an admin, such as a password reset request.
|
||||
|
||||
## Anonymous tracking
|
||||
|
||||
This option turns determines whether or not you allow [anonymous data about your usage of Metabase](../installation-and-operation/information-collection.md) to be sent back to us to help us improve the product. [Your database’s data is never tracked or sent](https://www.metabase.com/security).
|
||||
On self-hosted Metabases, this option turns determines whether or not you allow [anonymous data about your usage of Metabase](../installation-and-operation/information-collection.md) to be sent back to us to help us improve the product. [Your database’s data is never tracked or sent](https://www.metabase.com/security).
|
||||
|
||||
## Friendly table and field names
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ redirect_from:
|
||||
|
||||
# Set up Slack
|
||||
|
||||
If you want to have your [dashboard subscriptions](../dashboards/subscriptions.md) or [alerts](../questions/alerts.md) sent to Slack channels (or people on Slack), an admin must first integrate your Metabase with Slack.
|
||||
_Admin > Settings > Notification channels > Slack_
|
||||
|
||||
If you want to have your [dashboard subscriptions](../dashboards/subscriptions.md) or [alerts](../questions/alerts.md) sent to Slack channels (or people on Slack), you need to first integrate your Metabase with Slack.
|
||||
|
||||
Both admins and people with [settings access](../permissions/application.md#settings-access) can set up Slack.
|
||||
|
||||
@@ -14,84 +16,88 @@ Both admins and people with [settings access](../permissions/application.md#sett
|
||||
|
||||
For Metabase to post to your Slack channels, you’ll need to create a Slack App and make it available to Metabase.
|
||||
|
||||
From any Metabase page, go to **Admin** > **Settings** > **Notification channels** > **Slack**.
|
||||
1. In Metabase, go to **Admin > Settings > Notification channels**.
|
||||
2. Click **Connect to Slack**.
|
||||
3. Click **Create a Slack App**. Metabase will open a new tab with the Slack website.
|
||||
4. On the Slack website, click **Create an App**.
|
||||
5. Pick a workspace to develop your app.
|
||||
6. You may get a warning that says **This app is created from a 3rd party manifest. Always verify URLs and permissions below.**
|
||||
|
||||
Click on **Open Slack Apps**. Metabase will open a new browser tab and send you over to the Slack website to create the Slack app.
|
||||
This warning is expected (Metabase is the third party here). You can click on **Edit configurations** to see the app manifest Metabase sent along in the URL. Here is the manifest in YAML format:
|
||||
|
||||
On the Slack website, click **Create an App**.
|
||||
```yml
|
||||
_metadata:
|
||||
major_version: 1
|
||||
minor_version: 1
|
||||
display_information:
|
||||
name: Metabase
|
||||
description: Bringing the power of Metabase to your Slack #channels!
|
||||
background_color: "#509EE3"
|
||||
features:
|
||||
bot_user:
|
||||
display_name: Metabase
|
||||
oauth_config:
|
||||
scopes:
|
||||
bot:
|
||||
- users:read
|
||||
- channels:read
|
||||
- channels:join
|
||||
- files:write
|
||||
- chat:write
|
||||
- chat:write.customize
|
||||
- chat:write.public
|
||||
- groups:read
|
||||
```
|
||||
|
||||
### Pick a workspace to develop your app
|
||||
The manifest just take cares of some settings for your app and helps speed things along.
|
||||
|
||||
Select the workspace you want to create your app.
|
||||
|
||||
### The app manifest
|
||||
|
||||
When you click on **Open Slack App**, Metabase will pass along the app manifest, which Slack will use to set up your app.
|
||||
|
||||
You may get a warning that says:
|
||||
|
||||
**This app is created from a 3rd party manifest** Always verify URLs and permissions below.
|
||||
|
||||
This warning is expected (Metabase is the third party here). You can click on **Configure** to see the app manifest Metabase sent along in the URL. Here is the manifest in YAML format:
|
||||
|
||||
```yml
|
||||
_metadata:
|
||||
major_version: 1
|
||||
minor_version: 1
|
||||
display_information:
|
||||
name: Metabase
|
||||
description: Bringing the power of Metabase to your Slack #channels!
|
||||
background_color: "#509EE3"
|
||||
features:
|
||||
bot_user:
|
||||
display_name: Metabase
|
||||
oauth_config:
|
||||
scopes:
|
||||
bot:
|
||||
- users:read
|
||||
- channels:read
|
||||
- channels:join
|
||||
- files:write
|
||||
- chat:write
|
||||
- chat:write.customize
|
||||
- chat:write.public
|
||||
- groups:read
|
||||
```
|
||||
|
||||
The manifest just take cares of some settings for your app and helps speed things along.
|
||||
|
||||
Click the **Next** button. Then hit **Create** to set up your Slack app.
|
||||
7. Click the **Next** button. Then hit **Create** to set up your Slack app.
|
||||
|
||||
## Install your app to your workspace
|
||||
|
||||
On the Slack site for your newly created app, in the **Settings** > **Basic Information** tab, under **Install your app**, click on **Install to workspace**. On the next screen, click **Allow** to give Metabase access to your Slack workspace.
|
||||
1. On the Slack site for your newly created app, go to **Settings** > **Basic Information** tab
|
||||
2. Under **Install your app**, click on **Install to workspace**.
|
||||
3. On the next screen, click **Allow** to give Metabase access to your Slack workspace.
|
||||
|
||||
## The Bot User OAuth Token
|
||||
## Configure your app in Metabase
|
||||
|
||||
On the Slack site page for your Slack app, on the left in the **Features** section, click on **OAuth and Permissions** in the Slack Apps sidebar and then copy the **Bot User OAuth Token**. Return to the Slack settings page in your Metabase and paste this token in the Metabase field with the same name.
|
||||
Once you [created the app](#create-your-slack-app) and [installed it in your workspace](#install-your-app-to-your-workspace), you tell Metabase to use it by giving Metabase the OAuth token for the app.
|
||||
|
||||
## Save your changes in Metabase
|
||||
To obtain the OAuth token for the Slack aopp:
|
||||
|
||||
In Metabase, click on the **Save changes** button and that’s it! Metabase will automatically run a quick test to check that the API token is working properly. If something goes wrong, it'll give you an error message.
|
||||
1. Go to the Slack site page for your Slack app.
|
||||
2. On the Slack page for your app, go to **OAuth & Permissions** in the left sidebar.
|
||||
3. On the **OAuth & Permissions** page for your Slack app, copy the **Bot User OAuth Token**.
|
||||
|
||||
To set up the app in Metabase:
|
||||
|
||||
1. In Metabase, go to **Admin > Settings > Notification channels > Slack**. Click **Connect to Slack**.
|
||||
2. Paste the token into **Slack bot user OAuth token**.
|
||||
3. **Save changes** in Metabase.
|
||||
|
||||
Metabase will automatically run a quick test to check that the API token is working properly. If something goes wrong, it'll give you an error message.
|
||||
|
||||
## Sending alerts and subscriptions to private Slack channels
|
||||
|
||||
In order to send subscriptions and alerts to private Slack channels, you must first add the Metabase app to the private channel.
|
||||
In order to send [subscriptions](../dashboards/subscriptions.md) and [alerts](../questions/alerts.md) to private Slack channels, you must first add the Metabase app to the private channel.
|
||||
|
||||
In Slack, go to the private channel and mention the Metabase app. For example, if you called your Slack app "Metabase", you'd just type `@Metabase`. Slack will ask you if you want to invite your app to your channel, which you should.
|
||||
To add your app to a private Slack channel:
|
||||
|
||||
### Metabase not listing your private channel?
|
||||
1. In Slack, go to the private channel and mention the Metabase app. For example, if you called your Slack app "Metabase", you'd just type `@Metabase`.
|
||||
|
||||
It can take a little time for metabase to see all the channels the app has been invited to. New channels may not appear in listings for up to 10 minutes after inviting the app to the channel.
|
||||
2. Slack will ask you if you want to invite your app to your channel, which you should.
|
||||
|
||||
In order for metabase to see private channels, the app must have the `groups:read` oauth scope. Although this scope should be granted when setting up the app through metabase, older installations might not have this scope.
|
||||
If you think this might be the case [visit the app settings in slack](https://api.slack.com/apps/):
|
||||
Once you added the app to a private channel, you should see the channel in the list of channels when setting up a subscription or alerts within 10 mins (it can take a little time for Metabase to see all the channels the app has been invited to).
|
||||
|
||||
- Click on the metabase app in the app listing.
|
||||
- Click on **OAuth & Permissions** in the sidebar.
|
||||
- Under **Scopes** add the `groups:read` scope.
|
||||
- The app will the need to be reinstalled to the workspace
|
||||
by clicking the **Reinstall** button under **OAuth Tokens**.
|
||||
In order for metabase to see private channels, the app must have the `groups:read` OAuth scope. Although this scope should be granted when setting up the app through Metabase, older installations might not have this scope.
|
||||
|
||||
To check or edit your OAuth settings:
|
||||
|
||||
1. [Visit the app settings in slack](https://api.slack.com/apps/):
|
||||
2. Click on the Metabase app in the list of apps.
|
||||
3. Go to **OAuth & Permissions** in the sidebar.
|
||||
4. Under **Scopes**, add the `groups:read` scope if it's not added already.
|
||||
5. Reinstall the app by clicking the **Reinstall** button under **OAuth Tokens**.
|
||||
|
||||
## Further reading
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ You can make your database columns time zone aware by storing them as specific d
|
||||
|
||||
The exact data type will depend on your database. Some Metabase features only work with specific data types:
|
||||
|
||||
- [Report timezone setting](../configuring-metabase/localization.md#report-timezone)
|
||||
- [Report timezone setting](../configuring-metabase/localization.md#set-default-instance-report-timezone)
|
||||
- [`converttimezone` custom expression](../questions/query-builder/expressions/converttimezone.md)
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
@@ -11,25 +11,27 @@ Only admins and people with settings access can send alerts to webhooks.
|
||||
|
||||
For now, webhooks are only available for [alerts](../questions/alerts.md); you can't select a webhook as the recipient of a [dashboard subscription](../dashboards/subscriptions.md).
|
||||
|
||||
## Creating a webhook
|
||||
## Create a webhook
|
||||
|
||||
Before you can send an alert to a webhook, an admin will need to create a webhook to target.
|
||||
_Admin > Settings > Notification channels_
|
||||
|
||||
Admins and people with [settings access](../permissions/application.md#settings-access) can create a webhook by clicking the **grid icon** > **Admin** > **Notification channels**. In **Webhooks for alerts**, click **+ Add another**. Metabase will show you a short form to fill out:
|
||||
Before people can send alerts to a webhook, an admin or someone with [settings access](../permissions/application.md#settings-access) will need to create a webhook to target.
|
||||
|
||||
- **Webhook URL**. Where you want Metabase to send the results of an alert. Must be a valid URL.
|
||||
- **Give it a name**. Name the webhook so that people can pick the right hook when setting up an alert on a question.
|
||||
- **Description**. Tell people what the hook is for.
|
||||
- **Authentication method**. See below.
|
||||
To create a webhook:
|
||||
|
||||
## Webhook authentication method
|
||||
1. Click the **grid** icon in top right corner and select **Admin**.
|
||||
2. In the Admin console, go **Settings > Notification channels**.
|
||||
3. In **Webhooks for alerts**, click **Add a webhoor**.
|
||||
4. Metabase will show you a short form to fill out:
|
||||
|
||||
You can specify an auth method for the webhook.
|
||||
|
||||
- **None**: Anything goes.
|
||||
- **Basic**: Set a username and password.
|
||||
- **Bearer**: Include a [secret token](https://datatracker.ietf.org/doc/html/rfc6750).
|
||||
- **API key**: You can add the API key to the Header or as a Query param. Both approaches require a key and value (the API key itself).
|
||||
- **Webhook URL**. Where you want Metabase to send the results of an alert. Must be a valid URL.
|
||||
- **Give it a name**. Name the webhook so that people can pick the right hook when setting up an alert on a question.
|
||||
- **Description**. Tell people what the hook is for.
|
||||
- **Authentication method**:
|
||||
- **None**: Anything goes.
|
||||
- **Basic**: Set a username and password.
|
||||
- **Bearer**: Include a [secret token](https://datatracker.ietf.org/doc/html/rfc6750).
|
||||
- **API key**: You can add the API key to the Header or as a Query param. Both approaches require a key and value (the API key itself).
|
||||
|
||||
## Webhook payload
|
||||
|
||||
|
||||
@@ -4,52 +4,52 @@ title: Actions on dashboards
|
||||
|
||||
# Actions on dashboards
|
||||
|
||||
You can add buttons to dashboards to perform [actions](../actions/start.md) like creating or updating records, and combine them with dashboard [filter](./filters.md).
|
||||
|
||||

|
||||
|
||||
To put [actions](../actions/start.md) in action, you can combine them with a [model](../data-modeling/models.md) and a [filter](./filters.md) on a dashboard.
|
||||
## Create an action
|
||||
|
||||
## Adding an action to a dashboard
|
||||
To add an action button to a dashboard, you'll first need to create an action on a model. See [Model actions](../actions/introduction.md).
|
||||
|
||||
Visit a dashboard page and click on the **pencil** icon, then click on the **box with a mouse pointer** to add an action.
|
||||
## Add an action button to a dashboard
|
||||
|
||||
Metabase will add an action button to the dashboard grid, and open a sidebar with the buttons settings.
|
||||
Once you [created an action](../actions/introduction.md), you can add a button to a dashboard to perform that action:
|
||||
|
||||
### Button text
|
||||
1. Visit a dashboard page and click the **pencil** icon to start editing the dashboard.
|
||||
2. Click on the **box with a mouse pointer** to add an action.
|
||||
|
||||
A label explaining what the button does, e.g., "Ignition".
|
||||
Metabase will add an action button to the dashboard grid, and open a sidebar with button settings:
|
||||
|
||||
### Button variant
|
||||
- **Button text**: A label explaining what the button does, e.g., "Add record".
|
||||
- **Button variant**: How a button looks. You can select from a variety of handsome buttons:
|
||||
|
||||
You can select from a variety of handsome buttons:
|
||||

|
||||
|
||||
- Primary
|
||||
- Outline
|
||||
- Danger
|
||||
- Success
|
||||
- Borderless
|
||||
3. Pick an action to connect to the button.
|
||||
4. For every field in the action, select whether people should supply the value themselves, or whether the action should use the value from the dashboard filter. See [Connecting an action to a dashboard filter](#connect-an-action-to-a-dashboard-filter).
|
||||
5. Click **Done**, and **Save** the dashboard.
|
||||
|
||||

|
||||
|
||||
## Connecting an action to a dashboard filter
|
||||
|
||||
For most actions, when people click on a button, they'll be prompted to input values in the fields defined by that action.
|
||||
|
||||
Many types of actions rely on knowing the Entity ID for the model to determine which records to update or delete. To have people select the ID for the action, you'll need to:
|
||||
|
||||
1. Click on the pencil icon to edit the dashboard.
|
||||
2. Add an action to the dashboard (if you haven't already).
|
||||
3. [Add a filter](./filters.md) to a dashboard, wire it up to any cards that you want to, and click **Done** in the bottom of the sidebar.
|
||||
4. Hover over the action button and click on the **gear** icon, and select **Change action**.
|
||||
5. Click on the field's dropdown to select where the action should get it's value. Here you can select "Ask the user" or have it automatically populated by a dashboard filter. In this case, we'll select our "ID" filter.
|
||||
|
||||

|
||||
|
||||
If you also wire that filter up to a card with a model on the dashboard, you can filter for individual records in that model, view them in the model's card, and have the action buttons auto-populate the id.
|
||||
People viewing the dashboard will be able to click the button and perform your chosen action:
|
||||
|
||||

|
||||
|
||||
You can add as many buttons as you want, and wire them up to one or more filters.
|
||||
|
||||
## Connect an action to a dashboard filter
|
||||
|
||||
When people click the action button on a dashboard, they'll be prompted to input values in the fields defined by that action. Alternatively, you can pre-fill some action fields with the values selected for a dashboard filter. Passing filter values to action fields is useful when the dashboard filters by an ID, and you want the action button to update the record associated with that ID.
|
||||
|
||||
To connect an action field to a filter on a dashboard:
|
||||
|
||||
1. On a dashboard with an action button, click the **pencil icon** edit the dashboard.
|
||||
2. [Add a filter](./filters.md) to a dashboard, connect the filter to one or more cards, and click **Done** in the bottom of the sidebar.
|
||||
3. [Add an action button to the dashboard](#add-an-action-button-to-a-dashboard) (if you haven't already).
|
||||
4. Hover over the action button and click on the **gear** icon, and select **Change action**.
|
||||
5. Click the field's dropdown to select where the action should get its value, and pick a filter to connect to (filters will be referenced in the dropdown by their name):
|
||||
|
||||

|
||||
|
||||
## Actions are unavailable for public dashboards and dashboards in guest embeds
|
||||
|
||||
While you can add actions to dashboards and use them in your Metabase, actions won't work on dashboards accessed via [public links](./introduction.md#sharing-dashboards-with-public-links), or dashboards in guest embeds.
|
||||
@@ -59,3 +59,4 @@ If you want people outside of your Metabase to use an action, you can create a [
|
||||
## Further reading
|
||||
|
||||
- [Actions](../actions/start.md)
|
||||
- [Editable tables](../data-modeling/editable-tables.md)
|
||||
|
||||
@@ -13,7 +13,7 @@ Instead of creating duplicate dashboards to show different cuts of the same data
|
||||
|
||||
## Filter and parameter widgets
|
||||
|
||||
There are two basic types of widgets you can add. The type of filter or parameter widget you choose determines how the widget works, including which fields you can connect to the widget.
|
||||
There are two basic types of widgets you can add: filters or parameters. The type of filter or parameter widget you choose determines how the widget works, including which fields you can connect to the widget.
|
||||
|
||||
### Filter widgets
|
||||
|
||||
@@ -32,11 +32,32 @@ There are two basic types of widgets you can add. The type of filter or paramete
|
||||
|
||||
- [Time grouping](#time-grouping-parameter)
|
||||
|
||||
## Adding a filter or parameter widget
|
||||
## Dashboard, header, and card widgets
|
||||
|
||||
You can add widgets to the whole dashboard, to headers, or to individual dashboard cards.
|
||||
|
||||

|
||||
|
||||
- **Dashboard-level widgets** can connect to cards on the entire dashboard, across all tabs.
|
||||
- **Header-level widgets** can only be connected to cards on the same tab.
|
||||
- **Card-level widgets** can only be connected to their individual cards.
|
||||
|
||||
By default, prefer dashboard-level widgets.
|
||||
|
||||
If, however, your dashboard grows larger, and it doesn't make sense to break the dashboard up into multiple dashboards, you can add widgets to headings and cards so people can filter in place as they scroll through sections. In these cases, you should scope the effects of a heading widget to the section immediately below.
|
||||
|
||||
If you want tab-specific widgets, you could either:
|
||||
|
||||
- Use a dashboard widget, but only connect the widget to cards on a specific tab. The dashboard widgets will only be visible when people select the tab.
|
||||
- Use a header widget.
|
||||
|
||||
These are just rules of thumb to keep the user experience predictable. Do whatever works for your use case.
|
||||
|
||||
## Add a filter or parameter to a dashboard
|
||||
|
||||

|
||||
|
||||
1. Click the **pencil icon** to enter dashboard editing mode.
|
||||
1. While viewing a dashboard, click the **pencil icon** to enter dashboard editing mode.
|
||||
2. You can add a filter or parameter widget to:
|
||||
|
||||
- **The whole dashboard**. You'll need to connect these dashboard-level filters to individual cards. Metabase will only display dashboard-level widgets if they're connected to a card on the current tab.
|
||||
@@ -47,25 +68,14 @@ There are two basic types of widgets you can add. The type of filter or paramete
|
||||
|
||||
3. Select a [filter type](#filter-and-parameter-widgets).
|
||||
4. [Connect your filter or parameter widget to one or more dashboard cards](#connecting-a-filter-or-parameter-widget-to-dashboard-cards). Only dashboard-level widgets can be connected to cards on multiple tabs.
|
||||
5. [Configure your filter](#editing-a-filter).
|
||||
5. [Configure your filter](#configure-dashboard-filters-and-parameters).
|
||||
6. **Save** your changes.
|
||||
|
||||
If the filter isn't connected to any of the cards on the current tab, Metabase won't display the filter.
|
||||
|
||||
### When to add a widget to a heading or card
|
||||
## Filter types
|
||||
|
||||
These are just rules of thumb to keep the user experience predictable. Do whatever works for your use case.
|
||||
|
||||
By default, you should add widgets to the dashboard itself.
|
||||
|
||||
If, however, your dashboard grows larger, and it doesn't make sense to break the dashboard up into multiple dashboards, you can add widgets to headings and cards so people can filter in place as they scroll through sections. In these cases, you should scope the effects of a heading widget to the section immediately below. Filters on cards should generally only update the card itself.
|
||||
|
||||
If you want tab-specific widgets, you could either:
|
||||
|
||||
- Add them to the dashboard itself, but only connect the widget to cards on a specific tab.
|
||||
- Use a heading filter.
|
||||
|
||||
## Date picker filters
|
||||
### Date picker filters
|
||||
|
||||
When picking a Date picker filter, Metabase will prompt you to pick an operator:
|
||||
|
||||
@@ -78,13 +88,68 @@ When picking a Date picker filter, Metabase will prompt you to pick an operator:
|
||||
|
||||
Single Date and Date Range will provide a calendar widget, while the other options provide slightly different dropdown interfaces for picking values. To get a widget that's just like the date picker in the query builder, choose "All Options".
|
||||
|
||||
### Issues with connecting to a date variable
|
||||
#### Issues with connecting to a date variable
|
||||
|
||||
If you get a message like "A date variable in this card can only be connected to a time type with the Single date option," you can handle it in a couple of ways.
|
||||
If you get a message like "A date variable in this card can only be connected to a time type with the Single date option," it's probably because you're connecting your filter to a [basic variable](../questions/native-editor/basic-sql-parameters.md) (and not a field filter) in a native query. You can handle this scenario in a couple of ways:
|
||||
|
||||
- **If you want a date picker where people can select multiple dates**, you'll need to change the variable in your SQL code to a [field filter](../questions/native-editor/field-filters.md).
|
||||
- **If you want the filter widget to just take a single date**, you'll need to change the dashboard filter widget's type to "Single date".
|
||||
|
||||
### Location filters
|
||||
|
||||
There are four types of Location filters to choose from.
|
||||
|
||||
- City
|
||||
- State
|
||||
- ZIP or Postal Code
|
||||
- Country
|
||||
|
||||
Additionally, you can select an operator for the location filter:
|
||||
|
||||
- **Is**. Select one or more values from a list or search box. Use **Is** when you just want to plug a value into a variable. For multiple values, the card must be either a question built using the query builder, a native/SQL question using a [field filter](../questions/native-editor/field-filters.md), or a basic variable set to accept [multiple values](../questions/native-editor/basic-sql-parameters.md#basic-variable-that-allows-people-to-select-multiple-values).
|
||||
- **Is not**. Exclude one or more specific values.
|
||||
- **Contains**. Match values that contain the entered text.
|
||||
- **Does not contain**. Filter out values that contain the entered text.
|
||||
- **Starts with**. Match values that begin with the entered text.
|
||||
- **Ends with**. Match values that end with the entered text.
|
||||
|
||||
### ID filter
|
||||
|
||||
The ID filter provides a simple input box where you can type the ID of a user, order, and so on.
|
||||
|
||||
You can give people the option to pick one value or multiple values.
|
||||
|
||||
### Number filter
|
||||
|
||||
You can choose from:
|
||||
|
||||
- Equal to
|
||||
- Not equal to
|
||||
- Between
|
||||
- Greater than or equal to
|
||||
- Less than or equal to
|
||||
|
||||
### Text or category filter
|
||||
|
||||
A flexible filter type that lets you create either a dropdown menu or an input box to filter on any text or category field in your cards. Options include:
|
||||
|
||||
- **Is**. Select one or more values from a list or search box. Use **Is** when you just want to plug a value into a variable. For multiple values, the card must be a question built using the query builder, a native/SQL question using a [field filter](../questions/native-editor/field-filters.md), or a basic variable set to [multiple values](../questions/native-editor/basic-sql-parameters.md#basic-variable-that-allows-people-to-select-multiple-values).
|
||||
- **Is not**. Exclude one or more specific values.
|
||||
- **Contains**. Match values that contain the entered text.
|
||||
- **Does not contain**. Filter out values that contain the entered text.
|
||||
- **Starts with**. Match values that begin with the entered text.
|
||||
- **Ends with**. Match values that end with the entered text.
|
||||
|
||||
You can also select how people should filter on this column:
|
||||
|
||||
- Dropdown list
|
||||
- Search box
|
||||
- Input box
|
||||
|
||||
### Boolean filter
|
||||
|
||||
A boolean filter allows people to filter data based on true/false values.
|
||||
|
||||
## Time grouping parameter
|
||||
|
||||

|
||||
@@ -105,61 +170,6 @@ Grouping options include:
|
||||
|
||||
To connect a time grouping widget to a SQL question, you'll need to insert time grouping parameters in your code. See [time grouping parameters](../questions/native-editor/time-grouping-parameters.md).
|
||||
|
||||
## Location filters
|
||||
|
||||
There are four types of Location filters to choose from.
|
||||
|
||||
- City
|
||||
- State
|
||||
- ZIP or Postal Code
|
||||
- Country
|
||||
|
||||
Additionally, you can select an operator for the location filter:
|
||||
|
||||
- **Is**. Select one or more values from a list or search box. Use **Is** when you just want to plug a value into a variable. For multiple values, the card must be either a question built using the query builder, a native/SQL question using a [field filter](../questions/native-editor/field-filters.md), or a basic variable set to accept [multiple values](../questions/native-editor/basic-sql-parameters.md#basic-variable-that-allows-people-to-select-multiple-values).
|
||||
- **Is not**. Exclude one or more specific values.
|
||||
- **Contains**. Match values that contain the entered text.
|
||||
- **Does not contain**. Filter out values that contain the entered text.
|
||||
- **Starts with**. Match values that begin with the entered text.
|
||||
- **Ends with**. Match values that end with the entered text.
|
||||
|
||||
## ID filter
|
||||
|
||||
The ID filter provides a simple input box where you can type the ID of a user, order, and so on.
|
||||
|
||||
You can give people the option to pick one value or multiple values.
|
||||
|
||||
## Number filter
|
||||
|
||||
You can choose from:
|
||||
|
||||
- Equal to
|
||||
- Not equal to
|
||||
- Between
|
||||
- Greater than or equal to
|
||||
- Less than or equal to
|
||||
|
||||
## Text or category filter
|
||||
|
||||
A flexible filter type that lets you create either a dropdown menu or an input box to filter on any text or category field in your cards. Options include:
|
||||
|
||||
- **Is**. Select one or more values from a list or search box. Use **Is** when you just want to plug a value into a variable. For multiple values, the card must be a question built using the query builder, a native/SQL question using a [field filter](../questions/native-editor/field-filters.md), or a basic variable set to [multiple values](../questions/native-editor/basic-sql-parameters.md#basic-variable-that-allows-people-to-select-multiple-values).
|
||||
- **Is not**. Exclude one or more specific values.
|
||||
- **Contains**. Match values that contain the entered text.
|
||||
- **Does not contain**. Filter out values that contain the entered text.
|
||||
- **Starts with**. Match values that begin with the entered text.
|
||||
- **Ends with**. Match values that end with the entered text.
|
||||
|
||||
You can also select how people should filter on this column:
|
||||
|
||||
- Dropdown list
|
||||
- Search box
|
||||
- Input box
|
||||
|
||||
## Boolean filter
|
||||
|
||||
A boolean filter allows people to filter data based on true/false values.
|
||||
|
||||
## Connecting a filter or parameter widget to dashboard cards
|
||||
|
||||
For each widget that you add, you'll need to tell Metabase which column the filter should update for each card.
|
||||
@@ -177,13 +187,7 @@ Which cards you can connect a filter or parameter widget to depends on:
|
||||
|
||||
For cards built in the query builder, _filters_ can be connected to fields at any stage of the query. _Time grouping parameters_ can only be connected to fields in the last stage of the query, and so to make use of time grouping parameters, your query should have a summary by a datetime field as the last stage. For example, if you have a query that's just computing `Count` of products by `Category` and `Created At`, you'll be able to connect the time grouping parameter to the `Created At` grouping, but if you modify the query to filter for results with `Count > 100`, you won't be able to connect time grouping parameter on a dashboard to that query because the last stage is now a filter.
|
||||
|
||||
## Filtering dashboards with native/SQL questions
|
||||
|
||||
To connect a dashboard filter widget to a card with a question created with the native editor, you'll need to [add a field filter or basic variable to your query](../questions/native-editor/sql-parameters.md).
|
||||
|
||||
For parameters, check out [Time grouping parameters](../questions/native-editor/time-grouping-parameters.md).
|
||||
|
||||
## Auto-connecting filters to cards
|
||||
### Auto-connecting filters to cards
|
||||
|
||||
If there are other cards that also have the field you select, Metabase will offer to automatically connect the filter to those cards too (including relevant cards on other tabs). If you later add a card with the selected field, Metabase will also try to connect that filter to the new card.
|
||||
|
||||
@@ -193,47 +197,78 @@ To undo this auto-connecting of cards, click the notification that pops up when
|
||||
|
||||
You can even wire up filters to text cards, but only if [the text card includes a variable](./introduction.md#including-variables-in-text-cards).
|
||||
|
||||
## Requiring a filter or parameter
|
||||
## Filtering dashboards with native/SQL questions
|
||||
|
||||
You can tell Metabase to require a filter so that people _must_ filter the dashboard by a certain column on a card. If you require a filter, you'll also need to supply a default value for that filter.
|
||||
To connect a dashboard filter widget to a card with a question created with the native editor, you'll need to [add a field filter or basic variable to your query](../questions/native-editor/sql-parameters.md).
|
||||
|
||||
You might want to require a filter value if:
|
||||
If your native query uses a basic variable (and not a field filter), you'll only be able to use dashboard filter types that select a single value (so for example, you won't be able to use date range filters).
|
||||
|
||||
- Your unfiltered dashboard contains questions that pull lots of data, as enforcing filtered data can prevent people from inadvertently querying tons of data and running up your data warehouse costs.
|
||||
- The questions on your dashboard don't make sense without a filter. For example, if you create a dashboard intended to view data relevant to a specific customer (as opposed to all customers), you can require a customer ID so that the dashboard only loads data for a particular customer.
|
||||
For parameters, check out [Time grouping parameters in SQL](../questions/native-editor/time-grouping-parameters.md).
|
||||
|
||||
## Editing a filter
|
||||
## Configure dashboard filters and parameters
|
||||
|
||||
To edit a dashboard filter's settings:
|
||||
To edit settings for a dashboard filter or parameter:
|
||||
|
||||
1. Click the **pencil** icon to enter dashboard editing mode.
|
||||
2. Click the filter you want to edit.
|
||||
2. Click the widget you want to edit.
|
||||
3. A widget editing sidebar will open on the right side
|
||||
|
||||
From this filter editing view, you can wire up dashboard cards to the filter, or use the filter settings sidebar to:
|
||||
From this editing view, you can wire up dashboard cards to the filter, or use the filter settings sidebar to:
|
||||
|
||||
- [Rename filter](#rename-filter)
|
||||
- [Set filter input type](#set-filter-input-type)
|
||||
- [Set a default filter value](#set-a-default-filter-value)
|
||||
- [Remove a filter](#remove-a-filter)
|
||||
- [Reorder filters](#reorder-filters)
|
||||
- [Make a multi-select filter](#make-a-multi-select-filter)
|
||||
- [Change a filter's selectable values](#change-a-filters-selectable-values)
|
||||
- [Rename a dashboard widget](#rename-a-dashboard-widget)
|
||||
- [Change widget type](#change-filter-or-parameter-type)
|
||||
- [Change filter operator](#change-filter-operator)
|
||||
- [Set filter input type](#set-filter-input-type)
|
||||
- [Make a multi-select filter](#make-a-multi-select-filter)
|
||||
- [Change a filter's selectable values](#change-a-filters-selectable-values)
|
||||
- [Set a default filter value](#set-a-default-filter-value)
|
||||
- [Make a filter required](#make-a-filter-or-parameter-required)
|
||||
- [Move a filter](#move-a-filter-or-parameter)
|
||||
- [Remove a filter](#remove-a-filter-or-parameter)
|
||||
|
||||
### Rename filter
|
||||
### Rename a dashboard widget
|
||||
|
||||
1. In dashboard edit mode, click the filter you want to edit.
|
||||
2. Change the label text.
|
||||
2. In the filter editing sidebar, change the **Label** text.
|
||||
|
||||
Renaming the filter only affects the filter's display; the label has no effect on which columns Metabase filters.
|
||||
|
||||
### Change filter or parameter type
|
||||
|
||||
You can change the [type](#filter-types) or an existing filter (for example, from a Date filter to a Text filter). Any incompatible columns connected to the widget will be disconnected.
|
||||
|
||||
To change the widget type:
|
||||
|
||||
1. When viewing a dashboard, click the **pencil** icon to **Edit the dashboard**.
|
||||
2. Click on the filter or parameter you want to edit. A widget editing sidebar should pop up on the right side.
|
||||
3. In the sidebar, change the type of filter or parameter. Changing the widget type disconnects the widget from all cards. You'll need to reconnect to the relevant columns on each card for the new type. If you've changed the widget type by mistake and want to restore the widget and its connections to cards, you can exit dashboard edit mode without saving your changes.
|
||||
|
||||
### Change filter operator
|
||||
|
||||
For some [filter types](#filter-types), you can configure the filter operator - meaning, how Metabase should use the value that a person passed in the filter. For example, for a Number filter, you can configure whether Metabase should filter for values exactly equal to the value passed in the filter, greater than the value, or less than the value.
|
||||
|
||||
To change the operator for a dashboard filter:
|
||||
|
||||
1. When viewing a dashboard, click the **pencil** icon to **Edit the dashboard**.
|
||||
2. Click on the filter you want to edit. A widget editing sidebar should pop up on the right side.
|
||||
3. In **Filter operator**, pick the filter operator. The options will depend on the filter type. Some filter types (for example, ID) don't have operator selection.
|
||||
|
||||
### Set filter input type
|
||||
|
||||
You can set a widget to display options as a dropdown menu, search box, or input box.
|
||||
|
||||
[Metadata settings](../data-modeling/metadata-editing.md#filtering) for the filtered column, including [variable settings in native queries](../questions/native-editor/filter-widgets.md#dropdown-menu-and-search), can also affect the filter's appearance.
|
||||
|
||||
For example, if the column you're connecting a filter to is configured in the [metadata settings](../data-modeling/metadata-editing.md#filtering) to display a search box instead of a dropdown, that metadata setting will take precedence.
|
||||
|
||||
To configure filter input type:
|
||||
|
||||
1. In dashboard edit mode, click the filter you want to edit.
|
||||
2. From the filter settings sidebar, set **How should people filter on this column?**:
|
||||
|
||||
- [Dropdown list](#dropdown-list)
|
||||
- [Search box](#search-box)
|
||||
- [Input box](#plain-input-box)
|
||||
- [Dropdown list](#dropdown-list)
|
||||
- [Search box](#search-box)
|
||||
- [Input box](#plain-input-box)
|
||||
|
||||
#### Dropdown list
|
||||
|
||||
@@ -256,24 +291,6 @@ An input box that lets people enter plain text (no suggestion list).
|
||||
|
||||
Useful for looking up partial matches (such as the ["contains" filter](#text-or-category-filter)) in columns that contain free text, such as comments or descriptions. The input box is the default filter widget for columns with more than 100 unique values.
|
||||
|
||||
### Set a default filter value
|
||||
|
||||
1. In dashboard edit mode, click the filter you want to edit.
|
||||
2. From the sidebar, choose a value from the **Default value** input field.
|
||||
|
||||
For example, you might want to set a default filter value like "Active," so that when people load your dashboard, they only see data for "Active" records (not "Inactive," "Canceled," and so on).
|
||||
|
||||
### Remove a filter
|
||||
|
||||
1. In dashboard edit mode, click the filter you want to edit.
|
||||
2. From the sidebar, click **Remove**.
|
||||
|
||||
If you accidentally remove a filter, just click **Cancel** in the top-right to exit dashboard edit mode without saving your changes.
|
||||
|
||||
### Reorder filters
|
||||
|
||||
In dashboard edit mode, click the grabber handle (six dots) on the left side of a filter widget, then drag the widget to a different position.
|
||||
|
||||
### Make a multi-select filter
|
||||
|
||||
1. In dashboard edit mode, click the filter you want to edit.
|
||||
@@ -285,15 +302,65 @@ A multi-select filter with the widget type [Dropdown list](#dropdown-list) or [S
|
||||
|
||||
1. In dashboard edit mode, click the filter you want to edit.
|
||||
2. From the sidebar, find **How should users filter on this column?**.
|
||||
3. Select "Dropdown list".\*
|
||||
3. Select "Dropdown list". If you don't see "Dropdown list" as an option, go to [Filter widgets: Dropdown list](#dropdown-list) for more info.
|
||||
4. Click **Edit** (to the right of "Dropdown list") to specify where the values should come from:
|
||||
|
||||
- From connected fields
|
||||
- From another model or question
|
||||
- Custom list
|
||||
|
||||

|
||||

|
||||
|
||||
\* If you don't see "Dropdown list" as an option, go to [Filter widgets: Dropdown list](#dropdown-list) for more info.
|
||||
### Set a default filter value
|
||||
|
||||
You might want to set a default filter value, like "Active," so that when people load your dashboard, they only see data for "Active" records (not "Inactive," "Canceled," and so on).
|
||||
|
||||
1. When viewing a dashboard, click the **pencil** icon to **Edit the dashboard**.
|
||||
2. Click on the filter you want to make required. A filter sidebar should pop up on the right side.
|
||||
3. In the sidebar, scroll down and choose a value from the **Default value** input field.
|
||||
|
||||
### Make a filter or parameter required
|
||||
|
||||
You can tell Metabase to require a filter so that people _must_ filter the dashboard by a certain column on a card. If you require a filter, you'll also need to supply a default value for that filter.
|
||||
|
||||
You might want to require a filter value if:
|
||||
|
||||
- Your unfiltered dashboard contains questions that pull lots of data. Enforcing filtered data can prevent people from inadvertently querying tons of data and running up your data warehouse costs.
|
||||
- The questions on your dashboard don't make sense without a filter. For example, if you create a dashboard intended to view data relevant to a specific customer (as opposed to all customers), you can require a customer ID so that the dashboard only loads data for a particular customer.
|
||||
|
||||
To require a value for a dashboard filter:
|
||||
|
||||
1. When viewing a dashboard, click the **pencil** icon to **Edit the dashboard**.
|
||||
2. Click on the filter you want to make required. A filter sidebar should pop up on the right side.
|
||||
3. In the filter settings sidebar, scroll to the toggle **Always require a value**.
|
||||
4. Specify the default value for the filter.
|
||||
|
||||
### Move a filter or parameter
|
||||
|
||||
Widgets can be displayed and applied on dashboard, header, or card level, see [Dashboard, header, and card widgets](#dashboard-header-and-card-widgets). If you want to change the level of the filter - for example, move a dashboard filter into a card filter, or a header filter into a dashboard filter - you'll need to move it in the filter settings (you can't drag filters to a different level).
|
||||
|
||||
To move a filter:
|
||||
|
||||
1. In dashboard edit mode, click the filter or parameter you want to edit.
|
||||
2. From the sidebar, click **Move filter**.
|
||||
3. Select a card or header to move the widget to. Select "Top of the page" to make the widget into a dashboard-wide filter or parameter.
|
||||
4. Make sure that the widget is connected to the right columns.
|
||||
|
||||
### Remove a filter or parameter
|
||||
|
||||
1. In dashboard edit mode, click the filter you want to edit.
|
||||
2. From the sidebar, click **Remove**.
|
||||
|
||||
If you accidentally remove a filter, just click **Cancel** in the top-right to exit dashboard edit mode without saving your changes.
|
||||
|
||||
## Reorder filters
|
||||
|
||||
To reorder dashboard-wide filters:
|
||||
|
||||
1. While viewing a dashboard, click the **pencil** icon to enter edit mode.
|
||||
2. In dashboard edit mode, click the grabber handle (six dots) on the right side of a filter widget, then drag the widget to a different position.
|
||||
|
||||
You can't drag a dashboard level filter to a header or a card. To move a dashboard-level filter to a header or a card, see [Move filter](#move-a-filter-or-parameter).
|
||||
|
||||
## Linking filters
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 110 KiB |