If you’re developing a new feature or just generally need to get at some of the application data on the frontend, Entity Loaders are going to be your friend. They abstract away calling the API, handling loading and error state, cache previously loaded objects, invalidating the cache (in some cases) and let you easily perform updates, or create new items.
There are two ways to use loaders, either as React “render prop” components or as React component class decorators (“higher order components”).
In this example we’re going to load information about a specific database for a new page.
import React from "react";
import Databases from "metabase/entities/databases";
@Databases.load({ id: 4 })
class MyNewPage extends React.Component {
render() {
const { database } = this.props;
return (
<div>
<h1>{database.name}</h1>
</div>
);
}
}
This example uses a class decorator to ask for and then display a database with ID 4. If you instead wanted to use a render prop component your code would look like this.
import React from "react";
import Databases from "metabase/entities/databases";
class MyNewPage extends React.Component {
render() {
const { database } = this.props;
return (
<div>
<Databases.Loader id={4}>
{({ database }) => <h1>{database.name}</h1>}
</Databases.Loader>
</div>
);
}
}
Now you most likely don’t just want to display just one static item so for cases where some of the values you might need will be dynamic you can use a function to get at the props and return the value you need. If you’re using the component approach you can just pass props as you would normally for dynamic values.
@Databases.load({
id: (state, props) => props.params.databaseId
}))
Loading a list of items is as easy as applying the loadList
decorator:
import React from "react";
import Users from "metabase/entities/users";
@Users.loadList()
class MyList extends React.Component {
render() {
const { users } = this.props;
return <div>{users.map(u => u.first_name)}</div>;
}
}
Similar to the object loader’s id
argument you can also pass a query
object (if the API supports it):
@Users.loadList({
query: (state, props) => ({ archived: props.showArchivedOnly })
})
By default both EntityObject
and EntityList
loaders will handle loading state for you by using LoadingAndErrorWrapper
under the hood. If for some reason you want to handle loading on your own you can disable this behavior by setting loadingAndErrorWrapper: false
.
If you pass wrapped: true
to a loader then the object or objects will be wrapped with helper classes that let you do things like user.getName()
, user.delete()
, or user.update({ name: "new name" )
. Actions are automatically already bound to dispatch
.
This may incur a performance penalty if there are many objects.
Any additional selectors and actions defined in the entities’ objectSelectors
or objectActions
will appear as the wrapped object’s methods.
You can also use the Redux actions and selectors directly, for example, dispatch(Users.actions.loadList())
and Users.selectors.getList(state)
.
Metabase includes a comprehensive custom React and redux-form
based form library. It also integrates with Metabase’s Entities system.
The core React component of the system is metabase/containers/Form
.
Form definitions can be provided in two different ways, with a JavaScript-based form definition object, or inline React <FormField>
elements.
Pass a form definition to the form
prop:
<Form
form={{
fields: [
{
name: "email",
placeholder: "bob@metabase.com",
validate: validate.required().email(),
},
{
name: "password",
type: "password",
validate: validate.required().passwordComplexity(),
},
],
}}
onSubmit={values => alert(JSON.stringify(values))}
/>
If <Form>
lacks any children elements then it will use the metabase/components/StandardLayout
component to provide a default form layout.
The schema for this object is defined in Form.jsx
.
fields
and initial
(for initial values) can be provided directly or as functions that dynamically compute them based on the current form state and additional props.
{
"fields": (values) => [
{ name: "a", type: }
initial
, normalize
, and validate
properties can be provided at the top-level, or per-field. They can also be provided as props to the <Form>
and <FormField>
components For definitions can be provided
Form definition can also be provided via <FormField>
React elements (exported from the same metabase/containers/Form
module), which will also serve as the layout (this uses the metabase/components/CustomLayout
)
import Form, { FormField, FormFooter } from "metabase/containers/Form";
<Form onSubmit={values => alert(JSON.stringify(values))}>
<FormField
name="email"
placeholder="bob@metabase.com"
validate={validate.required()}
/>
<FormField
name="password"
type="password"
validate={validate.required().passwordComplexity()}
/>
<FormFooter />
</Form>;
You can also provide both the form
prop and children <FormField>
elements, in which case the form
prop will be merged with the <FormField>
s’ props.
Built-in field type
s are defined in metabase/components/form/FormWidget. You can also provide a React component as the type
property.
You might have noticed the validate
API above. These are simple chainable validators compatible with this form library, and are provided by metabase/lib/validate
. You can add additional validators in that file.
Server-side validation and other errors are returned in a standard format understood by <Form>
.
Field-level errors:
{ "errors": { "field_name": "error message" } }
Top-level errors:
{ "message": "error message" }
The Form library is integrated with Metabase’s Entities system (via the EntityForm
component), so that every entity includes a Form
component that can be used like so:
<Users.Form />
which uses the default form
defined on the entity, e.x.
const Users = createEntity({
name: "users",
path: "/api/user",
form: {
fields: [
{ name: "email" }
]
}
// Alternatively, it will take the first form from the `forms` object:
// form: {
// default: {
// fields: [
// { name: "email" }
// ]
// }
// }
}
You can also explicitly pass a different form object:
<Users.Form form={Users.forms.passwordReset} />
Entity Form
s will automatically be wired up to the correct REST endpoints for creating or updating entities.
If you need to load an object first, they compose nicely with the Entities Loader
render prop:
<Users.Load id={props.params.userId}>
{({ user }) => <Users.Form user={user} />}
</Users.Load>
Or higher-order component:
Users.load({ id: (state, props) => props.params.userId })(Users.Form);
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.
For the most part we follow the Airbnb React/JSX Style Guide. ESLint and Prettier should take care of a majority of the rules in the Airbnb style guide. Exceptions will be noted in this document.
value
and onChange
. Controls that have options (e.x. Radio
, Select
) usually take an options
array of objects with name
and value
properties.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
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)class MyComponent extends React.Component {
constructor(props) {
super(props);
// NO:
this.handleChange = this.handleChange.bind(this);
}
// YES:
handleChange = e => {
// ...
};
// no need to bind:
componentDidMount() {}
render() {
return <input onChange={this.handleChange} />;
}
}
styled-components
and “atomic” / “utility-first” CSS classes.grid-styled
’s Box
and Flex
components over raw div
.className
prop to the root element of the component. It can be merged with additional classes using the cx
function from the classnames
package.flex
class, but it shouldn’t include margin or flex-full
, full
, absolute
, spread
, etc. Those should be passed via className
or style
props by the consumer of the component, which knows how the component should be positioned within itself.render
method returns to what is in the state
or props
of 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>
);
}
import
s should be ordered by type, typically:
react
is often first, along with things like ttags
, underscore
, classnames
, etc)metabase/components/*
, metabase/containers/*
, etc)metabase/*/components/*
etc)lib
s, entities
, services
, Redux files, etcconst
to let
(and never use var
). Only use let
if you have a specific reason to reassign the identifier (note: this now enforced by ESLint)this
from the parent scope (there should almost never be a need to do const self = this;
etc), but usually even if you don’t (e.x. array.map(x => x * 2)
).function
declarations for top-level functions, including React function components. The exception is for one-liner functions that return a value// YES:
function MyComponent(props) {
return <div>...</div>;
}
// NO:
const MyComponent = props => {
return <div>...</div>;
};
// YES:
const double = n => n * 2;
// ALSO OK:
function double(n) {
return n * 2;
}
Array
methods over underscore
’s. We polyfill all ES6 features. Use Underscore for things that aren’t implemented natively.async
/await
over using promise.then(...)
etc directly.prettier
sometimes formats them with extra whitespace.
const { display_name } = column;
this
directly, e.x. const { foo } = this.props; const { bar } = this.state;
instead of const { props: { foo }, state: { bar } } = this;
switch
statement (when evaluation is more complex, like when branching on which React component to return):// don't do this
const foo = str == 'a' ? 123 : str === 'b' ? 456 : str === 'c' : 789 : 0;
// do this
const foo = {
a: 123,
b: 456,
c: 789,
}[str] || 0;
// or do this
switch (str) {
case 'a':
return <ComponentA />;
case 'b':
return <ComponentB />;
case 'c':
return <ComponentC />;
case 'd':
default:
return <ComponentD />;
}
If your nested ternaries are in the form of predicates evaluating to booleans, prefer an if/if-else/else
statement that is siloed to a separate, pure function:
const foo = getFoo(a, b);
function getFoo(a, b, c) {
if (a.includes("foo")) {
return 123;
} else if (a === b) {
return 456;
} else {
return 0;
}
}
// don't do this--the comment is redundant
// get the native permissions for this db
const nativePermissions = getNativePermissions(perms, groupId, {
databaseId: database.id,
});
// don't add TODOs -- they quickly become forgotten cruft
isSearchable(): boolean {
// TODO: this should return the thing instead
return this.isString();
}
// this is acceptable -- the implementer explains a not-obvious edge case of a third party library
// foo-lib seems to return undefined/NaN occasionally, which breaks things
if (isNaN(x) || isNaN(y)) {
return;
}
// 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) {
// ...
}
// do this
const MIN_HEIGHT = 200;
// also acceptable
const OBJECT_CONFIG_CONSTANT = {
camelCaseProps: "are OK",
abc: 123,
};
// this makes it harder to search for Widget
import Foo from "./Widget";
// do this to enforce using the proper name
import { Widget } from "./Widget";
// 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, () => ...);
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:
// 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);
When dealing with business logic you don’t want to be concerned with the specifics of the language. Instead of writing const query = new Question(card).query();
which entails instantiating a new Question
instance and calling a query
method on said instance, you should introduce a function like getQueryFromCard(card)
so that implementers can avoid thinking about what goes into getting a query
value from a card.
.Button.Button--primary {
color: -var(--color-brand);
}
.text-brand {
color: -var(--color-brand);
}
const Foo = () => <div className="text-brand" />;
const Foo = ({ color ) =>
<div style={{ color: color }} />
:local(.primary) {
color: -var(--color-brand);
}
import style from "./Foo.css";
const Foo = () => <div className={style.primary} />;
import styled from "@emotion/styled";
const FooWrapper = styled.div`
color: ${props => props.color};
`;
const Bar = ({ color }) => <Foo color={color} />;
e.x.
import styled from "@emotion/styled";
import { color } from "styled-system";
const Foo = styled.div`
${color}
`;
const Bar = ({ color }) => <Foo color={color} />;
Popovers are popups or modals.
In Metabase core, they are visually responsive: they appear above or below the element that triggers their appearance. Their height is automatically calculated to make them fit on the screen.
Ask a question
Custom question
Pick your starting data
is a <Popover />
.Sample Database
People
Here, clicking on the following will open <Popover />
components:
Columns
(right-hand side of section labeled Data
)Data
Add filters to narrow your answers
Pick the metric you want to see
Pick a column to group by
Sort
icon with arrows pointing up and down above Visualize
button