Compare commits
73 Commits
workspaces
...
testtestte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20cf4af60f | ||
|
|
5aeffcf173 | ||
|
|
721db4d40f | ||
|
|
442fd726a3 | ||
|
|
9e65cc60bf | ||
|
|
44ab6decf0 | ||
|
|
8b369333c3 | ||
|
|
c9e602db2f | ||
|
|
b1fb103a3f | ||
|
|
b631ee88a4 | ||
|
|
2df87d3a7f | ||
|
|
f486c415f9 | ||
|
|
ba5804041b | ||
|
|
9d6564cdaf | ||
|
|
591b453e4c | ||
|
|
5d087d0d18 | ||
|
|
719ebd0a7b | ||
|
|
ff6ec328f3 | ||
|
|
78978219a6 | ||
|
|
7c8a8f3694 | ||
|
|
45abe078f9 | ||
|
|
82b95611cf | ||
|
|
f04fb1bff2 | ||
|
|
f200fbe20c | ||
|
|
3413313d3a | ||
|
|
b5c0d93f8e | ||
|
|
71f7cbc41b | ||
|
|
c2619a5036 | ||
|
|
e4728d22b8 | ||
|
|
2d1b6c4a02 | ||
|
|
b8eb7d65f9 | ||
|
|
28b5fb79f1 | ||
|
|
53cf2ab305 | ||
|
|
135abb44a5 | ||
|
|
8699037548 | ||
|
|
03dad76c9f | ||
|
|
dca22d0865 | ||
|
|
a6f98dbd68 | ||
|
|
2fe9ebdec1 | ||
|
|
c31762e244 | ||
|
|
5585815455 | ||
|
|
2f8cc0f153 | ||
|
|
d00a66f474 | ||
|
|
19ba6a165a | ||
|
|
e0e3099787 | ||
|
|
6d62827416 | ||
|
|
5a8ec6631a | ||
|
|
d5a7dc2e49 | ||
|
|
0a3a826537 | ||
|
|
04cbeab190 | ||
|
|
248a95e704 | ||
|
|
c7e2dcd254 | ||
|
|
fafd6bb3d8 | ||
|
|
4fe0ecbeb9 | ||
|
|
e1c31c0d8e | ||
|
|
a8e592718f | ||
|
|
0b5bbd260f | ||
|
|
38c0aebd00 | ||
|
|
41b5904d52 | ||
|
|
a2975bdb44 | ||
|
|
e996561416 | ||
|
|
10e76e3040 | ||
|
|
25a97838db | ||
|
|
54c23276d7 | ||
|
|
58df0d448e | ||
|
|
35c649f88b | ||
|
|
d981e64f0a | ||
|
|
425856f92a | ||
|
|
5c87a6c12b | ||
|
|
a033c12089 | ||
|
|
39821ae14e | ||
|
|
739b062e7c | ||
|
|
ec73bb3bae |
75
.claude/skills/_shared/clojure-commands.md
Normal file
75
.claude/skills/_shared/clojure-commands.md
Normal file
@@ -0,0 +1,75 @@
|
||||
## Linting and Formatting
|
||||
|
||||
- **Lint PR:** `./bin/mage kondo-updated master` (or whatever target branch)
|
||||
- Call the command one time at the beginning, record the results, then work through the problems one at a time.
|
||||
- If the solution is obvious, then please apply the fix. Otherwise skip it.
|
||||
- If you fix all the issues (and verify by rerunning the kondo-updated command):
|
||||
- commit the change with a succinct and descriptive commit message
|
||||
- **Lint File:** `./bin/mage kondo <file or files>`
|
||||
- Use the linter as a way to know that you are adhering to conventions in place in the codebase
|
||||
- **Lint Changes:** `./bin/mage kondo-updated HEAD`
|
||||
- **Format:** `./bin/mage cljfmt-files [path]`
|
||||
|
||||
## Testing
|
||||
|
||||
- **Run a test:** `./bin/mage run-tests namespace/test-name`
|
||||
- **Run all tests in a namespace:** `./bin/mage run-tests namespace`
|
||||
|
||||
## Code Readability
|
||||
|
||||
- **Check Code Readability:** `./bin/mage -check-readable <file> [line-number]`
|
||||
- Run after every change to Clojure code
|
||||
- Check specific line first, then entire file if readable
|
||||
|
||||
## REPL Usage
|
||||
|
||||
- **Evaluating Clojure Code:** `./bin/mage -repl '<code>'`
|
||||
- See "Sending Code to the REPL" section for more details
|
||||
|
||||
### Sending Code to the REPL
|
||||
|
||||
- Send code to the metabase process REPL using: `./bin/mage -repl '(+ 1 1)'` where `(+ 1 1)` is your Clojure code.
|
||||
- See `./bin/mage -repl -h` for more details.
|
||||
- If the Metabase backend is not running, you'll see an error message with instructions on how to start it.
|
||||
|
||||
#### Working with Files and Namespaces
|
||||
|
||||
1. **Load a file and call functions with fully qualified names**:
|
||||
|
||||
To call `your.namespace/your-function` on `arg1` and `arg2`:
|
||||
|
||||
```
|
||||
./bin/mage -repl --namespace your.namespace '(your-function arg1 arg2)'
|
||||
```
|
||||
|
||||
DO NOT use "require", "load-file" etc in the code string argument.
|
||||
|
||||
#### Understanding the Response
|
||||
|
||||
The `./bin/mage -repl` command returns three separate, independent outputs:
|
||||
|
||||
- `value`: The return value of the last expression (best for data structures)
|
||||
- `stdout`: Any printed output from `println` etc. (best for messages)
|
||||
- `stderr`: Any error messages (best for warnings and errors)
|
||||
|
||||
Example call:
|
||||
|
||||
```bash
|
||||
./bin/mage -repl '(println "Hello, world!") '\''({0 1, 1 3, 2 0, 3 2} {0 2, 1 0, 2 3, 3 1})'
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```
|
||||
ns: user
|
||||
session: 32a35206-871c-4553-9bc9-f49491173d1c
|
||||
value: ({0 1, 1 3, 2 0, 3 2} {0 2, 1 0, 2 3, 3 1})
|
||||
stdout: Hello, world!
|
||||
stderr:
|
||||
```
|
||||
|
||||
For effective REPL usage:
|
||||
|
||||
- Return data structures as function return values
|
||||
- Use `println` for human-readable messages
|
||||
- Print errors to stderr
|
||||
153
.claude/skills/_shared/clojure-style-guide.md
Normal file
153
.claude/skills/_shared/clojure-style-guide.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# Metabase Clojure Style Guide
|
||||
|
||||
This guide covers Clojure and ClojureScript coding conventions for Metabase. See also: `CLOJURE_STYLE_GUIDE.adoc` for the Community Clojure Style Guide.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
**General Naming:**
|
||||
|
||||
- Acceptable abbreviations: `acc`, `i`, `pred`, `coll`, `n`, `s`, `k`, `f`
|
||||
- Use `kebab-case` for all variables, functions, and constants
|
||||
|
||||
**Function Naming:**
|
||||
|
||||
- Pure functions should be nouns describing the value they return (e.g., `age` not `calculate-age` or `get-age`)
|
||||
- Functions with side effects must end with `!`
|
||||
- Don't repeat namespace alias in function names
|
||||
|
||||
**Destructuring:**
|
||||
|
||||
- Map destructuring should use kebab-case local bindings even if the map uses `snake_case` keys
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
**Docstrings:**
|
||||
|
||||
- Every public var in `src` or `enterprise/backend/src` must have docstring
|
||||
- Format using Markdown conventions
|
||||
- Reference other vars with `[[other-var]]` not backticks
|
||||
|
||||
**Comments:**
|
||||
|
||||
- `TODO` format: `;; TODO (Name M/D/YY) -- description`
|
||||
|
||||
## Code Organization
|
||||
|
||||
**Visibility:**
|
||||
|
||||
- Make everything `^:private` unless it is used elsewhere
|
||||
- Try to organize namespaces to avoid `declare` (put public functions near the end)
|
||||
|
||||
**Size and Structure:**
|
||||
|
||||
- Break up functions > 20 lines
|
||||
- Lines ≤ 120 characters
|
||||
- No blank lines within definition forms (except pairwise `let`/`cond`)
|
||||
|
||||
## Style Conventions
|
||||
|
||||
**Keywords and Metadata:**
|
||||
|
||||
- Prefer namespaced keywords for internal use: `:query-type/normal` not `:normal`
|
||||
- Tag variables with `:arglists` metadata if they're functions but wouldn't otherwise have it
|
||||
|
||||
## Tests
|
||||
|
||||
**Organization:**
|
||||
|
||||
- Break large tests into separate `deftest` forms for logically separate test cases
|
||||
- Test names should end in `-test` or `-test-<number>`
|
||||
|
||||
**Performance:**
|
||||
|
||||
- Mark pure function tests `^:parallel`
|
||||
|
||||
## Modules
|
||||
|
||||
**OSS Modules:**
|
||||
|
||||
- Follow `metabase.<module>.*` pattern
|
||||
- Source in `src/metabase/<module>/`
|
||||
|
||||
**Enterprise Modules:**
|
||||
|
||||
- Follow `metabase-enterprise.<module>.*` pattern
|
||||
- Source in `enterprise/backend/src/metabase_enterprise/<module>/`
|
||||
|
||||
**Module Structure:**
|
||||
|
||||
- REST API endpoints go in `<module>.api` or `<module>.api.*` namespaces
|
||||
- Put module public API in `<module>.core` using Potemkin imports
|
||||
- Put Toucan models in `<module>.models.*`
|
||||
- Put settings in `<module>.settings`
|
||||
- Put schemas in `<module>.schema`
|
||||
|
||||
**Module Linters:**
|
||||
|
||||
- Do not cheat module linters with `:clj-kondo/ignore [:metabase/modules]`
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
**Required Elements:**
|
||||
|
||||
- All new endpoints must have response schemas (`:- <schema>` after route string)
|
||||
- All endpoints need Malli schemas for parameters (detailed and complete)
|
||||
- All new REST API endpoints MUST HAVE TESTS
|
||||
|
||||
**Naming Conventions:**
|
||||
|
||||
- Query parameters use kebab-case
|
||||
- Request bodies use `snake_case`
|
||||
- Routes use singular nouns (e.g., `/api/dashboard/:id`)
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- `GET` endpoints should not have side effects (except analytics)
|
||||
- `defendpoint` forms should be small wrappers around Toucan model code
|
||||
|
||||
## MBQL (Metabase Query Language)
|
||||
|
||||
**Restrictions:**
|
||||
|
||||
- No raw MBQL introspection outside of `lib`, `lib-be`, or `query-processor` modules
|
||||
- Use Lib and MBQL 5 in new source code; avoid legacy MBQL
|
||||
|
||||
## Database and Models
|
||||
|
||||
**Naming:**
|
||||
|
||||
- Model names and table names should be singular nouns
|
||||
- Application database uses `snake_case` identifiers
|
||||
|
||||
**Best Practices:**
|
||||
|
||||
- Use `t2/select-one-fn` instead of fetching entire rows for one column
|
||||
- Put correct behavior in Toucan methods, not separate helper functions
|
||||
|
||||
## Drivers
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- New driver multimethods must be mentioned in `docs/developers-guide/driver-changelog.md`
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- Driver implementations should pass `driver` argument to other driver multimethods
|
||||
- Don't hardcode driver names in implementations
|
||||
- Minimize logic inside `read-column-thunk` in JDBC-based drivers
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
**Examples:**
|
||||
|
||||
- Example data should be bird-themed if possible
|
||||
|
||||
**Linter Suppressions:**
|
||||
|
||||
- Use proper format for kondo suppressions
|
||||
- No `#_:clj-kondo/ignore` (keyword form)
|
||||
|
||||
**Configurable Options:**
|
||||
|
||||
- Don't define configurable options that can only be set with environment variables
|
||||
- Use `:internal` `defsetting` instead
|
||||
8
.claude/skills/_shared/development-workflow.md
Normal file
8
.claude/skills/_shared/development-workflow.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Autonomous Development Workflow
|
||||
|
||||
- Do not attempt to read or edit files outside the project folder
|
||||
- Add failing tests first, then fix them
|
||||
- Work autonomously in small, testable increments
|
||||
- Run targeted tests, and lint continuously during development
|
||||
- Prioritize understanding existing patterns before implementing
|
||||
- Don't commit changes, leave it for the user to review and make commits
|
||||
84
.claude/skills/_shared/metabase-style-guide.md
Normal file
84
.claude/skills/_shared/metabase-style-guide.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Metabase Writing Style Guide
|
||||
|
||||
## Core Principles
|
||||
|
||||
Write like you're talking to a colleague. Be conversational, not formal. Get people what they need quickly. Know your audience and match the complexity.
|
||||
|
||||
## Tone and Voice
|
||||
|
||||
**Do:**
|
||||
|
||||
- Use contractions ("can't" not "cannot")
|
||||
- Say "people" or "companies" instead of "users"
|
||||
- Be friendly but not peppy
|
||||
- Acknowledge limitations honestly ("that's on us, not them")
|
||||
- Jokes and Easter eggs are okay (permit them, don't suggest them)
|
||||
|
||||
**Don't:**
|
||||
|
||||
- Use exclamation points excessively
|
||||
- Rely on tired tropes about nerdiness
|
||||
- Use corporate jargon ("utilize", "offerings", "actionable insights")
|
||||
- Tell people something is cool (show them instead)
|
||||
|
||||
## Structure and Clarity
|
||||
|
||||
**Lead with the important stuff:**
|
||||
|
||||
- Most important information first
|
||||
- Lead with the ask, then provide context
|
||||
- Cut text that adds little value (when in doubt, cut it)
|
||||
- Each paragraph should have one clear purpose
|
||||
|
||||
**Make headings do the work:**
|
||||
|
||||
- Convey your actual point, not just the topic
|
||||
- "Use headings to highlight key points" not "How to write a good heading"
|
||||
- Use sentence case, no punctuation except question marks
|
||||
- No links in headings unless entire heading is a link
|
||||
|
||||
## Instructions and Examples
|
||||
|
||||
**Tell people what to do:**
|
||||
|
||||
- Give the action before explaining why
|
||||
- Put commands in execution order
|
||||
- Provide context, steps, and reasoning
|
||||
- Don't describe tasks as "easy" or "simple"
|
||||
|
||||
**Answer unasked questions:**
|
||||
|
||||
- Include answers to "dumb" questions you had when learning
|
||||
- Don't literally include the question, just give the answer
|
||||
|
||||
## Formatting
|
||||
|
||||
**Code vs. UI elements:**
|
||||
|
||||
- Backticks for code, variable names, parameters only
|
||||
- **Bold** for UI elements and labels (e.g., "Click the **Save** button")
|
||||
- Don't use backticks or quotes for UI elements
|
||||
|
||||
## Writing Mechanics
|
||||
|
||||
- Limit pronouns when introducing new terms (repeat the term to reinforce it)
|
||||
- Ampersands only in proper nouns, never as substitute for "and"
|
||||
|
||||
## Terminology Preferences
|
||||
|
||||
Use familiar terms from tools people already know:
|
||||
|
||||
- "Summarize" (like Excel) instead of "aggregate"
|
||||
- "Take a look at" instead of "reference"
|
||||
- "Filter" instead of technical database terms when appropriate
|
||||
|
||||
## Red Flags
|
||||
|
||||
Avoid these patterns:
|
||||
|
||||
- Multiple exclamation points
|
||||
- Linking "here"
|
||||
- Bullet lists to explain (use prose)
|
||||
- Numbers that will change (guard against change)
|
||||
- Describing things as "easy" or "simple"
|
||||
- Stock photography-worthy content
|
||||
21
.claude/skills/_shared/typescript-commands.md
Normal file
21
.claude/skills/_shared/typescript-commands.md
Normal file
@@ -0,0 +1,21 @@
|
||||
## Linting and Formatting
|
||||
|
||||
- **Lint:** `yarn lint-eslint-pure`
|
||||
- Run ESLint on the codebase
|
||||
- **Format:** `yarn prettier`
|
||||
- Format code using Prettier
|
||||
- **Type Check:** `yarn type-check-pure`
|
||||
- Run TypeScript type checking
|
||||
|
||||
## Testing
|
||||
|
||||
### JavaScript/TypeScript Tests
|
||||
|
||||
- **Test a specific file:** `yarn test-unit-keep-cljs path/to/file.unit.spec.js`
|
||||
- **Test by pattern:** `yarn test-unit-keep-cljs -t "pattern"`
|
||||
- Runs tests matching the given pattern
|
||||
|
||||
### ClojureScript Tests
|
||||
|
||||
- **Test ClojureScript:** `yarn test-cljs`
|
||||
- Run ClojureScript tests
|
||||
144
.claude/skills/clojure-review/SKILL.md
Normal file
144
.claude/skills/clojure-review/SKILL.md
Normal file
@@ -0,0 +1,144 @@
|
||||
---
|
||||
name: clojure-review
|
||||
description: Review Clojure and ClojureScript code changes for compliance with Metabase coding standards, style violations, and code quality issues. Use when reviewing pull requests or diffs containing Clojure/ClojureScript code.
|
||||
allowed-tools: Read, Grep, Bash, Glob
|
||||
---
|
||||
|
||||
# Clojure Code Review Skill
|
||||
|
||||
@./../_shared/clojure-style-guide.md
|
||||
@./../_shared/clojure-commands.md
|
||||
|
||||
## Review guidelines
|
||||
|
||||
**What to flag:**
|
||||
|
||||
- Check compliance with the Metabase Clojure style guide (included above)
|
||||
- If `CLOJURE_STYLE_GUIDE.adoc` exists in the working directory, also check compliance with the community Clojure style guide
|
||||
- Flag all style guide violations
|
||||
|
||||
**What NOT to post:**
|
||||
|
||||
- Do not post comments congratulating someone for trivial changes or for following style guidelines
|
||||
- Do not post comments confirming things "look good" or telling them they did something correctly
|
||||
- Only post comments about style violations or potential issues
|
||||
|
||||
Example bad code review comments to avoid:
|
||||
|
||||
> This TODO comment is properly formatted with author and date - nice work!
|
||||
|
||||
> Good addition of limit 1 to the query - this makes the test more efficient without changing its behavior.
|
||||
|
||||
> The kondo ignore comment is appropriately placed here
|
||||
|
||||
> Test name properly ends with -test as required by the style guide.
|
||||
|
||||
**Special cases:**
|
||||
|
||||
- Do not post comments about missing parentheses (these will be caught by the linter)
|
||||
|
||||
## Quick review checklist
|
||||
|
||||
Use this to scan through changes efficiently:
|
||||
|
||||
### Naming
|
||||
|
||||
- [ ] Descriptive names (no `tbl`, `zs'`)
|
||||
- [ ] Pure functions named as nouns describing their return value
|
||||
- [ ] `kebab-case` for all variables and functions
|
||||
- [ ] Side-effect functions end with `!`
|
||||
- [ ] No namespace-alias repetition in function names
|
||||
|
||||
### Documentation
|
||||
|
||||
- [ ] Public vars in `src` or `enterprise/backend/src` have useful docstrings
|
||||
- [ ] Docstrings use Markdown conventions
|
||||
- [ ] References use `[[other-var]]` not backticks
|
||||
- [ ] `TODO` comments include author and date: `;; TODO (Name 1/1/25) -- description`
|
||||
|
||||
### Code Organization
|
||||
|
||||
- [ ] Everything `^:private` unless used elsewhere
|
||||
- [ ] No `declare` when avoidable (public functions near end)
|
||||
- [ ] Functions under 20 lines when possible
|
||||
- [ ] No blank lines within definition forms (except pairwise constructs in `let`/`cond`)
|
||||
- [ ] Lines ≤ 120 characters
|
||||
|
||||
### Tests
|
||||
|
||||
- [ ] Separate `deftest` forms for distinct test cases
|
||||
- [ ] Pure tests marked `^:parallel`
|
||||
- [ ] Test names end in `-test` or `-test-<number>`
|
||||
|
||||
### Modules
|
||||
|
||||
- [ ] Correct module patterns (OSS: `metabase.<module>.*`, EE: `metabase-enterprise.<module>.*`)
|
||||
- [ ] API endpoints in `<module>.api` namespaces
|
||||
- [ ] Public API in `<module>.core` with Potemkin
|
||||
- [ ] No cheating module linters with `:clj-kondo/ignore [:metabase/modules]`
|
||||
|
||||
### REST API
|
||||
|
||||
- [ ] Response schemas present (`:- <schema>`)
|
||||
- [ ] Query params use kebab-case, bodies use `snake_case`
|
||||
- [ ] Routes use singular nouns (e.g., `/api/dashboard/:id`)
|
||||
- [ ] `GET` has no side effects (except analytics)
|
||||
- [ ] Malli schemas detailed and complete
|
||||
- [ ] All new endpoints have tests
|
||||
|
||||
### MBQL
|
||||
|
||||
- [ ] No raw MBQL manipulation outside `lib`, `lib-be`, or `query-processor` modules
|
||||
- [ ] Uses Lib and MBQL 5, not legacy MBQL
|
||||
|
||||
### Database
|
||||
|
||||
- [ ] Model and table names are singular nouns
|
||||
- [ ] Uses `t2/select-one-fn` instead of selecting full rows for one column
|
||||
- [ ] Logic in Toucan methods, not helper functions
|
||||
|
||||
### Drivers
|
||||
|
||||
- [ ] New multimethods documented in `docs/developers-guide/driver-changelog.md`
|
||||
- [ ] Passes `driver` argument to other driver methods (no hardcoded driver names)
|
||||
- [ ] Minimal logic in `read-column-thunk`
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- [ ] Example data is bird-themed when possible
|
||||
- [ ] Kondo linter suppressions use proper format (not `#_:clj-kondo/ignore` keyword form)
|
||||
|
||||
## Pattern matching table
|
||||
|
||||
Quick scan for common issues:
|
||||
|
||||
| Pattern | Issue |
|
||||
| -------------------------------------------- | ----------------------------------------------------------- |
|
||||
| `calculate-age`, `get-user` | Pure functions should be nouns: `age`, `user` |
|
||||
| `update-db`, `save-model` | Missing `!` for side effects: `update-db!`, `save-model!` |
|
||||
| `snake_case_var` | Should use kebab-case |
|
||||
| Public var without docstring | Add docstring explaining purpose |
|
||||
| `;; TODO fix this` | Missing author/date: `;; TODO (Name 1/1/25) -- description` |
|
||||
| `(defn foo ...)` in namespace used elsewhere | Should be `(defn ^:private foo ...)` |
|
||||
| Function > 20 lines | Consider breaking up into smaller functions |
|
||||
| `/api/dashboards/:id` | Use singular: `/api/dashboard/:id` |
|
||||
| Query params with `snake_case` | Use kebab-case for query params |
|
||||
| New API endpoint without tests | Add tests for the endpoint |
|
||||
|
||||
## Feedback format examples
|
||||
|
||||
**For style violations:**
|
||||
|
||||
> This pure function should be named as a noun describing its return value. Consider `user` instead of `get-user`.
|
||||
|
||||
**For missing documentation:**
|
||||
|
||||
> This public var needs a docstring explaining its purpose, inputs, and outputs.
|
||||
|
||||
**For organization issues:**
|
||||
|
||||
> This function is only used in this namespace, so it should be marked `^:private`.
|
||||
|
||||
**For API conventions:**
|
||||
|
||||
> Query parameters should use kebab-case. Change `user_id` to `user-id`.
|
||||
59
.claude/skills/clojure-write/SKILL.md
Normal file
59
.claude/skills/clojure-write/SKILL.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: clojure-write
|
||||
description: Guide Clojure and ClojureScript development using REPL-driven workflow, coding conventions, and best practices. Use when writing, developing, or refactoring Clojure/ClojureScript code.
|
||||
---
|
||||
|
||||
# Clojure Development Skill
|
||||
|
||||
@./../_shared/development-workflow.md
|
||||
@./../_shared/clojure-style-guide.md
|
||||
@./../_shared/clojure-commands.md
|
||||
|
||||
## REPL-Driven Development Workflow
|
||||
|
||||
- Start with small, fundamental functions:
|
||||
- Identify the core features or functionalities required for your task.
|
||||
- Break each feature down into the smallest, most basic functions that can be developed and tested independently.
|
||||
- Write and test in the REPL:
|
||||
- Write the code for each small function directly in the REPL (Read-Eval-Print Loop).
|
||||
- Test it thoroughly with a variety of inputs, including typical use cases and relevant edge cases, to ensure it
|
||||
behaves as expected.
|
||||
- Integrate into source code:
|
||||
- Once a function works correctly in the REPL, move it from the REPL environment into your source code files (e.g.,
|
||||
within appropriate namespaces).
|
||||
- Gradually increase complexity:
|
||||
- Build upon tested, basic functions to create more complex functions or components.
|
||||
- Compose smaller functions together, testing each new composition in the REPL to verify correctness step by step.
|
||||
- Ensure dependency testing:
|
||||
- Make sure every function is fully tested in the REPL before it is depended upon by other functions.
|
||||
- This ensures that each layer of your application is reliable before you build on it.
|
||||
- Use the REPL fully:
|
||||
- Use the REPL as your primary tool to experiment with different approaches, iterate quickly, and get immediate
|
||||
feedback on your code.
|
||||
- Follow functional programming principles:
|
||||
- Keep functions small, focused, and composable.
|
||||
- Use Clojure's functional programming features—like immutability, higher-order functions, and the standard
|
||||
library—to write concise, effective code.
|
||||
|
||||
## How to Evaluate Code
|
||||
|
||||
### Bottom-up Dev Loop
|
||||
|
||||
1. Write code into a file.
|
||||
2. Evaluate the file's namespace and make sure it loads correctly with:
|
||||
|
||||
```
|
||||
./bin/mage -repl --namespace metabase.app-db.connection
|
||||
```
|
||||
|
||||
3. Call functions in the namespace with test inputs, and observe that the outputs are correct
|
||||
Feel free to copy these REPL session trials into actual test cases using `deftest` and `is`.
|
||||
4. Once you know these functions are good, return to 1, and compose them into the task that you need to build.
|
||||
|
||||
## Critical Rules for Editing
|
||||
|
||||
- Be careful with parentheses counts when editing Clojure code
|
||||
- After EVERY change to Clojure code, verify readability with `-check-readable`
|
||||
- End all files with a newline
|
||||
- When editing tabular code, where the columns line up, try to keep them aligned
|
||||
- Spaces on a line with nothing after it is not allowed
|
||||
168
.claude/skills/docs-review/SKILL.md
Normal file
168
.claude/skills/docs-review/SKILL.md
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
name: docs-review
|
||||
description: Review documentation changes for compliance with the Metabase writing style guide. Use when reviewing pull requests, files, or diffs containing documentation markdown files.
|
||||
allowed-tools: Read, Grep, Bash, Glob
|
||||
---
|
||||
|
||||
# Documentation Review Skill
|
||||
|
||||
@./../_shared/metabase-style-guide.md
|
||||
|
||||
## Review mode detection
|
||||
|
||||
**IMPORTANT: Before starting the review, determine which mode to use:**
|
||||
|
||||
1. **PR review mode**: If the `mcp__github__create_pending_pull_request_review` tool is available, you are reviewing a GitHub PR
|
||||
- Use the pending review workflow to post all issues as one cohesive review
|
||||
- Follow the workflow steps in "PR review mode format" below
|
||||
|
||||
2. **Local review mode**: If the MCP tool is NOT available, output issues in the conversation
|
||||
- Format all issues in a numbered markdown list (as described in "Feedback format" below)
|
||||
|
||||
## Review process
|
||||
|
||||
1. **Detect review mode** - Check if `mcp__github__create_pending_pull_request_review` is available
|
||||
2. Read the changes through once to understand intent
|
||||
3. Check all issues that violate style guide or significantly impact readability
|
||||
4. Only flag issues worth mentioning - if it won't make a material difference to the reader, skip it
|
||||
5. **REQUIRED: Number ALL feedback sequentially** - Start from Issue 1 and increment for each issue found
|
||||
|
||||
## Review checklist
|
||||
|
||||
Run through the diff looking for these issues:
|
||||
|
||||
**Tone and voice:**
|
||||
|
||||
- [ ] Formal/corporate language ("utilize" not "use", "offerings", etc.)
|
||||
- [ ] "Users" instead of "people" or "companies"
|
||||
- [ ] Excessive exclamation points or overly peppy tone
|
||||
- [ ] Telling readers something is cool instead of showing them
|
||||
|
||||
**Structure and clarity:**
|
||||
|
||||
- [ ] Important information buried instead of leading
|
||||
- [ ] Verbose text that adds little value
|
||||
- [ ] Paragraphs without clear purpose
|
||||
- [ ] Vague headings that don't convey the point
|
||||
- [ ] Instructions explain "why" before telling "what to do"
|
||||
- [ ] Tasks described as "easy" or "simple"
|
||||
|
||||
**Links and references:**
|
||||
|
||||
- [ ] Linking the word "here" instead of descriptive text
|
||||
- [ ] Links in headings (unless entire heading is a link)
|
||||
|
||||
**Formatting:**
|
||||
|
||||
- [ ] Backticks on UI elements (should use **bold**)
|
||||
- [ ] Backticks used as quotation marks
|
||||
- [ ] Ampersands as "and" substitute (except proper nouns)
|
||||
- [ ] Inconsistent list formatting
|
||||
|
||||
**Code and examples:**
|
||||
|
||||
- [ ] Code examples that don't work or would error
|
||||
- [ ] Commands not in execution order
|
||||
- [ ] Full-width screenshots instead of scoped UI elements
|
||||
- [ ] Excessive or unnecessary images
|
||||
|
||||
**Sentence construction:**
|
||||
|
||||
- [ ] Overuse of pronouns when introducing new terms
|
||||
|
||||
## Quick scan table
|
||||
|
||||
| Pattern | Issue |
|
||||
| ----------------------------- | --------------------------------------------- |
|
||||
| `Button name` or `UI element` | Should use **bold** not backticks |
|
||||
| we can do X, our feature | Should be "Metabase" or "it" |
|
||||
| click here, read more here | Need descriptive link text |
|
||||
| easy, simple, just | Remove condescending qualifiers |
|
||||
| users | Should be "people" or "companies" if possible |
|
||||
|
||||
## Feedback format
|
||||
|
||||
**MANDATORY REQUIREMENT: Every single issue MUST be numbered sequentially starting from Issue 1.**
|
||||
|
||||
This numbered format is NON-NEGOTIABLE. It allows users to efficiently reference specific issues (e.g., "fix issues 1, 3, and 5") and track which feedback has been addressed.
|
||||
|
||||
### Local review mode format
|
||||
|
||||
When outputting issues in the conversation (local mode), use this format:
|
||||
|
||||
```markdown
|
||||
## Issues
|
||||
|
||||
**Issue 1: [Brief title]**
|
||||
Line X: Succinct description of the issue
|
||||
[code or example]
|
||||
Suggested fix or succinct explanation
|
||||
|
||||
**Issue 2: [Brief title]**
|
||||
Line Y: Description of the issue
|
||||
Suggested fix or explanation
|
||||
|
||||
**Issue 3: [Brief title]**
|
||||
...
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
|
||||
> **Issue 1: Backticks on UI elements**
|
||||
> Line 42: This uses backticks for the UI element. Use **bold** instead: **Filter** not `Filter`.
|
||||
|
||||
> **Issue 2: Formal tone**
|
||||
> Line 15: This could be more conversational. Consider: "You can't..." instead of "You cannot..."
|
||||
|
||||
> **Issue 3: Vague heading**
|
||||
> Line 8: The heading could be more specific. Try stating the point directly: "Run migrations before upgrading" vs "Upgrade process"
|
||||
|
||||
### PR review mode format
|
||||
|
||||
When posting to GitHub (PR mode), use the **pending review workflow**:
|
||||
|
||||
**Workflow steps:**
|
||||
|
||||
1. **Start a review**: Use `mcp__github__create_pending_pull_request_review` to begin a pending review
|
||||
- This creates a draft review that won't be visible until submitted
|
||||
|
||||
2. **Get diff information**: Use `mcp__github__get_pull_request_diff` to understand the code changes and line numbers
|
||||
- This helps you determine the correct file paths and line numbers for comments
|
||||
|
||||
3. **Identify ALL issues**: Read through all changes and identify every issue worth mentioning
|
||||
- Collect all issues before posting any comments
|
||||
- Number them sequentially (Issue 1, Issue 2, Issue 3, etc.)
|
||||
|
||||
4. **Add review comments**: Use `mcp__github__add_pull_request_review_comment_to_pending_review` for each issue
|
||||
- **CRITICAL**: Post ALL comments in a SINGLE response using multiple tool calls in parallel
|
||||
- Each comment should reference a specific file path and line number from the diff
|
||||
- Start each comment body with `**Issue N: [Brief title]**`
|
||||
- Include the description and suggested fix
|
||||
|
||||
5. **Submit the review**: Use `mcp__github__submit_pending_pull_request_review` to publish all comments at once
|
||||
- Use event type `"COMMENT"` (NOT "REQUEST_CHANGES") to make it non-blocking
|
||||
- **Do NOT include a body message** - Leave the body empty or omit it entirely
|
||||
- All comments will appear together as one cohesive review
|
||||
|
||||
**Comment format example:**
|
||||
```
|
||||
**Issue 1: Backticks on UI elements**
|
||||
|
||||
This uses backticks for the UI element. Use **bold** instead: **Filter** not `Filter`.
|
||||
```
|
||||
|
||||
**IMPORTANT**:
|
||||
- Each issue gets its own review comment attached to the pending review
|
||||
- Number ALL comments sequentially (Issue 1, Issue 2, Issue 3, etc.)
|
||||
- Always start the comment body with `**Issue N: [Brief title]**`
|
||||
- **MUST add all comments in parallel in a single response** - Do NOT add them one after another in separate responses
|
||||
- Do NOT output a summary message to the conversation - only post GitHub review comments
|
||||
- When submitting the review, do NOT include a body parameter (or leave it empty) to avoid cluttering the PR with summary text
|
||||
- The review will appear as a single review with multiple comments when submitted
|
||||
|
||||
## Final check
|
||||
|
||||
1. Remove any issues from your assessment that won't make a material difference to the reader if addressed. Only flag issues worth the author's time to fix.
|
||||
2. **Verify all issues are numbered sequentially** starting from Issue 1 with no gaps in numbering.
|
||||
3. Confirm the format exactly matches: `**Issue N: [Brief title]**` where N is the issue number.
|
||||
4. **In PR mode**: Verify each issue was posted as a separate GitHub comment (not output to conversation).
|
||||
96
.claude/skills/docs-write/SKILL.md
Normal file
96
.claude/skills/docs-write/SKILL.md
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
name: docs-write
|
||||
description: Write documentation following Metabase's conversational, clear, and user-focused style. Use when creating or editing documentation files (markdown, MDX, etc.).
|
||||
allowed-tools: Read, Write, Grep, Bash, Glob
|
||||
---
|
||||
|
||||
# Documentation Writing Skill
|
||||
|
||||
@./../_shared/metabase-style-guide.md
|
||||
|
||||
## When writing documentation
|
||||
|
||||
### Start here
|
||||
|
||||
1. **Who is this for?** Match complexity to audience. Don't oversimplify hard things or overcomplicate simple ones.
|
||||
2. **What do they need?** Get them to the answer fast. Nobody wants to be in docs longer than necessary.
|
||||
3. **What did you struggle with?** Those common questions you had when learning? Answer them (without literally including the question).
|
||||
|
||||
### Writing process
|
||||
|
||||
**Draft:**
|
||||
|
||||
- Write out the steps/explanation as you'd tell a colleague
|
||||
- Lead with what to do, then explain why
|
||||
- Use headings that state your point: "Set SAML before adding users" not "SAML configuration timing"
|
||||
|
||||
**Edit:**
|
||||
|
||||
- Read aloud. Does it sound like you talking? If it's too formal, simplify.
|
||||
- Cut anything that doesn't directly help the reader
|
||||
- Check each paragraph has one clear purpose
|
||||
- Verify examples actually work (don't give examples that error)
|
||||
|
||||
**Polish:**
|
||||
|
||||
- Make links descriptive (never "here")
|
||||
- Backticks only for code/variables, **bold** for UI elements
|
||||
- American spelling, serial commas
|
||||
- Keep images minimal and scoped tight
|
||||
|
||||
**Format:**
|
||||
|
||||
- Run prettier on the file after making edits: `yarn prettier --write <file-path>`
|
||||
- This ensures consistent formatting across all documentation
|
||||
|
||||
### Common patterns
|
||||
|
||||
**Instructions:**
|
||||
|
||||
```markdown
|
||||
Run:
|
||||
\`\`\`
|
||||
command-to-run
|
||||
\`\`\`
|
||||
|
||||
Then:
|
||||
\`\`\`
|
||||
next-command
|
||||
\`\`\`
|
||||
|
||||
This ensures you're getting the latest changes.
|
||||
```
|
||||
|
||||
Not: "(remember to run X before Y...)" buried in a paragraph.
|
||||
|
||||
**Headings:**
|
||||
|
||||
- "Use environment variables for configuration" ✅
|
||||
- "Environment variables" ❌ (too vague)
|
||||
- "How to use environment variables for configuration" ❌ (too wordy)
|
||||
|
||||
**Links:**
|
||||
|
||||
- "Check out the [SAML documentation](link)" ✅
|
||||
- "Read the docs [here](link)" ❌
|
||||
|
||||
### Watch out for
|
||||
|
||||
- Describing tasks as "easy" (you don't know the reader's context)
|
||||
- Using "we" when talking about Metabase features (use "Metabase" or "it")
|
||||
- Formal language: "utilize", "reference", "offerings"
|
||||
- Too peppy: multiple exclamation points
|
||||
- Burying the action in explanation
|
||||
- Code examples that don't work
|
||||
- Numbers that will become outdated
|
||||
|
||||
### Quick reference
|
||||
|
||||
| Write This | Not This |
|
||||
| -------------------------- | ------------------ |
|
||||
| people, companies | users |
|
||||
| summarize | aggregate |
|
||||
| take a look at | reference |
|
||||
| can't, don't | cannot, do not |
|
||||
| **Filter** button | \`Filter\` button |
|
||||
| Check out [the docs](link) | Click [here](link) |
|
||||
19
.claude/skills/typescript-review/SKILL.md
Normal file
19
.claude/skills/typescript-review/SKILL.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: typescript-review
|
||||
description: Review TypeScript and JavaScript code changes for compliance with Metabase coding standards, style violations, and code quality issues. Use when reviewing pull requests or diffs containing TypeScript/JavaScript code.
|
||||
allowed-tools: Read, Grep, Bash, Glob
|
||||
---
|
||||
|
||||
# TypeScript/JavaScript Code Review Skill
|
||||
|
||||
@./../_shared/typescript-commands.md
|
||||
|
||||
## Code Review Guidelines
|
||||
|
||||
Review pull requests with a focus on:
|
||||
|
||||
- Compliance with project coding standards and conventions
|
||||
- Code quality and best practices
|
||||
- Clear and correct JSDoc comments
|
||||
- Type safety and proper TypeScript usage
|
||||
- React best practices (when applicable)
|
||||
9
.claude/skills/typescript-write/SKILL.md
Normal file
9
.claude/skills/typescript-write/SKILL.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
name: typescript-write
|
||||
description: Write TypeScript and JavaScript code following Metabase coding standards and best practices. Use when developing or refactoring TypeScript/JavaScript code.
|
||||
---
|
||||
|
||||
# TypeScript/JavaScript Development Skill
|
||||
|
||||
@./../_shared/development-workflow.md
|
||||
@./../_shared/typescript-commands.md
|
||||
@@ -208,8 +208,8 @@
|
||||
|
||||
auth-identity
|
||||
{:team "UX West"
|
||||
:api #{metabase.auth-identity.core
|
||||
metabase.auth-identity.init}
|
||||
:api #{metabase.auth-identity.core
|
||||
metabase.auth-identity.init}
|
||||
:uses #{channel
|
||||
events
|
||||
login-history
|
||||
@@ -989,7 +989,7 @@
|
||||
metabase.queries.core
|
||||
metabase.queries.init
|
||||
metabase.queries.models.query ; TODO FIXME
|
||||
metabase.queries.schema} ; TODO FIXME
|
||||
metabase.queries.schema} ; TODO FIXME
|
||||
:uses #{analytics
|
||||
analyze
|
||||
api
|
||||
@@ -1118,7 +1118,7 @@
|
||||
:uses #{api
|
||||
config
|
||||
embedding
|
||||
permissions ; FIXME
|
||||
permissions ; FIXME
|
||||
session
|
||||
settings
|
||||
users
|
||||
@@ -1757,7 +1757,7 @@
|
||||
|
||||
enterprise/auth-identity
|
||||
{:team "UX West"
|
||||
:api #{}
|
||||
:api #{}
|
||||
:uses #{premium-features}}
|
||||
|
||||
enterprise/auth-provider
|
||||
@@ -2201,6 +2201,19 @@
|
||||
settings
|
||||
util}}
|
||||
|
||||
enterprise/support-access-grants
|
||||
{:team "UX West"
|
||||
:api #{metabase-enterprise.support-access-grants.api
|
||||
metabase-enterprise.support-access-grants.init}
|
||||
:uses #{api
|
||||
auth-identity
|
||||
events
|
||||
models
|
||||
request
|
||||
settings
|
||||
system
|
||||
util}}
|
||||
|
||||
enterprise/transforms
|
||||
{:team "Querying"
|
||||
:api #{metabase-enterprise.transforms.api
|
||||
|
||||
@@ -344,5 +344,11 @@ module.exports = {
|
||||
"import/no-commonjs": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/*.stories.tsx", "**/preview.tsx"],
|
||||
rules: {
|
||||
"import/no-default-export": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
5
.github/actions/get-cache-keys/action.yml
vendored
5
.github/actions/get-cache-keys/action.yml
vendored
@@ -13,6 +13,9 @@ outputs:
|
||||
eslint-cache-key:
|
||||
description: "The cache key for Eslint cache"
|
||||
value: ${{ steps.keys-factory.outputs.eslint-cache-key }}
|
||||
cypress-cache-key:
|
||||
description: "The cache key for Cypress cache"
|
||||
value: ${{ steps.keys-factory.outputs.cypress-cache-key }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -23,10 +26,12 @@ runs:
|
||||
M2_CACHE_PREFIX='M2'
|
||||
NODE_CACHE_PREFIX='node-modules'
|
||||
ESLINT_CACHE_PREFIX='eslint'
|
||||
CYPRESS_CACHE_PREFIX='cypress'
|
||||
WEEK_OF_YEAR=$(/bin/date "+%G-%V")
|
||||
CACHE_SUFFIX="${{ runner.os }}-$WEEK_OF_YEAR"
|
||||
|
||||
echo "m2-cache-key=$M2_CACHE_PREFIX-$CACHE_SUFFIX" >> $GITHUB_OUTPUT
|
||||
echo "node-cache-key=$NODE_CACHE_PREFIX-$CACHE_SUFFIX" >> $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
|
||||
|
||||
5
.github/actions/prepare-backend/action.yml
vendored
5
.github/actions/prepare-backend/action.yml
vendored
@@ -25,11 +25,6 @@ runs:
|
||||
curl -O https://download.clojure.org/install/linux-install-${{ inputs.clojure-version }}.sh &&
|
||||
sudo bash ./linux-install-${{ inputs.clojure-version }}.sh
|
||||
|
||||
- name: Fetch CI config
|
||||
shell: bash
|
||||
run: |
|
||||
curl -o ci-test-config.json https://raw.githubusercontent.com/metabase/metabase/refs/heads/master/ci-test-config.json
|
||||
|
||||
- name: Get M2 cache key
|
||||
uses: ./.github/actions/get-cache-keys
|
||||
id: get-cache-keys
|
||||
|
||||
14
.github/actions/prepare-cypress/action.yml
vendored
14
.github/actions/prepare-cypress/action.yml
vendored
@@ -22,16 +22,16 @@ runs:
|
||||
chrome-version: 1097615
|
||||
id: setup-chrome
|
||||
|
||||
- name: Get current week of year
|
||||
id: get-date
|
||||
run: |
|
||||
echo "week-of-year=$(/bin/date "+%G-%V")" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
- name: Get cache keys
|
||||
uses: ./.github/actions/get-cache-keys
|
||||
id: get-cache-keys
|
||||
|
||||
- name: Get Cypress cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-Cypress-${{ steps.get-date.outputs.week-of-year }}
|
||||
key: ${{ steps.get-cache-keys.outputs.cypress-cache-key }}
|
||||
|
||||
- name: Ensure that Cypress executable is ready
|
||||
run: |
|
||||
yarn cypress install
|
||||
|
||||
14
.github/actions/prepare-frontend/action.yml
vendored
14
.github/actions/prepare-frontend/action.yml
vendored
@@ -6,7 +6,7 @@ inputs:
|
||||
apply-patches:
|
||||
description: "Whether to patch frontend dependencies with patch-package."
|
||||
required: true
|
||||
default: 'true'
|
||||
default: "true"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
@@ -28,11 +28,21 @@ runs:
|
||||
~/.gitlibs
|
||||
key: ${{ steps.get-cache-keys.outputs.m2-cache-key }}
|
||||
|
||||
- name: Get yarn cache directory
|
||||
id: yarn-cache-dir
|
||||
run: |
|
||||
YARN_CACHE_DIR=$(yarn cache dir)
|
||||
echo "Yarn cache directory: $YARN_CACHE_DIR"
|
||||
echo "dir=$YARN_CACHE_DIR" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Restore node_modules cache
|
||||
uses: actions/cache/restore@v4
|
||||
id: node-modules-cache
|
||||
with:
|
||||
path: node_modules
|
||||
path: |
|
||||
${{ steps.yarn-cache-dir.outputs.dir }}
|
||||
node_modules
|
||||
key: ${{ steps.get-cache-keys.outputs.node-cache-key }}
|
||||
|
||||
# postinstall lifecycle hook can be dangerous in CI so we only run it locally (see package.json)
|
||||
|
||||
41
.github/actions/test-driver/action.yml
vendored
41
.github/actions/test-driver/action.yml
vendored
@@ -10,21 +10,54 @@ inputs:
|
||||
required: true
|
||||
default: false
|
||||
|
||||
outputs:
|
||||
quarantine-skipped:
|
||||
description: Whether the driver test was skipped due to quarantine
|
||||
value: ${{ steps.check-driver.outputs.skip }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Check if driver is ignored
|
||||
id: check-driver
|
||||
shell: bash
|
||||
run: |
|
||||
# If DRIVERS env var is not set, proceed with tests
|
||||
if [ -z "$DRIVERS" ]; then
|
||||
echo "No DRIVERS env var set. Proceeding with tests."
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Fetch CI config and check if driver should be skipped
|
||||
curl -sSf -o ci-test-config.json https://raw.githubusercontent.com/metabase/ci-test-config/refs/heads/master/ci-test-config.json || true
|
||||
|
||||
# Check if the driver is in the ignored list (gracefully handle missing/invalid config)
|
||||
IS_IGNORED=$(jq --arg driver "$DRIVERS" '.ignored.drivers // [] | contains([$driver])' ci-test-config.json 2>/dev/null || echo "false")
|
||||
|
||||
if [ "$IS_IGNORED" = "true" ]; then
|
||||
echo "⚠️ Driver '$DRIVERS' is in the ignored list. Skipping tests."
|
||||
echo "View the ignored list here: https://github.com/metabase/ci-test-config/blob/master/ci-test-config.json"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Driver '$DRIVERS' is not ignored. Proceeding with tests."
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Prepare back-end environment
|
||||
if: steps.check-driver.outputs.skip != 'true'
|
||||
uses: ./.github/actions/prepare-backend
|
||||
- name: Prepare front-end environment
|
||||
if: ${{ steps.check-driver.outputs.skip != 'true' && inputs.build-static-viz == 'true' }}
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
if: ${{ inputs.build-static-viz == 'true' }}
|
||||
- name: Build static viz frontend
|
||||
if: ${{ steps.check-driver.outputs.skip != 'true' && inputs.build-static-viz == 'true' }}
|
||||
run: yarn build-static-viz
|
||||
shell: bash
|
||||
if: ${{ inputs.build-static-viz == 'true' }}
|
||||
env:
|
||||
MB_EDITION: ee
|
||||
- name: Test database driver
|
||||
if: steps.check-driver.outputs.skip != 'true'
|
||||
run: clojure -X:dev:ci:ee:ee-dev:drivers:drivers-dev:test ${{ inputs.test-args }}
|
||||
shell: bash
|
||||
id: run-test
|
||||
@@ -37,7 +70,7 @@ runs:
|
||||
# As per the documentation: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#steps-context
|
||||
# test.outcome gives us access to the result before `continue-on-error` overrides it and sets it to 'success'.
|
||||
# We want test reports even if the test step gets cancelled (e.g. times out), hence we match anything that is not a 'success'.
|
||||
if: steps.run-test.outcome != 'success'
|
||||
if: ${{ steps.run-test.outcome != 'success' && steps.check-driver.outputs.skip != 'true' }}
|
||||
with:
|
||||
path: "target/junit/**/*_test.xml"
|
||||
name: JUnit Test Report ${{ inputs.junit-name }}
|
||||
@@ -46,7 +79,7 @@ runs:
|
||||
list-tests: failed
|
||||
fail-on-error: false
|
||||
- name: Upload Logs on Failure
|
||||
if: always() && steps.run-test.outcome != 'success'
|
||||
if: always() && steps.run-test.outcome != 'success' && steps.check-driver.outputs.skip != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-logs-${{ github.job }}
|
||||
|
||||
4
.github/file-paths.yaml
vendored
4
.github/file-paths.yaml
vendored
@@ -19,7 +19,7 @@ frontend_ci: &frontend_ci
|
||||
frontend_embedding_sdk_ci: &frontend_embedding_sdk_ci
|
||||
- ".github/actions/prepare-frontend/**"
|
||||
- ".github/actions/prepare-backend/**"
|
||||
- ".github/workflows/*embedding-sdk*.yml"
|
||||
- ".github/workflows/embedding-sdk*.yml"
|
||||
|
||||
frontend_configs: &frontend_configs
|
||||
- "yarn.lock"
|
||||
@@ -130,7 +130,7 @@ e2e_specs: &e2e_specs
|
||||
e2e_embedding_sdk:
|
||||
- *frontend_embedding_sdk_ci
|
||||
- *sources
|
||||
- "e2e/component/**/*.cy.*.{js,ts}"
|
||||
- "e2e/test-component/**/*.cy.*.{js,jsx,ts,tsx}"
|
||||
|
||||
e2e_embedding_sdk_compatibility:
|
||||
- *default
|
||||
|
||||
3
.github/team.json
vendored
3
.github/team.json
vendored
@@ -106,7 +106,8 @@
|
||||
"chodorowicz",
|
||||
"kamilmielnik",
|
||||
"stasgavrylov",
|
||||
"uladzimirdev"
|
||||
"uladzimirdev",
|
||||
"egorgrushin"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
15
.github/workflows/backend.yml
vendored
15
.github/workflows/backend.yml
vendored
@@ -48,13 +48,11 @@ jobs:
|
||||
- name: Enterprise Tests
|
||||
edition: ee
|
||||
test-group-name: ee
|
||||
build-static-viz: 'false'
|
||||
test-args: >-
|
||||
:only '"enterprise/backend/test"'
|
||||
- name: EE App DB Tests (Part 1)
|
||||
edition: ee
|
||||
test-group-name: ee-app-db-1
|
||||
build-static-viz: 'true'
|
||||
test-args: >-
|
||||
:only '["test" ".clj-kondo/test"]'
|
||||
:partition/total 2
|
||||
@@ -62,7 +60,6 @@ jobs:
|
||||
- name: EE App DB Tests (Part 2)
|
||||
edition: ee
|
||||
test-group-name: ee-app-db-2
|
||||
build-static-viz: 'true'
|
||||
test-args: >-
|
||||
:only '["test" ".clj-kondo/test"]'
|
||||
:partition/total 2
|
||||
@@ -70,7 +67,6 @@ jobs:
|
||||
- name: OSS App DB Tests (Part 1)
|
||||
edition: oss
|
||||
test-group-name: oss-app-db-1
|
||||
build-static-viz: 'true'
|
||||
test-args: >-
|
||||
:only '["test" ".clj-kondo/test"]'
|
||||
:partition/total 2
|
||||
@@ -78,7 +74,6 @@ jobs:
|
||||
- name: OSS App DB Tests (Part 2)
|
||||
edition: oss
|
||||
test-group-name: oss-app-db-2
|
||||
build-static-viz: 'true'
|
||||
test-args: >-
|
||||
:only '["test" ".clj-kondo/test"]'
|
||||
:partition/total 2
|
||||
@@ -103,25 +98,15 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Prepare front-end environment
|
||||
if: ${{ matrix.job.build-static-viz == 'true' }}
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
java-version: "${{ matrix.java-version }}"
|
||||
# Depending on which files changed, we might either have to build static-viz
|
||||
# from scratch or download the previously built artifact
|
||||
- name: Build static-viz frontend
|
||||
if: ${{ matrix.job.build-static-viz == 'true' && !inputs.skip }}
|
||||
run: yarn build-static-viz
|
||||
env:
|
||||
MB_EDITION: ${{ matrix.job.edition }}
|
||||
- name: Download Static Viz Bundle Artifact
|
||||
if: ${{ matrix.job.build-static-viz == 'true' && !inputs.skip }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: static-viz-bundle-${{ github.sha }}
|
||||
path: resources/frontend_client/app/dist
|
||||
|
||||
- name: "Test Java ${{ matrix.java-version }} ${{ matrix.job.name }}"
|
||||
id: run-java-tests
|
||||
|
||||
37
.github/workflows/cache-generator.yml
vendored
37
.github/workflows/cache-generator.yml
vendored
@@ -30,8 +30,8 @@ name: Cache Generator
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * 1' # Mondays at 03:00 UTC
|
||||
workflow_dispatch: # manually update the cache
|
||||
- cron: "0 3 * * 1" # Mondays at 03:00 UTC
|
||||
workflow_dispatch: # manually update the cache
|
||||
|
||||
concurrency:
|
||||
group: cache-generator
|
||||
@@ -49,6 +49,7 @@ jobs:
|
||||
m2-cache-key: ${{ steps.get-cache-keys.outputs.m2-cache-key }}
|
||||
node-cache-key: ${{ steps.get-cache-keys.outputs.node-cache-key }}
|
||||
eslint-cache-key: ${{ steps.get-cache-keys.outputs.eslint-cache-key }}
|
||||
cypress-cache-key: ${{ steps.get-cache-keys.outputs.cypress-cache-key }}
|
||||
|
||||
generate-m2-cache:
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -63,7 +64,7 @@ jobs:
|
||||
|
||||
# Add or remove aliases to affect the cache size and its contents.
|
||||
# Adjust to taste, then redeploy this workflow.
|
||||
- name: Pre-resolve dependencies
|
||||
- name: Pre-resolve Clojure dependencies
|
||||
run: clojure -A:build:dev:ee:ee-dev:drivers:drivers-dev:cljs -P
|
||||
|
||||
- name: Update M2 cache
|
||||
@@ -81,6 +82,7 @@ jobs:
|
||||
env:
|
||||
NODE_CACHE_KEY: ${{ needs.get-cache-keys.outputs.node-cache-key }}
|
||||
ESLINT_CACHE_KEY: ${{ needs.get-cache-keys.outputs.eslint-cache-key }}
|
||||
CYPRESS_CACHE_KEY: ${{ needs.get-cache-keys.outputs.cypress-cache-key }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# `prepare-frontend` action (1) prepares Node.js and (2) fetches the node_modules cache if it exists.
|
||||
@@ -92,16 +94,39 @@ jobs:
|
||||
- name: Install front-end dependencies
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
with:
|
||||
apply-patches: 'false'
|
||||
apply-patches: "false"
|
||||
|
||||
- name: Get yarn cache directory
|
||||
id: yarn-cache-dir
|
||||
run: |
|
||||
YARN_CACHE_DIR=$(yarn cache dir)
|
||||
echo "Yarn cache directory: $YARN_CACHE_DIR"
|
||||
echo "dir=$YARN_CACHE_DIR" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Ensure that Cypress executable is ready
|
||||
run: |
|
||||
yarn cypress install
|
||||
yarn cypress cache path
|
||||
yarn cypress cache list
|
||||
yarn cypress verify
|
||||
shell: bash
|
||||
|
||||
- name: Update node_modules cache
|
||||
uses: ./.github/actions/update-cache
|
||||
with:
|
||||
path: node_modules
|
||||
path: |
|
||||
${{ steps.yarn-cache-dir.outputs.dir }}
|
||||
node_modules
|
||||
key: ${{ env.NODE_CACHE_KEY }}
|
||||
|
||||
- name: Update eslint cache
|
||||
uses: ./.github/actions/update-cache
|
||||
with:
|
||||
path: .eslintcache
|
||||
path: ${{ github.workspace}}/.eslintcache
|
||||
key: ${{ env.ESLINT_CACHE_KEY }}
|
||||
|
||||
- name: Update Cypress cache
|
||||
uses: ./.github/actions/update-cache
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ env.CYPRESS_CACHE_KEY }}
|
||||
|
||||
12
.github/workflows/claude-code-bug-fix.yml
vendored
12
.github/workflows/claude-code-bug-fix.yml
vendored
@@ -159,15 +159,15 @@ jobs:
|
||||
1. **Analyze the issue description and comments** - Understand what the issue is describing
|
||||
a. Pay particular attention to reproduction steps and if there's an agreed upon fix.
|
||||
2. **Search the codebase** - Use Grep and Glob tools to find relevant files
|
||||
3. **Implement a fix** - Follow existing code patterns and conventions
|
||||
4. **Test the fix** - Run appropriate tests using the commands in CLAUDE.md:
|
||||
- JavaScript/TypeScript: `yarn test-unit path/to/file.unit.spec.js`
|
||||
- Clojure: `clojure -X:dev:test :only namespace/test-name`
|
||||
- Lint: `yarn lint-eslint-pure` for JS/TS, `./bin/mage kondo <file>` for Clojure
|
||||
3. **Implement a fix** - Use the appropriate write skill based on the file types:
|
||||
- Use clojure-write for Clojure/ClojureScript files
|
||||
- Use typescript-write for TypeScript/JavaScript files
|
||||
- Follow the coding standards and patterns in each skill
|
||||
4. **Test the fix** - Run appropriate tests as specified in the skills
|
||||
5. **Verify the fix** - Ensure tests pass and code follows conventions
|
||||
|
||||
## Important Guidelines
|
||||
- Follow the development patterns in CLAUDE.md
|
||||
- Use the appropriate write skill for the language you're working in
|
||||
- Write failing tests first, then fix them
|
||||
- Work in small, testable increments
|
||||
- Don't commit changes - leave that for the workflow
|
||||
|
||||
15
.github/workflows/claude-code-review.yaml
vendored
15
.github/workflows/claude-code-review.yaml
vendored
@@ -79,17 +79,14 @@ jobs:
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Review this pull request with a focus on:
|
||||
Review this pull request for compliance with project coding standards and conventions.
|
||||
|
||||
- Compliance with project coding standards and conventions
|
||||
- Code quality and best practices
|
||||
- Clear and correct docstrings
|
||||
- Clear and correct comments explaining what the code is doing
|
||||
**Use the appropriate review skill based on the file types changed:**
|
||||
- Use clojure-review for Clojure/ClojureScript files
|
||||
- Use typescript-review for TypeScript/JavaScript files
|
||||
- Follow the review guidelines and checklists in each skill
|
||||
|
||||
DO NOT post any comments confirming that something is correct or congratulating someone for following our
|
||||
guidelines -- only post comments for style violations or concerns.
|
||||
|
||||
Follow additional guidelines in the "Code Review Guidelines" section of CLAUDE.md .
|
||||
DO NOT post any comments confirming that something is correct - only post comments for style violations or concerns.
|
||||
|
||||
Note: The PR branch is already checked out in the current working directory.
|
||||
|
||||
|
||||
100
.github/workflows/claude-docs-review.yml
vendored
Normal file
100
.github/workflows/claude-docs-review.yml
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
name: "Claude Docs Review"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
paths:
|
||||
- 'docs/**/*.md'
|
||||
- 'docs/**/*.mdx'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to review'
|
||||
required: true
|
||||
type: number
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}-claude-docs-review
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
# Make sure the value of GITHUB_TOKEN will not be persisted in repo's config
|
||||
persist-credentials: false
|
||||
|
||||
- name: Generate GitHub App Token for Claude
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: claude-app-token
|
||||
with:
|
||||
app-id: ${{ secrets.METABASE_BOT_APP_ID }}
|
||||
private-key: ${{ secrets.METABASE_BOT_APP_PRIVATE_KEY }}
|
||||
permission-contents: read # Required for reading repository files and PR diffs
|
||||
permission-pull-requests: write # Required for creating inline comments and PR comments
|
||||
|
||||
- name: Delete old Claude docs review comments
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.claude-app-token.outputs.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
|
||||
shell: bash
|
||||
run: |
|
||||
# Delete pending reviews first (these can be deleted entirely)
|
||||
pending_review_ids=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/reviews" --jq '.[] | select(.user.login=="metabase-bot[bot]" and .state=="PENDING") | .id')
|
||||
if [ -n "$pending_review_ids" ]; then
|
||||
echo "Deleting pending reviews: $pending_review_ids"
|
||||
for id in $pending_review_ids; do
|
||||
gh api -X DELETE "/repos/$REPO/pulls/$PR_NUMBER/reviews/$id"
|
||||
echo "Deleted pending review $id"
|
||||
done
|
||||
fi
|
||||
|
||||
# Delete individual inline comments from submitted reviews (can't delete the review itself)
|
||||
old_line_comment_ids=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/comments" --jq '.[] | select(.user.login=="metabase-bot[bot]") | .id')
|
||||
if [ -n "$old_line_comment_ids" ]; then
|
||||
echo "Deleting line comments: $old_line_comment_ids"
|
||||
for id in $old_line_comment_ids; do
|
||||
gh api -X DELETE "/repos/$REPO/pulls/comments/$id"
|
||||
echo "Deleted line comment $id"
|
||||
done
|
||||
fi
|
||||
|
||||
# Delete top-level review comments
|
||||
old_top_level_comment_ids=$(gh api "/repos/$REPO/issues/$PR_NUMBER/comments" --jq '.[] | select(.user.login=="metabase-bot[bot]") | .id')
|
||||
if [ -n "$old_top_level_comment_ids" ]; then
|
||||
echo "Deleting top-level comments: $old_top_level_comment_ids"
|
||||
for id in $old_top_level_comment_ids; do
|
||||
gh api -X DELETE "/repos/$REPO/issues/comments/$id"
|
||||
echo "Deleted top-level comment $id"
|
||||
done
|
||||
fi
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ steps.claude-app-token.outputs.token }}
|
||||
show_full_output: true
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
|
||||
|
||||
Review the documentation changes in this pull request.
|
||||
|
||||
Use the docs-review skill to review all changed documentation files for compliance with the Metabase writing style guide.
|
||||
|
||||
DO NOT post any comments confirming that something is correct - only post comments for style violations or concerns.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "Skill,mcp__github__get_pull_request,mcp__github__get_pull_request_files,mcp__github__create_pending_pull_request_review,mcp__github__get_pull_request_diff,mcp__github__add_comment_to_pending_review,mcp__github__add_pull_request_review_comment_to_pending_review,mcp__github__delete_pending_pull_request_review,mcp__github__submit_pending_pull_request_review"
|
||||
6
.github/workflows/docs-links.yml
vendored
6
.github/workflows/docs-links.yml
vendored
@@ -2,8 +2,6 @@ name: Check doc links
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "docs/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -15,7 +13,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
sparse-checkout: |
|
||||
docs
|
||||
@@ -29,7 +27,7 @@ jobs:
|
||||
|
||||
- name: Run link checker
|
||||
id: link-check
|
||||
uses: lycheeverse/lychee-action@v2
|
||||
uses: lycheeverse/lychee-action@v2.3.0
|
||||
with:
|
||||
fail: true
|
||||
args: docs --config ./.lychee/config.toml --offline
|
||||
|
||||
4
.github/workflows/docs-release-generate.yml
vendored
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.56.x"
|
||||
default: "release-x.57.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.56.x' }}
|
||||
target_branch: ${{ inputs.target_branch || 'release-x.57.x' }}
|
||||
secrets:
|
||||
automation_token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
|
||||
|
||||
72
.github/workflows/drivers.yml
vendored
72
.github/workflows/drivers.yml
vendored
@@ -27,21 +27,6 @@ jobs:
|
||||
# Fetch the base branch to ensure its history is available for diffing
|
||||
git fetch --depth=1 origin ${{ github.event.pull_request.base.ref }}:${{ github.event.pull_request.base.ref }}
|
||||
# Only run this if it's a pull request and the base ref exists
|
||||
- name: Get Date
|
||||
id: get-date
|
||||
shell: bash
|
||||
run: |
|
||||
echo "date=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_OUTPUT
|
||||
- name: Restore Babashka cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.m2
|
||||
~/.deps.clj
|
||||
restore-keys: |
|
||||
mage-drivers-
|
||||
key: >-
|
||||
mage-drivers-${{ steps.get-date.outputs.date }}
|
||||
- name: Check if driver tests should run
|
||||
id: check-branch-and-files-changed
|
||||
run: |
|
||||
@@ -82,6 +67,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test Athena driver
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-athena-ee'
|
||||
@@ -90,7 +76,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -144,7 +130,7 @@ jobs:
|
||||
test-args: ${{ matrix.job.test-args }}
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -215,7 +201,7 @@ jobs:
|
||||
test-args: ${{ matrix.job.test-args }}
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -250,6 +236,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test Druid driver
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-druid-ee'
|
||||
@@ -258,7 +245,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -287,6 +274,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test Druid JDBC driver
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-druid-jdbc-ee'
|
||||
@@ -295,7 +283,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -324,6 +312,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test Databricks driver
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-databricks-ee'
|
||||
@@ -332,7 +321,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -362,6 +351,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test Databricks driver with multi-catalog
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-databricks-multi-catalog-ee'
|
||||
@@ -370,7 +360,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -399,7 +389,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -528,7 +518,7 @@ jobs:
|
||||
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -597,7 +587,7 @@ jobs:
|
||||
test-args: ${{ matrix.job.test-args }}
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -660,6 +650,7 @@ jobs:
|
||||
curl https://raw.githubusercontent.com/metabase/metabase-qa/master/dbs/mongo/certificates/metaca.crt \
|
||||
-o ./test_resources/ssl/mongo/metaca.crt
|
||||
- name: Test ${{ matrix.version.name }}
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: ${{ matrix.version.junit-name }}
|
||||
@@ -668,7 +659,7 @@ jobs:
|
||||
:only-tags [:mb/driver-tests]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -695,6 +686,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test MongoDB driver (Sharded cluster)
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-mongo-sharded-cluster-ee'
|
||||
@@ -702,7 +694,7 @@ jobs:
|
||||
:only metabase.driver.mongo.sharded-cluster-test
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -767,6 +759,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test ${{ matrix.version.name }}
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: ${{ matrix.version.junit-name }}
|
||||
@@ -775,7 +768,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -878,7 +871,7 @@ jobs:
|
||||
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -946,6 +939,7 @@ jobs:
|
||||
-file /tmp/presto-ssl-ca.der
|
||||
-trustcacerts
|
||||
- name: Test Presto JDBC driver
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-presto-jdbc-ee'
|
||||
@@ -954,7 +948,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -1017,7 +1011,7 @@ jobs:
|
||||
${{ matrix.job.test-args }}
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: ${{ !matrix.job.skip && !cancelled() }}
|
||||
if: ${{ !matrix.job.skip && !cancelled() && steps.test-driver.outputs.quarantine-skipped != 'true' }}
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -1034,7 +1028,8 @@ jobs:
|
||||
logs-artifact-suffix: ${{ matrix.job.name }} ${{ matrix.version.name }}
|
||||
|
||||
be-tests-snowflake-ee:
|
||||
if: ${{ !inputs.skip }}
|
||||
needs: determine-driver-skips
|
||||
if: ${{ !inputs.skip && !(needs.determine-driver-skips.outputs.skip == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -1086,7 +1081,7 @@ jobs:
|
||||
test-args: ${{ matrix.job.test-args }}
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -1119,6 +1114,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test Spark driver
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-sparksql-ee'
|
||||
@@ -1127,7 +1123,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -1149,6 +1145,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test SQLite driver
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-sqlite-ee'
|
||||
@@ -1157,7 +1154,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -1222,7 +1219,7 @@ jobs:
|
||||
test-args: ${{ matrix.job.test-args }}
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
@@ -1329,6 +1326,7 @@ jobs:
|
||||
- name: Make plugins directory
|
||||
run: mkdir plugins
|
||||
- name: Test Vertica driver
|
||||
id: test-driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'be-tests-vertica-ee'
|
||||
@@ -1337,7 +1335,7 @@ jobs:
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
if: always() && steps.test-driver.outputs.quarantine-skipped != 'true'
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
# This workflow does the backward compatibility tests
|
||||
# between the Embedding SDK package of version N and Embedding SDK Bundle of version N - 1
|
||||
# N - 1 is a target of a PR
|
||||
name: Embedding SDK backward-compatibility E2E Component Tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
cached-embedding-sdk-dist-artifact-name:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: "SDK backward-compatibility E2E > Check which files changed"
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
e2e_embedding_sdk: ${{ steps.changes.outputs.e2e_embedding_sdk }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test which files changed
|
||||
uses: dorny/paths-filter@v3.0.0
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-paths.yaml
|
||||
|
||||
decide-target-ref:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
target-ref: ${{ steps.get-target-ref.outputs.value }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Decide ref to checkout
|
||||
id: get-target-ref
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
echo "value=${{ github.base_ref }}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "value=$(git rev-parse HEAD^1)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# if this is a test on a release branch, we need to check the build requirements
|
||||
get-build-requirements:
|
||||
name: "SDK backward-compatibility E2E > [Backport only] Get build requirements"
|
||||
needs: [decide-target-ref]
|
||||
if: |
|
||||
!cancelled() &&
|
||||
contains(github.base_ref || github.ref, 'release-x')
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
java_version: ${{ fromJson(steps.dependencies.outputs.result).java_version }}
|
||||
node_version: ${{ fromJson(steps.dependencies.outputs.result).node_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.decide-target-ref.outputs.target-ref }}
|
||||
sparse-checkout: release
|
||||
- name: Prepare build scripts
|
||||
run: cd ${{ github.workspace }}/release && yarn && yarn build
|
||||
- name: Get build dependencies
|
||||
uses: actions/github-script@v7
|
||||
id: dependencies
|
||||
with:
|
||||
script: | # js
|
||||
const {
|
||||
getBuildRequirements,
|
||||
getVersionFromReleaseBranch,
|
||||
} = require('${{ github.workspace }}/release/dist/index.cjs');
|
||||
const targetBranchName = '${{ github.base_ref || github.ref }}';
|
||||
|
||||
const version = getVersionFromReleaseBranch(targetBranchName);
|
||||
const requirements = getBuildRequirements(version);
|
||||
|
||||
return {
|
||||
java_version: requirements.java,
|
||||
node_version: requirements.node,
|
||||
};
|
||||
|
||||
e2e-tests:
|
||||
name: "SDK backward-compatibility E2E > Run e2e tests"
|
||||
needs: [files-changed, decide-target-ref, get-build-requirements]
|
||||
if: |
|
||||
!cancelled() &&
|
||||
needs.files-changed.outputs.e2e_embedding_sdk == 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
MB_EDITION: ee
|
||||
DISPLAY: ""
|
||||
MB_RUN_MODE: "e2e"
|
||||
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
|
||||
# Any env starting with `CYPRESS_` will be available to all Cypress tests via `Cypress.env()`
|
||||
# Example: you can get `CYPRESS_FOO` with `Cypress.env("FOO")`
|
||||
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
|
||||
CYPRESS_MB_STARTER_CLOUD_TOKEN: ${{ secrets.STAGING_MB_STARTER_CLOUD_TOKEN }}
|
||||
CYPRESS_MB_PRO_CLOUD_TOKEN: ${{ secrets.STAGING_MB_PRO_CLOUD_TOKEN }}
|
||||
CYPRESS_MB_PRO_SELF_HOSTED_TOKEN: ${{ secrets.STAGING_MB_PRO_SELF_HOSTED_TOKEN }}
|
||||
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title || github.event.head_commit.message || github.event.head.sha }}
|
||||
TZ: US/Pacific # to make node match the instance tz
|
||||
CYPRESS_CI: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.decide-target-ref.outputs.target-ref }}
|
||||
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
java-version: "${{ needs.get-build-requirements.outputs.java_version || 21 }}"
|
||||
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
with:
|
||||
node-version: "${{ needs.get-build-requirements.outputs.node_version }}"
|
||||
|
||||
- name: Build uberjar with ./bin/build.sh
|
||||
run: ./bin/build.sh
|
||||
|
||||
- name: Retrieve Embedding SDK dist artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ inputs.cached-embedding-sdk-dist-artifact-name }}
|
||||
path: ${{ github.workspace }}/resources/embedding-sdk
|
||||
|
||||
- name: Prepare Cypress environment
|
||||
id: cypress-prep
|
||||
uses: ./.github/actions/prepare-cypress
|
||||
|
||||
- name: Run Metabase
|
||||
run: node e2e/runner/run_cypress_ci.js start
|
||||
|
||||
- name: Make app db snapshot
|
||||
run: node e2e/runner/run_cypress_ci.js snapshot --browser ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
|
||||
- name: Run component tests for Embedding SDK
|
||||
run: |
|
||||
node e2e/runner/run_cypress_ci.js component \
|
||||
--browser ${{ steps.cypress-prep.outputs.chrome-path }} \
|
||||
--env grepTags="-@skip-backward-compatibility",grepOmitFiltered=true
|
||||
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit
|
||||
output-name: e2e-embedding-sdk
|
||||
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
|
||||
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
|
||||
- name: Upload Cypress Artifacts upon failure
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: cypress-recording-embedding-sdk-cross-version
|
||||
path: |
|
||||
./cypress
|
||||
./logs/test.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Publish Summary
|
||||
if: failure()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: | #js
|
||||
const {
|
||||
generateReport,
|
||||
parseReport,
|
||||
formatSummary
|
||||
} = require("./.github/scripts/handle-mochawesome-report.js");
|
||||
|
||||
const report = await generateReport();
|
||||
const results = parseReport(report);
|
||||
const summary = formatSummary(results);
|
||||
|
||||
await core.summary.addRaw(summary).write();
|
||||
@@ -1,203 +0,0 @@
|
||||
name: E2E Component Tests for Embedding SDK
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
cached-embedding-sdk-dist-artifact-name:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: "SDK e2e > Check which files changed"
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
e2e_embedding_sdk: ${{ steps.changes.outputs.e2e_embedding_sdk }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test which files changed
|
||||
uses: dorny/paths-filter@v3.0.0
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-paths.yaml
|
||||
|
||||
# if this is a test on a release branch, we need to check the build requirements
|
||||
get-build-requirements:
|
||||
name: "SDK e2e > [Backport only] Get build requirements"
|
||||
if: |
|
||||
!cancelled() &&
|
||||
contains(github.base_ref || github.ref, 'release-x')
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
java_version: ${{ fromJson(steps.dependencies.outputs.result).java_version }}
|
||||
node_version: ${{ fromJson(steps.dependencies.outputs.result).node_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: release
|
||||
- name: Prepare build scripts
|
||||
run: cd ${{ github.workspace }}/release && yarn && yarn build
|
||||
- name: Get build dependencies
|
||||
uses: actions/github-script@v7
|
||||
id: dependencies
|
||||
with:
|
||||
script: | # js
|
||||
const {
|
||||
getBuildRequirements,
|
||||
getVersionFromReleaseBranch,
|
||||
} = require('${{ github.workspace }}/release/dist/index.cjs');
|
||||
const targetBranchName = '${{ github.base_ref || github.ref }}';
|
||||
|
||||
const version = getVersionFromReleaseBranch(targetBranchName);
|
||||
const requirements = getBuildRequirements(version);
|
||||
|
||||
return {
|
||||
java_version: requirements.java,
|
||||
node_version: requirements.node,
|
||||
};
|
||||
|
||||
build-uberjar:
|
||||
name: "SDK e2e > Build uberjar"
|
||||
needs: [files-changed, get-build-requirements]
|
||||
if: |
|
||||
!cancelled() &&
|
||||
needs.files-changed.outputs.e2e_embedding_sdk == 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
MB_EDITION: ee
|
||||
INTERACTIVE: false
|
||||
# make sure that builds on release branches get licenses, because we use them for releases
|
||||
SKIP_LICENSES: ${{ github.event_name == 'pull_request' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
with:
|
||||
node-version: "${{ needs.get-build-requirements.outputs.node_version }}"
|
||||
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
java-version: "${{ needs.get-build-requirements.outputs.java_version || 21 }}"
|
||||
|
||||
- name: Build uberjar with ./bin/build.sh
|
||||
run: ./bin/build.sh
|
||||
|
||||
- name: Prepare uberjar artifact
|
||||
uses: ./.github/actions/prepare-uberjar-artifact
|
||||
with:
|
||||
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar-sdk-tests
|
||||
|
||||
e2e-tests:
|
||||
name: "SDK e2e > Run e2e tests (React ${{ matrix.react-version }})"
|
||||
needs: [get-build-requirements, build-uberjar]
|
||||
if: |
|
||||
!cancelled() &&
|
||||
needs.build-uberjar.result == 'success'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
matrix:
|
||||
react-version: [18, 19]
|
||||
fail-fast: false
|
||||
env:
|
||||
MB_EDITION: ee
|
||||
DISPLAY: ""
|
||||
MB_RUN_MODE: "e2e"
|
||||
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
|
||||
# Any env starting with `CYPRESS_` will be available to all Cypress tests via `Cypress.env()`
|
||||
# Example: you can get `CYPRESS_FOO` with `Cypress.env("FOO")`
|
||||
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
|
||||
CYPRESS_MB_STARTER_CLOUD_TOKEN: ${{ secrets.STAGING_MB_STARTER_CLOUD_TOKEN }}
|
||||
CYPRESS_MB_PRO_CLOUD_TOKEN: ${{ secrets.STAGING_MB_PRO_CLOUD_TOKEN }}
|
||||
CYPRESS_MB_PRO_SELF_HOSTED_TOKEN: ${{ secrets.STAGING_MB_PRO_SELF_HOSTED_TOKEN }}
|
||||
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title || github.event.head_commit.message || github.event.head.sha }}
|
||||
TZ: US/Pacific # to make node match the instance tz
|
||||
CYPRESS_CI: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Retrieve uberjar artifact for ee
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar-sdk-tests
|
||||
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
|
||||
- name: Prepare JDK ${{ needs.get-build-requirements.outputs.java_version || 21 }}
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ needs.get-build-requirements.outputs.java_version || 21 }}
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Prepare Cypress environment
|
||||
id: cypress-prep
|
||||
uses: ./.github/actions/prepare-cypress
|
||||
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
|
||||
- name: Retrieve Embedding SDK dist artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ inputs.cached-embedding-sdk-dist-artifact-name }}
|
||||
path: ${{ github.workspace }}/resources/embedding-sdk
|
||||
|
||||
- name: Run Metabase
|
||||
run: node e2e/runner/run_cypress_ci.js start
|
||||
|
||||
- name: Make app db snapshot
|
||||
run: node e2e/runner/run_cypress_ci.js snapshot --browser ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
|
||||
# Has to be run after the app db snapshot is made, as it is not compatible with React 19
|
||||
- name: Change React version to ${{ matrix.react-version }}
|
||||
run: ./bin/embedding-sdk/change-react-version.bash ${{ matrix.react-version }}
|
||||
|
||||
- name: Run component tests for Embedding SDK
|
||||
run: node e2e/runner/run_cypress_ci.js component --browser ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit
|
||||
output-name: e2e-embedding-sdk-react-${{ matrix.react-version }}
|
||||
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
|
||||
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
|
||||
- name: "SDK e2e > Upload Cypress Artifacts upon failure"
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: cypress-recording-embedding-sdk-react-${{ matrix.react-version }}
|
||||
path: |
|
||||
./cypress
|
||||
./logs/test.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: "SDK e2e > Publish Summary"
|
||||
if: failure()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: | #js
|
||||
const {
|
||||
generateReport,
|
||||
parseReport,
|
||||
formatSummary
|
||||
} = require("./.github/scripts/handle-mochawesome-report.js");
|
||||
|
||||
const report = await generateReport();
|
||||
const results = parseReport(report);
|
||||
const summary = formatSummary(results);
|
||||
|
||||
await core.summary.addRaw(summary).write();
|
||||
28
.github/workflows/e2e-stress-test-flake-fix.yml
vendored
28
.github/workflows/e2e-stress-test-flake-fix.yml
vendored
@@ -37,26 +37,26 @@ on:
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
determine-suite:
|
||||
name: Determine test suite
|
||||
determine-test-type:
|
||||
name: Determine test type
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
suite: ${{ steps.detect.outputs.suite }}
|
||||
type: ${{ steps.detect.outputs.type }}
|
||||
steps:
|
||||
- name: Detect suite from spec path
|
||||
- name: Detect test type from spec path
|
||||
id: detect
|
||||
run: |
|
||||
SPEC='${{ inputs.spec }}'
|
||||
if [[ "$SPEC" == e2e/test-component/scenarios/embedding-sdk/* ]]; then
|
||||
echo "suite=component" >> $GITHUB_OUTPUT
|
||||
echo "type=component" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "suite=e2e" >> $GITHUB_OUTPUT
|
||||
echo "type=e2e" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
workflow-summary:
|
||||
name: Stress test inputs
|
||||
needs: [determine-suite]
|
||||
needs: [determine-test-type]
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
echo '' >> $GITHUB_STEP_SUMMARY
|
||||
echo '- `branch`: ${{ github.ref_name }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '- `spec`: ${{ inputs.spec }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '- `test_suite` (inferred): ${{ needs.determine-suite.outputs.suite }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '- `test_type` (inferred): ${{ needs.determine-test-type.outputs.type }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '- `burn_in`: ${{ inputs.burn_in }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '- `grep`: "${{ inputs.grep }}"' >> $GITHUB_STEP_SUMMARY
|
||||
echo '- `qa_db`: "${{ inputs.qa_db }}"' >> $GITHUB_STEP_SUMMARY
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
echo 'triggered by: @${{ github.event.sender.login }}' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
stress-test-flake-fix:
|
||||
needs: [determine-suite]
|
||||
needs: [determine-test-type]
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
name: Stress test E2E flake fix
|
||||
@@ -122,12 +122,12 @@ jobs:
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
|
||||
- name: Prepare back-end environment
|
||||
if: ${{ needs.determine-suite.outputs.suite == 'component' }}
|
||||
if: ${{ needs.determine-test-type.outputs.type == 'component' }}
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
m2-cache-key: "cljs"
|
||||
- name: Build Embedding SDK package
|
||||
if: ${{ needs.determine-suite.outputs.suite == 'component' }}
|
||||
if: ${{ needs.determine-test-type.outputs.type == 'component' }}
|
||||
run: yarn build-embedding-sdk-package
|
||||
|
||||
- name: Prepare JDK 21
|
||||
@@ -148,12 +148,12 @@ jobs:
|
||||
- name: Stress-test ${{ github.event.inputs.spec }} ${{ github.event.inputs.burn_in }} times
|
||||
env:
|
||||
GREP: ${{ github.event.inputs.grep }}
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
run: |
|
||||
node e2e/runner/run_cypress_ci.js ${{ needs.determine-suite.outputs.suite }} \
|
||||
node e2e/runner/run_cypress_ci.js ${{ needs.determine-test-type.outputs.type }} \
|
||||
--spec '${{ github.event.inputs.spec }}' \
|
||||
--env burn=${{ github.event.inputs.burn_in }} \
|
||||
--config-file e2e/support/cypress-stress-test.config.js \
|
||||
--browser ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
--config-file e2e/support/cypress-stress-test.config.js
|
||||
|
||||
- name: Upload Cypress Artifacts upon failure
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
14
.github/workflows/e2e-test.yml
vendored
14
.github/workflows/e2e-test.yml
vendored
@@ -120,6 +120,8 @@ jobs:
|
||||
run: node e2e/runner/run_cypress_ci.js start
|
||||
|
||||
- name: Generate database snapshots
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
run: node e2e/runner/run_cypress_ci.js snapshot
|
||||
|
||||
- name: Run auto-split Cypress tests on ${{ inputs.name }}
|
||||
@@ -133,13 +135,13 @@ jobs:
|
||||
# do not write cypress-split summary to github
|
||||
SPLIT_SUMMARY: false
|
||||
SPEC_PATH: ${{ env.specs || inputs.specs }}
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Running tests with specs: $SPEC_PATH"
|
||||
node e2e/runner/run_cypress_ci.js e2e \
|
||||
--env grepTags="-@mongo+-@python+-@OSS+-@skip",grepOmitFiltered=true \
|
||||
--spec "$SPEC_PATH" \
|
||||
--browser ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
--env grepTags="-@mongo+-@python+-@OSS+-@skip" \
|
||||
--spec "$SPEC_PATH"
|
||||
# Trunk Quarantine controls the final state of the run in the CI!
|
||||
# Even if this step fails we let it pass, and then `trunk-io/analytics-uploader` determines
|
||||
# whether to fail or to pass the whole job, based on the quarantine list.
|
||||
@@ -150,12 +152,12 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
SPEC_PATH: ${{ env.specs || inputs.specs }}
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
run: |
|
||||
echo "Running tests with specs: $SPEC_PATH"
|
||||
node e2e/runner/run_cypress_ci.js e2e \
|
||||
--env grepTags="${{inputs.tags && format('{0}+-@skip', inputs.tags) || '-@skip'}}",grepOmitFiltered=true \
|
||||
--spec "$SPEC_PATH" \
|
||||
--browser ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
--env grepTags="${{inputs.tags && format('{0}+-@skip', inputs.tags) || '-@skip'}}" \
|
||||
--spec "$SPEC_PATH"
|
||||
# Trunk Quarantine controls the final state of the run in the CI!
|
||||
# Even if this step fails we let it pass, and then `trunk-io/analytics-uploader` determines
|
||||
# whether to fail or to pass the whole job, based on the quarantine list.
|
||||
|
||||
254
.github/workflows/embedding-sdk-component-tests.yml
vendored
Normal file
254
.github/workflows/embedding-sdk-component-tests.yml
vendored
Normal file
@@ -0,0 +1,254 @@
|
||||
name: Embedding SDK Component Tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
cached-embedding-sdk-dist-artifact-name:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
sdk-component-tests:
|
||||
name: "SDK component tests (React ${{ matrix.react-version }})"
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
matrix:
|
||||
react-version: [18, 19]
|
||||
fail-fast: false
|
||||
env:
|
||||
MB_EDITION: ee
|
||||
DISPLAY: ""
|
||||
MB_RUN_MODE: "e2e"
|
||||
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
|
||||
# Any env starting with `CYPRESS_` will be available to all Cypress tests via `Cypress.env()`
|
||||
# Example: you can get `CYPRESS_FOO` with `Cypress.env("FOO")`
|
||||
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
|
||||
CYPRESS_MB_STARTER_CLOUD_TOKEN: ${{ secrets.STAGING_MB_STARTER_CLOUD_TOKEN }}
|
||||
CYPRESS_MB_PRO_CLOUD_TOKEN: ${{ secrets.STAGING_MB_PRO_CLOUD_TOKEN }}
|
||||
CYPRESS_MB_PRO_SELF_HOSTED_TOKEN: ${{ secrets.STAGING_MB_PRO_SELF_HOSTED_TOKEN }}
|
||||
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title || github.event.head_commit.message || github.event.head.sha }}
|
||||
TZ: US/Pacific # to make node match the instance tz
|
||||
CYPRESS_CI: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Retrieve uberjar artifact for ee
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
with:
|
||||
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
|
||||
extract-path: "target/uberjar"
|
||||
output-name: "metabase.jar"
|
||||
|
||||
- name: Retrieve Embedding SDK dist artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ inputs.cached-embedding-sdk-dist-artifact-name }}
|
||||
path: ${{ github.workspace }}/resources/embedding-sdk
|
||||
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
|
||||
- name: Prepare JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 21
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Prepare Cypress environment
|
||||
id: cypress-prep
|
||||
uses: ./.github/actions/prepare-cypress
|
||||
|
||||
- name: Run Metabase
|
||||
run: node e2e/runner/run_cypress_ci.js start
|
||||
|
||||
- name: Make app db snapshot
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
run: node e2e/runner/run_cypress_ci.js snapshot
|
||||
|
||||
# Has to be run after the app db snapshot is made, as it is not compatible with React 19
|
||||
- name: Change React version to ${{ matrix.react-version }}
|
||||
run: ./bin/embedding-sdk/change-react-version.bash ${{ matrix.react-version }}
|
||||
|
||||
- name: Run component tests for Embedding SDK
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
run: node e2e/runner/run_cypress_ci.js component
|
||||
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit
|
||||
output-name: e2e-embedding-sdk-react-${{ matrix.react-version }}
|
||||
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
|
||||
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
|
||||
- name: "SDK component tests > Upload Cypress Artifacts upon failure"
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: cypress-recording-embedding-sdk-react-${{ matrix.react-version }}
|
||||
path: |
|
||||
./cypress
|
||||
./logs/test.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: "SDK component tests > Publish Summary"
|
||||
if: failure()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: | #js
|
||||
const {
|
||||
generateReport,
|
||||
parseReport,
|
||||
formatSummary
|
||||
} = require("./.github/scripts/handle-mochawesome-report.js");
|
||||
|
||||
const report = await generateReport();
|
||||
const results = parseReport(report);
|
||||
const summary = formatSummary(results);
|
||||
|
||||
await core.summary.addRaw(summary).write();
|
||||
|
||||
decide-target-ref:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
target-ref: ${{ steps.get-target-ref.outputs.value }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Decide ref to checkout
|
||||
id: get-target-ref
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
echo "value=${{ github.base_ref }}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "value=$(git rev-parse HEAD^1)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# This job ensures that the Embedding SDK package from the current commit (N) works with the
|
||||
# Embedding SDK Bundle (Metabase JAR) from the previous commit (N-1).
|
||||
# N - 1 is a target of a pull request, or a literal previous commit when this job is run on push in a branch.
|
||||
sdk-component-tests-backward-compatibility:
|
||||
name: "SDK component tests (Backward compatibility)"
|
||||
needs: [decide-target-ref]
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
MB_EDITION: ee
|
||||
DISPLAY: ""
|
||||
MB_RUN_MODE: "e2e"
|
||||
METASTORE_DEV_SERVER_URL: ${{ secrets.METASTORE_DEV_SERVER_URL }}
|
||||
# Any env starting with `CYPRESS_` will be available to all Cypress tests via `Cypress.env()`
|
||||
# Example: you can get `CYPRESS_FOO` with `Cypress.env("FOO")`
|
||||
CYPRESS_MB_ALL_FEATURES_TOKEN: ${{ secrets.STAGING_MB_ALL_FEATURES_TOKEN }}
|
||||
CYPRESS_MB_STARTER_CLOUD_TOKEN: ${{ secrets.STAGING_MB_STARTER_CLOUD_TOKEN }}
|
||||
CYPRESS_MB_PRO_CLOUD_TOKEN: ${{ secrets.STAGING_MB_PRO_CLOUD_TOKEN }}
|
||||
CYPRESS_MB_PRO_SELF_HOSTED_TOKEN: ${{ secrets.STAGING_MB_PRO_SELF_HOSTED_TOKEN }}
|
||||
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title || github.event.head_commit.message || github.event.head.sha }}
|
||||
TZ: US/Pacific # to make node match the instance tz
|
||||
CYPRESS_CI: true
|
||||
|
||||
steps:
|
||||
- name: Check out the code from the previous commit
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.decide-target-ref.outputs.target-ref }}
|
||||
|
||||
- name: Get the actual commit SHA from the checked out branch
|
||||
id: get-commit-sha
|
||||
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
|
||||
- name: Attempt to retrieve the ee uberjar artifact from the previous commit
|
||||
id: download-uberjar
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
with:
|
||||
name: metabase-ee-${{ steps.get-commit-sha.outputs.sha }}-uberjar
|
||||
extract-path: "target/uberjar"
|
||||
output-name: "metabase.jar"
|
||||
continue-on-error: true # do not break the whole job if this step doesn't succeed
|
||||
|
||||
# Build the uberjar from the previous commit only if we couldn't fetch it.
|
||||
# `step.foo.OUTCOME` is important because it looks at the step status *before* continue-on-error is applied.
|
||||
# Do not confuse it with the `step.foo.conclusion`.
|
||||
- name: Build MB uberjar from the commit N-1
|
||||
if: steps.download-uberjar.outcome != 'success'
|
||||
run: ./bin/build.sh
|
||||
|
||||
- name: Retrieve Embedding SDK dist artifact built from the current commit N
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ inputs.cached-embedding-sdk-dist-artifact-name }}
|
||||
path: ${{ github.workspace }}/resources/embedding-sdk
|
||||
|
||||
- name: Prepare Cypress environment
|
||||
id: cypress-prep
|
||||
uses: ./.github/actions/prepare-cypress
|
||||
|
||||
- name: Run Metabase
|
||||
run: node e2e/runner/run_cypress_ci.js start
|
||||
|
||||
- name: Make app db snapshot
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
run: node e2e/runner/run_cypress_ci.js snapshot
|
||||
|
||||
- name: Run component tests for Embedding SDK
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
run: |
|
||||
node e2e/runner/run_cypress_ci.js component \
|
||||
--env grepTags="-@skip-backward-compatibility"
|
||||
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit
|
||||
output-name: e2e-embedding-sdk
|
||||
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
|
||||
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
|
||||
- name: Upload Cypress Artifacts upon failure
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: cypress-recording-embedding-sdk-cross-version
|
||||
path: |
|
||||
./cypress
|
||||
./logs/test.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Publish Summary
|
||||
if: failure()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: | #js
|
||||
const {
|
||||
generateReport,
|
||||
parseReport,
|
||||
formatSummary
|
||||
} = require("./.github/scripts/handle-mochawesome-report.js");
|
||||
|
||||
const report = await generateReport();
|
||||
const results = parseReport(report);
|
||||
const summary = formatSummary(results);
|
||||
|
||||
await core.summary.addRaw(summary).write();
|
||||
@@ -1,78 +1,18 @@
|
||||
name: "[NOT REQUIRED] SDK Compatibility tests"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "release-**"
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches:
|
||||
- "master"
|
||||
- "release-**"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
workflow_call:
|
||||
inputs:
|
||||
cached-embedding-sdk-dist-artifact-name:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Check which files changed
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
e2e_embedding_sdk_compatibility: ${{ steps.changes.outputs.e2e_embedding_sdk_compatibility }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test which files changed
|
||||
uses: dorny/paths-filter@v3.0.0
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-paths.yaml
|
||||
|
||||
# if this is a test on a release branch, we need to check the build requirements
|
||||
get-build-requirements:
|
||||
if: |
|
||||
!cancelled() &&
|
||||
contains(github.base_ref || github.ref, 'release-x')
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
java_version: ${{ fromJson(steps.dependencies.outputs.result).java_version }}
|
||||
node_version: ${{ fromJson(steps.dependencies.outputs.result).node_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: release
|
||||
- name: Prepare build scripts
|
||||
run: cd ${{ github.workspace }}/release && yarn && yarn build
|
||||
- name: Get build dependencies
|
||||
uses: actions/github-script@v7
|
||||
id: dependencies
|
||||
with:
|
||||
script: | # js
|
||||
const {
|
||||
getBuildRequirements,
|
||||
getVersionFromReleaseBranch,
|
||||
} = require('${{ github.workspace }}/release/dist/index.cjs');
|
||||
const targetBranchName = '${{ github.base_ref || github.ref }}';
|
||||
|
||||
const version = getVersionFromReleaseBranch(targetBranchName);
|
||||
const requirements = getBuildRequirements(version);
|
||||
|
||||
return {
|
||||
java_version: requirements.java,
|
||||
node_version: requirements.node,
|
||||
};
|
||||
|
||||
get-sample-apps-data:
|
||||
if: |
|
||||
!cancelled()
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
# we must support last two stable releases
|
||||
# we must support the last two stable releases
|
||||
SUPPORTED_RELEASES_COUNT: 2
|
||||
TEST_SUITES: |
|
||||
[
|
||||
@@ -145,51 +85,6 @@ jobs:
|
||||
matrix: []
|
||||
};
|
||||
|
||||
build:
|
||||
needs: [files-changed, get-build-requirements, get-sample-apps-data]
|
||||
if: |
|
||||
!cancelled() &&
|
||||
needs.files-changed.outputs.e2e_embedding_sdk_compatibility == 'true' &&
|
||||
fromJSON(needs.get-sample-apps-data.outputs.matrix)[0] != null
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
MB_EDITION: ee
|
||||
INTERACTIVE: false
|
||||
# make sure that builds on release branches get licenses, because we use them for releases
|
||||
SKIP_LICENSES: ${{ github.event_name == 'pull_request' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
with:
|
||||
node-version: "${{ needs.get-build-requirements.outputs.node_version }}"
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
with:
|
||||
java-version: "${{ needs.get-build-requirements.outputs.java_version || 21 }}"
|
||||
|
||||
- name: Build uberjar with ./bin/build.sh
|
||||
run: ./bin/build.sh
|
||||
|
||||
- name: Prepare uberjar artifact
|
||||
uses: ./.github/actions/prepare-uberjar-artifact
|
||||
with:
|
||||
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
|
||||
|
||||
- name: Build Embedding SDK package
|
||||
run: |
|
||||
# Remove the matchers to not report false positives on every PR
|
||||
# unrelated to embedding (EMB-765)
|
||||
echo "::remove-matcher owner=tsc::"
|
||||
yarn build-embedding-sdk-package
|
||||
|
||||
- name: Prepare Embedding SDK artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: embedding-sdk-${{ github.event.pull_request.head.sha || github.sha }}
|
||||
path: resources/embedding-sdk
|
||||
|
||||
# In order to check compatibility between Sample Apps and Embedding SDK, the job for each sample app:
|
||||
# - pulls a Sample App
|
||||
# - starts it
|
||||
@@ -197,13 +92,13 @@ jobs:
|
||||
# The job is not required, though if it's failing better to be sure that it's not caused by a breaking change.
|
||||
# For more details see `Sample Apps compatibility with Embedding SDK tests` in `enterprise/frontend/src/embedding-sdk-package/dev.md`
|
||||
e2e-sample-apps-tests:
|
||||
needs: [get-build-requirements, get-sample-apps-data, build]
|
||||
needs: [get-sample-apps-data]
|
||||
strategy:
|
||||
matrix:
|
||||
test_suite: ${{ fromJSON(needs.get-sample-apps-data.outputs.matrix) }}
|
||||
environment: ["production", "development"]
|
||||
if: |
|
||||
!cancelled() && needs.build.result == 'success'
|
||||
!cancelled() && needs.get-sample-apps-data.result == 'success'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 45
|
||||
name: e2e-sample-app-${{ matrix.test_suite }}-tests-${{ matrix.environment }}-bundle
|
||||
@@ -240,9 +135,11 @@ jobs:
|
||||
echo "sample_app_name=${test_suite%-e2e}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Retrieve uberjar artifact for ee
|
||||
uses: actions/download-artifact@v4
|
||||
uses: ./.github/actions/fetch-artifact
|
||||
with:
|
||||
name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
|
||||
extract-path: "target/uberjar"
|
||||
output-name: "metabase.jar"
|
||||
|
||||
- name: Retrieve Embedding SDK dist artifact
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -298,7 +195,9 @@ jobs:
|
||||
id: run-e2e-tests
|
||||
if: ${{ steps.check-sample-app-branch.outputs.sample_app_branch_exists == 'true' }}
|
||||
continue-on-error: true
|
||||
run: node e2e/runner/run_cypress_ci.js ${{ matrix.test_suite }} --browser ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
run: node e2e/runner/run_cypress_ci.js ${{ matrix.test_suite }}
|
||||
|
||||
- name: Upload Cypress Artifacts upon failure
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -322,7 +221,6 @@ jobs:
|
||||
# The job is not required, though if it's failing better to be sure that it's not caused by a breaking change.
|
||||
# For more details see `Embedding SDK integration tests with Host Apps` in `enterprise/frontend/src/embedding-sdk-package/dev.md`
|
||||
e2e-host-apps-tests:
|
||||
needs: [get-build-requirements, build]
|
||||
strategy:
|
||||
matrix:
|
||||
test_suite:
|
||||
@@ -333,8 +231,6 @@ jobs:
|
||||
"angular-20-host-app-e2e",
|
||||
]
|
||||
environment: ["production", "development"]
|
||||
if: |
|
||||
!cancelled() && needs.build.result == 'success'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 45
|
||||
name: e2e-host-app-${{ matrix.test_suite }}-tests-${{ matrix.environment }}-bundle
|
||||
@@ -392,7 +288,9 @@ jobs:
|
||||
- name: Run e2e tests for Host App
|
||||
id: run-e2e-tests
|
||||
continue-on-error: true
|
||||
run: node e2e/runner/run_cypress_ci.js ${{ matrix.test_suite }} --browser ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
run: node e2e/runner/run_cypress_ci.js ${{ matrix.test_suite }}
|
||||
|
||||
- name: Upload Cypress Artifacts upon failure
|
||||
uses: actions/upload-artifact@v4
|
||||
36
.github/workflows/embedding-sdk.yml
vendored
36
.github/workflows/embedding-sdk.yml
vendored
@@ -14,6 +14,7 @@ jobs:
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
e2e_embedding_sdk: ${{ steps.changes.outputs.e2e_embedding_sdk }}
|
||||
e2e_embedding_sdk_compatibility: ${{ steps.changes.outputs.e2e_embedding_sdk_compatibility }}
|
||||
frontend_embedding_sdk_sources: ${{ steps.changes.outputs.frontend_embedding_sdk_sources }}
|
||||
embedding_documentation: ${{ steps.changes.outputs.embedding_documentation }}
|
||||
steps:
|
||||
@@ -25,7 +26,7 @@ jobs:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-paths.yaml
|
||||
|
||||
build:
|
||||
build-sdk-package:
|
||||
needs: [files-changed]
|
||||
if: |
|
||||
!cancelled() &&
|
||||
@@ -53,10 +54,10 @@ jobs:
|
||||
path: resources/embedding-sdk
|
||||
|
||||
embedding-sdk-cli-snippets-type-check:
|
||||
needs: [files-changed, build]
|
||||
needs: [files-changed, build-sdk-package]
|
||||
if: |
|
||||
needs.files-changed.outputs.frontend_embedding_sdk_sources == 'true' &&
|
||||
needs.build.result == 'success'
|
||||
needs.build-sdk-package.result == 'success'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
@@ -72,10 +73,10 @@ jobs:
|
||||
run: yarn run embedding-sdk:cli-snippets:type-check
|
||||
|
||||
embedding-sdk-docs-snippets-type-check:
|
||||
needs: [files-changed, build]
|
||||
needs: [files-changed, build-sdk-package]
|
||||
if: |
|
||||
needs.files-changed.outputs.embedding_documentation == 'true' &&
|
||||
needs.build.result == 'success'
|
||||
needs.build-sdk-package.result == 'success'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
@@ -91,10 +92,10 @@ jobs:
|
||||
run: yarn run embedding-sdk:docs-snippets:type-check
|
||||
|
||||
embedding-sdk-api-documentation-generation-validation:
|
||||
needs: [files-changed, build]
|
||||
needs: [files-changed, build-sdk-package]
|
||||
if: |
|
||||
needs.files-changed.outputs.frontend_embedding_sdk_sources == 'true' &&
|
||||
needs.build.result == 'success'
|
||||
needs.build-sdk-package.result == 'success'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
@@ -140,18 +141,20 @@ jobs:
|
||||
run: exit 1
|
||||
|
||||
# See `Component e2e tests` in `enterprise/frontend/src/embedding-sdk-package/dev.md`
|
||||
sdk-e2e-tests:
|
||||
name: "SDK e2e tests"
|
||||
uses: ./.github/workflows/e2e-component-tests-embedding-sdk.yml
|
||||
needs: [build]
|
||||
embedding-sdk-component-tests:
|
||||
name: "SDK component tests"
|
||||
needs: [build-sdk-package, files-changed]
|
||||
if: needs.files-changed.outputs.e2e_embedding_sdk == 'true'
|
||||
uses: ./.github/workflows/embedding-sdk-component-tests.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
cached-embedding-sdk-dist-artifact-name: embedding-sdk-${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
e2e-component-tests-embedding-sdk-backward-compatibility:
|
||||
name: "SDK e2e backward compatibility tests"
|
||||
uses: ./.github/workflows/e2e-component-tests-embedding-sdk-backward-compatibility.yml
|
||||
needs: [build]
|
||||
not-required-embedding-sdk-apps-compatibility-tests:
|
||||
name: "SDK e2e tests for compatibility with (host/sample) apps"
|
||||
needs: [build-sdk-package, files-changed]
|
||||
if: needs.files-changed.outputs.e2e_embedding_sdk_compatibility == 'true'
|
||||
uses: ./.github/workflows/embedding-sdk-e2e-tests.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
cached-embedding-sdk-dist-artifact-name: embedding-sdk-${{ github.event.pull_request.head.sha || github.sha }}
|
||||
@@ -161,8 +164,7 @@ jobs:
|
||||
- embedding-sdk-cli-snippets-type-check
|
||||
- embedding-sdk-docs-snippets-type-check
|
||||
- embedding-sdk-api-documentation-generation-validation
|
||||
- sdk-e2e-tests
|
||||
- e2e-component-tests-embedding-sdk-backward-compatibility
|
||||
- embedding-sdk-component-tests
|
||||
if: always() && !cancelled()
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
2
.github/workflows/frontend.yml
vendored
2
.github/workflows/frontend.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
- name: Restore ESLint cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: .eslintcache
|
||||
path: ${{ github.workspace}}/.eslintcache
|
||||
key: ${{ steps.get-cache-keys.outputs.eslint-cache-key }}
|
||||
|
||||
- name: Compile CLJS
|
||||
|
||||
227
.github/workflows/minimal-tests.yml
vendored
Normal file
227
.github/workflows/minimal-tests.yml
vendored
Normal file
@@ -0,0 +1,227 @@
|
||||
name: Minimal Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Only run if the PR has the minimal-tests label
|
||||
check-label:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 1
|
||||
outputs:
|
||||
should-run: ${{ steps.check.outputs.result }}
|
||||
steps:
|
||||
- name: Check for minimal-tests label
|
||||
id: check
|
||||
run: |
|
||||
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'minimal-tests') }}" == "true" ]]; then
|
||||
echo "result=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "result=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Postgres app-db tests - single version, single job
|
||||
postgres-app-db-test:
|
||||
needs: check-label
|
||||
if: needs.check-label.outputs.should-run == 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: postgres
|
||||
MB_DB_TYPE: postgres
|
||||
MB_DB_PORT: 5432
|
||||
MB_DB_HOST: localhost
|
||||
MB_DB_DBNAME: mb_test
|
||||
MB_DB_USER: mb_test
|
||||
MB_POSTGRESQL_TEST_USER: mb_test
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
ports:
|
||||
- "5432:5432"
|
||||
env:
|
||||
POSTGRES_USER: mb_test
|
||||
POSTGRES_DB: mb_test
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
name: Postgres Latest EE App DB Tests (Part 1)
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test Postgres App DB
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
build-static-viz: true
|
||||
junit-name: 'minimal-be-tests-postgres-ee'
|
||||
test-args: >-
|
||||
:only '["test" "enterprise/backend/test"]'
|
||||
:exclude-tags '[:mb/driver-tests :mb/transforms-python-test]'
|
||||
:partition/total 2
|
||||
:partition/index 0
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
|
||||
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
|
||||
# Postgres driver tests
|
||||
postgres-driver-test:
|
||||
needs: check-label
|
||||
if: needs.check-label.outputs.should-run == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CI: 'true'
|
||||
DRIVERS: postgres
|
||||
MB_DB_TYPE: postgres
|
||||
MB_DB_PORT: 5432
|
||||
MB_DB_HOST: localhost
|
||||
MB_DB_DBNAME: mb_test
|
||||
MB_DB_USER: mb_test
|
||||
MB_POSTGRESQL_TEST_USER: mb_test
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
ports:
|
||||
- "5432:5432"
|
||||
env:
|
||||
POSTGRES_USER: mb_test
|
||||
POSTGRES_DB: mb_test
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
name: Postgres Latest Driver Tests
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test Postgres Driver
|
||||
uses: ./.github/actions/test-driver
|
||||
with:
|
||||
junit-name: 'minimal-be-tests-postgres-driver-ee'
|
||||
test-args: >-
|
||||
:only-tags [:mb/driver-tests]
|
||||
:exclude-tags [:mb/transforms-python-test]
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit/
|
||||
output-name: ${{ github.job }}
|
||||
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
|
||||
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
|
||||
# Frontend unit tests
|
||||
frontend-unit-tests:
|
||||
needs: check-label
|
||||
if: needs.check-label.outputs.should-run == 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
JEST_JUNIT_OUTPUT_DIR: ./target/junit
|
||||
JEST_JUNIT_OUTPUT_NAME: test-report-frontend-unit.xml
|
||||
name: Frontend Unit Tests
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
- name: Run frontend unit tests
|
||||
run: yarn run test-unit --silent
|
||||
continue-on-error: true
|
||||
- name: Upload Test Results
|
||||
uses: ./.github/actions/upload-test-results
|
||||
if: always()
|
||||
with:
|
||||
input-path: ./target/junit
|
||||
output-name: frontend-unit
|
||||
bucket: ${{ vars.AWS_S3_TEST_RESULTS_BUCKET }}
|
||||
aws-access-key-id: ${{ secrets.AWS_TEST_RESULTS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_TEST_RESULTS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
|
||||
# Summary job
|
||||
minimal-tests-result:
|
||||
needs:
|
||||
- postgres-app-db-test
|
||||
- postgres-driver-test
|
||||
- frontend-unit-tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
name: minimal-tests-result
|
||||
if: always() && !cancelled()
|
||||
steps:
|
||||
- name: Check job status
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
needs: ${{ toJson(needs) }}
|
||||
with:
|
||||
script: | # js
|
||||
const needs = JSON.parse(process.env.needs);
|
||||
|
||||
const jobs = Object.entries(needs).map(
|
||||
([jobName, jobValues]) => ({
|
||||
name: jobName,
|
||||
result: jobValues.result
|
||||
}));
|
||||
|
||||
// Check if all jobs either succeeded or were skipped
|
||||
// Skipped jobs are acceptable (they were intentionally skipped when the label is absent)
|
||||
const allPassedOrSkipped = jobs.every(job =>
|
||||
job.result === 'success' || job.result === 'skipped'
|
||||
);
|
||||
|
||||
// Check if any jobs actually failed (not skipped, not cancelled)
|
||||
const anyFailed = jobs.some(job =>
|
||||
job.result === 'failure'
|
||||
);
|
||||
|
||||
if (allPassedOrSkipped && !anyFailed) {
|
||||
console.log("");
|
||||
console.log(" _------. ");
|
||||
console.log(" / , \_ ");
|
||||
console.log(" / / /{}\ |o\_ ");
|
||||
console.log(" / \ `--' /-' \ ");
|
||||
console.log(" | \ \ | ");
|
||||
console.log(" | |`-, | ");
|
||||
console.log(" / /__/)/ ");
|
||||
console.log(" | | ");
|
||||
console.log("");
|
||||
console.log("All minimal tests have passed. Fast feedback achieved!");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// otherwise, something failed
|
||||
console.log("");
|
||||
console.log(" .::::::::::. .::::::::::. ");
|
||||
console.log(" .::::''''''::::. .::::''''''::::. ");
|
||||
console.log(" .:::' `::::.... ....::::' `:::. ");
|
||||
console.log(" .::' `:::::::| |:::::::' `::. ");
|
||||
console.log(" .::| |::::::|_ ___ __|::::::| |::. ");
|
||||
console.log(" `--' |::::::|_()__()_|::::::| `--' ");
|
||||
console.log(" ::: |::-o::| |::o-::| ::: ");
|
||||
console.log(" `::. .|::::::| |::::::|. .::' ");
|
||||
console.log(" `:::. .::\-----' `-----/::. .:::' ");
|
||||
console.log(" `::::......::::' `::::......::::' ");
|
||||
console.log(" `::::::::::' `::::::::::' ");
|
||||
console.log("");
|
||||
console.log("Minimal tests have failed.");
|
||||
|
||||
jobs.forEach((job) => {
|
||||
if (job.result !== 'success' && job.result !== 'skipped') {
|
||||
console.log(`${job.name} - ${job.result}`);
|
||||
}
|
||||
});
|
||||
|
||||
process.exit(1);
|
||||
14
.github/workflows/pre-release.yml
vendored
14
.github/workflows/pre-release.yml
vendored
@@ -194,19 +194,21 @@ jobs:
|
||||
|
||||
- name: Run a few important OSS Cypress tests as sanity check
|
||||
if: ${{ matrix.edition == 'oss' }}
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
run: |
|
||||
node e2e/runner/run_cypress_ci.js e2e \
|
||||
--env grepTags="@smoke+-@EE",grepOmitFiltered=true \
|
||||
--spec './e2e/test/scenarios/**/*.cy.spec.(js|ts)' \
|
||||
--browser ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
--env grepTags="@smoke+-@EE" \
|
||||
--spec './e2e/test/scenarios/**/*.cy.spec.(js|ts)'
|
||||
|
||||
- name: Run a few important EE Cypress tests as sanity check
|
||||
if: ${{ matrix.edition == 'ee' }}
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
run: |
|
||||
node e2e/runner/run_cypress_ci.js e2e \
|
||||
--env grepTags="@smoke+-@OSS",grepOmitFiltered=true \
|
||||
--spec './e2e/test/scenarios/**/*.cy.spec.(js|ts)' \
|
||||
--browser ${{ steps.cypress-prep.outputs.chrome-path }}
|
||||
--env grepTags="@smoke+-@OSS" \
|
||||
--spec './e2e/test/scenarios/**/*.cy.spec.(js|ts)'
|
||||
|
||||
- name: Upload Cypress Artifacts upon failure
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
40
.github/workflows/run-tests.yml
vendored
40
.github/workflows/run-tests.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name,'ci skip') }}
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name,'ci skip') && !contains(github.event.pull_request.labels.*.name,'minimal-tests') }}
|
||||
name: Check which files changed
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
@@ -36,49 +36,19 @@ jobs:
|
||||
list-files: csv
|
||||
filters: .github/file-paths.yaml
|
||||
|
||||
static-viz-files-changed:
|
||||
name: Check whether static-viz files changed
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name,'ci skip') }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
static_viz: ${{ steps.static_viz.outputs.static_viz }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Prepare front-end environment
|
||||
uses: ./.github/actions/prepare-frontend
|
||||
- name: Prepare back-end environment
|
||||
uses: ./.github/actions/prepare-backend
|
||||
- name: Build static-viz frontend
|
||||
run: yarn build-static-viz
|
||||
env:
|
||||
MB_EDITION: ee
|
||||
- name: Upload Static Viz Bundle Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: static-viz-bundle-${{ github.sha }}
|
||||
path: resources/frontend_client/app/dist
|
||||
|
||||
- name: Check for static viz changes
|
||||
uses: dorny/paths-filter@v3.0.0
|
||||
id: static_viz
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/static-viz-sources.yaml
|
||||
|
||||
uberjar:
|
||||
needs: [files-changed, static-viz-files-changed]
|
||||
needs: [files-changed]
|
||||
if: ${{ !cancelled() && !(needs.files-changed.outputs.documentation == 'true' && needs.files-changed.outputs.backend_all == 'false' && needs.files-changed.outputs.frontend_all == 'false' && needs.files-changed.outputs.e2e_all == 'false') }}
|
||||
uses: ./.github/workflows/uberjar.yml
|
||||
secrets: inherit
|
||||
|
||||
backend-tests:
|
||||
if: ${{ !cancelled() && needs.files-changed.result == 'success' }}
|
||||
needs: [files-changed, static-viz-files-changed]
|
||||
needs: [files-changed]
|
||||
uses: ./.github/workflows/backend.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
skip: ${{ needs.files-changed.outputs.backend_all != 'true' && needs.static-viz-files-changed.outputs.static_viz != 'true' }}
|
||||
skip: ${{ needs.files-changed.outputs.backend_all != 'true' }}
|
||||
|
||||
semantic-search-tests:
|
||||
needs: files-changed
|
||||
@@ -123,7 +93,7 @@ jobs:
|
||||
specs: ${{ needs.files-changed.outputs.e2e_all_files == needs.files-changed.outputs.e2e_specs_files && needs.files-changed.outputs.e2e_specs_files || '' }}
|
||||
|
||||
sdk-tests:
|
||||
needs: files-changed
|
||||
needs: [uberjar]
|
||||
uses: ./.github/workflows/embedding-sdk.yml
|
||||
secrets: inherit
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
@@ -1,5 +1,8 @@
|
||||
accept = ["200..=204", "429"]
|
||||
exclude_path = ["docs/embedding/sdk"]
|
||||
exclude = ["\\.html(#.*)?$"]
|
||||
exclude_path = ["docs/util"]
|
||||
no_progress = true
|
||||
include_fragments = true
|
||||
verbose = "error"
|
||||
verbose = "error"
|
||||
include_verbatim = false
|
||||
cache = false
|
||||
|
||||
@@ -2,4 +2,4 @@ frontend/src/cljs
|
||||
docs/configuring-metabase/environment-variables.md
|
||||
docs/api/*
|
||||
docs/api-documentation.md
|
||||
enterprise/frontend/src/embedding-sdk-shared/.storybook/msw-public/mockServiceWorker.js
|
||||
frontend/src/embedding-sdk-shared/.storybook/msw-public/mockServiceWorker.js
|
||||
|
||||
724
CLAUDE.md
724
CLAUDE.md
@@ -1,722 +1,20 @@
|
||||
# Metabase Development Guide
|
||||
|
||||
## Autonomous Development Workflow
|
||||
# Skills
|
||||
|
||||
- Do not attempt to read or edit files outside the project folder
|
||||
- Add failing tests first, then fix them
|
||||
- Work autonomously in small, testable increments
|
||||
- Run targeted tests, and lint continuously during development
|
||||
- Prioritize understanding existing patterns before implementing
|
||||
- Don't commit changes, leave it for the user to review and make commits
|
||||
For detailed guidance on writing and reviewing code and documentation, see the skills in [.claude/skills/](.claude/skills/):
|
||||
|
||||
## Quick Commands
|
||||
## Clojure
|
||||
|
||||
### JavaScript/TypeScript
|
||||
- **[clojure-write](.claude/skills/clojure-write/SKILL.md)** - Clojure/ClojureScript development with REPL-driven workflow and coding conventions
|
||||
- **[clojure-review](.claude/skills/clojure-review/SKILL.md)** - Clojure/ClojureScript code review guidelines and style enforcement
|
||||
|
||||
- **Lint:** `yarn lint-eslint-pure`
|
||||
- **Test:** `yarn test-unit-keep-cljs path/to/file.unit.spec.js` or `yarn test-unit-keep-cljs -t "pattern"`
|
||||
- **Format:** `yarn prettier`
|
||||
- **Type Check:** `yarn type-check-pure`
|
||||
## TypeScript
|
||||
|
||||
### Clojure
|
||||
- **[typescript-write](.claude/skills/typescript-write/SKILL.md)** - TypeScript/JavaScript development and best practices
|
||||
- **[typescript-review](.claude/skills/typescript-review/SKILL.md)** - TypeScript/JavaScript code review guidelines
|
||||
|
||||
- **Lint PR:** `./bin/mage kondo-updated master` (or whatever target branch)
|
||||
- Call the command one time at the beginning, record the results, then work through the problems one at a time.
|
||||
- If the solution is obvious, then please apply the fix. Otherwise skip it.
|
||||
- If you fix all the issues (and verify by rerunning the kondo-updated command):
|
||||
- commit the change with a succinct and descriptive commit message
|
||||
- **Lint File:** `./bin/mage kondo <file or files>` (or whatever target branch)
|
||||
- Use the linter as a way to know that you are adhering to conventions in place in the codebase
|
||||
- **Lint Changes:** `./bin/mage kondo-updated HEAD`
|
||||
- **Format:** `./bin/mage cljfmt-files [path]`
|
||||
- **Run a test:** `./bin-mage run-tests namespace/test-name`
|
||||
- **Run all tests in a namespace:** `./bin-mage run-tests namespace`
|
||||
- **Check Code Readability** `./bin/mage -check-readable` with optional line-number
|
||||
- Run this after every change to Clojure code, only accept readable code
|
||||
- **Evaluating Clojure Code** `./bin/mage -repl '<code>'`
|
||||
- See `Sending code to the REPL` for more details
|
||||
## Documentation
|
||||
|
||||
### ClojureScript
|
||||
|
||||
- **Test:** `yarn test-cljs`
|
||||
|
||||
## Clojure REPL-driven development
|
||||
|
||||
- Start with small, fundamental functions:
|
||||
- Identify the core features or functionalities required for your task.
|
||||
- Break each feature down into the smallest, most basic functions that can be developed and tested independently.
|
||||
- Write and test in the REPL:
|
||||
- Write the code for each small function directly in the REPL (Read-Eval-Print Loop).
|
||||
- Test it thoroughly with a variety of inputs, including typical use cases and relevant edge cases, to ensure it
|
||||
behaves as expected.
|
||||
- Integrate into source code:
|
||||
- Once a function works correctly in the REPL, move it from the REPL environment into your source code files (e.g.,
|
||||
within appropriate namespaces).
|
||||
- Gradually increase complexity:
|
||||
- Build upon tested, basic functions to create more complex functions or components.
|
||||
- Compose smaller functions together, testing each new composition in the REPL to verify correctness step by step.
|
||||
- Ensure dependency testing:
|
||||
- Make sure every function is fully tested in the REPL before it is depended upon by other functions.
|
||||
- This ensures that each layer of your application is reliable before you build on it.
|
||||
- Leverage the REPL fully:
|
||||
- Use the REPL as your primary tool to experiment with different approaches, iterate quickly, and get immediate
|
||||
feedback on your code.
|
||||
- Follow functional programming principles:
|
||||
- Keep functions small, focused, and composable.
|
||||
- Leverage Clojure's functional programming features—like immutability, higher-order functions, and the standard
|
||||
library—to write concise, effective code.
|
||||
|
||||
### How to evaluate code
|
||||
|
||||
#### Keeping Code Readable
|
||||
|
||||
The `./bin/mage -check-readable <file> <optional: line-number>` command checks if your Clojure code can be properly parsed.
|
||||
This ensures your changes maintain valid syntax and structure.
|
||||
|
||||
- Edit Clojure files one step at a time.
|
||||
- After EVERY change to a Clojure form, call `mage -check-readable src/metabase/thefile.clj <line-number>` with the line number.
|
||||
- If it's readable then call `mage -check-readable dev/src/dev.clj` without the line number to check the entire file.
|
||||
- If the change results in unreadable code, try again until it is readable.
|
||||
- To overcome errors about parens, pay close attention to them. Count opening/closing parens you add/remove.
|
||||
|
||||
#### Bottom-up dev loop
|
||||
|
||||
1. Write code into a file.
|
||||
2. Evaluate the file's namespace and make sure it loads correctly with:
|
||||
|
||||
```
|
||||
mage -repl --namespace metabase.app-db.connection
|
||||
```
|
||||
|
||||
3. Call functions in the namespace with test inputs, and observe that the outputs are correct 3.1
|
||||
Feel free to copy these REPL session trials into actual test cases using `deftest` and `is`.
|
||||
4. Once you know these functions are good, return to 1, and compose them into the task that you need to build.
|
||||
|
||||
#### Sending code to the REPL
|
||||
|
||||
- Send code to the metabase process REPL using: `./bin/mage -repl '(+ 1 1)'` where `(+ 1 1)` is your Clojure code.
|
||||
- See `./bin/mage -repl -h` for more details.
|
||||
- If the Metabase backend is not running, you'll see an error message with instructions on how to start it.
|
||||
|
||||
##### Working with files and namespaces
|
||||
|
||||
1. **Load a file and call functions with fully qualified names**:
|
||||
|
||||
To call `your.namespace/your-function` on `arg1` and `arg2`:
|
||||
|
||||
```
|
||||
./bin/mage -repl --namespace your.namespace '(your-function arg1 arg2)'
|
||||
```
|
||||
|
||||
DO NOT use "require", "load-file" etc in the code string argument.
|
||||
|
||||
##### Understanding the response
|
||||
|
||||
The `./bin/mage -repl` command returns three separate, independent outputs:
|
||||
|
||||
- `value`: The return value of the last expression (best for data structures)
|
||||
- `stdout`: Any printed output from `println` etc. (best for messages)
|
||||
- `stderr`: Any error messages (best for warnings and errors)
|
||||
|
||||
Example call:
|
||||
|
||||
```bash
|
||||
./bin/mage -repl '(println "Hello, world!") '\''({0 1, 1 3, 2 0, 3 2} {0 2, 1 0, 2 3, 3 1})'
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```
|
||||
ns: user
|
||||
session: 32a35206-871c-4553-9bc9-f49491173d1c
|
||||
value: ({0 1, 1 3, 2 0, 3 2} {0 2, 1 0, 2 3, 3 1})
|
||||
stdout: Hello, world!
|
||||
stderr:
|
||||
```
|
||||
|
||||
For effective REPL usage:
|
||||
|
||||
- Return data structures as function return values
|
||||
- Use `println` for human-readable messages
|
||||
- Print errors to stderr
|
||||
|
||||
## Tips
|
||||
|
||||
- End all files with a newline.
|
||||
- When editing tabular code, where the columns line up, try to keep them aligned.
|
||||
- Spaces on a line with nothing after it is not allowed
|
||||
|
||||
## Critical REPL Usage Rules
|
||||
|
||||
- Be careful with parentheses counts when editing Clojure code
|
||||
- After EVERY change to Clojure code, verify readability with `-check-readable`
|
||||
|
||||
# Code Review Guidelines
|
||||
|
||||
Review pull requests with a focus on:
|
||||
|
||||
- Compliance with project coding standards and conventions
|
||||
|
||||
- Code quality and best practices
|
||||
|
||||
- Clear and correct docstrings
|
||||
|
||||
- Clear and correct comments explaining what the code is doing
|
||||
|
||||
## More Instructions
|
||||
|
||||
- Carefully read the Community Clojure Style Guide from `CLOJURE_STYLE_GUIDE.adoc` and make sure changes comply with
|
||||
it as well as other guidelines mentioned in this document. Flag _all_ violations of the Community Clojure Style
|
||||
Guide.
|
||||
|
||||
- Do not post comments congratulating someone for trivial changes, or for following the style guidelines, or
|
||||
confirming that things "look good", or telling them that they did something correctly; only post comments about
|
||||
style violations or potential issues. Example bad code review comments to avoid:
|
||||
|
||||
> This TODO comment is properly formatted with author and date - nice work!
|
||||
|
||||
> Good addition of limit 1 to the query - this makes the test more efficient without changing its behavior.
|
||||
|
||||
> The kondo ignore comment is appropriately placed here
|
||||
|
||||
> The kondo ignore is properly used here. The comment on lines 42-45 explains why hardcoded driver names are needed,
|
||||
> which is good context.
|
||||
|
||||
> Test name properly ends with -test as required by the style guide.
|
||||
|
||||
- Do not post comments about missing parentheses.
|
||||
|
||||
# Code Conventions and Style Guide
|
||||
|
||||
## Clojure and ClojureScript
|
||||
|
||||
### General Conventions
|
||||
|
||||
- Prefer longer, more verbose names for functions and variables; avoid abbreviations unless they are well-known and
|
||||
conventional in the Clojure world. `acc`, `i`, `pred`, `coll`, `n`, `s`, `k`, and `f` are examples of well-known
|
||||
conventions; any Clojure developer has seen them before and can tell you what they mean. Avoid unconventional
|
||||
abbreviations like `tbl` and unclear variable names like `zs'`. A good function or variable name should make its
|
||||
purpose immediately clear. Error on the side of longer variable names if needed.
|
||||
|
||||
```clj
|
||||
;; too cryptic
|
||||
(defn mayb+1 [n]
|
||||
(when n
|
||||
(inc n)))
|
||||
|
||||
;; just right
|
||||
(defn maybe-inc [n]
|
||||
(when n
|
||||
(inc n)))
|
||||
```
|
||||
|
||||
**Why?** Code is read many more times than it is written, and clearer variable names make using and tweaking your
|
||||
code easier for others.
|
||||
|
||||
Conventions used widely in our codebase (like `mp` for a Metadata Provider) are acceptable; one-offs like `zs'` are
|
||||
not.
|
||||
|
||||
- Avoid misleading variable and function names. The names of a variable or function should clearly and unambiguously
|
||||
describe its purpose and match what it does.
|
||||
|
||||
```clj
|
||||
;; bad
|
||||
(defn nil-or-maplist? [v] ; coll would be a better variable name because it's more specific
|
||||
(or (nil? v)
|
||||
(and (sequential? v) ; v can actually be an array, vector, list, or lazy seq
|
||||
(every? map? v))))
|
||||
|
||||
;; good
|
||||
(defn nil-or-sequence-of-maps? [coll]
|
||||
(or (nil? coll)
|
||||
(and (sequential? coll)
|
||||
(every? map? coll))))
|
||||
```
|
||||
|
||||
**Why?** Poorly-named functions are prone to being used in cases where they're inappropriate or avoided in cases
|
||||
when they would be suitable.
|
||||
|
||||
- Pure function names should be nouns describing the value they return.
|
||||
|
||||
For example, a function to compute a user's age based on their birthdate should be called `age`, not `calculate-age`
|
||||
or `get-age`.
|
||||
|
||||
**Why?** A pure function is one which can be replaced with its value without affecting the result, so the name
|
||||
should reflect that.
|
||||
|
||||
- Don't repeat the usual alias of the namespace a function belongs to in the name of a function itself.
|
||||
|
||||
```clj
|
||||
(ns metabase.config)
|
||||
|
||||
;; bad
|
||||
(defn config-is-dev? [] ...)
|
||||
|
||||
;; good
|
||||
(defn is-dev? [] ...)
|
||||
```
|
||||
|
||||
**Why?** It's obvious that `is-dev?` in the example above is referring to `dev`, because it's in the `config`
|
||||
namespace. It's also needlessly noisy when using the function in another namespace:
|
||||
|
||||
```clj
|
||||
;; bad
|
||||
(when (config/config-is-dev?)
|
||||
...)
|
||||
|
||||
;; good
|
||||
(when (config/is-dev?)
|
||||
...)
|
||||
```
|
||||
|
||||
In some cases, following this rule will require you to use a `(:refer-clojure :exclude [...])` form in your namespace
|
||||
declaration. This is acceptable, and should be taken as a sign that you're following this rule correctly.
|
||||
|
||||
- If you shadow a `clojure.core` var, make sure you use `(:refer-clojure :exclude ...)` in the `ns` form.
|
||||
|
||||
- Make everything `^:private` unless it is used elsewhere.
|
||||
|
||||
Don't make things public just for the sake of tests. Use the var form (e.g. `#'redshift/execute!`) instead in your
|
||||
tests.
|
||||
|
||||
**Why?** It's much easier to read and refactor code when you know its scope is limited to the current namespace.
|
||||
|
||||
- Tag variables with `:arglists` metadata if they are functions but wouldn't otherwise have it, such as when using
|
||||
`def` to define partial functions or function compositions e.g.
|
||||
|
||||
```clj
|
||||
(def ^{:arglists '([n])} plus-one (partial + 1))
|
||||
```
|
||||
|
||||
**Why?** Good editors use this metadata show the expected arguments to a function as you're writing code.
|
||||
|
||||
- Try to organize namespaces in such a way that you don't need to use `declare`. This usually means putting the public
|
||||
portion of a namespace near the end of a file.
|
||||
|
||||
**Why?** Avoiding declare when unnecessary forces us to read and write code in a consistent manner, that is, from
|
||||
top to bottom. When code is written in this consistent order we can safely assume referenced functions sit somewhere
|
||||
above their reference in the namespace; this makes the code easier to navigate.
|
||||
|
||||
- Don't mark things `^:const` unless you have a really good reason for doing so. Add a comment explaining why you
|
||||
marked it `^:const`.
|
||||
|
||||
- Every public var in `src` or `enterprise/backend/src` must have a _useful_ docstring. A useful docstring
|
||||
should clearly explain the purpose of the function, its inputs and outputs, and anything else that is otherwise not
|
||||
immediately clear. If there are other functions that have similar purposes, explain how the use-cases for this
|
||||
function differ.
|
||||
|
||||
Tests and other vars in the `test` or `enterprise/backend/test` do not _require_ docstrings, but general helper
|
||||
functions used across many namespaces it should have docstrings.
|
||||
|
||||
Schemas defined with `mr/def` e.g. `(mr/def ::my-schema)` do not require docstrings.
|
||||
|
||||
- Format docstrings according to Markdown conventions. (https://guide.clojure.style/#markdown-docstrings)
|
||||
|
||||
- Mentions of other vars in docstrings should use `[[some-other-var]]` (for vars in the same namespace) or
|
||||
`[[metabase.namespace/some-other-var]]` (for vars in a different namespace) instead of backticks; references in
|
||||
docstrings should be valid (i.e., point to something that exists).
|
||||
(https://guide.clojure.style/#document-references)
|
||||
|
||||
- Judiciously use comments to explain sections of code that would not immediately be clear to someone else. Avoid
|
||||
comments that do little more than repeat what the code already says.
|
||||
|
||||
- Make sure to update comments and docstrings when you change the code they describe.
|
||||
|
||||
- `TODO` comments should include the author and date, for example
|
||||
|
||||
```clj
|
||||
;; TODO (Cam 10/7/25) -- this is a properly formatted TODO comment
|
||||
(...)
|
||||
```
|
||||
|
||||
- Break up larger functions (> 20 lines) in source code (`src` or `enterprise/backend/src`) whenever possible. Small
|
||||
functions are much easier to test, understand, and tweak. Tests are allowed to be longer, especially when they
|
||||
contain mock data, but they should probably never be more than 100 lines.
|
||||
|
||||
- Try to keep lines 120 characters wide or less; use this as a guideline when formatting docstrings or comments.
|
||||
|
||||
- **No Blank Lines Within Definition Forms** Do not place blank lines in the middle of a function or macro definition.
|
||||
An exception can be made to indicate grouping of pairwise constructs as found in e.g. `let` and `cond`, in case
|
||||
those don’t fit on the same line. `deftest` is **NOT** an exception to this rule.
|
||||
(https://guide.clojure.style/#no-blank-lines-within-def-forms) Verify that the line in question is **actually
|
||||
blank** (i.e., only contains whitespace for the **entire line**) before warning about this. **A line that contains
|
||||
any non-whitespace character is not a blank line.** You will be shut down if you make this mistake too many times,
|
||||
so be careful.
|
||||
|
||||
- Use `kebab-case` names for variables and defs, including constants.
|
||||
(https://guide.clojure.style/#naming-functions-and-variables)
|
||||
|
||||
```clj
|
||||
;;; BAD
|
||||
(def MY_CONSTANT 100)
|
||||
|
||||
;;; GOOD
|
||||
(def my-constant 100)
|
||||
|
||||
;;; BAD
|
||||
(defn myFunction [my_arg] ...)
|
||||
|
||||
;;; GOOD
|
||||
(defn my-function [my-arg] ...)
|
||||
```
|
||||
|
||||
- Map destructuring should use kebab-case local bindings even if the map it was destructured from uses `snake_case`
|
||||
keys or if it is returned as a value for a `snake_case` key.
|
||||
|
||||
```clj
|
||||
;; Good
|
||||
(let [{database-id :database_id} some-object]
|
||||
{:database_id database-id, :table_id 100})
|
||||
|
||||
;; Bad
|
||||
(let [{database_id :database_id} some-object]
|
||||
{:database_id database_id, :table_id 100})
|
||||
```
|
||||
|
||||
- Prefer namespaced keywords for keywords that are used internally (i.e., not returned by the REST API or persisted by
|
||||
the app DB):
|
||||
|
||||
```clj
|
||||
;;; good
|
||||
(defn query-type [x]
|
||||
(if (some-pred? x)
|
||||
:query-type/normal
|
||||
:query-type/crazy))
|
||||
|
||||
;;; bad
|
||||
(defn query-type [x]
|
||||
(if (some-pred? x)
|
||||
:normal
|
||||
:crazy))
|
||||
```
|
||||
|
||||
These are easy to search across the entire application and makes their origin clearer.
|
||||
|
||||
- Functions that have side-effects such as writing to the application database or mutating the global state of the
|
||||
application should have names that end in exclamation points. Exclamation points should be considered "sticky", so
|
||||
if a function uses another function with a name ending in an exclamation point, it too should have a name that ends
|
||||
in an exclamation point. (https://guide.clojure.style/#naming-unsafe-functions)
|
||||
|
||||
An exception is functions that write log messages or other output to the console; these don't need exclamation
|
||||
points.
|
||||
|
||||
### Tests
|
||||
|
||||
- Large tests should be broken out into separate `deftest` forms when they consist of several logically separate test
|
||||
cases.
|
||||
|
||||
```clj
|
||||
(deftest ^:parallel my-test
|
||||
(testing "Some logically discrete test case"
|
||||
(is ...)))
|
||||
|
||||
(deftest ^:parallel my-other-test
|
||||
(testing "Another logically discrete test case"
|
||||
(is ...)))
|
||||
|
||||
(deftest ^:parallel my-third-different-test
|
||||
(testing "A third logically discrete test case"
|
||||
(is ...)))
|
||||
|
||||
(deftest ^:parallel my-amazing-test
|
||||
(testing "A fourth logically discrete test case"
|
||||
(is ...)))
|
||||
```
|
||||
|
||||
is preferable to
|
||||
|
||||
```clj
|
||||
(deftest ^:parallel my-test
|
||||
(testing "Some logically discrete test case"
|
||||
(is ...))
|
||||
|
||||
(testing "Another logically discrete test case"
|
||||
(is ...))
|
||||
|
||||
(testing "A third logically discrete test case"
|
||||
(is ...))
|
||||
|
||||
(testing "A fourth logically discrete test case"
|
||||
(is ...)))
|
||||
```
|
||||
|
||||
- Mark pure function tests `^:parallel`.
|
||||
|
||||
- Test utility functions that are not thread-safe/safe in `^:parallel` tests should have names that end in an
|
||||
exclamation mark.
|
||||
|
||||
- Test names in `deftest` forms should end in `-test` or `-test-<number>` e.g. `whatever-test` or `whatever-test-2` or
|
||||
even `whatever-test-2b`. (https://guide.clojure.style/#test-naming)
|
||||
|
||||
### Modules
|
||||
|
||||
- The backend codebase is broken out into separate modules.
|
||||
|
||||
- The module configuration file lives in `.clj-kondo/config/modules/config.edn`. There is one entry for each module.
|
||||
The entry has several keys, but the important ones are `:api` -- the list of namespaces this module provides for use
|
||||
outside of the module -- and `:uses` -- the list of modules this module directly relies on.
|
||||
|
||||
- An OSS follows the pattern `metabase.<module>.*` (for the Clojure namespace) and `src/metabase/<module>/**`(for the
|
||||
source files) with tests inside the corresponding `test/metabase/<module>/` directory, e.g. the `dashboards` module
|
||||
is everything inside `src/metabase/dashboards/` and `test/metabase/dashboards/`; it might have a
|
||||
`metabase.dashboards-rest.api` namespace that corresponds to the file `src/metabase/dashboards/api.clj`.
|
||||
|
||||
- An EE module follows the pattern `metabase-enterprise.<module>.*` (for the Clojure namespaces) and
|
||||
`enterprise/backend/src/metabase_enterprise/<module>/**` (for the source files). The module name uses the
|
||||
`enterprise/` prefix. For example, `enterprise/billing` is everything in the
|
||||
`enterprise/backend/src/metabase_enterprise/billing/` directory (for source code) and
|
||||
`enterprise/backend/test/metabase_enterprise/billing/` (for tests); it might have a namespace called
|
||||
`metabase-enterprise.billing.api` which corresponds to the file
|
||||
`enterprise/backend/src/metabase_enterprise/billing/api.clj`.
|
||||
|
||||
- REST API endpoints (defined by the `defendpoint` macro) should live in a `<module>.api` or `<module>.api.*`
|
||||
namespace, e.g. `metabase.dashboards.api` or `metabase.dashboards.api.x`.
|
||||
|
||||
- When you add a new API endpoint namespace, you need to add a mapping for it in `metabase.api-routes.routes`.
|
||||
|
||||
- Put any functions used by other modules (the API meant for the rest of the backend code) in `<module>.core` .
|
||||
`.core` should import stuff with Potemkin/ `metabase.util.namespace` and not be used inside the module itself. It’s
|
||||
also nice to put a `:consistent-alias` entry for this namespace in the Kondo config.
|
||||
|
||||
- The `<module>.core` namespace should generally be empty other than importing things from other namespaces with
|
||||
Potemkin (`potemkin/import-vars` and the like).
|
||||
|
||||
- Put Toucan models related to a feature in `<module>.models.*` and add mappings in `metabase.models.resolution`.
|
||||
|
||||
- Put scheduled Quartz tasks in `<module>.task.*`.
|
||||
|
||||
- Put event handlers (things that use the events subsystem in `metabase.events.core`) in `<module>.event.*`.
|
||||
|
||||
- Put `defsetting`s (Settings) in `<module>.settings`.
|
||||
|
||||
- Quartz tasks, event handlers, and Settings all need to be loaded on launch, so if you have any of the above add
|
||||
them to a `<module>.init` namespace and require it in `metabase[-enterprise].core.init`.
|
||||
|
||||
- Don't try to cheat the module linters by using things like `#_{:clj-kondo/ignore [:metabase/modules]}`. Note that
|
||||
this does not apply to using `:clj-kondo/ignore` to disable warnings for other linters besides `:metabase/modules`.
|
||||
|
||||
- Put Malli schemas in `<module>.schema`.
|
||||
|
||||
- Aim to keep the number of namespaces used outside of a module small. A module should only need at most a subset of
|
||||
`<module>.api`, `<module>.settings`, `<module>.schema`, `<module>.init`, and `<module>.core`.
|
||||
|
||||
- Try to minimize the direct dependencies of a module as well as the indirect dependencies. Modules that are used by
|
||||
lots of other modules (such as `util`) ideally will have no dependencies on other modules. Our goal is to make as
|
||||
many modules as possible be "leaf nodes".
|
||||
|
||||
- Module names should be match the customer-facing name of the feature they concern, usually a plural noun, for
|
||||
example, `dashboards` rather than `dashboard`.
|
||||
|
||||
### Settings
|
||||
|
||||
- Don't define configurable options that can only be set with environment variables; use an `:internal` `defsetting`
|
||||
instead. We have lots of tooling around `defsetting`.
|
||||
|
||||
### REST API Endpoints
|
||||
|
||||
- All new REST API Endpoints (defined by `defendpoint`) should have a response schema (denoted by `:- <schema>` after
|
||||
the route string).
|
||||
|
||||
```clj
|
||||
;;; BAD
|
||||
(api.macros/defendpoint :get "/"
|
||||
"Get a list of all transform tags."
|
||||
...)
|
||||
|
||||
;;; GOOD
|
||||
(api.macros/defendpoint :get "/" :- [:sequential ::whatever-this-returns]
|
||||
"Get a list of all transform tags."
|
||||
...)
|
||||
```
|
||||
|
||||
- Query parameters should also use kebab-case e.g. `GET /api/dashboards?include-archived=true` is good while `GET
|
||||
/api/dashboards?include_archived=true` or `GET /api/dashboards?includeArchived=true` is bad.
|
||||
|
||||
- HTTP request bodies should use `snake_case`.
|
||||
|
||||
- REST API endpoints should have routes that use singular nouns, for example `GET /api/dashboard/:id` to get a
|
||||
Dashboard rather than `GET /api/dashboards/:id`.
|
||||
|
||||
- `GET` endpoints should not have side effects outside of analytics like updating last-viewed-at timestamps or
|
||||
recording usage metrics. A `GET` endpoint should not be creating new rows in the application database representing
|
||||
user-facing objects, for example `GET /api/dashboard` should not attempt to populate empty Dashboards with content
|
||||
by creating new rows in the application database.
|
||||
|
||||
- `defendpoint` forms should be small wrappers around Toucan model code. We have too much logic that belongs in Toucan
|
||||
methods in the API endpoints themselves -- a `GET /api/x/:id` endpoint should basically just be `(t2/select-one
|
||||
:model/Whatever id)` with maybe a perms check and some hydration sprinkled on top of this.
|
||||
|
||||
- All API endpoints should have Malli schemas for any parameters that aren't ignored. `_route-params` doesn't need a
|
||||
schema, but `{:keys [x]}` should have one.
|
||||
|
||||
- Malli schemas for REST API endpoints should be detailed enough that someone can look at them and know how to use the
|
||||
endpoint.
|
||||
|
||||
Example: for an endpoint like this:
|
||||
|
||||
```clj
|
||||
(api.macros/defendpoint :post "/check_transform"
|
||||
"Check a proposed edit to a transform, and return the card, transform, etc. IDs for things that will break."
|
||||
[_route-params
|
||||
_query-params
|
||||
{:keys [id source target] :as _body} :- ::transform-body]
|
||||
...)
|
||||
```
|
||||
|
||||
This is a bad schema for the request body:
|
||||
|
||||
```clj
|
||||
;;; Bad, does not specify the shape of :source and :target in any real detail
|
||||
(mr/def ::transform-body
|
||||
[:map
|
||||
[:id {:optional false} ms/PositiveInt]
|
||||
[:name {:optional true} :string]
|
||||
[:source {:optional true} [:maybe ms/Map]]
|
||||
[:target {:optional true} [:maybe ms/Map]]])
|
||||
```
|
||||
|
||||
While this is better:
|
||||
|
||||
```clj
|
||||
;;; Better, because it tells us the expected shape of :source and :target
|
||||
(mr/def ::transform-body
|
||||
[:map
|
||||
[:id {:optional false} ms/PositiveInt]
|
||||
[:name {:optional true} :string]
|
||||
[:source {:optional true} [:maybe [:map [:x pos-int?] [:y pos-int?]]]]
|
||||
[:target {:optional true} [:maybe [:map [:a pos-int?] [:b pos-int?]]]]])
|
||||
```
|
||||
|
||||
- All new REST API endpoints **MUST HAVE TESTS**.
|
||||
|
||||
### MBQL
|
||||
|
||||
- No raw MBQL introspection or manipulation should be done outside of Lib (the `lib` and `lib-be` modules) or the
|
||||
Query Processor (the `query-processor` module) modules. MBQL maps include the `:model/Card` `dataset_query`, you can
|
||||
usually recognize it when you see a map with a `:database` key and either `:type` or `:lib/type`. You should treat
|
||||
this map as an opaque object outside of the aforementioned modules; pretend you didn't know it has a `:database` or
|
||||
`:type` key.
|
||||
|
||||
- Use Lib and MBQL 5 in all new **source** code instead of legacy MBQL; avoid use of the `legacy-mbql` module, or the
|
||||
`metabase.query-processor.store` namespace, in new code. Any code that checks whether a query `:type` is `:native`
|
||||
or `:query` is a gigantic code smell. Exception: we will allow legacy MBQL in tests... for now.
|
||||
|
||||
- Use of `qp.store/with-metadata-provider` or `mt/with-metadata-provider` should be discouraged and phased out.
|
||||
|
||||
## Models and the Application Database
|
||||
|
||||
- Model names should be singular nouns, e.g. `:model/Dashboard` and not `:model/Dashboards`.
|
||||
|
||||
- Table names in the application database should be singular nouns, e.g. `transform` instead of `transforms`.
|
||||
|
||||
- The application database should use `snake_case` identifiers for table and column names.
|
||||
|
||||
## Using Toucan
|
||||
|
||||
- Never fetch an entire row from the application database only to immediately discard everything except for the value
|
||||
of one column, this is super inefficient and also icky. Use `t2/select-one-fn` instead.
|
||||
|
||||
```clj
|
||||
;;; Bad
|
||||
(:database_id (t2/select-one :model/Card :id 1))
|
||||
|
||||
;;; Good
|
||||
(t2/select-one-fn :database_id :model/Card :id 1)
|
||||
|
||||
;;; Even better -- this does SELECT database_id instead of SELECT *
|
||||
(t2/select-one-fn :database_id [:model/Card :database_id] :id 1)
|
||||
```
|
||||
|
||||
- One of the big ideas behind Toucan is there's never supposed to be a question of "what function to I use to create
|
||||
an X correctly" or "what function do I use to update a Y correctly" -- the answer is always supposed to be
|
||||
`t2/select`, `t2/update!`, etc. That's why they're backed by multimethods, so you can put the correct behavior in
|
||||
the model's method and then it becomes impossible for you to forget to call the correct function. Avoid adding
|
||||
functions like `select-dashboards` or `update-dashboard!` -- put this functionality in Toucan methods for the model
|
||||
in question. This includes things like firing off events.
|
||||
|
||||
- Avoid the `mi/define-batched-hydration-method` and `mi/define-simple-hydration-method` helpers in new code; define
|
||||
method implementations with `(methodical/defmethod t2/batched-hydrate ...)` directly instead.
|
||||
|
||||
### Drivers
|
||||
|
||||
- All new driver multimethods must be mentioned in `docs/developers-guide/driver-changelog.md`.
|
||||
|
||||
- All new driver multimethods should use Lib-style kebab-cased metadata and MBQL 5 queries.
|
||||
|
||||
- Driver multimethod implementations (`defmethod` forms) should explicitly pass any `driver` argument to any other
|
||||
driver multimethods it invokes, rather than hardcoding the driver name.
|
||||
|
||||
```clj
|
||||
;;; Good
|
||||
(defmethod driver/driver-method :postgres
|
||||
[driver x]
|
||||
(driver/some-other-method driver x))
|
||||
|
||||
;;; Bad
|
||||
(defmethod driver/driver-method :postgres
|
||||
[_driver x]
|
||||
(driver/some-other-method :postgres x))
|
||||
```
|
||||
|
||||
This is because other drivers may inherit from this driver and we need to propagate the driver name passed in to any
|
||||
method implementations it inherits.
|
||||
|
||||
- Drivers should only use methods from the `driver` or `driver-api` modules.
|
||||
|
||||
- Be sure to minimize the amount of logic you're doing inside the `read-column-thunk` in JDBC-based drivers as much as
|
||||
possible. Here's our Athena code for java.sql.Types/TIMESTAMP_WITH_TIMEZONE for example:
|
||||
|
||||
```clj
|
||||
(defmethod sql-jdbc.execute/read-column-thunk [:athena Types/TIMESTAMP_WITH_TIMEZONE]
|
||||
[_driver ^ResultSet rs _rs-meta ^Long i]
|
||||
(fn []
|
||||
;; Using ZonedDateTime if available to conform tests first. OffsetDateTime if former is not available.
|
||||
(when-some [^Timestamp timestamp (.getObject rs i Timestamp)]
|
||||
(let [timestamp-instant (.toInstant timestamp)
|
||||
results-timezone (driver-api/results-timezone-id)]
|
||||
...))))
|
||||
```
|
||||
|
||||
Here's a tiny change that will avoid calling (driver-api/results-timezone-id) potentially 10 million times (for a
|
||||
potential million-row result with 10 timestamp columns)
|
||||
|
||||
```clj
|
||||
(defmethod sql-jdbc.execute/read-column-thunk [:athena Types/TIMESTAMP_WITH_TIMEZONE]
|
||||
[_driver ^ResultSet rs _rs-meta ^Long i]
|
||||
(let [results-timezone (driver-api/results-timezone-id)]
|
||||
(fn []
|
||||
;; Using ZonedDateTime if available to conform tests first. OffsetDateTime if former is not available.
|
||||
(when-some [^Timestamp timestamp (.getObject rs i Timestamp)]
|
||||
(let [timestamp-instant (.toInstant timestamp)]
|
||||
...)))))
|
||||
```
|
||||
|
||||
- Avoid defining new datasets with `defdataset` as much as possible, since loading test data in cloud-based databases
|
||||
is crazy slow.
|
||||
|
||||
### Misc
|
||||
|
||||
- Example data should be bird-themed if possible.
|
||||
|
||||
- Any comments that are written by or mention `Cam` should be given bonus points.
|
||||
|
||||
### Kondo
|
||||
|
||||
- Kondo linter warnings are suppressed with a comment proceeding the form to ignore warnings in e.g.
|
||||
|
||||
```clj
|
||||
#_{:clj-kondo/ignore [:metabase/disallow-hardcoded-driver-names-in-tests]}
|
||||
(do-x ...)
|
||||
```
|
||||
|
||||
to ignore the `:metabase/disallow-hardcoded-driver-names-in-tests` Linter inside the `(do-x ...)` form. You do not
|
||||
need to point these out to us, we know what they do.
|
||||
|
||||
```clj
|
||||
^{:clj-kondo/ignore [:metabase/disallow-hardcoded-driver-names-in-tests]}
|
||||
(do-x ...)
|
||||
```
|
||||
|
||||
is an acceptable alternative.
|
||||
|
||||
- Avoid ignoring **everything** with a `#_:clj-kondo/ignore` (a keyword instead of a map).
|
||||
- **[docs-write](.claude/skills/docs-write/SKILL.md)** - Documentation writing with Metabase style guide
|
||||
- **[docs-review](.claude/skills/docs-review/SKILL.md)** - Documentation review checklist
|
||||
|
||||
@@ -20,8 +20,9 @@ const REPLACES_MAP = {
|
||||
"metabase-types": "frontend/src/metabase-types",
|
||||
metabase: "frontend/src/metabase",
|
||||
"embedding-sdk-package": "enterprise/frontend/src/embedding-sdk-package",
|
||||
"embedding-sdk-bundle": "enterprise/frontend/src/embedding-sdk-bundle",
|
||||
"embedding-sdk-shared": "enterprise/frontend/src/embedding-sdk-shared",
|
||||
"embedding-sdk-bundle": "frontend/src/embedding-sdk-bundle",
|
||||
"embedding-sdk-shared": "frontend/src/embedding-sdk-shared",
|
||||
"embedding-sdk-ee": "enterprise/frontend/src/embedding-sdk-ee",
|
||||
cljs: "target/cljs_release",
|
||||
};
|
||||
|
||||
@@ -31,13 +32,15 @@ const API_EXTRACTOR_CONFIG_PATH = path.join(
|
||||
);
|
||||
|
||||
const getLogger = (prefix) => {
|
||||
const verbose = process.env.SDK_FIXUP_VERBOSE_LOGS === "true";
|
||||
return {
|
||||
verbose: (message) => verbose && console.log(`[${prefix}] ${message}`),
|
||||
log: (message) => console.log(`[${prefix}] ${message}`),
|
||||
error: (message) => console.error(`[${prefix}] ${message}`),
|
||||
};
|
||||
};
|
||||
|
||||
const { log } = getLogger("dts fixup");
|
||||
const { log, verbose } = getLogger("dts fixup");
|
||||
|
||||
const getRelativePath = (fromPath, toPath) => {
|
||||
const relativePath = path.relative(path.dirname(fromPath), toPath);
|
||||
@@ -67,7 +70,7 @@ const replaceAliasedImports = (filePath) => {
|
||||
});
|
||||
|
||||
fs.writeFileSync(filePath, fileContent, { encoding: "utf-8" });
|
||||
log(`Edited file: ${filePath}`);
|
||||
verbose(`Edited file: ${filePath}`);
|
||||
};
|
||||
|
||||
const removeUnresolvedReexports = (filePath) => {
|
||||
@@ -94,7 +97,7 @@ const removeUnresolvedReexports = (filePath) => {
|
||||
!fs.existsSync(`${path.resolve(dirPath, target)}.d.ts`);
|
||||
|
||||
if (isUnresolved) {
|
||||
log(`Removing unresolved re-export: ${line}`);
|
||||
verbose(`Removing unresolved re-export: ${line}`);
|
||||
isModified = true;
|
||||
}
|
||||
|
||||
@@ -117,7 +120,7 @@ const fixupTypesAfterCompilation = ({ isWatchMode }) => {
|
||||
removeUnresolvedReexports(filePath);
|
||||
});
|
||||
|
||||
log("Done!");
|
||||
console.log("[dts fixup] Done!");
|
||||
|
||||
if (!isWatchMode) {
|
||||
generateDtsRollup();
|
||||
@@ -176,7 +179,7 @@ const generateDtsRollup = () => {
|
||||
});
|
||||
});
|
||||
|
||||
log("Dts rollup done!");
|
||||
console.log("[dts rollup] Dts rollup done!");
|
||||
};
|
||||
|
||||
const watchFilesAndFixThem = () => {
|
||||
|
||||
14
deps.edn
14
deps.edn
@@ -74,7 +74,7 @@
|
||||
:exclusions [commons-beanutils/commons-beanutils
|
||||
commons-digester/commons-digester
|
||||
commons-logging/commons-logging]}
|
||||
compojure/compojure {:mvn/version "1.7.1" ; HTTP Routing library built on Ring
|
||||
compojure/compojure {:mvn/version "1.7.2" ; HTTP Routing library built on Ring
|
||||
:exclusions [ring/ring-codec]}
|
||||
crypto-random/crypto-random {:mvn/version "1.2.1"} ; library for generating cryptographically secure random bytes and strings
|
||||
diehard/diehard {:mvn/version "0.12.0"}
|
||||
@@ -87,7 +87,7 @@
|
||||
instaparse/instaparse {:mvn/version "1.5.0"} ; Make your own parser
|
||||
io.github.camsaul/toucan2 {:mvn/version "1.0.569"}
|
||||
io.github.eerohele/pp {#_#_:git/tag "2024-11-13.77" ; super fast pretty-printing library
|
||||
:git/sha "4998a1fdd9d3713d7034d04509e9212204773bf2"
|
||||
:git/sha "ffc4edc482c057cd9412c62f018a41e9140c618b"
|
||||
:git/url "https://github.com/eerohele/pp"}
|
||||
;; TODO: replace back to normal artifact
|
||||
io.github.metabase/macaw {:mvn/version "0.2.29"} ; Parse native SQL queries
|
||||
@@ -108,7 +108,7 @@
|
||||
org.bouncycastle/bcprov-jdk18on]}
|
||||
metabase/throttle {:mvn/version "1.0.2"} ; Tools for throttling access to API endpoints and other code pathways
|
||||
methodical/methodical {:mvn/version "1.0.127"} ; drop-in replacements for Clojure multimethods and adds several advanced features
|
||||
metosin/malli {:mvn/version "0.19.2"} ; Data-driven Schemas for Clojure/Script and babashka
|
||||
metosin/malli {:mvn/version "0.20.0"} ; Data-driven Schemas for Clojure/Script and babashka
|
||||
nano-id/nano-id {:mvn/version "1.1.0"} ; NanoID generator for generating entity_ids
|
||||
net.cgrand/macrovich {:mvn/version "0.2.2"} ; utils for writing macros for both Clojure & ClojureScript
|
||||
net.clojars.wkok/openai-clojure {:mvn/version "0.23.0"
|
||||
@@ -138,7 +138,7 @@
|
||||
org.apache.sshd/sshd-core {:mvn/version "3.0.0-M2" ; ssh tunneling and test server
|
||||
:exclusions [org.slf4j/slf4j-api
|
||||
org.slf4j/jcl-over-slf4j]}
|
||||
org.apache.tika/tika-core {:mvn/version "3.2.2"}
|
||||
org.apache.tika/tika-core {:mvn/version "3.2.3"}
|
||||
org.apache.xmlgraphics/batik-all {:mvn/version "1.19"} ; SVG -> image
|
||||
org.bouncycastle/bcpkix-jdk18on {:mvn/version "1.82"} ; Bouncy Castle crypto library -- explicit version of BC specified to resolve illegal reflective access errors
|
||||
org.bouncycastle/bcprov-jdk18on {:mvn/version "1.82"}
|
||||
@@ -180,7 +180,7 @@
|
||||
org.mariadb.jdbc/mariadb-java-client ^:antq/exclude ; 3.x only support jdbc:mariadb, so using 2.x for now
|
||||
{:mvn/version "2.7.10"} ; MySQL/MariaDB driver
|
||||
org.mindrot/jbcrypt {:mvn/version "0.4"} ; Crypto library
|
||||
org.postgresql/postgresql {:mvn/version "42.7.7"} ; Postgres driver
|
||||
org.postgresql/postgresql {:mvn/version "42.7.8"} ; Postgres driver
|
||||
org.slf4j/slf4j-api {:mvn/version "2.0.17"} ; abstraction for logging frameworks -- allows end user to plug in desired logging framework at deployment time
|
||||
org.tcrawley/dynapath {:mvn/version "1.1.0"} ; Dynamically add Jars (e.g. Oracle or Vertica) to classpath
|
||||
org.threeten/threeten-extra {:mvn/version "1.8.0"} ; extra Java 8 java.time classes like DayOfMonth and Quarter
|
||||
@@ -249,7 +249,7 @@
|
||||
{cider/cider-nrepl {:mvn/version "0.58.0"} ; nREPL server for CIDER
|
||||
clj-http-fake/clj-http-fake {:mvn/version "1.0.4"
|
||||
:exclusions [slingshot/slingshot]}
|
||||
clj-kondo/clj-kondo {:mvn/version "2025.07.28"} ; included here mainly to facilitate working on the stuff in `.clj-kondo/src`
|
||||
clj-kondo/clj-kondo {:mvn/version "2025.10.23"} ; included here mainly to facilitate working on the stuff in `.clj-kondo/src`
|
||||
cloverage/cloverage {:mvn/version "1.2.4"}
|
||||
com.clojure-goes-fast/clj-async-profiler
|
||||
{:mvn/version "1.6.2"} ; Enables local profiling and heat map generation
|
||||
@@ -467,7 +467,7 @@
|
||||
;; Use this to only run Kondo against specific files.
|
||||
:kondo
|
||||
{:replace-deps
|
||||
{clj-kondo/clj-kondo {:mvn/version "2025.07.28"}}
|
||||
{clj-kondo/clj-kondo {:mvn/version "2025.10.23"}}
|
||||
|
||||
:main-opts
|
||||
["-m" "clj-kondo.main"]}
|
||||
|
||||
@@ -356,8 +356,8 @@
|
||||
|
||||
([deps module-x module-y]
|
||||
(let [module-x-ns->module-y-ns (->> (external-usages deps module-y)
|
||||
(filter #(= (:module %) module-x))
|
||||
(map (juxt :namespace :depends-on-namespace)))]
|
||||
(filter #(= (:module %) module-x))
|
||||
(map (juxt :namespace :depends-on-namespace)))]
|
||||
(reduce
|
||||
(fn [m [module-x-ns module-y-ns]]
|
||||
(update m module-x-ns (fn [deps]
|
||||
|
||||
@@ -3853,7 +3853,7 @@
|
||||
"required" : [ "type", "subject", "body" ]
|
||||
} ]
|
||||
},
|
||||
"metabase.collections.api.DashboardQuestionCandidate" : {
|
||||
"metabase.collections-rest.api.DashboardQuestionCandidate" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"description" : {
|
||||
@@ -3885,13 +3885,13 @@
|
||||
},
|
||||
"required" : [ "id", "name", "sole_dashboard_info" ]
|
||||
},
|
||||
"metabase.collections.api.DashboardQuestionCandidatesResponse" : {
|
||||
"metabase.collections-rest.api.DashboardQuestionCandidatesResponse" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"data" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"$ref" : "#/components/schemas/metabase.collections.api.DashboardQuestionCandidate"
|
||||
"$ref" : "#/components/schemas/metabase.collections-rest.api.DashboardQuestionCandidate"
|
||||
}
|
||||
},
|
||||
"total" : {
|
||||
@@ -3900,7 +3900,7 @@
|
||||
},
|
||||
"required" : [ "data", "total" ]
|
||||
},
|
||||
"metabase.collections.api.MoveDashboardQuestionCandidatesResponse" : {
|
||||
"metabase.collections-rest.api.MoveDashboardQuestionCandidatesResponse" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"moved" : {
|
||||
@@ -9211,7 +9211,7 @@
|
||||
"type" : "string",
|
||||
"enum" : [ "events" ]
|
||||
},
|
||||
"metabase.users.api.user-group-membership" : {
|
||||
"metabase.users-rest.api.user-group-membership" : {
|
||||
"description" : "Group Membership info of a User.\n In which :is_group_manager is only included if `advanced-permissions` is enabled.",
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
@@ -9267,12 +9267,12 @@
|
||||
},
|
||||
"required" : [ "schedule_type" ]
|
||||
},
|
||||
"metabase.warehouse-schema.api.table.data-authority-write" : {
|
||||
"metabase.warehouse-schema-rest.api.table.data-authority-write" : {
|
||||
"description" : "Schema for writing a valid table data authority.",
|
||||
"type" : "string",
|
||||
"enum" : [ "computed", "authoritative", "unconfigured", "ingested" ]
|
||||
},
|
||||
"metabase.warehouses.api.available-settings" : {
|
||||
"metabase.warehouses-rest.api.available-settings" : {
|
||||
"type" : "object",
|
||||
"additionalProperties" : {
|
||||
"type" : "object",
|
||||
@@ -12530,7 +12530,7 @@
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/metabase.collections.api.DashboardQuestionCandidatesResponse"
|
||||
"$ref" : "#/components/schemas/metabase.collections-rest.api.DashboardQuestionCandidatesResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12656,7 +12656,7 @@
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/metabase.collections.api.MoveDashboardQuestionCandidatesResponse"
|
||||
"$ref" : "#/components/schemas/metabase.collections-rest.api.MoveDashboardQuestionCandidatesResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12914,7 +12914,7 @@
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/metabase.collections.api.DashboardQuestionCandidatesResponse"
|
||||
"$ref" : "#/components/schemas/metabase.collections-rest.api.DashboardQuestionCandidatesResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13049,7 +13049,7 @@
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/metabase.collections.api.MoveDashboardQuestionCandidatesResponse"
|
||||
"$ref" : "#/components/schemas/metabase.collections-rest.api.MoveDashboardQuestionCandidatesResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15119,7 +15119,7 @@
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"settings" : {
|
||||
"$ref" : "#/components/schemas/metabase.warehouses.api.available-settings"
|
||||
"$ref" : "#/components/schemas/metabase.warehouses-rest.api.available-settings"
|
||||
}
|
||||
},
|
||||
"required" : [ "settings" ]
|
||||
@@ -28814,7 +28814,7 @@
|
||||
"enum" : [ "technical", "hidden", "cruft" ]
|
||||
},
|
||||
"data_authority" : {
|
||||
"$ref" : "#/components/schemas/metabase.warehouse-schema.api.table.data-authority-write"
|
||||
"$ref" : "#/components/schemas/metabase.warehouse-schema-rest.api.table.data-authority-write"
|
||||
},
|
||||
"show_in_getting_started" : {
|
||||
"type" : "boolean"
|
||||
@@ -28977,7 +28977,7 @@
|
||||
"enum" : [ "technical", "hidden", "cruft" ]
|
||||
},
|
||||
"data_authority" : {
|
||||
"$ref" : "#/components/schemas/metabase.warehouse-schema.api.table.data-authority-write"
|
||||
"$ref" : "#/components/schemas/metabase.warehouse-schema-rest.api.table.data-authority-write"
|
||||
},
|
||||
"show_in_getting_started" : {
|
||||
"type" : "boolean"
|
||||
@@ -30473,7 +30473,7 @@
|
||||
"user_group_memberships" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"$ref" : "#/components/schemas/metabase.users.api.user-group-membership"
|
||||
"$ref" : "#/components/schemas/metabase.users-rest.api.user-group-membership"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -30611,7 +30611,7 @@
|
||||
"user_group_memberships" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"$ref" : "#/components/schemas/metabase.users.api.user-group-membership"
|
||||
"$ref" : "#/components/schemas/metabase.users-rest.api.user-group-membership"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30772,4 +30772,4 @@
|
||||
"url" : "http://localhost:3000",
|
||||
"description" : "Localhost"
|
||||
} ]
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,6 @@ For the most part we follow the [Airbnb React/JSX Style Guide](https://github.co
|
||||
- Prefer React [function components over class components](https://reactjs.org/docs/components-and-props.html#function-and-class-components)
|
||||
- Avoid creating new components within the `containers` folder, as this approach has been deprecated. Instead, store both connected and view components in the `components` folder for a more unified and efficient organization. If a connected component grows substantially in size and you need to extract a view component, opt for using the `View` suffix.
|
||||
- For control components, typically we use `value` and `onChange`. Controls that have options (e.x. `Radio`, `Select`) usually take an `options` array of objects with `name` and `value` properties.
|
||||
- Components named like `FooModal` and `FooPopover` typically refer to the modal/popover _content_ which should be used inside a `Modal`/`ModalWithTrigger` or `Popover`/`PopoverWithTrigger`
|
||||
- Components named like `FooWidget` typically include a `FooPopover` inside a `PopoverWithTrigger` with some sort of trigger element, often `FooName`
|
||||
|
||||
- Use arrow function instance properties if you need to bind a method in a class (instead of `this.method = this.method.bind(this);` in the constructor), but only if the function needs to be bound (e.x. if you're passing it as a prop to a React component)
|
||||
|
||||
@@ -4,9 +4,7 @@ title: Metabase release versioning
|
||||
|
||||
# Metabase release versioning
|
||||
|
||||
We follow our own flavor of the [semantic versioning guidelines](https://semver.org/) in order to distinguish the [open-source version](https://www.metabase.com/product/starter) of Metabase from the paid, source-available version of Metabase (available in the [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans).
|
||||
|
||||
Semantic versioning typically follows the format: `Major.Minor.Point.Patch`. For example, version `3.15.2` or `3.15.2.1`.
|
||||
We follow our own flavor of the [semantic versioning guidelines](https://semver.org/). Standard semantic versioning uses `Major.Minor.Patch` format, but we've adapted it to distinguish the [open-source version](https://www.metabase.com/product/starter) of Metabase from the paid, source-available version of Metabase (available in the [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans).
|
||||
|
||||
With Metabase releases, we prefix the version with a `0` or `1`, depending on the license.
|
||||
|
||||
@@ -16,13 +14,13 @@ With Metabase releases, we prefix the version with a `0` or `1`, depending on th
|
||||
License.Major.Point.Hotfix
|
||||
```
|
||||
|
||||
E.g.,
|
||||
For example:
|
||||
|
||||
```
|
||||
v0.46.3.1
|
||||
v0.57.3.1
|
||||
```
|
||||
|
||||
`v0.46.3.1` would be for a hotfix (`1`) for the third (`3`) point release of Metabase `46`, the open-source edition (`0`).
|
||||
`v0.57.3.1` would be for a hotfix (`1`) for the third (`3`) point release of Metabase `57`, the open-source edition (`0`).
|
||||
|
||||
### License
|
||||
|
||||
@@ -31,33 +29,39 @@ v0.46.3.1
|
||||
|
||||
### Major
|
||||
|
||||
We release major version when we introduce new features or breaking changes.
|
||||
We release major versions when we introduce new features or breaking changes. See [release announcements](https://www.metabase.com/releases).
|
||||
|
||||
### Point
|
||||
|
||||
Sometimes called a minor release, we issue point releases when we add bug fixes and refinements to existing features.
|
||||
Sometimes called a minor release, we issue point releases when we add bug fixes and refinements to existing features. See the [changelog](https://www.metabase.com/changelog)
|
||||
|
||||
### Hotfix
|
||||
|
||||
Sometimes called a patch release, we issue these hotfix releases to fix security issues in a timely manner, or to undo a horrific regression.
|
||||
Sometimes called a patch release, we issue these releases nightly so we can fix bugs or security issues as soon as we can.
|
||||
|
||||
You'll see releases ending as `.x` in [releases on GitHub](https://github.com/metabase/metabase/releases). These are placeholder links. The actual link will go to the latest hotfix. For example:
|
||||
|
||||
- `metabase/metabase:v0.57.2.x` will automatically pull the latest patch version in for the latest minor version (like `v0.57.2.1`, `v0.57.2.2`, etc.).
|
||||
|
||||
## Other release terms
|
||||
|
||||
### The Gold Release
|
||||
### The beta release
|
||||
|
||||
The gold release is the first release of a new major version of Metabase. So for Metabase version 46, the gold releases would be:
|
||||
The beta release is the beta release of a new major version of Metabase. So for Metabase version 57, the gold releases would be:
|
||||
|
||||
- `v0.46.0` (the OSS version)
|
||||
- `v1.46.0` (the EE version)
|
||||
- `v0.57.0` (the OSS version)
|
||||
- `v1.57.0` (the EE version)
|
||||
|
||||
### Release Candidates
|
||||
### The gold release
|
||||
|
||||
We usually publish release candidates to kick the tires on new features before releasing a new major version (a gold release). To distinguish these release candidates, we append an `-RC#` at the end.
|
||||
The gold release is the first stable release of a new major version of Metabase. So for Metabase version 57, the gold releases would be:
|
||||
|
||||
E.g.,
|
||||
- `v0.57.1` (the OSS version)
|
||||
- `v1.57.1` (the EE version)
|
||||
|
||||
- `v1.46.0-RC1` (the first release candidate for the EE version)
|
||||
- `v0.46.0-RC3` (the third release candidate for the OSS version)
|
||||
### Cloud builds
|
||||
|
||||
If you see a version followed by an `-X`, like `v1.56.2-X01`, these are version numbers related to builds for customers on [Metabase Cloud](https://www.metabase.com/cloud/).
|
||||
|
||||
## Further reading
|
||||
|
||||
|
||||
BIN
docs/documents/images/supporting-text.png
Normal file
BIN
docs/documents/images/supporting-text.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
@@ -5,7 +5,7 @@ summary: Create documents to combine text and charts for long-form analysis. Add
|
||||
|
||||
# Documents
|
||||
|
||||
{% include plans-blockquote.html feature="Documents" %}
|
||||
{% include plans-blockquote.html feature="Documents" is_plural=true%}
|
||||
|
||||

|
||||
|
||||
@@ -64,10 +64,11 @@ Questions that are created directly in the document (either through [Metabot](..
|
||||
|
||||
To edit a chart, click the **three-dot** menu in the chart's upper right. You can:
|
||||
|
||||
- Edit visualization
|
||||
- Edit query
|
||||
- Replace the chart with another question or model
|
||||
- [Download results](../questions/exporting-results.md#exporting-results-from-document-cards)
|
||||
- **Add supporting text** to provide context next to the chart
|
||||
- **Edit visualization** to change how the chart displays data
|
||||
- **Edit query** to modify the underlying question
|
||||
- **Replace** the chart with another question or model
|
||||
- [**Download results**](../questions/exporting-results.md#exporting-results-from-document-cards)
|
||||
|
||||
Edits only affect the chart in the document. If you added the chart from a collection, Metabase will have created a copy of the chart. Any changes made to the chart in the document won't affect the original question.
|
||||
|
||||
@@ -77,16 +78,31 @@ Edits only affect the chart in the document. If you added the chart from a colle
|
||||
|
||||
To rearrange charts, simply click and hold on the chart and drag it to where you want it in the document.
|
||||
|
||||
You can put up to three charts side by side (more than three charts side-by-side gets crowded). To resize charts, you can click on the edges to adjust their size.
|
||||
|
||||
When there are multiple charts in the same row, you can resize them horizontally by clicking between the charts and sliding left or right to resize.
|
||||
You can put up to three items side by side in a group. This includes both charts and supporting text blocks. When you have multiple items in the same row, you can resize them horizontally by clicking between the items and sliding left or right to resize.
|
||||
|
||||
To resize a chart's height, click the bottom of the chart and resize the chart up or down.
|
||||
|
||||
For now, you can only put charts side-by-side (not text elements).
|
||||
|
||||
After arranging your charts, save your changes!
|
||||
|
||||
## Adding supporting text to charts
|
||||
|
||||

|
||||
|
||||
You can add a supporting text block next to charts to provide context.
|
||||
|
||||
To add supporting text to a chart:
|
||||
|
||||
1. Hover over the chart.
|
||||
2. Click the **three-dot** menu in the chart's upper right.
|
||||
3. Select **Add supporting text**.
|
||||
4. Type your text in the supporting text block.
|
||||
|
||||
You can only have one supporting text block per group of charts. A group can contain up to three items total (charts and supporting text block combined).
|
||||
|
||||
You can drag and drop a supporting text block to reorder them within a group of charts. You can only move supporting text within its current group; you can't drag it out to create a standalone text block.
|
||||
|
||||
Supporting text blocks support the same Markdown formatting as the rest of the document, including headings, lists, blockquotes, and code blocks. But you can't add charts in supporting text.
|
||||
|
||||
## Formatting text
|
||||
|
||||
Documents support Markdown syntax for:
|
||||
|
||||
@@ -26,9 +26,11 @@ Here's the [Shoppy source code](https://github.com/metabase/shoppy).
|
||||
|
||||
## Quickstarts
|
||||
|
||||
- [Quickstart](./quickstart.md) (If you have Metabase and an app)
|
||||
- [Quickstart CLI](./quickstart-cli.md) (If you have an app, but no Metabase)
|
||||
- [Quickstart with sample React app](./quickstart-with-sample-app.md) (If you don't have either)
|
||||
The best way to get started with Embedded Analytics SDK depends on what you already have:
|
||||
|
||||
- You have an app and a Metabase instance: go to [main quickstart](./quickstart.md)
|
||||
- You have an app but no Metabase: go to [quickstart with CLI](./quickstart-cli.md)
|
||||
- You don't have an app: go to [quickstart with a sample React app](./quickstart-with-sample-app.md)
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ Copy the token immediately after generating it.
|
||||
|
||||
### 3. Connect your development Metabase to your repository
|
||||
|
||||

|
||||

|
||||
|
||||
You can put any Metabase into Read-write mode. We also offer [Development instances](./development-instance.md) that you can use for Remote Sync or any other kind of development.
|
||||
|
||||
@@ -130,7 +130,7 @@ Copy the token immediately after generating it — you'll need to paste it into
|
||||
|
||||
### 7. Connect your production Metabase to your repository
|
||||
|
||||

|
||||

|
||||
|
||||
In your production Metabase instance:
|
||||
|
||||
@@ -156,7 +156,7 @@ In your production Metabase instance:
|
||||
|
||||
In Read-only mode, the synced collection appears in the regular collections list with a special icon to indicate it's versioned and read-only.
|
||||
|
||||

|
||||

|
||||
|
||||
At this point, you should be all set up. Exit Admin settings, then reload your browser. You should see your synced collection in your production Metabase.
|
||||
|
||||
|
||||
@@ -150,6 +150,10 @@ you could put the snippet in one query and have the snippet parameter map to a `
|
||||
|
||||
If you have multiple snippets containing parameters with the same name, the question using those snippets will only use one instance of the parameter. For example, if `{% raw %}{{snippet: 1}}{% endraw %}` contains parameter `{% raw %}{{var}}{% endraw %}` and `{% raw %}{{snippet: 2}}{% endraw %}` also contains parameter `{% raw %}{{var}}{% endraw %}`, the question will display only one `{% raw %}{{var}}{% endraw %}` parameter and use its value in both snippets.
|
||||
|
||||
### Sharing parameters across questions
|
||||
|
||||
You can also use snippets to share parameters across multiple SQL questions, including questions that build on one another (nested questions). For example, you have a question “Orders by date” that filters orders using `{% raw %}{{start_date}}{% endraw %}`. You then create another question, say “Revenue by product,” that uses the results from “Orders by date.” To keep using the same `{% raw %}{{start_date}}{% endraw %}` parameter in both questions, move the SQL that contains the parameter from “Orders by date” into a snippet and update both questions to reference that snippet. Now, both questions surface the same parameter, and a single dashboard date filter can control both cards that use the snippet.
|
||||
|
||||
## Snippet permissions
|
||||
|
||||
Any user who has native editor permissions to at least one of your connected databases will be able to view the snippets sidebar, and will be able to create, edit, and archive or unarchive any and all snippets — even snippets intended to be used with databases the user lacks SQL editing access to.
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export const BACKEND_PORT = process.env.BACKEND_PORT ?? 4000;
|
||||
export const BACKEND_HOST = process.env.MB_JETTY_HOST || "localhost";
|
||||
export const BACKEND_PORT =
|
||||
process.env.MB_JETTY_PORT || process.env.BACKEND_PORT || 4000;
|
||||
|
||||
40
e2e/runner/cypress-node-js-runner.js
Normal file
40
e2e/runner/cypress-node-js-runner.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const cypress = require("cypress");
|
||||
|
||||
const { FAILURE_EXIT_CODE } = require("./constants/exit-code");
|
||||
|
||||
/**
|
||||
* This simply runs Cypress through the Cypress Module API rather than the CLI.
|
||||
* See: https://docs.cypress.io/app/references/module-api
|
||||
*
|
||||
* @param {Partial<CypressCommandLine.CypressRunOptions> | Partial<CypressCommandLine.CypressOpenOptions>} options - Cypress configuration options for running or opening tests
|
||||
* @param {function(number): Promise<void>} exitFunction - Async function to call on exit with the exit code
|
||||
* @returns {Promise<CypressCommandLine.CypressRunResult | CypressCommandLine.CypressFailedRunResult | void>}
|
||||
* @throws {Error} When Cypress fails to run tests
|
||||
*/
|
||||
const runCypress = async (options, exitFunction) => {
|
||||
const openMode = process.env.OPEN_UI === "true";
|
||||
|
||||
try {
|
||||
const { status, message, totalFailed, failures } = openMode
|
||||
? await cypress.open(options)
|
||||
: await cypress.run(options);
|
||||
|
||||
// At least one test failed
|
||||
if (totalFailed > 0) {
|
||||
await exitFunction(FAILURE_EXIT_CODE);
|
||||
}
|
||||
|
||||
// Something went wrong and Cypress failed to even run tests
|
||||
if (status === "failed" && failures) {
|
||||
console.error(message);
|
||||
|
||||
await exitFunction(failures);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to run Cypress!\n", e);
|
||||
|
||||
await exitFunction(FAILURE_EXIT_CODE);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = runCypress;
|
||||
@@ -6,18 +6,18 @@ const http = require("http");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
const { BACKEND_HOST, BACKEND_PORT } = require("./constants/backend-port");
|
||||
const { delay } = require("./cypress-runner-utils");
|
||||
|
||||
const CypressBackend = {
|
||||
server: null,
|
||||
createServer(port = process.env.BACKEND_PORT || 4000) {
|
||||
createServer(port = BACKEND_PORT) {
|
||||
const generateTempDbPath = () =>
|
||||
path.join(os.tmpdir(), `metabase-test-${process.pid}.db`);
|
||||
|
||||
const server = {
|
||||
dbFile: generateTempDbPath(),
|
||||
host: `http://localhost:${port}`,
|
||||
port,
|
||||
};
|
||||
|
||||
this.server = server;
|
||||
@@ -41,8 +41,8 @@ const CypressBackend = {
|
||||
const metabaseConfig = {
|
||||
MB_DB_TYPE: "h2",
|
||||
MB_DB_FILE: this.server.dbFile,
|
||||
MB_JETTY_HOST: "0.0.0.0",
|
||||
MB_JETTY_PORT: this.server.port,
|
||||
MB_JETTY_HOST: BACKEND_HOST,
|
||||
MB_JETTY_PORT: BACKEND_PORT,
|
||||
MB_ENABLE_TEST_ENDPOINTS: "true",
|
||||
MB_DANGEROUS_UNSAFE_ENABLE_TESTING_H2_CONNECTIONS_DO_NOT_ENABLE: "true",
|
||||
MB_LAST_ANALYTICS_CHECKSUM: "-1",
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
const cypress = require("cypress");
|
||||
|
||||
const { BACKEND_PORT } = require("./constants/backend-port");
|
||||
const { FAILURE_EXIT_CODE } = require("./constants/exit-code");
|
||||
const { parseArguments, args } = require("./cypress-runner-utils");
|
||||
const {
|
||||
HOST_APP_SETUP_CONFIGS,
|
||||
} = require("./embedding-sdk/host-apps/constants/host-app-setup-configs");
|
||||
const {
|
||||
SAMPLE_APP_SETUP_CONFIGS,
|
||||
} = require("./embedding-sdk/sample-apps/constants/sample-app-setup-configs");
|
||||
|
||||
const getHost = (port = BACKEND_PORT) => `http://localhost:${port}`;
|
||||
|
||||
const getEmbeddingSdkAppE2eConfig = async ({
|
||||
baseUrl,
|
||||
env,
|
||||
project,
|
||||
specPattern,
|
||||
}) => {
|
||||
process.env = {
|
||||
...process.env,
|
||||
...env,
|
||||
};
|
||||
|
||||
const defaultConfig = {
|
||||
browser: process.env.CYPRESS_BROWSER ?? "chrome",
|
||||
project,
|
||||
configFile: "e2e/support/cypress.config.js",
|
||||
config: {
|
||||
baseUrl,
|
||||
specPattern,
|
||||
env,
|
||||
},
|
||||
testingType: "e2e",
|
||||
openMode: args["--open"] || process.env.OPEN_UI === "true",
|
||||
};
|
||||
|
||||
const userArgs = await parseArguments(args);
|
||||
|
||||
return Object.assign({}, defaultConfig, userArgs);
|
||||
};
|
||||
|
||||
const getSampleAppE2eConfig = (suite) => ({
|
||||
[suite]: async () => {
|
||||
const { appName, env } = SAMPLE_APP_SETUP_CONFIGS[suite];
|
||||
const { CLIENT_PORT } = env;
|
||||
|
||||
return getEmbeddingSdkAppE2eConfig({
|
||||
// If the `clientPort` is not set, it means we have multiple apps running on different ports,
|
||||
// so we control the `baseUrl` based on other `env` variables on the Sample App tests level.
|
||||
baseUrl: CLIENT_PORT ? getHost(CLIENT_PORT) : "",
|
||||
env,
|
||||
project: ["e2e/tmp", appName].join("/"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const getHostAppE2eConfig = (suite) => ({
|
||||
[suite]: async () => {
|
||||
const { appName, env } = HOST_APP_SETUP_CONFIGS[suite];
|
||||
|
||||
return getEmbeddingSdkAppE2eConfig({
|
||||
baseUrl: getHost(),
|
||||
env,
|
||||
specPattern: [
|
||||
"e2e/test-host-app/shared/**/*.cy.spec.{js,ts}",
|
||||
["e2e/test-host-app", appName, "**/*.cy.spec.{js,ts}"].join("/"),
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// This is a map of all possible Cypress configurations we can run.
|
||||
const configs = {
|
||||
e2e: async () => {
|
||||
const defaultConfig = {
|
||||
browser: process.env.CYPRESS_BROWSER ?? "chrome",
|
||||
configFile: "e2e/support/cypress.config.js",
|
||||
config: {
|
||||
baseUrl: getHost(),
|
||||
},
|
||||
testingType: "e2e",
|
||||
openMode: args["--open"] || process.env.OPEN_UI === "true",
|
||||
};
|
||||
|
||||
const userArgs = await parseArguments(args);
|
||||
|
||||
const finalConfig = Object.assign({}, defaultConfig, userArgs);
|
||||
return finalConfig;
|
||||
},
|
||||
...getSampleAppE2eConfig("metabase-nodejs-react-sdk-embedding-sample-e2e"),
|
||||
...getSampleAppE2eConfig("metabase-nextjs-sdk-embedding-sample-e2e"),
|
||||
...getSampleAppE2eConfig("shoppy-e2e"),
|
||||
...getHostAppE2eConfig("vite-6-host-app-e2e"),
|
||||
...getHostAppE2eConfig("next-15-app-router-host-app-e2e"),
|
||||
...getHostAppE2eConfig("next-15-pages-router-host-app-e2e"),
|
||||
...getHostAppE2eConfig("angular-20-host-app-e2e"),
|
||||
snapshot: async () => {
|
||||
// We only ever care about a browser out of all possible user arguments,
|
||||
// when it comes to the snapshot generation.
|
||||
// Anything else could result either in a failure or in a wrong database snapshot!
|
||||
const { browser } = await parseArguments(args);
|
||||
|
||||
const snapshotConfig = {
|
||||
browser: browser ?? process.env.CYPRESS_BROWSER ?? "chrome",
|
||||
configFile: "e2e/support/cypress-snapshots.config.js",
|
||||
config: {
|
||||
baseUrl: getHost(),
|
||||
},
|
||||
testingType: "e2e",
|
||||
openMode: false,
|
||||
};
|
||||
|
||||
return snapshotConfig;
|
||||
},
|
||||
component: async () => {
|
||||
const { browser } = await parseArguments(args);
|
||||
|
||||
const sdkComponentConfig = {
|
||||
browser: browser ?? process.env.CYPRESS_BROWSER ?? "chrome",
|
||||
configFile: "e2e/support/cypress-embedding-sdk-component-test.config.js",
|
||||
config: {
|
||||
baseUrl: getHost(),
|
||||
},
|
||||
testingType: "component",
|
||||
openMode: args["--open"] || process.env.OPEN_UI === "true",
|
||||
};
|
||||
|
||||
const userArgs = await parseArguments(args);
|
||||
|
||||
const finalConfig = Object.assign({}, sdkComponentConfig, userArgs);
|
||||
return finalConfig;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* This simply runs cypress through the javascript API rather than the CLI, and
|
||||
* lets us conditionally load a config file and some other options along with it.
|
||||
*/
|
||||
const runCypress = async (suite = "e2e", exitFunction) => {
|
||||
if (!configs[suite]) {
|
||||
console.error(
|
||||
`Invalid suite: ${suite}, try one of: ${Object.keys(configs)}`,
|
||||
);
|
||||
await exitFunction(FAILURE_EXIT_CODE);
|
||||
}
|
||||
|
||||
const config = await configs[suite]();
|
||||
|
||||
try {
|
||||
const { status, message, totalFailed, failures } = config.openMode
|
||||
? await cypress.open(config)
|
||||
: await cypress.run(config);
|
||||
|
||||
// At least one test failed
|
||||
if (totalFailed > 0) {
|
||||
await exitFunction(FAILURE_EXIT_CODE);
|
||||
}
|
||||
|
||||
// Something went wrong and Cypress failed to even run tests
|
||||
if (status === "failed" && failures) {
|
||||
console.error(message);
|
||||
|
||||
await exitFunction(failures);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to run Cypress!\n", e);
|
||||
|
||||
await exitFunction(FAILURE_EXIT_CODE);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = runCypress;
|
||||
@@ -1,6 +1,5 @@
|
||||
const { execSync, spawn } = require("child_process");
|
||||
|
||||
const arg = require("arg");
|
||||
const chalk = require("chalk");
|
||||
const cypress = require("cypress");
|
||||
|
||||
@@ -8,16 +7,7 @@ function printBold(message) {
|
||||
console.log(`\n${chalk.bold(chalk.magenta(message.trim()))}\n`);
|
||||
}
|
||||
|
||||
const args = arg(
|
||||
{
|
||||
"--open": [Boolean], // Run Cypress in open mode or not? Doesn't accept additional arguments
|
||||
},
|
||||
{ permissive: true }, // Passes all other flags and args to the Cypress parser
|
||||
);
|
||||
|
||||
async function parseArguments(args) {
|
||||
const cliArgs = args._;
|
||||
|
||||
async function parseArguments(cliArgs) {
|
||||
// cypress.cli.parseArguments requires `cypress run` as the first two arguments
|
||||
if (cliArgs[0] !== "cypress") {
|
||||
cliArgs.unshift("cypress");
|
||||
@@ -92,7 +82,6 @@ function delay(durationMs) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
args,
|
||||
booleanify,
|
||||
unBooleanify,
|
||||
parseArguments,
|
||||
|
||||
81
e2e/runner/resolve-sdk-e2e-config.js
Normal file
81
e2e/runner/resolve-sdk-e2e-config.js
Normal file
@@ -0,0 +1,81 @@
|
||||
const { BACKEND_PORT } = require("./constants/backend-port");
|
||||
const {
|
||||
HOST_APP_SETUP_CONFIGS,
|
||||
} = require("./embedding-sdk/host-apps/constants/host-app-setup-configs");
|
||||
const {
|
||||
SAMPLE_APP_SETUP_CONFIGS,
|
||||
} = require("./embedding-sdk/sample-apps/constants/sample-app-setup-configs");
|
||||
|
||||
const getHost = (port = BACKEND_PORT) => `http://localhost:${port}`;
|
||||
|
||||
const getEmbeddingSdkAppE2eConfig = ({
|
||||
baseUrl,
|
||||
env,
|
||||
project,
|
||||
specPattern,
|
||||
}) => {
|
||||
process.env = {
|
||||
...process.env,
|
||||
...env,
|
||||
};
|
||||
|
||||
return {
|
||||
project,
|
||||
configFile: "e2e/support/cypress.config.js",
|
||||
config: {
|
||||
baseUrl,
|
||||
specPattern,
|
||||
env,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const getSampleAppE2eConfig = (suite) => ({
|
||||
[suite]: () => {
|
||||
const { appName, env } = SAMPLE_APP_SETUP_CONFIGS[suite];
|
||||
const { CLIENT_PORT } = env;
|
||||
|
||||
return getEmbeddingSdkAppE2eConfig({
|
||||
// If the `clientPort` is not set, it means we have multiple apps running on different ports,
|
||||
// so we control the `baseUrl` based on other `env` variables on the Sample App tests level.
|
||||
baseUrl: CLIENT_PORT ? getHost(CLIENT_PORT) : "",
|
||||
env,
|
||||
project: ["e2e/tmp", appName].join("/"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const getHostAppE2eConfig = (suite) => ({
|
||||
[suite]: () => {
|
||||
const { env } = HOST_APP_SETUP_CONFIGS[suite];
|
||||
|
||||
return getEmbeddingSdkAppE2eConfig({
|
||||
baseUrl: getHost(),
|
||||
env,
|
||||
specPattern: "e2e/test-host-app/shared/**/*.cy.spec.{js,ts}",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// This is a map of all possible custom SDK configurations we can run.
|
||||
const configs = {
|
||||
...getSampleAppE2eConfig("metabase-nodejs-react-sdk-embedding-sample-e2e"),
|
||||
...getSampleAppE2eConfig("metabase-nextjs-sdk-embedding-sample-e2e"),
|
||||
...getSampleAppE2eConfig("shoppy-e2e"),
|
||||
...getHostAppE2eConfig("vite-6-host-app-e2e"),
|
||||
...getHostAppE2eConfig("next-15-app-router-host-app-e2e"),
|
||||
...getHostAppE2eConfig("next-15-pages-router-host-app-e2e"),
|
||||
...getHostAppE2eConfig("angular-20-host-app-e2e"),
|
||||
};
|
||||
|
||||
const resolveSdkE2EConfig = (suite) => {
|
||||
if (!configs[suite]) {
|
||||
throw new Error(
|
||||
`Invalid suite: ${suite}, try one of: ${Object.keys(configs)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return configs[suite]();
|
||||
};
|
||||
|
||||
module.exports = { resolveSdkE2EConfig };
|
||||
@@ -1,14 +1,15 @@
|
||||
const { FAILURE_EXIT_CODE } = require("./constants/exit-code");
|
||||
const runCypress = require("./cypress-node-js-runner");
|
||||
const CypressBackend = require("./cypress-runner-backend");
|
||||
const runCypress = require("./cypress-runner-run-tests");
|
||||
const { printBold } = require("./cypress-runner-utils");
|
||||
const { parseArguments, printBold } = require("./cypress-runner-utils");
|
||||
const { resolveSdkE2EConfig } = require("./resolve-sdk-e2e-config");
|
||||
|
||||
const modeOrTestSuite = process.argv?.[2]?.trim();
|
||||
const command = process.argv?.[2]?.trim();
|
||||
const cliArguments = process.argv.slice(3);
|
||||
|
||||
const availableModes = ["start", "snapshot"];
|
||||
const availableTestSuites = [
|
||||
"e2e",
|
||||
"component",
|
||||
const availableTestingTypes = ["e2e", "component"];
|
||||
const availableSDKTestSuites = [
|
||||
"metabase-nodejs-react-sdk-embedding-sample-e2e",
|
||||
"metabase-nextjs-sdk-embedding-sample-e2e",
|
||||
"shoppy-e2e",
|
||||
@@ -18,11 +19,14 @@ const availableTestSuites = [
|
||||
"angular-20-host-app-e2e",
|
||||
];
|
||||
|
||||
if (
|
||||
!availableModes.includes(modeOrTestSuite) &&
|
||||
!availableTestSuites.includes(modeOrTestSuite)
|
||||
) {
|
||||
console.error(`Invalid mode or test suite: ${modeOrTestSuite}`);
|
||||
const permittedCommands = [
|
||||
...availableModes,
|
||||
...availableTestingTypes,
|
||||
...availableSDKTestSuites,
|
||||
];
|
||||
|
||||
if (!permittedCommands.includes(command)) {
|
||||
console.error(`Invalid command: ${command}`);
|
||||
process.exit(FAILURE_EXIT_CODE);
|
||||
}
|
||||
|
||||
@@ -31,13 +35,37 @@ const startServer = async () => {
|
||||
await CypressBackend.start();
|
||||
};
|
||||
|
||||
const runTests = async (testSuite) => {
|
||||
printBold(`Running ${testSuite} Cypress Tests`);
|
||||
await runCypress(testSuite, process.exit);
|
||||
const runTests = async (config, cliArguments = []) => {
|
||||
const userOverrides = await parseArguments(cliArguments);
|
||||
await runCypress({ ...config, ...userOverrides }, process.exit);
|
||||
};
|
||||
|
||||
if (modeOrTestSuite === "start") {
|
||||
// Custom "modes"
|
||||
if (command === "start") {
|
||||
startServer();
|
||||
} else {
|
||||
runTests(modeOrTestSuite);
|
||||
}
|
||||
|
||||
if (command === "snapshot") {
|
||||
runTests({ configFile: "e2e/support/cypress-snapshots.config.js" });
|
||||
}
|
||||
|
||||
// Metabase component or e2e tests
|
||||
if (command === "component") {
|
||||
runTests(
|
||||
{
|
||||
configFile: "e2e/support/cypress-embedding-sdk-component-test.config.js",
|
||||
testingType: "component",
|
||||
},
|
||||
cliArguments,
|
||||
);
|
||||
}
|
||||
|
||||
if (command === "e2e") {
|
||||
runTests({ configFile: "e2e/support/cypress.config.js" }, cliArguments);
|
||||
}
|
||||
|
||||
// Custom SDK Host/Sample App e2e tests
|
||||
if (availableSDKTestSuites.includes(command)) {
|
||||
const config = resolveSdkE2EConfig(command);
|
||||
runTests(config);
|
||||
}
|
||||
|
||||
@@ -2,27 +2,31 @@ import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
|
||||
import { BACKEND_PORT } from "./constants/backend-port";
|
||||
import { FAILURE_EXIT_CODE, SUCCESS_EXIT_CODE } from "./constants/exit-code";
|
||||
import runCypress from "./cypress-node-js-runner";
|
||||
import CypressBackend from "./cypress-runner-backend";
|
||||
import runCypress from "./cypress-runner-run-tests";
|
||||
import {
|
||||
booleanify,
|
||||
parseArguments,
|
||||
printBold,
|
||||
shell,
|
||||
unBooleanify,
|
||||
} from "./cypress-runner-utils";
|
||||
import { startHostAppContainers } from "./embedding-sdk/host-apps/start-host-app-containers";
|
||||
import { startSampleAppContainers } from "./embedding-sdk/sample-apps/start-sample-app-containers";
|
||||
import { resolveSdkE2EConfig } from "./resolve-sdk-e2e-config";
|
||||
|
||||
let tempSampleDBDir: string | null = null;
|
||||
|
||||
// if you want to change these, set them as environment variables in your shell
|
||||
const userOptions = {
|
||||
TEST_SUITE: "e2e", // e2e | component
|
||||
CYPRESS_TESTING_TYPE: "e2e", // e2e | component
|
||||
SDK_TEST_SUITE: undefined, // one of the many sample-app, or host-app Embedding SDK suites
|
||||
MB_EDITION: "ee", // ee | oss
|
||||
START_CONTAINERS: true,
|
||||
STOP_CONTAINERS: false,
|
||||
BACKEND_PORT: 4000,
|
||||
BACKEND_PORT: BACKEND_PORT, // override with MB_JETTY_PORT in your env
|
||||
OPEN_UI: true,
|
||||
SHOW_BACKEND_LOGS: false,
|
||||
GENERATE_SNAPSHOTS: true,
|
||||
@@ -61,20 +65,24 @@ if (options.MB_EDITION === "ee" && missingTokens.length > 0) {
|
||||
}
|
||||
|
||||
printBold(`Running Cypress with options:
|
||||
- TEST_SUITE : ${options.TEST_SUITE}
|
||||
- MB_EDITION : ${options.MB_EDITION}
|
||||
- START_CONTAINERS : ${options.START_CONTAINERS}
|
||||
- STOP_CONTAINERS : ${options.STOP_CONTAINERS}
|
||||
- BUILD_JAR : ${options.BUILD_JAR}
|
||||
- GENERATE_SNAPSHOTS : ${options.GENERATE_SNAPSHOTS}
|
||||
- BACKEND_PORT : ${options.BACKEND_PORT}
|
||||
- START_BACKEND : ${options.START_BACKEND}
|
||||
- OPEN_UI : ${options.OPEN_UI}
|
||||
- SHOW_BACKEND_LOGS : ${options.SHOW_BACKEND_LOGS}
|
||||
- TZ : ${options.TZ}
|
||||
- CYPRESS_TESTING_TYPE : ${options.CYPRESS_TESTING_TYPE}
|
||||
- SDK_TEST_SUITE : ${options.SDK_TEST_SUITE}
|
||||
- MB_EDITION : ${options.MB_EDITION}
|
||||
- START_CONTAINERS : ${options.START_CONTAINERS}
|
||||
- STOP_CONTAINERS : ${options.STOP_CONTAINERS}
|
||||
- BUILD_JAR : ${options.BUILD_JAR}
|
||||
- GENERATE_SNAPSHOTS : ${options.GENERATE_SNAPSHOTS}
|
||||
- BACKEND_PORT : ${options.BACKEND_PORT}
|
||||
- START_BACKEND : ${options.START_BACKEND}
|
||||
- OPEN_UI : ${options.OPEN_UI}
|
||||
- SHOW_BACKEND_LOGS : ${options.SHOW_BACKEND_LOGS}
|
||||
- TZ : ${options.TZ}
|
||||
`);
|
||||
|
||||
const init = async () => {
|
||||
const cliArguments = process.argv.slice(2);
|
||||
const userOverrides = await parseArguments(cliArguments);
|
||||
|
||||
if (options.START_CONTAINERS) {
|
||||
printBold("⏳ Starting containers");
|
||||
shell("docker compose -f ./e2e/test/scenarios/docker-compose.yml up -d");
|
||||
@@ -121,37 +129,64 @@ const init = async () => {
|
||||
// reset cache
|
||||
shell("rm -f e2e/support/cypress_sample_instance_data.json");
|
||||
|
||||
printBold("⏳ Generating snapshots");
|
||||
await runCypress("snapshot", cleanup);
|
||||
printBold("⏳ Generating app db snapshots");
|
||||
process.env.OPEN_UI = "false";
|
||||
await runCypress(
|
||||
{ configFile: "e2e/support/cypress-snapshots.config.js" },
|
||||
cleanup,
|
||||
);
|
||||
process.env.OPEN_UI = `${options.OPEN_UI}`;
|
||||
} else {
|
||||
printBold("Skipping snapshot generation, beware of stale snapshot caches");
|
||||
shell("echo 'Existing snapshots:' && ls -1 e2e/snapshots");
|
||||
}
|
||||
|
||||
const isFrontendRunning = shell("lsof -ti:8080 || echo ''", { quiet: true });
|
||||
if (!isFrontendRunning && options.TEST_SUITE === "e2e") {
|
||||
if (!isFrontendRunning && options.CYPRESS_TESTING_TYPE === "e2e") {
|
||||
printBold(
|
||||
"⚠️⚠️ You don't have your frontend running. You should probably run yarn build-hot ⚠️⚠️",
|
||||
);
|
||||
}
|
||||
|
||||
switch (options.TEST_SUITE) {
|
||||
case "metabase-nodejs-react-sdk-embedding-sample-e2e":
|
||||
case "metabase-nextjs-sdk-embedding-sample-e2e":
|
||||
case "shoppy-e2e":
|
||||
await startSampleAppContainers(options.TEST_SUITE);
|
||||
break;
|
||||
if (options.SDK_TEST_SUITE) {
|
||||
switch (options.SDK_TEST_SUITE) {
|
||||
case "metabase-nodejs-react-sdk-embedding-sample-e2e":
|
||||
case "metabase-nextjs-sdk-embedding-sample-e2e":
|
||||
case "shoppy-e2e":
|
||||
await startSampleAppContainers(options.SDK_TEST_SUITE);
|
||||
break;
|
||||
|
||||
case "vite-6-host-app-e2e":
|
||||
case "next-15-app-router-host-app-e2e":
|
||||
case "next-15-pages-router-host-app-e2e":
|
||||
case "angular-20-host-app-e2e":
|
||||
await startHostAppContainers(options.TEST_SUITE);
|
||||
break;
|
||||
case "vite-6-host-app-e2e":
|
||||
case "next-15-app-router-host-app-e2e":
|
||||
case "next-15-pages-router-host-app-e2e":
|
||||
case "angular-20-host-app-e2e":
|
||||
await startHostAppContainers(options.SDK_TEST_SUITE);
|
||||
break;
|
||||
}
|
||||
|
||||
printBold("⏳ Starting Sample/Host App Cypress Tests");
|
||||
const config = resolveSdkE2EConfig(options.SDK_TEST_SUITE);
|
||||
await runCypress(config, cleanup);
|
||||
}
|
||||
|
||||
printBold("⏳ Starting Cypress");
|
||||
await runCypress(options.TEST_SUITE, cleanup);
|
||||
if (options.CYPRESS_TESTING_TYPE === "component") {
|
||||
printBold("⏳ Starting Cypress SDK component tests");
|
||||
await runCypress(
|
||||
{
|
||||
configFile:
|
||||
"e2e/support/cypress-embedding-sdk-component-test.config.js",
|
||||
testingType: "component",
|
||||
},
|
||||
cleanup,
|
||||
);
|
||||
}
|
||||
|
||||
if (options.CYPRESS_TESTING_TYPE === "e2e") {
|
||||
const config = { configFile: "e2e/support/cypress.config.js" };
|
||||
|
||||
printBold("⏳ Starting Cypress");
|
||||
await runCypress({ ...config, ...userOverrides }, cleanup);
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = async (exitCode: string | number = SUCCESS_EXIT_CODE) => {
|
||||
|
||||
@@ -4,6 +4,8 @@ import path from "node:path";
|
||||
import cypressOnFix from "cypress-on-fix";
|
||||
import installLogsPrinter from "cypress-terminal-report/src/installLogsPrinter";
|
||||
|
||||
import { BACKEND_HOST, BACKEND_PORT } from "../runner/constants/backend-port";
|
||||
|
||||
import * as ciTasks from "./ci_tasks";
|
||||
import { collectFailingTests } from "./collectFailedTests";
|
||||
import {
|
||||
@@ -27,8 +29,6 @@ const isCI = process.env["CYPRESS_CI"] === "true";
|
||||
|
||||
const snowplowMicroUrl = process.env["MB_SNOWPLOW_URL"];
|
||||
|
||||
const isQaDatabase = process.env["QA_DB_ENABLED"] === "true";
|
||||
|
||||
// docs say that tsconfig paths should handle aliases, but they don't
|
||||
const assetsResolverPlugin = {
|
||||
name: "assetsResolver",
|
||||
@@ -123,14 +123,11 @@ const defaultConfig = {
|
||||
** CONFIG **
|
||||
********************************************************************/
|
||||
|
||||
if (!isQaDatabase) {
|
||||
config.excludeSpecPattern = "e2e/snapshot-creators/qa-db.cy.snap.js";
|
||||
}
|
||||
|
||||
// `grepIntegrationFolder` needs to point to the root!
|
||||
// See: https://github.com/cypress-io/cypress/issues/24452#issuecomment-1295377775
|
||||
config.env.grepIntegrationFolder = "../../";
|
||||
config.env.grepFilterSpecs = true;
|
||||
config.env.grepOmitFiltered = true;
|
||||
|
||||
config.env.IS_ENTERPRISE = isEnterprise;
|
||||
config.env.SNOWPLOW_MICRO_URL = snowplowMicroUrl;
|
||||
@@ -156,6 +153,8 @@ const defaultConfig = {
|
||||
|
||||
return config;
|
||||
},
|
||||
baseUrl: `http://${BACKEND_HOST}:${BACKEND_PORT}`,
|
||||
defaultBrowser: process.env.CYPRESS_BROWSER ?? "chrome",
|
||||
supportFile: "e2e/support/cypress.js",
|
||||
chromeWebSecurity: false,
|
||||
modifyObstructiveCode: false,
|
||||
@@ -203,19 +202,9 @@ const mainConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
const snapshotsConfig = {
|
||||
...defaultConfig,
|
||||
specPattern: "e2e/snapshot-creators/**/*.cy.snap.js",
|
||||
video: false,
|
||||
};
|
||||
|
||||
const stressTestConfig = {
|
||||
...defaultConfig,
|
||||
retries: 0,
|
||||
};
|
||||
|
||||
const embeddingSdkComponentTestConfig = {
|
||||
...defaultConfig,
|
||||
baseUrl: undefined, // baseUrl should not be set for component tests,
|
||||
defaultCommandTimeout: 10000,
|
||||
requestTimeout: 10000,
|
||||
video: false,
|
||||
@@ -235,8 +224,7 @@ const embeddingSdkComponentTestConfig = {
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
defaultConfig,
|
||||
mainConfig,
|
||||
snapshotsConfig,
|
||||
stressTestConfig,
|
||||
embeddingSdkComponentTestConfig,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
const { embeddingSdkComponentTestConfig } = require("./config");
|
||||
|
||||
module.exports = {
|
||||
component: embeddingSdkComponentTestConfig,
|
||||
component: {
|
||||
...embeddingSdkComponentTestConfig,
|
||||
baseUrl: undefined, // baseUrl is not a valid *component* config option,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
const { defineConfig } = require("cypress");
|
||||
|
||||
const { snapshotsConfig } = require("./config");
|
||||
const { defaultConfig } = require("./config");
|
||||
|
||||
module.exports = defineConfig({ e2e: snapshotsConfig });
|
||||
const isQaDatabase = process.env["QA_DB_ENABLED"] === "true";
|
||||
|
||||
module.exports = defineConfig({
|
||||
e2e: {
|
||||
...defaultConfig,
|
||||
specPattern: "e2e/snapshot-creators/**/*.cy.snap.js",
|
||||
excludeSpecPattern: !isQaDatabase
|
||||
? "e2e/snapshot-creators/qa-db.cy.snap.js"
|
||||
: undefined,
|
||||
video: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
const { defineConfig } = require("cypress");
|
||||
|
||||
const {
|
||||
stressTestConfig,
|
||||
embeddingSdkComponentTestConfig,
|
||||
} = require("./config");
|
||||
const { defaultConfig, embeddingSdkComponentTestConfig } = require("./config");
|
||||
|
||||
module.exports = defineConfig({
|
||||
e2e: stressTestConfig,
|
||||
e2e: { ...defaultConfig, retries: 0 },
|
||||
component: { ...embeddingSdkComponentTestConfig, retries: 0 },
|
||||
});
|
||||
|
||||
@@ -163,3 +163,88 @@ export const DOCUMENT_WITH_THREE_CARDS_AND_COLUMNS = {
|
||||
],
|
||||
type: "doc",
|
||||
};
|
||||
|
||||
export const DOCUMENT_WITH_SUPPORTING_TEXT = {
|
||||
type: "doc",
|
||||
content: [
|
||||
{
|
||||
type: "resizeNode",
|
||||
attrs: {
|
||||
height: 350,
|
||||
minHeight: 280,
|
||||
_id: "1",
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: "flexContainer",
|
||||
attrs: {
|
||||
_id: "1a",
|
||||
columnWidths: [33.33333333333333, 66.66666666666666],
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: "supportingText",
|
||||
attrs: { _id: "1b" },
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
attrs: { _id: "1c" },
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Lorem ipsum",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "cardEmbed",
|
||||
attrs: {
|
||||
id: ORDERS_QUESTION_ID,
|
||||
name: null,
|
||||
_id: "2a2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Standalone card below",
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
_id: "3",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "resizeNode",
|
||||
attrs: {
|
||||
height: 350,
|
||||
minHeight: 280,
|
||||
_id: "4",
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: "cardEmbed",
|
||||
attrs: {
|
||||
id: ORDERS_COUNT_QUESTION_ID,
|
||||
name: null,
|
||||
_id: "4a",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
attrs: {
|
||||
_id: "5",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -59,19 +59,6 @@ export function showDashboardCardActions(index = 0) {
|
||||
return getDashboardCard(index).realHover({ scrollBehavior: "bottom" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a dashcard HTML element, will return the element for the action icon
|
||||
* with the given label text (e.g. "Click behavior", "Replace", "Duplicate", etc)
|
||||
*/
|
||||
export function findDashCardAction(
|
||||
dashcardElement: Cypress.Chainable<JQuery<HTMLElement>>,
|
||||
labelText: string,
|
||||
) {
|
||||
return dashcardElement
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText(labelText);
|
||||
}
|
||||
|
||||
export function removeDashboardCard(index = 0) {
|
||||
getDashboardCard(index)
|
||||
.realHover()
|
||||
@@ -151,9 +138,10 @@ export function setDashCardFilter(
|
||||
subType?: string,
|
||||
name?: string,
|
||||
) {
|
||||
findDashCardAction(getDashboardCard(dashcardIndex), "Add a filter").click({
|
||||
force: true,
|
||||
});
|
||||
getDashboardCard(dashcardIndex)
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Add a filter")
|
||||
.click({ force: true });
|
||||
_setFilter(type, subType, name);
|
||||
}
|
||||
|
||||
|
||||
@@ -109,33 +109,33 @@ const visitDocumentById = (id: DocumentId) => {
|
||||
cy.wait(`@${alias}`);
|
||||
};
|
||||
|
||||
export function dragAndDropCardOnAnotherCard(
|
||||
sourceCardTitle: string,
|
||||
targetCardTitle: string,
|
||||
{
|
||||
side = "left",
|
||||
}: {
|
||||
side?: "left" | "right";
|
||||
} = {},
|
||||
) {
|
||||
// Perform drag and drop: drag sourceCard card onto targetCard
|
||||
getDocumentCard(sourceCardTitle)
|
||||
export function documentsDragAndDrop({
|
||||
getSource,
|
||||
getTarget,
|
||||
side = "left",
|
||||
}: {
|
||||
getSource: () => Cypress.Chainable;
|
||||
getTarget: () => Cypress.Chainable;
|
||||
side?: "left" | "right";
|
||||
}) {
|
||||
// Perform drag and drop: drag source onto target
|
||||
getSource()
|
||||
.should("exist")
|
||||
.then(($ordersCard) => {
|
||||
getDocumentCard(targetCardTitle)
|
||||
.then(($source) => {
|
||||
getTarget()
|
||||
.should("exist")
|
||||
.then(($ordersCountCard) => {
|
||||
const sourceRect = $ordersCard[0].getBoundingClientRect();
|
||||
.then(($target) => {
|
||||
const sourceRect = $source[0].getBoundingClientRect();
|
||||
|
||||
// Start drag from center of Orders card
|
||||
getDocumentCard(sourceCardTitle).trigger("mousedown", {
|
||||
// Start drag from center of source
|
||||
getSource().trigger("mousedown", {
|
||||
x: 10,
|
||||
y: 10,
|
||||
scrollBehavior: false,
|
||||
force: true,
|
||||
});
|
||||
const dataTransfer = new DataTransfer();
|
||||
getDocumentCard(sourceCardTitle).trigger("dragstart", {
|
||||
getSource().trigger("dragstart", {
|
||||
dataTransfer,
|
||||
clientX: sourceRect.left + 10,
|
||||
clientY: sourceRect.top + 10,
|
||||
@@ -143,8 +143,8 @@ export function dragAndDropCardOnAnotherCard(
|
||||
force: true,
|
||||
});
|
||||
|
||||
const targetRect = $ordersCountCard[0].getBoundingClientRect();
|
||||
// Calculate position for target card side drop (20% from left or 20% from right based on 'side' param)
|
||||
const targetRect = $target[0].getBoundingClientRect();
|
||||
// Calculate position for target side drop (20% from left or 20% from right based on 'side' param)
|
||||
const sideX =
|
||||
targetRect.left + targetRect.width * (side === "left" ? 0.2 : 0.8);
|
||||
const centerY = targetRect.top + 10;
|
||||
@@ -153,24 +153,24 @@ export function dragAndDropCardOnAnotherCard(
|
||||
clientY: centerY,
|
||||
scrollBehavior: false,
|
||||
});
|
||||
getDocumentCard(targetCardTitle).trigger("dragover", {
|
||||
getTarget().trigger("dragover", {
|
||||
clientX: sideX,
|
||||
clientY: centerY,
|
||||
scrollBehavior: false,
|
||||
force: true,
|
||||
});
|
||||
|
||||
getDocumentCard(sourceCardTitle).realMouseUp({
|
||||
getSource().realMouseUp({
|
||||
scrollBehavior: false,
|
||||
});
|
||||
getDocumentCard(targetCardTitle).trigger("drop", {
|
||||
getTarget().trigger("drop", {
|
||||
dataTransfer,
|
||||
clientX: sideX,
|
||||
clientY: centerY,
|
||||
scrollBehavior: false,
|
||||
force: true,
|
||||
});
|
||||
getDocumentCard(sourceCardTitle).trigger("dragend", {
|
||||
getSource().trigger("dragend", {
|
||||
scrollBehavior: false,
|
||||
force: true,
|
||||
});
|
||||
@@ -178,6 +178,20 @@ export function dragAndDropCardOnAnotherCard(
|
||||
});
|
||||
}
|
||||
|
||||
export function dragAndDropCardOnAnotherCard(
|
||||
sourceCardTitle: string,
|
||||
targetCardTitle: string,
|
||||
options: {
|
||||
side?: "left" | "right";
|
||||
} = {},
|
||||
) {
|
||||
return documentsDragAndDrop({
|
||||
getSource: () => getDocumentCard(sourceCardTitle),
|
||||
getTarget: () => getDocumentCard(targetCardTitle),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function documentUndo() {
|
||||
const macOSX = Cypress.platform === "darwin";
|
||||
|
||||
|
||||
105
e2e/support/helpers/e2e-embedding-iframe-interactive-helpers.ts
Normal file
105
e2e/support/helpers/e2e-embedding-iframe-interactive-helpers.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { getIframeBody } from "./e2e-embedding-helpers";
|
||||
|
||||
/**
|
||||
* Base interface for interactive embedding test page options
|
||||
*/
|
||||
interface BaseEmbedTestPageOptions {
|
||||
dashboardId: number | string;
|
||||
iframeSelector?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and loads a test fixture for interactive iframe embedding tests
|
||||
*/
|
||||
export function loadInteractiveIframeEmbedTestPage({
|
||||
iframeSelector,
|
||||
...options
|
||||
}: BaseEmbedTestPageOptions) {
|
||||
const testPageSource = getInteractiveHtml(options);
|
||||
|
||||
const testPageUrl = `${origin}/interactive-iframe-test-page`;
|
||||
|
||||
cy.intercept("GET", testPageUrl, {
|
||||
body: testPageSource,
|
||||
headers: { "content-type": "text/html" },
|
||||
}).as("dynamicPage");
|
||||
|
||||
cy.visit(testPageUrl);
|
||||
cy.title().should("include", "Metabase Embed Test");
|
||||
|
||||
return getIframeBody(iframeSelector);
|
||||
}
|
||||
|
||||
interface PostMessageOptions {
|
||||
/**
|
||||
* CSS selector for the iframe to post the message to
|
||||
* @example `iframe[src*="localhost:4000/dashboard"]`
|
||||
*/
|
||||
iframeSelector: string;
|
||||
|
||||
/**
|
||||
* Something like `{ metabase: { type: "location", location: "/dashboard/9999990" } }` for instance
|
||||
*/
|
||||
messageData: any;
|
||||
}
|
||||
|
||||
export function postMessageToIframe(options: PostMessageOptions) {
|
||||
const { messageData, iframeSelector } = options;
|
||||
|
||||
// this madness is necessary to simulate a real MessageEvent coming from the parent window
|
||||
// because in frontend/src/metabase/lib/embed.js we check e.source === window.parent
|
||||
cy.get(iframeSelector)
|
||||
.should("be.visible")
|
||||
.then(($iframe) => {
|
||||
const iframeEl = $iframe[0] as HTMLIFrameElement;
|
||||
// Get the actual parent window from the iframe's perspective
|
||||
const actualParent = iframeEl.ownerDocument.defaultView;
|
||||
const targetWin = iframeEl.contentWindow;
|
||||
|
||||
if (!actualParent || !targetWin) {
|
||||
throw new Error("Could not access iframe or its parent window");
|
||||
}
|
||||
|
||||
// Create a MessageEvent manually with the correct source
|
||||
const event = new MessageEvent("message", {
|
||||
data: messageData,
|
||||
origin: actualParent.location.origin,
|
||||
source: actualParent,
|
||||
});
|
||||
|
||||
targetWin.dispatchEvent(event);
|
||||
});
|
||||
|
||||
return getIframeBody(iframeSelector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base HTML template for embedding test pages
|
||||
*/
|
||||
function getInteractiveHtml({ dashboardId }: BaseEmbedTestPageOptions) {
|
||||
const MB_BASE_URL = "http://localhost:4000";
|
||||
|
||||
const url = `${MB_BASE_URL}/dashboard/${dashboardId}`;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Metabase Embed Test</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<iframe
|
||||
src="${url}"
|
||||
style="position: absolute; height: 100%; width:99%; border: none; border: 2px solid red"
|
||||
allowtransparency
|
||||
></iframe>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export * from "./e2e-dragndrop-helpers";
|
||||
export * from "./e2e-element-visibility-helpers";
|
||||
export * from "./e2e-email-helpers";
|
||||
export * from "./e2e-embedding-helpers";
|
||||
export * from "./e2e-embedding-iframe-interactive-helpers";
|
||||
export * from "./e2e-embedding-iframe-sdk-helpers";
|
||||
export * from "./e2e-embedding-sdk-assertion-helpers";
|
||||
export * from "./e2e-enterprise-helpers";
|
||||
|
||||
@@ -318,6 +318,23 @@ describe("scenarios > embedding-sdk > interactive-dashboard", () => {
|
||||
|
||||
cy.wait("@dashcardDownload").then((interception) => {
|
||||
expect(interception.response?.statusCode).to.equal(200);
|
||||
|
||||
cy.log(
|
||||
"content-disposition is allowed in cross-origin requests (metabase#61708)",
|
||||
);
|
||||
expect(
|
||||
interception.response?.headers?.["access-control-expose-headers"],
|
||||
).to.include("Content-Disposition");
|
||||
|
||||
cy.log(
|
||||
"question name is prefixed in the downloaded file name (metabase#61708)",
|
||||
);
|
||||
expect(
|
||||
interception.response?.headers?.["content-disposition"],
|
||||
).to.include('filename="orders_');
|
||||
expect(
|
||||
interception.response?.headers?.["content-disposition"],
|
||||
).not.to.include('filename="query_result_');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,8 +148,7 @@ describe(
|
||||
assertScoreFormPrefilled(firstScoreRow);
|
||||
});
|
||||
});
|
||||
|
||||
cy.icon("close").click();
|
||||
cy.button("Close").click();
|
||||
});
|
||||
objectDetailModal().icon("close").click();
|
||||
|
||||
|
||||
@@ -597,7 +597,7 @@ describe("Remote Sync", () => {
|
||||
|
||||
cy.visit("/admin/settings/remote-sync");
|
||||
cy.findByLabelText("Sync branch").clear().type("test");
|
||||
cy.button("Save changes").click();
|
||||
cy.findByTestId("remote-sync-submit-button").click();
|
||||
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Success")
|
||||
@@ -609,7 +609,7 @@ describe("Remote Sync", () => {
|
||||
|
||||
H.waitForTask({ taskName: "import" });
|
||||
|
||||
cy.findByRole("button", { name: "Save changes" }).should("be.disabled");
|
||||
cy.findByTestId("remote-sync-submit-button").should("be.disabled");
|
||||
|
||||
cy.visit("/");
|
||||
|
||||
|
||||
170
e2e/test/scenarios/admin/tools/help.cy.spec.ts
Normal file
170
e2e/test/scenarios/admin/tools/help.cy.spec.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
describe("scenarios > admin > tools > help", { tags: "@OSS" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
it("should link `Get help` to help", () => {
|
||||
cy.visit("/admin/tools/help");
|
||||
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Metabase Admin");
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Get help")
|
||||
.parents("a")
|
||||
.should("have.prop", "href")
|
||||
.and(
|
||||
"match",
|
||||
/^https:\/\/www\.metabase\.com\/help\?utm_source=in-product&utm_medium=troubleshooting&utm_campaign=help&instance_version=v(?:(?!diag=).)+$/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("scenarios > admin > tools > help (EE)", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
});
|
||||
|
||||
it("should link `Get Help` to help-premium", () => {
|
||||
cy.visit("/admin/tools/help");
|
||||
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Get help")
|
||||
.parents("a")
|
||||
.should("have.prop", "href")
|
||||
.and(
|
||||
"match",
|
||||
/^https:\/\/www\.metabase\.com\/help-premium\?utm_source=in-product&utm_medium=troubleshooting&utm_campaign=help&instance_version=v.+&diag=%7B.+%7D$/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("scenarios > admin > tools > help > helping hand", () => {
|
||||
const executeCreateGrantAccessFlow = (
|
||||
durationOption?: "96 hours" | "48 hours" | "24 hours",
|
||||
ticket?: string,
|
||||
notes?: string,
|
||||
) => {
|
||||
cy.findByTestId("access-grant-list-table").should("not.exist");
|
||||
cy.button("Request a helping hand").should("be.enabled");
|
||||
cy.button("Request a helping hand").click();
|
||||
|
||||
// Clicking the button should open the modal
|
||||
cy.findByTestId("grant-access-modal").should("be.visible");
|
||||
cy.findByTestId("grant-access-modal").within(() => {
|
||||
cy.findByRole("heading", { name: "Grant Access?" }).should("be.visible");
|
||||
|
||||
if (durationOption) {
|
||||
cy.findByLabelText(/Access duration/).click();
|
||||
cy.document()
|
||||
.findByRole("listbox")
|
||||
.findByText(new RegExp(durationOption, "g"))
|
||||
.click();
|
||||
}
|
||||
|
||||
ticket && cy.findByLabelText("Ticket").type(ticket);
|
||||
notes && cy.findByLabelText("Notes").type(notes);
|
||||
|
||||
cy.button("Grant access").click();
|
||||
});
|
||||
|
||||
H.undoToast()
|
||||
.findByText("Access grant created successfully")
|
||||
.should("be.visible");
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.mockSessionPropertiesTokenFeatures({ "support-users": true });
|
||||
});
|
||||
|
||||
it("should only display the `Helping hand` section for cloud customers", () => {
|
||||
cy.visit("/admin/tools/help");
|
||||
cy.findByRole("heading", { name: "Helping hand" }).should("not.exist");
|
||||
|
||||
H.activateToken("pro-self-hosted");
|
||||
cy.reload();
|
||||
cy.findByRole("heading", { name: "Helping hand" }).should("not.exist");
|
||||
|
||||
H.activateToken("starter");
|
||||
cy.reload();
|
||||
cy.findByRole("heading", { name: "Helping hand" }).should("be.visible");
|
||||
|
||||
H.activateToken("pro-cloud");
|
||||
cy.reload();
|
||||
cy.findByRole("heading", { name: "Helping hand" }).should("be.visible");
|
||||
});
|
||||
|
||||
it("should allow creating a new access grant", () => {
|
||||
H.activateToken("pro-cloud");
|
||||
cy.visit("/admin/tools/help");
|
||||
|
||||
cy.findByTestId("access-grant-list-table").should("not.exist");
|
||||
|
||||
executeCreateGrantAccessFlow();
|
||||
|
||||
cy.findByTestId("access-grant-list-table").should("be.visible");
|
||||
cy.findByTestId("access-grant-list-table").within(() => {
|
||||
cy.get("tbody").findAllByRole("row").should("have.length", 1);
|
||||
});
|
||||
});
|
||||
|
||||
it("allow creating an access grant with a ticket number and custom notes", () => {
|
||||
H.activateToken("pro-cloud");
|
||||
cy.visit("/admin/tools/help");
|
||||
|
||||
executeCreateGrantAccessFlow("48 hours", "TICKET-999", "Custom notes");
|
||||
|
||||
cy.findByTestId("access-grant-list-table").within(() => {
|
||||
cy.findByRole("cell", {
|
||||
name: new RegExp(dayjs().format("MMM D, YYYY"), "g"),
|
||||
}).should("be.visible");
|
||||
cy.findByRole("cell", { name: /TICKET-999/ }).should("be.visible");
|
||||
cy.findByRole("cell", { name: /Custom notes/ }).should("be.visible");
|
||||
cy.findByText(/48 hours left/).should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
it("should disallow more than one active access grant", () => {
|
||||
H.activateToken("pro-cloud");
|
||||
cy.visit("/admin/tools/help");
|
||||
|
||||
executeCreateGrantAccessFlow();
|
||||
|
||||
cy.button("Request a helping hand").should("be.disabled");
|
||||
cy.button("Request a helping hand")
|
||||
.siblings()
|
||||
.findByText("You can only have one active request at a time")
|
||||
.should("be.visible");
|
||||
});
|
||||
|
||||
it("can revoke an access grant", () => {
|
||||
H.activateToken("pro-cloud");
|
||||
cy.visit("/admin/tools/help");
|
||||
|
||||
executeCreateGrantAccessFlow();
|
||||
cy.button("Request a helping hand").should("be.disabled");
|
||||
|
||||
H.undoToast().icon("close").click();
|
||||
|
||||
cy.findByTestId("access-grant-list-table")
|
||||
.button("Revoke access grant")
|
||||
.click();
|
||||
H.modal()
|
||||
.findByRole("heading", { name: "Revoke access grant?" })
|
||||
.should("be.visible");
|
||||
H.modal().button("Revoke").click();
|
||||
H.undoToast()
|
||||
.findByText("Access grant revoked successfully")
|
||||
.should("be.visible");
|
||||
|
||||
cy.button("Request a helping hand").should("be.enabled");
|
||||
});
|
||||
});
|
||||
@@ -1,52 +1,10 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import type { NativeQuestionDetails } from "e2e/support/helpers";
|
||||
import { createMockTask } from "metabase-types/api/mocks";
|
||||
|
||||
const { H } = cy;
|
||||
|
||||
describe("scenarios > admin > tools > help", { tags: "@OSS" }, () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
});
|
||||
|
||||
it("should link `Get help` to help", () => {
|
||||
cy.visit("/admin/tools/help");
|
||||
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Metabase Admin");
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Get help")
|
||||
.parents("a")
|
||||
.should("have.prop", "href")
|
||||
.and(
|
||||
"match",
|
||||
/^https:\/\/www\.metabase\.com\/help\?utm_source=in-product&utm_medium=troubleshooting&utm_campaign=help&instance_version=v(?:(?!diag=).)+$/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("scenarios > admin > tools > help (EE)", () => {
|
||||
beforeEach(() => {
|
||||
H.restore();
|
||||
cy.signInAsAdmin();
|
||||
H.activateToken("pro-self-hosted");
|
||||
});
|
||||
|
||||
it("should link `Get Help` to help-premium", () => {
|
||||
cy.visit("/admin/tools/help");
|
||||
|
||||
cy.findByTestId("admin-layout-content")
|
||||
.findByText("Get help")
|
||||
.parents("a")
|
||||
.should("have.prop", "href")
|
||||
.and(
|
||||
"match",
|
||||
/^https:\/\/www\.metabase\.com\/help-premium\?utm_source=in-product&utm_medium=troubleshooting&utm_campaign=help&instance_version=v.+&diag=%7B.+%7D$/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue 14636", () => {
|
||||
const total = 57;
|
||||
const limit = 50;
|
||||
@@ -56,7 +14,7 @@ describe("issue 14636", () => {
|
||||
* @param {(0|1)} payload.page
|
||||
* @param {("first"|"second")} payload.alias
|
||||
*/
|
||||
function stubPageResponses({ page, alias }) {
|
||||
function stubPageResponses({ page, alias }: { page: number; alias: string }) {
|
||||
const offset = page * limit;
|
||||
|
||||
cy.intercept(
|
||||
@@ -80,7 +38,7 @@ describe("issue 14636", () => {
|
||||
* @param {(0|1)} page
|
||||
* @returns Row[]
|
||||
*/
|
||||
function stubPageRows(page) {
|
||||
function stubPageRows(page: number) {
|
||||
// There rows details don't really matter.
|
||||
// We're generating two types of rows. One for each page.
|
||||
const tasks = ["field values scanning", "analyze"];
|
||||
@@ -103,11 +61,10 @@ describe("issue 14636", () => {
|
||||
const pageRows = [limit, total - limit];
|
||||
const length = pageRows[page];
|
||||
|
||||
const stubbedRows = Array.from({ length }, (_, index) => ({
|
||||
return Array.from({ length }, (_, index) => ({
|
||||
...row,
|
||||
id: index + 1,
|
||||
}));
|
||||
return stubbedRows;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -356,7 +313,7 @@ describe("scenarios > admin > tools > logs", () => {
|
||||
* The formatted timestamp may vary depending on the timezone in which the test is run.
|
||||
* This function makes test assertions timezone-agnostic.
|
||||
*/
|
||||
function formatTimestamp(timestamp) {
|
||||
function formatTimestamp(timestamp: string) {
|
||||
return dayjs(timestamp).format();
|
||||
}
|
||||
});
|
||||
@@ -381,7 +338,7 @@ describe("admin > tools > erroring questions ", () => {
|
||||
display: "scalar",
|
||||
};
|
||||
|
||||
function fixQuestion(name) {
|
||||
function fixQuestion(name: string) {
|
||||
cy.findByTestId("visualization-root").findByText(name).click();
|
||||
|
||||
cy.findByText("Open Editor").click();
|
||||
@@ -396,7 +353,7 @@ describe("admin > tools > erroring questions ", () => {
|
||||
});
|
||||
}
|
||||
|
||||
function selectQuestion(name) {
|
||||
function selectQuestion(name: string) {
|
||||
cy.findByText(name)
|
||||
.closest("tr")
|
||||
.within(() => {
|
||||
@@ -435,7 +392,7 @@ describe("admin > tools > erroring questions ", () => {
|
||||
|
||||
describe("with the existing broken questions", () => {
|
||||
beforeEach(() => {
|
||||
H.createNativeQuestion(brokenQuestionDetails, {
|
||||
H.createNativeQuestion(brokenQuestionDetails as NativeQuestionDetails, {
|
||||
loadMetadata: true,
|
||||
});
|
||||
|
||||
@@ -1060,8 +1060,8 @@ describe("scenarios > dashboard > dashboard cards > click behavior", () => {
|
||||
cy.findByText(COUNT_COLUMN_ID).should("exist");
|
||||
cy.findByText(CREATED_AT_COLUMN_ID).should("exist");
|
||||
cy.findByText(DASHBOARD_FILTER_TEXT.name).should("exist");
|
||||
cy.realPress("Escape");
|
||||
});
|
||||
H.modal().findByText("Values you can reference").click();
|
||||
H.modal().within(() => {
|
||||
cy.findByRole("textbox").type(URL_WITH_PARAMS, {
|
||||
parseSpecialCharSequences: false,
|
||||
|
||||
@@ -133,7 +133,10 @@ describe("scenarios > dashboard cards > replace question", () => {
|
||||
it("should replace a dashboard card question (metabase#36984)", () => {
|
||||
visitDashboardAndEdit();
|
||||
|
||||
H.findDashCardAction(findHeadingDashcard(), "Replace").should("not.exist");
|
||||
findHeadingDashcard()
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Replace")
|
||||
.should("not.exist");
|
||||
|
||||
// Ensure can replace with a question
|
||||
replaceQuestion(findTargetDashcard(), {
|
||||
@@ -168,7 +171,7 @@ describe("scenarios > dashboard cards > replace question", () => {
|
||||
it("should undo the question replace action", () => {
|
||||
visitDashboardAndEdit();
|
||||
|
||||
overwriteDashCardTitle(findTargetDashcard(), "Custom name");
|
||||
overwriteDashCardTitle("Custom name");
|
||||
connectDashboardFilter(findTargetDashcard(), {
|
||||
filterName: PARAMETER.UNUSED.name,
|
||||
columnName: "Discount",
|
||||
@@ -275,8 +278,11 @@ function assertDashCardTitle(title) {
|
||||
cy.findByTestId("legend-caption-title").should("have.text", title);
|
||||
}
|
||||
|
||||
function overwriteDashCardTitle(dashcardElement, textTitle) {
|
||||
H.findDashCardAction(dashcardElement, "Show visualization options").click();
|
||||
function overwriteDashCardTitle(textTitle) {
|
||||
findTargetDashcard()
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Show visualization options")
|
||||
.click();
|
||||
H.modal().within(() => {
|
||||
cy.findByLabelText("Title").type(`{selectall}{del}${textTitle}`).blur();
|
||||
cy.button("Done").click();
|
||||
|
||||
@@ -79,7 +79,10 @@ describe("scenarios > dashboard cards > duplicate", () => {
|
||||
H.visitDashboard("@dashboardId");
|
||||
cy.findByLabelText("Edit dashboard").click();
|
||||
|
||||
H.findDashCardAction(H.getDashboardCard(0), "Duplicate").click();
|
||||
H.getDashboardCard(0)
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Duplicate")
|
||||
.click();
|
||||
H.expectUnstructuredSnowplowEvent(EVENTS.duplicateDashcard);
|
||||
|
||||
// check that the new card loads _before_ saving
|
||||
|
||||
@@ -430,7 +430,10 @@ describe("dashboard filters auto-wiring", () => {
|
||||
|
||||
goToFilterMapping();
|
||||
|
||||
H.findDashCardAction(H.getDashboardCard(1), "Replace").click();
|
||||
H.getDashboardCard(1)
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Replace")
|
||||
.click();
|
||||
|
||||
H.modal().findByText("Orders, Count").click();
|
||||
|
||||
|
||||
@@ -1234,7 +1234,10 @@ describe("scenarios > dashboard > parameters", () => {
|
||||
H.editDashboard();
|
||||
});
|
||||
|
||||
H.findDashCardAction(H.getDashboardCard(0), "Duplicate").click();
|
||||
H.getDashboardCard(0)
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Duplicate")
|
||||
.click();
|
||||
|
||||
H.getDashboardCard(2).within(() => {
|
||||
cy.findByDisplayValue("Heading Text").should("exist");
|
||||
@@ -2315,7 +2318,10 @@ describe("scenarios > dashboard > parameters", () => {
|
||||
H.selectDashboardFilter(H.getDashboardCard(0), "Count");
|
||||
H.dashboardParameterSidebar().button("Done").click();
|
||||
|
||||
H.findDashCardAction(H.getDashboardCard(0), "Duplicate").click();
|
||||
H.getDashboardCard(0)
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Duplicate")
|
||||
.click();
|
||||
|
||||
H.getDashboardCard(1).within(() => {
|
||||
cy.findByTestId("chart-container").should("exist");
|
||||
|
||||
@@ -195,7 +195,10 @@ describe("scenarios > dashboard > visualizer > basics", () => {
|
||||
|
||||
H.visitDashboard(ORDERS_DASHBOARD_ID);
|
||||
H.editDashboard();
|
||||
H.findDashCardAction(dashCard(), "Visualize another way").click();
|
||||
dashCard()
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Visualize another way")
|
||||
.click();
|
||||
|
||||
H.modal().within(() => {
|
||||
H.assertDataSourceColumnSelected("Orders", "ID");
|
||||
@@ -213,14 +216,19 @@ describe("scenarios > dashboard > visualizer > basics", () => {
|
||||
});
|
||||
});
|
||||
|
||||
H.findDashCardAction(dashCard(), "Visualize another way").should(
|
||||
"not.exist",
|
||||
);
|
||||
H.findDashCardAction(dashCard(), "Show visualization options").should(
|
||||
"not.exist",
|
||||
);
|
||||
dashCard()
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Visualize another way")
|
||||
.should("not.exist");
|
||||
dashCard()
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Show visualization options")
|
||||
.should("not.exist");
|
||||
dashCard()
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Edit visualization")
|
||||
.click();
|
||||
|
||||
H.findDashCardAction(dashCard(), "Edit visualization").click();
|
||||
H.modal().button("Save").should("be.disabled");
|
||||
});
|
||||
|
||||
@@ -388,10 +396,10 @@ describe("scenarios > dashboard > visualizer > basics", () => {
|
||||
});
|
||||
|
||||
H.editDashboard();
|
||||
H.findDashCardAction(
|
||||
H.getDashboardCard(0),
|
||||
"Visualize another way",
|
||||
).click();
|
||||
H.getDashboardCard(0)
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Visualize another way")
|
||||
.click();
|
||||
|
||||
H.modal().within(() => {
|
||||
H.selectVisualization("bar");
|
||||
@@ -439,10 +447,10 @@ describe("scenarios > dashboard > visualizer > basics", () => {
|
||||
H.tooltip().findByText("Original question description").should("exist");
|
||||
|
||||
H.editDashboard();
|
||||
H.findDashCardAction(
|
||||
H.getDashboardCard(0),
|
||||
"Visualize another way",
|
||||
).click();
|
||||
H.getDashboardCard(0)
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Visualize another way")
|
||||
.click();
|
||||
|
||||
H.modal().within(() => {
|
||||
cy.findByDisplayValue("Original Question Title").should("exist");
|
||||
@@ -825,10 +833,10 @@ describe("scenarios > dashboard > visualizer > basics", () => {
|
||||
);
|
||||
|
||||
H.editDashboard();
|
||||
H.findDashCardAction(
|
||||
H.getDashboardCard(0),
|
||||
"Visualize another way",
|
||||
).click();
|
||||
H.getDashboardCard(0)
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Visualize another way")
|
||||
.click();
|
||||
|
||||
cy.intercept("GET", "/api/card/*/query_metadata").as("queryMetadata");
|
||||
|
||||
@@ -868,7 +876,10 @@ describe("scenarios > dashboard > visualizer > basics", () => {
|
||||
cy.findByText("1").should("exist");
|
||||
});
|
||||
|
||||
H.findDashCardAction(H.getDashboardCard(0), "Edit visualization").click();
|
||||
H.getDashboardCard(0)
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Edit visualization")
|
||||
.click();
|
||||
H.modal().within(() => {
|
||||
H.dataImporter().findByText(baseQuestion.name).should("exist");
|
||||
H.dataImporter().findByText(invalidQuestion.name).should("exist");
|
||||
|
||||
@@ -139,10 +139,10 @@ describe("issue 61521", () => {
|
||||
});
|
||||
|
||||
H.editDashboard();
|
||||
H.findDashCardAction(
|
||||
H.getDashboardCard(0),
|
||||
"Visualize another way",
|
||||
).click();
|
||||
H.getDashboardCard(0)
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Visualize another way")
|
||||
.click();
|
||||
|
||||
H.modal().within(() => {
|
||||
H.switchToAddMoreData();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
DOCUMENT_WITH_SUPPORTING_TEXT,
|
||||
DOCUMENT_WITH_THREE_CARDS_AND_COLUMNS,
|
||||
DOCUMENT_WITH_TWO_CARDS,
|
||||
} from "e2e/support/document-initial-data";
|
||||
@@ -455,4 +456,112 @@ describe("documents supporting text", () => {
|
||||
H.dragAndDropCardOnAnotherCard("Orders, Count", targetCardTitle);
|
||||
H.documentContent().findByText("Lorem ipsum").should("not.exist");
|
||||
});
|
||||
|
||||
describe("drag and drop", () => {
|
||||
beforeEach(() => {
|
||||
H.createDocument({
|
||||
name: "DnD Test Document",
|
||||
document: DOCUMENT_WITH_SUPPORTING_TEXT,
|
||||
collection_id: null,
|
||||
alias: "document",
|
||||
idAlias: "documentId",
|
||||
});
|
||||
H.visitDocument("@documentId");
|
||||
});
|
||||
|
||||
const getSupportingText = (contents: string = "Lorem ipsum") => {
|
||||
const testId = "document-card-supporting-text";
|
||||
return cy
|
||||
.findAllByTestId(testId)
|
||||
.contains(contents)
|
||||
.closest(`[data-testid="${testId}"]`);
|
||||
};
|
||||
|
||||
const assertHorizontalLayout = (
|
||||
c1: Cypress.Chainable<JQuery<HTMLElement>>,
|
||||
c2: Cypress.Chainable<JQuery<HTMLElement>>,
|
||||
) => {
|
||||
c1.then(($c1) => {
|
||||
c2.then(($c2) => {
|
||||
const rect1 = $c1[0].getBoundingClientRect();
|
||||
const rect2 = $c2[0].getBoundingClientRect();
|
||||
expect(rect2.left).to.gte(rect1.right);
|
||||
expect(rect1.top).to.be.closeTo(rect2.top, 2);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const assertVerticalLayout = (
|
||||
c1: Cypress.Chainable<JQuery<HTMLElement>>,
|
||||
c2: Cypress.Chainable<JQuery<HTMLElement>>,
|
||||
) => {
|
||||
c1.then(($c1) => {
|
||||
c2.then(($c2) => {
|
||||
const rect1 = $c1[0].getBoundingClientRect();
|
||||
const rect2 = $c2[0].getBoundingClientRect();
|
||||
expect(rect2.top).to.gte(rect1.bottom);
|
||||
expect(rect1.left).to.be.closeTo(rect2.left, 2);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
it("should reorder when dropping a supporting text block onto a card", () => {
|
||||
H.getDocumentCard("Orders").should("exist");
|
||||
H.documentsDragAndDrop({
|
||||
getSource: () => getSupportingText().find("[data-drag-handle]"),
|
||||
getTarget: () => H.getDocumentCard("Orders"),
|
||||
side: "right",
|
||||
});
|
||||
|
||||
assertHorizontalLayout(H.getDocumentCard("Orders"), getSupportingText());
|
||||
assertVerticalLayout(
|
||||
H.getDocumentCard("Orders"),
|
||||
H.getDocumentCard("Orders, Count"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should reorder when dropping a card onto a supporting text block", () => {
|
||||
H.getDocumentCard("Orders").should("exist");
|
||||
H.documentsDragAndDrop({
|
||||
getSource: () => H.getDocumentCard("Orders"),
|
||||
getTarget: () => getSupportingText(),
|
||||
side: "left",
|
||||
});
|
||||
|
||||
assertHorizontalLayout(H.getDocumentCard("Orders"), getSupportingText());
|
||||
assertVerticalLayout(
|
||||
H.getDocumentCard("Orders"),
|
||||
H.getDocumentCard("Orders, Count"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should insert a card when dropped onto a supporting text block", () => {
|
||||
H.getDocumentCard("Orders").should("exist");
|
||||
H.documentsDragAndDrop({
|
||||
getSource: () => H.getDocumentCard("Orders, Count"),
|
||||
getTarget: () => getSupportingText(),
|
||||
side: "left",
|
||||
});
|
||||
|
||||
assertHorizontalLayout(
|
||||
H.getDocumentCard("Orders, Count"),
|
||||
getSupportingText(),
|
||||
);
|
||||
assertHorizontalLayout(getSupportingText(), H.getDocumentCard("Orders"));
|
||||
});
|
||||
|
||||
it("should do nothing if dragging a supporting text block outside of a group", () => {
|
||||
H.getDocumentCard("Orders").should("exist");
|
||||
H.documentsDragAndDrop({
|
||||
getSource: () => getSupportingText().find("[data-drag-handle]"),
|
||||
getTarget: () => H.getDocumentCard("Orders, Count"),
|
||||
});
|
||||
|
||||
assertHorizontalLayout(getSupportingText(), H.getDocumentCard("Orders"));
|
||||
assertVerticalLayout(
|
||||
getSupportingText(),
|
||||
H.getDocumentCard("Orders, Count"),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -391,6 +391,45 @@ describe("scenarios > embedding > dashboard parameters", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow searching dashboard parameters in preview embed modal", () => {
|
||||
H.visitDashboard("@dashboardId");
|
||||
|
||||
H.openStaticEmbeddingModal({
|
||||
activeTab: "parameters",
|
||||
previewMode: "preview",
|
||||
});
|
||||
|
||||
H.modal().within(() => {
|
||||
// Set the Name parameter to enabled so we can test searching
|
||||
cy.findByText("Name")
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.findByText("Disabled").click();
|
||||
});
|
||||
});
|
||||
|
||||
H.popover().findByText("Editable").click();
|
||||
|
||||
// Test the preview iframe parameter search functionality
|
||||
H.getIframeBody().within(() => {
|
||||
// Open the Name filter dropdown
|
||||
cy.findByTestId("dashboard-parameters-widget-container")
|
||||
.findByText("Name")
|
||||
.click();
|
||||
|
||||
// Test searching for names containing specific text
|
||||
cy.findByPlaceholderText("Search by Name").type("Af");
|
||||
|
||||
// Verify that search results are filtered
|
||||
H.popover().within(() => {
|
||||
// Should show names containing "Af"
|
||||
cy.findByText("Afton Lesch").should("be.visible");
|
||||
// Should not show names that don't match the search
|
||||
cy.findByText("Lina Heaney").should("not.exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should render error message when `params` is not an object (metabase#14474)", () => {
|
||||
cy.get("@dashboardId").then((dashboardId) => {
|
||||
cy.request("PUT", `/api/dashboard/${dashboardId}`, {
|
||||
|
||||
@@ -1590,6 +1590,89 @@ describe("scenarios > embedding > full app", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("navigation through postMessage", () => {
|
||||
const assertIsLost = () => {
|
||||
cy.get("@iframeBody")
|
||||
.find("[role=status]")
|
||||
.should("contain", "We're a little lost");
|
||||
};
|
||||
|
||||
const assertIsDashboard = () => {
|
||||
cy.get("@iframeBody")
|
||||
.find("[data-testid=table-footer]")
|
||||
.should("contain", "Showing first 2,000 rows");
|
||||
};
|
||||
|
||||
const assertIsQuestion = () => {
|
||||
cy.get("@iframeBody")
|
||||
.find("[data-testid=question-row-count]")
|
||||
.should("contain", "Showing first 2,000 rows");
|
||||
};
|
||||
|
||||
const goTo = (url) => {
|
||||
H.postMessageToIframe({
|
||||
iframeSelector: 'iframe[src*="localhost:4000/dashboard"]',
|
||||
messageData: {
|
||||
metabase: { type: "location", location: url },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it("should handle invalid questions/dashboards (metabase#65500)", () => {
|
||||
cy.signInAsAdmin();
|
||||
|
||||
H.createDashboardWithTabs({
|
||||
dashboard: {
|
||||
name: "Dashboard with tabs",
|
||||
},
|
||||
dashcards: [
|
||||
createMockDashboardCard({
|
||||
card_id: ORDERS_QUESTION_ID,
|
||||
size_x: 10,
|
||||
size_y: 8,
|
||||
}),
|
||||
],
|
||||
}).then((dashboard) => {
|
||||
H.loadInteractiveIframeEmbedTestPage({
|
||||
dashboardId: dashboard.id,
|
||||
iframeSelector: 'iframe[src*="localhost:4000/dashboard"]',
|
||||
});
|
||||
|
||||
cy.get('iframe[src*="localhost:4000/dashboard"]')
|
||||
.its("0.contentDocument.body")
|
||||
.should("not.be.empty")
|
||||
.then(cy.wrap)
|
||||
.as("iframeBody");
|
||||
|
||||
assertIsDashboard();
|
||||
|
||||
// invalid dashboard -> valid dashboard
|
||||
goTo("/dashboard/9999990");
|
||||
assertIsLost();
|
||||
goTo(`/dashboard/${dashboard.id}`);
|
||||
assertIsDashboard();
|
||||
|
||||
// invalid question -> valid question
|
||||
goTo("/question/9999990");
|
||||
assertIsLost();
|
||||
goTo(`/question/${ORDERS_QUESTION_ID}`);
|
||||
assertIsQuestion();
|
||||
|
||||
// invalid question -> valid dashboard
|
||||
goTo("/question/9999990");
|
||||
assertIsLost();
|
||||
goTo(`/dashboard/${dashboard.id}`);
|
||||
assertIsDashboard();
|
||||
|
||||
// invalid dashboard -> valid question
|
||||
goTo("/dashboard/9999990");
|
||||
assertIsLost();
|
||||
goTo(`/question/${ORDERS_QUESTION_ID}`);
|
||||
assertIsQuestion();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should send `frame` message with dashboard height when the dashboard is resized (metabase#37437)", () => {
|
||||
const TAB_1 = { id: 1, name: "Tab 1" };
|
||||
const TAB_2 = { id: 2, name: "Tab 2" };
|
||||
|
||||
@@ -34,6 +34,10 @@ describe("scenarios > embedding > sdk iframe embedding > view and curate content
|
||||
.click();
|
||||
|
||||
cy.log("should show question view");
|
||||
cy.get("iframe[data-metabase-embed]").should(($iframe) => {
|
||||
const body = $iframe.contents().find("body");
|
||||
expect(body).to.exist;
|
||||
});
|
||||
H.getSimpleEmbedIframeContent()
|
||||
.findByTestId("query-visualization-root")
|
||||
.should("be.visible");
|
||||
|
||||
@@ -4604,7 +4604,10 @@ describe("issue 62627", () => {
|
||||
|
||||
cy.log("add an inline card filter");
|
||||
H.showDashboardCardActions();
|
||||
H.findDashCardAction(H.getDashboardCard(), "Add a filter").click();
|
||||
H.getDashboardCard()
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Add a filter")
|
||||
.click();
|
||||
H.popover().findByText("Text or Category").click();
|
||||
H.selectDashboardFilter(H.getDashboardCard(), "Category");
|
||||
H.setDashboardParameterName("Category");
|
||||
|
||||
@@ -228,6 +228,59 @@ describe("scenarios > metabot > transforms codegen", () => {
|
||||
assertEditorContent("python", "pd.DataFrame({'value': [3]})");
|
||||
assertAcceptRejectUI({ visible: false });
|
||||
});
|
||||
|
||||
it("should create SQL transform with model reference via metabot and run successfully", () => {
|
||||
// Create a model first
|
||||
H.getTableId({ name: SOURCE_TABLE, databaseId: WRITABLE_DB_ID }).as(
|
||||
"tableId",
|
||||
);
|
||||
|
||||
cy.get("@tableId").then((tableId) => {
|
||||
H.createQuestion({
|
||||
name: "Test Model",
|
||||
type: "model",
|
||||
query: {
|
||||
"source-table": tableId,
|
||||
limit: 5,
|
||||
},
|
||||
}).as("model");
|
||||
});
|
||||
|
||||
cy.get("@model").then(({ body: model }) => {
|
||||
visitTransformListPage();
|
||||
H.openMetabotViaSearchButton(true);
|
||||
|
||||
cy.log("Ask metabot for a new transform that references the model");
|
||||
const modelTagName = `#${model.id}-test-model`;
|
||||
const queryWithModelRef = `SELECT * FROM {{${modelTagName}}}`;
|
||||
|
||||
H.mockMetabotResponse({
|
||||
body: createMockTransformSuggestionResponse(
|
||||
"I'll create a transform that queries your model.",
|
||||
createMockNativeTransformJSON(
|
||||
null,
|
||||
WRITABLE_DB_ID,
|
||||
queryWithModelRef,
|
||||
),
|
||||
),
|
||||
});
|
||||
H.sendMetabotMessage(
|
||||
"Create a transform that queries the Test Model",
|
||||
);
|
||||
assertSuggestionInSidebar({ newSourcePartial: "SELECT * FROM" });
|
||||
|
||||
cy.log("Should be able to visit the new transform");
|
||||
viewLastSuggestion();
|
||||
cy.url().should("include", "/admin/transforms/new/native");
|
||||
assertEditorContent("native", "SELECT * FROM");
|
||||
|
||||
cy.log(
|
||||
"Should be able to run the transform successfully (verifies template tags were parsed)",
|
||||
);
|
||||
cy.findByTestId("native-query-editor-container").icon("play").click();
|
||||
cy.findByTestId("query-visualization-root").should("be.visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("update existing transform", () => {
|
||||
|
||||
@@ -91,10 +91,10 @@ describe("scenarios > metrics > dashboard", () => {
|
||||
);
|
||||
H.editDashboard();
|
||||
|
||||
H.findDashCardAction(
|
||||
H.getDashboardCard(0),
|
||||
"Visualize another way",
|
||||
).click();
|
||||
H.getDashboardCard(0)
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Visualize another way")
|
||||
.click();
|
||||
H.modal().within(() => {
|
||||
H.switchToAddMoreData();
|
||||
H.selectDataset(PRODUCTS_SCALAR_METRIC.name);
|
||||
|
||||
@@ -137,10 +137,10 @@ describe("issue 44171", () => {
|
||||
H.sidebar().findByText("Metric 44171-A").click();
|
||||
|
||||
H.showDashboardCardActions(0);
|
||||
H.findDashCardAction(
|
||||
H.getDashboardCard(0),
|
||||
"Visualize another way",
|
||||
).click();
|
||||
H.getDashboardCard(0)
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Visualize another way")
|
||||
.click();
|
||||
H.modal().within(() => {
|
||||
H.switchToAddMoreData();
|
||||
H.selectDataset("Metric 44171-B");
|
||||
|
||||
@@ -323,10 +323,10 @@ describe("issue 21559", { tags: "@external" }, () => {
|
||||
it("should respect dashboard card visualization (metabase#21559)", () => {
|
||||
cy.intercept("POST", "/api/card/*/query").as("cardQuery");
|
||||
|
||||
H.findDashCardAction(
|
||||
H.getDashboardCard(0),
|
||||
"Visualize another way",
|
||||
).click();
|
||||
H.getDashboardCard(0)
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Visualize another way")
|
||||
.click();
|
||||
|
||||
H.modal().within(() => {
|
||||
H.switchToAddMoreData();
|
||||
|
||||
@@ -1,22 +1,56 @@
|
||||
/**
|
||||
* Warning!
|
||||
* Do not modify SAMPLE_DB data to test any table editing features.
|
||||
* It is used in multiple tests and any changes will break them.
|
||||
*/
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { SAMPLE_DB_ID, WRITABLE_DB_ID } from "e2e/support/cypress_data";
|
||||
import {
|
||||
SAMPLE_DB_ID,
|
||||
USER_GROUPS,
|
||||
WRITABLE_DB_ID,
|
||||
} from "e2e/support/cypress_data";
|
||||
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
|
||||
import { resetSnowplow } from "e2e/support/helpers/e2e-snowplow-helpers";
|
||||
import { DataPermissionValue } from "metabase/admin/permissions/types";
|
||||
|
||||
const { H } = cy;
|
||||
const { ALL_USERS_GROUP } = USER_GROUPS;
|
||||
const { ORDERS_ID, PRODUCTS_ID, ORDERS, PRODUCTS } = SAMPLE_DATABASE;
|
||||
|
||||
const { ORDERS_ID, PRODUCTS_ID, PEOPLE_ID, ORDERS, PRODUCTS } = SAMPLE_DATABASE;
|
||||
const EDITABLE_SOURCE_TABLE_NAME = "many_data_types";
|
||||
const EDITABLE_SOURCE_TABLE_NAME_REGEX = new RegExp("Many Data Types", "i");
|
||||
const INLINE_EDIT_TEST_TABLE_NAME = "editing_test";
|
||||
const DEFAULT_FIELD = "UUID";
|
||||
|
||||
describe("scenarios > table-editing", () => {
|
||||
beforeEach(() => {
|
||||
resetSnowplow();
|
||||
|
||||
H.restore();
|
||||
H.restore("postgres-writable");
|
||||
H.resetTestTable({
|
||||
type: "postgres",
|
||||
table: EDITABLE_SOURCE_TABLE_NAME,
|
||||
});
|
||||
|
||||
cy.signInAsAdmin();
|
||||
cy.updatePermissionsGraph({
|
||||
[ALL_USERS_GROUP]: {
|
||||
[WRITABLE_DB_ID]: {
|
||||
"view-data": DataPermissionValue.UNRESTRICTED,
|
||||
"create-queries": DataPermissionValue.QUERY_BUILDER_AND_NATIVE,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
H.resyncDatabase({
|
||||
dbId: WRITABLE_DB_ID,
|
||||
tableName: EDITABLE_SOURCE_TABLE_NAME,
|
||||
});
|
||||
|
||||
H.activateToken("bleeding-edge");
|
||||
setTableEditingEnabledForDB(SAMPLE_DB_ID);
|
||||
|
||||
setTableEditingEnabledForDB(WRITABLE_DB_ID);
|
||||
|
||||
cy.intercept("GET", "/api/database").as("getDatabases");
|
||||
cy.intercept("GET", "/api/table/*").as("getTable");
|
||||
@@ -24,24 +58,28 @@ describe("scenarios > table-editing", () => {
|
||||
|
||||
it("should show edit icon on table browser", () => {
|
||||
openTableBrowser();
|
||||
getTableEditIcon("People").should("be.visible");
|
||||
getTableEditIcon(EDITABLE_SOURCE_TABLE_NAME_REGEX).should("be.visible");
|
||||
});
|
||||
|
||||
it("should not show edit icon on table browser if user is not admin", () => {
|
||||
cy.signInAsNormalUser();
|
||||
|
||||
openTableBrowser();
|
||||
getTableEditIcon("People").should("not.exist");
|
||||
getTableEditIcon(EDITABLE_SOURCE_TABLE_NAME_REGEX).should("not.exist");
|
||||
});
|
||||
|
||||
it("should allow to open table data edit mode", () => {
|
||||
openTableBrowser();
|
||||
openTableEdit("People");
|
||||
openTableEdit(EDITABLE_SOURCE_TABLE_NAME_REGEX);
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "edit_data_button_clicked",
|
||||
triggered_from: "table-browser",
|
||||
target_id: PEOPLE_ID,
|
||||
H.getTableId({
|
||||
name: EDITABLE_SOURCE_TABLE_NAME,
|
||||
}).then((tableId) => {
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "edit_data_button_clicked",
|
||||
triggered_from: "table-browser",
|
||||
target_id: tableId,
|
||||
});
|
||||
});
|
||||
|
||||
cy.wait("@getTable");
|
||||
@@ -49,7 +87,7 @@ describe("scenarios > table-editing", () => {
|
||||
cy.findByTestId("edit-table-data-root")
|
||||
.should("be.visible")
|
||||
.within(() => {
|
||||
cy.findByText("People").should("be.visible");
|
||||
cy.findByText(EDITABLE_SOURCE_TABLE_NAME_REGEX).should("be.visible");
|
||||
cy.findByTestId("head-crumbs-container")
|
||||
.findByText("Edit")
|
||||
.should("be.visible");
|
||||
@@ -57,16 +95,18 @@ describe("scenarios > table-editing", () => {
|
||||
cy.findByTestId("table-root").should("be.visible");
|
||||
});
|
||||
|
||||
cy.findByTestId("head-crumbs-container").findByText("People").click();
|
||||
cy.findByTestId("head-crumbs-container")
|
||||
.findByText(EDITABLE_SOURCE_TABLE_NAME_REGEX)
|
||||
.click();
|
||||
|
||||
cy.findByTestId("query-builder-root").should("be.visible");
|
||||
});
|
||||
|
||||
describe("db setting is disabled", () => {
|
||||
it("should not allow to open table data view by default", () => {
|
||||
setTableEditingEnabledForDB(SAMPLE_DB_ID, false);
|
||||
setTableEditingEnabledForDB(WRITABLE_DB_ID, false);
|
||||
openTableBrowser();
|
||||
getTableEditIcon("People").should("not.exist");
|
||||
getTableEditIcon(EDITABLE_SOURCE_TABLE_NAME_REGEX).should("not.exist");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,8 +116,13 @@ describe("scenarios > table-editing", () => {
|
||||
});
|
||||
|
||||
it("should not allow to open table data view", () => {
|
||||
cy.visit(`/browse/databases/${SAMPLE_DB_ID}/tables/${ORDERS_ID}/edit`);
|
||||
cy.findByTestId("edit-table-data-restricted").should("be.visible");
|
||||
H.getTableId({
|
||||
databaseId: WRITABLE_DB_ID,
|
||||
name: EDITABLE_SOURCE_TABLE_NAME,
|
||||
}).then((tableId) => {
|
||||
cy.visit(`/browse/databases/${WRITABLE_DB_ID}/tables/${tableId}/edit`);
|
||||
cy.findByTestId("edit-table-data-restricted").should("be.visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,26 +130,59 @@ describe("scenarios > table-editing", () => {
|
||||
beforeEach(() => {
|
||||
resetSnowplow();
|
||||
|
||||
cy.intercept("GET", `/api/table/${ORDERS_ID}/query_metadata`).as(
|
||||
"getOrdersTable",
|
||||
H.queryWritableDB(
|
||||
`CREATE TABLE IF NOT EXISTS ${INLINE_EDIT_TEST_TABLE_NAME} AS SELECT id, uuid, integer, tinyint, string, date, datetime, boolean FROM ${EDITABLE_SOURCE_TABLE_NAME}`,
|
||||
"postgres",
|
||||
);
|
||||
H.resyncDatabase({ dbId: WRITABLE_DB_ID });
|
||||
|
||||
cy.intercept("POST", "api/dataset").as("getTableDataQuery");
|
||||
cy.intercept("POST", "api/ee/action-v2/execute-bulk").as(
|
||||
"updateTableData",
|
||||
H.getTableId({
|
||||
databaseId: WRITABLE_DB_ID,
|
||||
name: INLINE_EDIT_TEST_TABLE_NAME,
|
||||
}).then((tableId) => {
|
||||
/*
|
||||
Adjusting "string" column type.
|
||||
By default it is set to "type/Category" due to only 2 unique values in test dataset.
|
||||
This causes to "string" column cell rendering a dropdown instead of an input field
|
||||
when editing.
|
||||
*/
|
||||
H.getFieldId({
|
||||
tableId,
|
||||
name: "string",
|
||||
}).then((fieldId) => {
|
||||
cy.request("PUT", `/api/field/${fieldId}`, {
|
||||
semantic_type: null,
|
||||
});
|
||||
});
|
||||
|
||||
cy.intercept("GET", `/api/table/${tableId}/query_metadata`).as(
|
||||
"getDataTable",
|
||||
);
|
||||
cy.intercept("POST", "api/dataset").as("getTableDataQuery");
|
||||
cy.intercept("POST", "api/ee/action-v2/execute-bulk").as(
|
||||
"updateTableData",
|
||||
);
|
||||
|
||||
cy.visit(`/browse/databases/${WRITABLE_DB_ID}/tables/${tableId}/edit`);
|
||||
|
||||
cy.wait("@getDataTable");
|
||||
cy.log("table data loaded");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
H.queryWritableDB(
|
||||
`DROP TABLE IF EXISTS ${INLINE_EDIT_TEST_TABLE_NAME}`,
|
||||
"postgres",
|
||||
);
|
||||
|
||||
cy.visit(`/browse/databases/${SAMPLE_DB_ID}/tables/${ORDERS_ID}/edit`);
|
||||
|
||||
cy.wait("@getOrdersTable");
|
||||
});
|
||||
|
||||
it("should allow to filter table data", () => {
|
||||
cy.findByTestId("edit-table-data-root").findByText("Filter").click();
|
||||
|
||||
H.popover().within(() => {
|
||||
cy.findByText("Discount").click();
|
||||
cy.findByText("Between").click();
|
||||
cy.findByText("ID").click();
|
||||
cy.findByText("Is").click();
|
||||
});
|
||||
|
||||
H.menu().findByText("Not empty").click();
|
||||
@@ -132,18 +210,18 @@ describe("scenarios > table-editing", () => {
|
||||
|
||||
it("should allow to sort table data", () => {
|
||||
cy.findByTestId("table-header").within(() => {
|
||||
cy.findByText("Quantity").click();
|
||||
cy.findByText(DEFAULT_FIELD).click();
|
||||
|
||||
cy.findAllByTestId("header-sort-indicator").should("have.length", 1);
|
||||
|
||||
cy.findByText("Quantity")
|
||||
cy.findByText(DEFAULT_FIELD)
|
||||
.closest("[role=button]")
|
||||
.findByLabelText("chevronup icon")
|
||||
.should("be.visible");
|
||||
|
||||
cy.findByText("Quantity").click();
|
||||
cy.findByText(DEFAULT_FIELD).click();
|
||||
|
||||
cy.findByText("Quantity")
|
||||
cy.findByText(DEFAULT_FIELD)
|
||||
.closest("[role=button]")
|
||||
.findByLabelText("chevrondown icon")
|
||||
.should("be.visible");
|
||||
@@ -154,19 +232,19 @@ describe("scenarios > table-editing", () => {
|
||||
cy.wait("@getTableDataQuery");
|
||||
|
||||
cy.findByTestId("table-header").within(() => {
|
||||
cy.findByText("Quantity")
|
||||
cy.findByText(DEFAULT_FIELD)
|
||||
.closest("[role=button]")
|
||||
.findByLabelText("chevrondown icon")
|
||||
.should("be.visible");
|
||||
|
||||
cy.findByText("Quantity").click();
|
||||
cy.findByText(DEFAULT_FIELD).click();
|
||||
|
||||
cy.findAllByTestId("header-sort-indicator").should("have.length", 0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow to view row details", () => {
|
||||
openEditRowModal(2);
|
||||
openEditRowModal(1);
|
||||
H.modal().within(() => {
|
||||
cy.get("@rowId").then((rowId) => {
|
||||
cy.findByTestId("ID-field-input").should("have.text", rowId);
|
||||
@@ -175,11 +253,11 @@ describe("scenarios > table-editing", () => {
|
||||
});
|
||||
|
||||
it("should allow to edit a row using a modal", () => {
|
||||
openEditRowModal(2);
|
||||
openEditRowModal(1);
|
||||
|
||||
H.modal().within(() => {
|
||||
cy.findByTestId("Quantity-field-input")
|
||||
.type("{backspace}{backspace}{backspace}123")
|
||||
cy.findByTestId("Integer-field-input")
|
||||
.type("{selectAll}{backspace}123")
|
||||
.blur();
|
||||
|
||||
cy.findByTestId("update-row-save-button").click();
|
||||
@@ -187,64 +265,53 @@ describe("scenarios > table-editing", () => {
|
||||
|
||||
cy.wait("@updateTableData").then(({ response }) => {
|
||||
expect(response?.body.outputs[0].op).to.equal("updated");
|
||||
expect(response?.body.outputs[0].row.QUANTITY).to.equal(123);
|
||||
expect(response?.body.outputs[0].row.integer).to.equal(123);
|
||||
});
|
||||
|
||||
H.modal().should("not.exist");
|
||||
|
||||
H.undoToast().findByText("Successfully updated").should("be.visible");
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "edit_data_record_modified",
|
||||
event_detail: "update",
|
||||
target_id: ORDERS_ID,
|
||||
triggered_from: "modal",
|
||||
result: "success",
|
||||
H.getTableId({
|
||||
databaseId: WRITABLE_DB_ID,
|
||||
name: INLINE_EDIT_TEST_TABLE_NAME,
|
||||
}).then((tableId) => {
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "edit_data_record_modified",
|
||||
event_detail: "update",
|
||||
target_id: tableId,
|
||||
triggered_from: "modal",
|
||||
result: "success",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("inline cell editing", () => {
|
||||
// we use randomized values here to allow multiple runs in one session (since non-changed values are not updated)
|
||||
const cases = [
|
||||
{
|
||||
tableId: PRODUCTS_ID,
|
||||
dataType: "string",
|
||||
column: "EAN",
|
||||
value: Math.floor(Math.random() * 100000)
|
||||
.toString()
|
||||
.padStart(13, "0"),
|
||||
dataType: "integer",
|
||||
column: "integer",
|
||||
value: Math.floor(Math.random() * 10000),
|
||||
},
|
||||
{
|
||||
tableId: ORDERS_ID,
|
||||
dataType: "number",
|
||||
column: "TAX",
|
||||
value: Math.floor(Math.random() * 1000),
|
||||
dataType: "tinyint",
|
||||
column: "tinyint",
|
||||
value: Math.floor(Math.random() * 256),
|
||||
},
|
||||
{
|
||||
dataType: "string",
|
||||
column: "string",
|
||||
value: "test",
|
||||
},
|
||||
// TODO: fix flaky test due to timezone issues (runs locally but fails on CI)
|
||||
// {
|
||||
// tableId: PEOPLE_ID,
|
||||
// dataType: "date",
|
||||
// column: "BIRTH_DATE",
|
||||
// value: dayjs(
|
||||
// new Date(
|
||||
// Date.now() -
|
||||
// Math.floor(Math.random() * 1000 * 60 * 60 * 24 * 365),
|
||||
// ),
|
||||
// ).format("YYYY-MM-DD"),
|
||||
// },
|
||||
];
|
||||
|
||||
cases.forEach(({ dataType, column, value, tableId }) => {
|
||||
cases.forEach(({ dataType, column, value }) => {
|
||||
it(`should allow to edit a cell with type ${dataType}`, () => {
|
||||
cy.visit(`/browse/databases/${SAMPLE_DB_ID}/tables/${tableId}/edit`);
|
||||
|
||||
// Locate the table and the specific cell to edit
|
||||
cy.findByTestId("table-root")
|
||||
.findAllByRole("row")
|
||||
.eq(1) // Select the second row (index 1)
|
||||
.within(() => {
|
||||
cy.get(`[data-column-id='${column}']`)
|
||||
.as("targetCell")
|
||||
.click({ scrollBehavior: "center" }); // Activate inline editing
|
||||
cy.get(`[data-column-id='${column}']`).as("targetCell").click(); // Activate inline editing
|
||||
});
|
||||
|
||||
// Edit the cell value
|
||||
@@ -266,29 +333,59 @@ describe("scenarios > table-editing", () => {
|
||||
}
|
||||
});
|
||||
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "edit_data_record_modified",
|
||||
event_detail: "update",
|
||||
target_id: tableId,
|
||||
triggered_from: "inline",
|
||||
result: "success",
|
||||
H.getTableId({
|
||||
databaseId: WRITABLE_DB_ID,
|
||||
name: INLINE_EDIT_TEST_TABLE_NAME,
|
||||
}).then((tableId) => {
|
||||
H.expectUnstructuredSnowplowEvent({
|
||||
event: "edit_data_record_modified",
|
||||
event_detail: "update",
|
||||
target_id: tableId,
|
||||
triggered_from: "inline",
|
||||
result: "success",
|
||||
});
|
||||
});
|
||||
|
||||
H.undoToast().findByText("Successfully updated").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow to edit a cell with datetime type", () => {
|
||||
cy.visit(`/browse/databases/${SAMPLE_DB_ID}/tables/${ORDERS_ID}/edit`);
|
||||
|
||||
// Locate the table and the specific cell to edit
|
||||
it("should allow to edit a cell with date type", () => {
|
||||
cy.findByTestId("table-root")
|
||||
.findAllByRole("row")
|
||||
.eq(1) // Select the second row (index 1)
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get("[data-column-id='CREATED_AT']").as("targetCell").click({
|
||||
cy.get("[data-column-id='date']").as("targetCell").click({
|
||||
scrollBehavior: false,
|
||||
}); // Activate inline editing
|
||||
});
|
||||
});
|
||||
|
||||
const day = Math.floor(Math.random() * 10) + 10; // 10-20
|
||||
|
||||
H.popover().within(() => {
|
||||
cy.findByRole("button", { name: `${day} February 2020` }).click();
|
||||
});
|
||||
|
||||
cy.wait("@updateTableData").then(({ response }) => {
|
||||
const targetDate = dayjs(new Date(2020, 1, day)).format("YYYY-MM-DD");
|
||||
const responseDate = dayjs(response?.body.outputs[0].row.date).format(
|
||||
"YYYY-MM-DD",
|
||||
);
|
||||
|
||||
expect(responseDate).to.equal(targetDate);
|
||||
});
|
||||
|
||||
H.undoToast().findByText("Successfully updated").should("be.visible");
|
||||
});
|
||||
|
||||
it("should allow to edit a cell with datetime type", () => {
|
||||
cy.findByTestId("table-root")
|
||||
.findAllByRole("row")
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get("[data-column-id='datetime']").as("targetCell").click({
|
||||
scrollBehavior: false,
|
||||
});
|
||||
});
|
||||
|
||||
const day = Math.floor(Math.random() * 10) + 10; // 10-20
|
||||
@@ -296,9 +393,11 @@ describe("scenarios > table-editing", () => {
|
||||
const minute = Math.floor(Math.random() * 60);
|
||||
|
||||
H.popover().within(() => {
|
||||
cy.findByRole("button", { name: `${day} May 2024` }).click();
|
||||
cy.findByRole("button", { name: `${day} February 2020` }).click();
|
||||
cy.findAllByRole("spinbutton").eq(0).type(hour.toString());
|
||||
cy.findAllByRole("spinbutton").eq(1).type(minute.toString());
|
||||
cy.get('select[data-am-pm="true"]').as("ampmSelect");
|
||||
cy.get("@ampmSelect").select("AM");
|
||||
// It's safe to click the last button because we're in the popover
|
||||
// eslint-disable-next-line no-unsafe-element-filtering
|
||||
cy.findAllByRole("button").last().click();
|
||||
@@ -306,11 +405,11 @@ describe("scenarios > table-editing", () => {
|
||||
|
||||
cy.wait("@updateTableData").then(({ response, request }) => {
|
||||
const targetDate = dayjs(
|
||||
new Date(2024, 4, day, hour, minute, 0),
|
||||
new Date(2020, 1, day, hour, minute, 0),
|
||||
).format("YYYY-MM-DDTHH:mm:ss");
|
||||
|
||||
const requestDate = request.body.params.CREATED_AT;
|
||||
const responseDate = response?.body.outputs[0].row.CREATED_AT;
|
||||
const requestDate = request.body.params.datetime;
|
||||
const responseDate = response?.body.outputs[0].row.datetime;
|
||||
|
||||
// Check if request date matches the response date.
|
||||
// In theory FE should preserve the initial input date timezone offset (might be based on the CI environment)
|
||||
@@ -324,24 +423,18 @@ describe("scenarios > table-editing", () => {
|
||||
});
|
||||
|
||||
it("should allow to edit a cell with select type", () => {
|
||||
cy.visit(`/browse/databases/${SAMPLE_DB_ID}/tables/${ORDERS_ID}/edit`);
|
||||
|
||||
cy.findByTestId("table-root")
|
||||
.findAllByRole("row")
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get("[data-column-id='USER_ID']").as("targetCell").click({
|
||||
cy.get("[data-column-id='boolean']").as("targetCell").click({
|
||||
scrollBehavior: false,
|
||||
});
|
||||
});
|
||||
|
||||
H.popover().within(() => {
|
||||
const randomIndex = Math.floor(2 + Math.random() * 18);
|
||||
|
||||
cy.findAllByRole("option")
|
||||
.should("have.length", 20)
|
||||
.eq(randomIndex)
|
||||
.click();
|
||||
// 3: true, false, null
|
||||
cy.findAllByRole("option").should("have.length", 3).eq(0).click();
|
||||
|
||||
cy.wait("@updateTableData").then(({ response }) => {
|
||||
expect(response?.body.outputs[0].op).to.equal("updated");
|
||||
@@ -352,15 +445,11 @@ describe("scenarios > table-editing", () => {
|
||||
});
|
||||
|
||||
it("should not allow to edit PK cells", () => {
|
||||
cy.visit(
|
||||
`/browse/databases/${SAMPLE_DB_ID}/tables/${PRODUCTS_ID}/edit`,
|
||||
);
|
||||
|
||||
cy.findByTestId("table-root")
|
||||
.findAllByRole("row")
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get("[data-column-id='ID']")
|
||||
cy.get("[data-column-id='id']")
|
||||
.as("targetCell")
|
||||
.click({
|
||||
scrollBehavior: false,
|
||||
@@ -371,24 +460,20 @@ describe("scenarios > table-editing", () => {
|
||||
});
|
||||
|
||||
it("should handle errors", () => {
|
||||
cy.visit(
|
||||
`/browse/databases/${SAMPLE_DB_ID}/tables/${PRODUCTS_ID}/edit`,
|
||||
);
|
||||
|
||||
cy.findByTestId("table-root")
|
||||
.findAllByRole("row")
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get("[data-column-id='EAN']")
|
||||
cy.get("[data-column-id='tinyint']")
|
||||
.as("targetCell")
|
||||
.click({
|
||||
scrollBehavior: false,
|
||||
})
|
||||
.find("input")
|
||||
// Should be more than 13 characters to trigger the error
|
||||
.type("{selectAll}{backspace}aaaaaaaaaaaaaaaaaaaaaaaaa", {
|
||||
// Entering a big number into tinyint column
|
||||
.type("{selectAll}{backspace}9999999", {
|
||||
scrollBehavior: false,
|
||||
}) // Enter the new value
|
||||
})
|
||||
.blur(); // Trigger the save action by blurring the input
|
||||
});
|
||||
|
||||
@@ -502,8 +587,9 @@ describe("scenarios > table-editing", () => {
|
||||
|
||||
describe("table editing bugs", () => {
|
||||
it("WRK-907: should not allow to create new values for FK fields", () => {
|
||||
setTableEditingEnabledForDB(SAMPLE_DB_ID);
|
||||
cy.intercept("GET", `/api/table/${ORDERS_ID}/query_metadata`).as(
|
||||
"getOrdersTable",
|
||||
"getDataTable",
|
||||
);
|
||||
const NON_EXISTING_ID = "999999";
|
||||
cy.intercept(
|
||||
@@ -517,7 +603,7 @@ describe("scenarios > table-editing", () => {
|
||||
|
||||
cy.visit(`/browse/databases/${SAMPLE_DB_ID}/tables/${ORDERS_ID}/edit`);
|
||||
|
||||
cy.wait("@getOrdersTable");
|
||||
cy.wait("@getDataTable");
|
||||
|
||||
cy.findByTestId("new-record-button").click();
|
||||
|
||||
@@ -543,7 +629,7 @@ describe("scenarios > table-editing", () => {
|
||||
cy.findByTestId("head-crumbs-container")
|
||||
.findByText("Sample Database")
|
||||
.click();
|
||||
openTableEdit("Products");
|
||||
openTableEdit(new RegExp("Products", "i"));
|
||||
|
||||
cy.wait("@getProductsTable");
|
||||
|
||||
@@ -569,13 +655,13 @@ function setTableEditingEnabledForDB(dbId: number, enabled = true) {
|
||||
});
|
||||
}
|
||||
|
||||
function openTableBrowser(databaseName: string = "Sample Database") {
|
||||
function openTableBrowser(databaseName: string = "Writable Postgres12") {
|
||||
cy.visit("/browse/databases");
|
||||
cy.wait("@getDatabases");
|
||||
cy.findByTestId("database-browser").findByText(databaseName).click();
|
||||
}
|
||||
|
||||
function getTableEditIcon(tableName: string) {
|
||||
function getTableEditIcon(tableName: RegExp) {
|
||||
return cy
|
||||
.findByTestId("browse-schemas")
|
||||
.contains(tableName)
|
||||
@@ -583,30 +669,26 @@ function getTableEditIcon(tableName: string) {
|
||||
.findByTestId("edit-table-icon");
|
||||
}
|
||||
|
||||
function openTableEdit(tableName: string) {
|
||||
function openTableEdit(tableName: RegExp) {
|
||||
getTableEditIcon(tableName).click();
|
||||
}
|
||||
|
||||
function openEditRowModal(rowIndex: number) {
|
||||
cy.findByTestId("table-root")
|
||||
.findAllByRole("row")
|
||||
.should("have.length.gte", 10)
|
||||
.should("have.length.gte", 2)
|
||||
.eq(rowIndex)
|
||||
.within(() => {
|
||||
cy.findAllByTestId("cell-data")
|
||||
.first()
|
||||
.eq(0)
|
||||
.invoke("text")
|
||||
.then((text) => {
|
||||
cy.wrap(text).as("rowId");
|
||||
});
|
||||
|
||||
cy.findAllByTestId("cell-data").first().realHover({
|
||||
scrollBehavior: false,
|
||||
});
|
||||
cy.findAllByTestId("cell-data").first().realHover();
|
||||
|
||||
cy.findByTestId("row-edit-icon").click({
|
||||
scrollBehavior: false,
|
||||
});
|
||||
cy.findByTestId("row-edit-icon").click();
|
||||
});
|
||||
|
||||
H.modal().findByText("Edit record").should("be.visible");
|
||||
|
||||
@@ -645,10 +645,10 @@ describe("issue 21665", () => {
|
||||
H.editDashboard();
|
||||
});
|
||||
|
||||
H.findDashCardAction(
|
||||
H.getDashboardCard(0),
|
||||
"Visualize another way",
|
||||
).click();
|
||||
H.getDashboardCard(0)
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Visualize another way")
|
||||
.click();
|
||||
H.modal().within(() => {
|
||||
H.switchToAddMoreData();
|
||||
H.selectDataset(Q2.name);
|
||||
|
||||
@@ -1276,7 +1276,10 @@ describe("issue 52339", () => {
|
||||
});
|
||||
|
||||
H.editDashboard();
|
||||
H.findDashCardAction(H.getDashboardCard(0), "Click behavior").click();
|
||||
H.getDashboardCard(0)
|
||||
.realHover({ scrollBehavior: "bottom" })
|
||||
.findByLabelText("Click behavior")
|
||||
.click();
|
||||
|
||||
H.sidebar().within(() => {
|
||||
cy.findByText("Go to a custom destination").click();
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
[metabase-enterprise.semantic-search.api]
|
||||
[metabase-enterprise.serialization.api]
|
||||
[metabase-enterprise.stale.api]
|
||||
[metabase-enterprise.support-access-grants.api]
|
||||
[metabase-enterprise.transforms-python.api]
|
||||
[metabase-enterprise.transforms.api]
|
||||
[metabase-enterprise.upload-management.api]
|
||||
@@ -72,6 +73,7 @@
|
||||
:upload-management (deferred-tru "Upload Management")
|
||||
:database-routing (deferred-tru "Database Routing")
|
||||
:cloud-custom-smtp (deferred-tru "Custom SMTP")
|
||||
:support-users (deferred-tru "Support Users")
|
||||
:transforms-python (deferred-tru "Transforms Python")})
|
||||
|
||||
(defn- premium-handler [handler required-feature]
|
||||
@@ -128,6 +130,7 @@
|
||||
"/semantic-search" (premium-handler metabase-enterprise.semantic-search.api/routes :semantic-search)
|
||||
"/serialization" (premium-handler metabase-enterprise.serialization.api/routes :serialization)
|
||||
"/stale" (premium-handler metabase-enterprise.stale.api/routes :collection-cleanup)
|
||||
"/support-access-grant" (premium-handler metabase-enterprise.support-access-grants.api/routes :support-users)
|
||||
"/transform" (premium-handler metabase-enterprise.transforms.api/routes :transforms)
|
||||
"/transform-job" (premium-handler metabase-enterprise.transforms.api/transform-job-routes :transforms)
|
||||
"/transform-tag" (premium-handler metabase-enterprise.transforms.api/transform-tag-routes :transforms)
|
||||
|
||||
@@ -22,5 +22,6 @@
|
||||
[metabase-enterprise.semantic-search.init]
|
||||
[metabase-enterprise.sso.init]
|
||||
[metabase-enterprise.stale.init]
|
||||
[metabase-enterprise.support-access-grants.init]
|
||||
[metabase-enterprise.transforms-python.init]
|
||||
[metabase-enterprise.transforms.init]))
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
[metabase-enterprise.documents.core :as documents]
|
||||
[metabase-enterprise.metabot-v3.tools.util :as metabot-v3.tools.u]
|
||||
[metabase.api.common :as api]
|
||||
;; TODO (Cam 10/10/25) -- update MetaBot to use Lib + MBQL 5
|
||||
^{:clj-kondo/ignore [:discouraged-namespace]} [metabase.legacy-mbql.normalize :as mbql.normalize]
|
||||
[metabase.lib-be.core :as lib-be]
|
||||
[metabase.lib.core :as lib]
|
||||
[metabase.lib.metadata :as lib.metadata]
|
||||
@@ -378,23 +376,23 @@
|
||||
{:output "invalid document_id"}))
|
||||
|
||||
(defn- execute-query
|
||||
[query-id legacy-query]
|
||||
(let [legacy-query (mbql.normalize/normalize legacy-query)
|
||||
[query-id query-input]
|
||||
(let [normalized-query (lib-be/normalize-query query-input)
|
||||
field-id-prefix (metabot-v3.tools.u/query-field-id-prefix query-id)
|
||||
database-id (:database legacy-query)
|
||||
database-id (:database normalized-query)
|
||||
_ (api/read-check :model/Database database-id)
|
||||
mp (lib-be/application-database-metadata-provider database-id)
|
||||
query (lib/query mp legacy-query)
|
||||
query (lib/query mp normalized-query)
|
||||
returned-cols (lib/returned-columns query)]
|
||||
{:type :query
|
||||
:query-id query-id
|
||||
:query legacy-query
|
||||
:query normalized-query
|
||||
:result-columns (into []
|
||||
(map-indexed #(metabot-v3.tools.u/->result-column query %2 %1 field-id-prefix))
|
||||
returned-cols)}))
|
||||
|
||||
(defn get-query-details
|
||||
"Get the details of a (legacy) query."
|
||||
"Get the details of a query (supports both MBQL v4 and v5)."
|
||||
[{:keys [query]}]
|
||||
(lib-be/with-metadata-provider-cache
|
||||
{:structured-output (execute-query (u/generate-nano-id) query)}))
|
||||
|
||||
@@ -350,13 +350,14 @@
|
||||
(let [{:keys [table-id query query-id report-id]} data-source
|
||||
model-id (lib.util/legacy-string-table-id->card-id table-id)
|
||||
handle-query (fn [query query-id]
|
||||
(let [database-id (:database query)
|
||||
(let [normalized-query (lib-be/normalize-query query)
|
||||
database-id (:database normalized-query)
|
||||
_ (api/read-check :model/Database database-id)
|
||||
mp (lib-be/application-database-metadata-provider database-id)]
|
||||
[(if query-id
|
||||
(metabot-v3.tools.u/query-field-id-prefix query-id)
|
||||
metabot-v3.tools.u/any-prefix-pattern)
|
||||
(-> (lib/query mp query) lib/append-stage)]))]
|
||||
(-> (lib/query mp normalized-query) lib/append-stage)]))]
|
||||
(cond
|
||||
model-id
|
||||
(if-let [model-query (metabot-v3.tools.u/card-query model-id)]
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"Secret"
|
||||
"SemanticSearchTokenTracking"
|
||||
"Session"
|
||||
"SupportAccessGrantLog"
|
||||
"TaskHistory"
|
||||
"TransformJobRun"
|
||||
"TransformRun"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user