These are the docs for the Metabase master branch. Some features documented here may not yet be available in the current release. Check out the docs for the current stable version, Metabase v0.58.
Frontend
Typescript
We are aggressively pressing toward having our entire frontend codebase in typescript. If you find yourself working on a javascript file, consider making a small initial PR to convert it to typescript before making further changes (and making it a functional component if you’ve happened upon a class component 😱).
Avoid typecasts, and avoid use of any at all costs.
Most of our widely-used types are located in metabase-types. Generally they break down as
- API Types: should reflect the data we receive from the backend API
- Store Types: should reflect the shape of data in the redux store
In cases where types are only used by some local components, they should be defined locally in a types.ts file. (see e.g. DataGrid types)
Redux
We use Redux for global state. You will find domain-specific actions, reducers, and selectors generally grouped with the components that use them. e.g:
Use Redux global state as little as possible, and wherever possible, prefer local component state, or narrowly-defined context.
Data Fetching
We use RTK Query for data fetching and caching. All API endpoints are defined in metabase/api. These should be properly typed, and should not depend on any other application code or contain business logic outside of invalidating tags within the API.
Entity Loaders
Legacy code uses metabase/entities to load data. These entity loaders are deprecated, and wherever possible, use of RTK query apis should be preferred.
UI Library
Our UI library in metabase/ui is built on top of Mantine. You should almost always prefer using mantine components above anything else in the codebase. We have a lot of customization on top of mantine, but no business logic should leak into the metabase/ui folder. It should remain purely display-level. All components added to the UI library must have a storybook file to demonstrate usage.
Styling
You’ll note several styling patterns in the codebase. Currently you should prefer
- Mantine Style Props for most simple styling
- CSS Modules for more complex styling
Other patterns, such as emotion styled components and global utility CSS classes are deprecated and should not be used for new code. Where convenient, please updated deprecated styling patterns to the updated ones.
Familiarize yourself with Mantine’s Layout components. You can often save a lot of CSS with built-in components like Center and SimpleGrid
Colors
You may not define new bespoke colors or use any color literals like black or white. Colors can be referenced directly in components using mantine color props like c="text-primary" or in css modules using variables color: var(--mb-color-text-primary); Using these colors ensures consistent visual design and user experience across both dark and light modes. The full palette is defined in metabase/lib/colors.ts.
Unit testing
All code must be tested. Unit tests should always be preferred over end-to-end tests; they are much faster to run and debug, even if they take a little longer to write initially.
Unit tests should be placed with the components they are testing.
Setting up unit tests in metabase can be quite complex due to all the data mocking that must be done for even simple components. We have many helpers to make this faster, for app context providers, data mocking, and API mocking. (also note: LLMs are quite good at absorbing existing mocking patterns and helping set up mock data)
Setup pattern
We use the following pattern to setup test components:
import React from "react";
import userEvent from "@testing-library/user-event";
import { Collection } from "metabase-types/api";
import { createMockCollection } from "metabase-types/api/mocks";
import { renderWithProviders, screen } from "__support__/ui";
import CollectionHeader from "./CollectionHeader";
interface SetupOpts {
collection: Collection;
}
const setup = ({ collection }: SetupOpts) => {
const onUpdateCollection = jest.fn();
renderWithProviders(
<CollectionHeader
collection={collection}
onUpdateCollection={onUpdateCollection}
/>,
);
return { onUpdateCollection };
};
describe("CollectionHeader", () => {
it("should be able to update the name of the collection", () => {
const collection = createMockCollection({
name: "Old name",
});
const { onUpdateCollection } = setup({
collection,
});
await userEvent.clear(screen.getByDisplayValue("Old name"));
await userEvent.type(screen.getByPlaceholderText("Add title"), "New title");
await userEvent.tab();
expect(onUpdateCollection).toHaveBeenCalledWith({
...collection,
name: "New name",
});
});
});
Key points:
setupfunctionrenderWithProvidersadds providers used by the app, includingredux
Request mocking
We use fetch-mock to mock requests:
import fetchMock from "fetch-mock";
import { setupCollectionsEndpoints } from "__support__/server-mocks";
interface SetupOpts {
collections: Collection[];
}
const setup = ({ collections }: SetupOpts) => {
setupCollectionsEndpoints({ collections });
// renderWithProviders and other setup
};
describe("Component", () => {
it("renders correctly", async () => {
setup();
expect(await screen.findByText("Collection")).toBeInTheDocument();
});
});
Key points:
setupfunction- Call helpers from
__support__/server-mocksto setup endpoints for your data
Localization
The frontend uses ttag to localize strings. All user-facing strings must be tagged, and automation will handle the rest. It is often helpful to add context for strings, especially when interpolating parameters
<div>{t`This is a user-facing string`}</div>
<div>
{c("{0} is a number of engineers").t`${numEngineers} engineers at metabase`}
</div>
As much as possible, try to translate phrases, rather than words, to make localization across languages with different structures possible.
// ❌
const output = name + t` is going to the ` + place + t`with` + anotherName;
// ✅
const output = t`${name} is going to the ${place} with ${anotherName}`;
// 😍
const output = c("{0} and {2} are people's names, and {1} is a place")
.t`${name} is going to the ${place} with ${anotherName}`;
Style Guide
The first rule of frontend style, is we want to avoid talking about frontend style. Wherever possible, style-level considerations should be encapsulated in lint rules.
Prettier + Eslint
We use Prettier to format our JavaScript code, and it is enforced by CI. We recommend setting your editor to “format on save”. You can also format code using yarn prettier, and verify it has been formatted correctly using yarn lint-prettier.
We use ESLint to enforce additional rules. It is integrated into the Webpack build, or you can manually run yarn lint-eslint to check. Nitpicky things like import order, spacing, etc. are all enforced by eslint.
Miscellaneous notes on coding style
- Avoid creating separate
ContainerandComponentsdirectories. In some cases it makes sense to separate components for data loading and viewing, but this is easy to do in a single file. - Avoid nested ternaries as they often result in code that is difficult to read. If you have logical branches in your code that are dependent on the value of a string, prefer using an object as a map to multiple values (when evaluation is trivial) or a
switchstatement. Where logic is complex, we often use ts-pattern over a set of if/else statement. - Be conservative with what comments you add to the codebase. Ideally, code should be written in such a way that it explains itself clearly. When it does not, you should first try rewriting the code. If for whatever reason you are unable to write something clearly, add a comment to explain the “why”.
- Avoid breaking JSX up into separate method calls within a single component. Prefer inlining JSX so that you can better see what the relation is of the JSX a
rendermethod returns to what is in thestateorpropsof a component. By inlining JSX you’ll also get a better sense of what parts should and should not be separate components.
// don't do this
render () {
return (
<div>
{this.renderThing1()}
{this.renderThing2()}
{this.state.thing3Needed && this.renderThing3()}
</div>
);
}
// do this
render () {
return (
<div>
<button onClick={this.toggleThing3Needed}>toggle</button>
<Thing2 randomProp={this.props.foo} />
{this.state.thing3Needed && <Thing3 randomProp2={this.state.bar} />}
</div>
);
}
- Avoid complex logical expressions inside of if statements. Often extracting logic to a well-named boolean variable can make code much easier to read.
// don't do this
if (typeof children === "string" && children.split(/\n/g).length > 1) {
// ...
}
// do this
const isMultilineText =
typeof children === "string" && children.split(/\n/g).length > 1;
if (isMultilineText) {
// ...
}
- Use ALL_CAPS for constants
// do this
const MIN_HEIGHT = 200;
// also acceptable
const OBJECT_CONFIG_CONSTANT = {
camelCaseProps: "are OK",
abc: 123,
};
- Avoid magic strings and numbers
// don't do this
const options = _.times(10, () => ...);
// do this in a constants file
export const MAX_NUM_OPTIONS = 10;
const options = _.times(MAX_NUM_OPTIONS, () => ...);
-
prefer declarative over imperative patterns where possible. You should write code with other engineers in mind as other engineers will spend more time reading than you spend writing (and re-writing). Code is more readable when it tells the computer “what to do” versus “how to do.” Avoid imperative patterns like for loops: ```javascript // don’t do this let foo = []; for (let i = 0; i < list.length; i++) { if (list[i].bar === false) { continue; }
foo.push(list[i]); }
// do this const foo = list.filter((entry) => entry.bar !== false); ```
Read docs for other versions of Metabase.