Фронтенд¶
Загрузчики сущностей¶
Если вы разрабатываете новую функцию или просто хотите получить некоторые данные приложения во фронтенде, вам помогут загрузчики сущностей. Они абстрагируются от вызова API, обработки состояния загрузки и ошибки, кэширования ранее загруженных объектов, аннулирования кэша (в некоторых случаях) и позволяют легко выполнять обновления или создавать новые элементы.
Хорошее применение загрузчиков сущностей¶
Мне нужно получить определённый X (пользователь, база данных и т. д.) и отобразить его.
Мне нужно получить список X (базы данных, вопросы и т. д.) и отобразить его.
В настоящее время доступны объекты:¶
запросы, дашборды, пульсы;
коллекции;
базы данных, таблицы, поля, сегменты, метрики;
пользователи, группы.
Полный текущий список сущностей перечислен в исходном коде Metabase: frontend/src/metabase/entities.
Есть два способа использования загрузчиков: либо в качестве компонентов React «render prop», либо в качестве декораторов классов компонентов React («компоненты более высокого уровня»).
Загрузка объекта¶
В этом примере мы собираемся загрузить информацию о конкретной базе данных для новой страницы.
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>
);
}
}
В этом примере используется декоратор класса для запроса и последующего отображения базы данных с идентификатором 4. Если вместо этого вы хотите использовать компонент рендеринга, ваш код будет выглядеть следующим образом.
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>
);
}
}
Теперь вы, скорее всего, не хотите просто отображать только один статический элемент. Поэтому в случаях, когда некоторые из значений, которые вам могут понадобиться, будут динамическими, вы можете использовать функцию, чтобы вернуть нужное вам значение. Если вы используете компонентный подход, вы можете просто передавать свойства, как обычно для динамических значений.
@Databases.load({
id: (state, props) => props.params.databaseId
}))
Загрузка списка¶
Загрузить список элементов так же просто, как применить декоратор loadList
:
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>;
}
}
Подобно аргументу id
загрузчика объектов, вы также можете передать объект query
(если API поддерживает):
@Users.loadList({
query: (state, props) => ({ archived: props.showArchivedOnly })
})
Контроль загрузки и ошибок¶
По умолчанию загрузчики EntityObject
и EntityList
будут обрабатывать состояние загрузки, используя под капотом LoadingAndErrorWrapper
. Если по какой-то причине вы хотите обрабатывать загрузку самостоятельно, вы можете отключить это поведение, установив loadingAndErrorWrapper: false
.
Обёрнутые объекты¶
Если вы передадите wrapped: true
загрузчику, то объект или объекты будут обёрнуты вспомогательными классами, которые позволят вам делать такие вещи, как user.getName()
, user.delete()
или user.update( {имя: "новое имя")
. Действия уже автоматически привязаны к dispatch
.
Это может привести к снижению производительности, если объектов много.
Любые дополнительные селекторы и действия, определённые в объектах objectSelectors
или objectActions
, будут отображаться как методы обернутого объекта.
Расширенное использование¶
Вы также можете напрямую использовать действия и селекторы Redux, например, dispatch(Users.actions.loadList())
и Users.selectors.getList(state)
.
Руководство по стилю¶
Настройка Prettier¶
Мы используем Prettier для форматирования нашего кода JavaScript, и это обеспечивается CI. Мы рекомендуем настроить ваш редактор на «форматирование при сохранении». Вы также можете форматировать код, используя yarn prettier
, и проверять правильность его форматирования, используя yarn lint-prettier
.
Мы используем ESLint для обеспечения соблюдения дополнительных правил. Он интегрирован в сборку Webpack, также вы можете вручную запустить yarn lint-eslint
для проверки.
Руководство по стилю React и JSX¶
По большей части мы следуем Руководству по стилю Airbnb React/JSX. ESLint и Prettier должны позаботиться о большинстве правил руководства по стилю Airbnb. Исключения будут отмечены в этом документе.
Предпочитайте функциональные компоненты над компонентами класса
Избегайте создания новых компонентов в папке
containers
, поскольку этот подход устарел. Вместо этого храните как подключенные, так и просмотренные компоненты в папкеcomponents
для более унифицированной и эффективной организации. Если подключенный компонент существенно увеличивается в размерах, и вам необходимо извлечь компонент представления, выберите использование суффиксаView
.Для компонентов управления обычно используются
value
иonChange
. Элементы управления с параметрами (например,Радио
,Выбрать
) обычно принимают массив объектовoptions
со свойствамиname
иvalue
.Компоненты с именами вроде
FooModal
иFooPopover
обычно относятся к content модального/всплывающего окна, которое следует использовать внутриModal
/ModalWithTrigger
илиPopover
/PopoverWithTrigger
Компоненты с именами типа
FooWidget
обычно включаютFooPopover
внутриPopoverWithTrigger
с каким-то триггерным элементом, частоFooName
Используйте свойства экземпляра стрелочной функции, если вам нужно связать метод в классе (вместо
this.method = this.method.bind(this);
в конструкторе), но только если функцию необходимо связать (например, если вы передаете его как свойство компоненту React)
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
и atomic/utility-first классы CSS.Предпочитайте использование
grid-style
для компонентов Box и Flex, вместо div.Компоненты обычно должны передавать своё свойство
className
корневому элементу компонента. Его можно объединить с дополнительными классами, используя функциюcx
из пакетаclassnames
.Чтобы сделать компоненты более пригодными для повторного использования, компонент должен применять классы или стили только к корневому элементу компонента, что влияет на макет/стиль его собственного содержимого, но не на макет самого себя в его родительском контейнере. Например, он может включать отступы или класс
flex
, но не должен включать margin илиflex-full
,full
,absolute
,spread
и т. д. Они должны передаваться черезclassName
или свойстваstyle
потребителем компонента, который знает, как компонент должен быть расположен внутри себя.Избегайте разбиения JSX на отдельные вызовы методов внутри одного компонента. Предпочитайте встраивание JSX, чтобы вы могли лучше видеть, какое отношение имеет JSX, метод
render
возвращает к тому, что находится вstate
илиprops
компонента. Встраивая JSX, вы также лучше понимаете, какие части должны и не должны быть отдельными компонентами.
// 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>
);
}
Соглашения JavaScript¶
import
ы должны быть упорядочены по типу, как правило:внешние библиотеки (на первом месте часто стоят
react
, а также такие вещи, какttags
,underscore
,classnames
и т. д.)Компоненты и контейнеры React верхнего уровня Glarus BI (
metabase/components/*
,metabase/containers/*
и т. д.)Компоненты и контейнеры React Glarus BI, специфичные для этой части приложения (
metabase/*/components/*
и т. д.)Библиотеки Glarus BI, сущности, сервисы, файлы Redux и т. д.
Предпочитайте
const
вместоlet
(и никогда не используйтеvar
). Используйтеlet
только в том случае, если у вас есть конкретная причина для переназначения идентификатора (примечание: теперь это применяется ESLint)Предпочитайте стрелочные функции для встроенных функций, особенно если вам нужно ссылаться на
this
из родительской области. (почти никогда не должно быть необходимости делатьconst self = this;
и т. д.), но обычно, даже если вы этого не делаете (например,array.map(x => x * 2)
).Предпочитайте объявления
function
для функций верхнего уровня, включая компоненты функций React. Исключение составляют однострочные функции, которые возвращают значение
// 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
, а неunderscore
. Мы полифилим все функции ES6. Используйтеunderscore
для вещей, которые не реализованы изначально.Предпочитайте
async
/await
вместо использованияpromise.then(...)
и т.д. напрямую.Вы можете использовать деструктуризацию присваивания или деструктуризацию аргументов, но избегайте глубоко вложенной деструктуризации, поскольку она может быть трудной для чтения, а
prettier
иногда форматирует с дополнительными пробелами.избегать деструктуризации свойств объектов, подобных «сущностям», например. не делайте
const { display_name } = column;
не деструктурируйте
this
напрямую, напримерconst { foo } = this.props; const {bar} = this.state;
вместоconst {props: {foo}, state: {bar}} = this;
Избегайте вложенных тернарных кодов, так как они часто приводят к трудночитаемому коду. Если в вашем коде есть логические ветвления, которые зависят от значения строки, предпочтите использовать объект в качестве карты для нескольких значений (когда вычисление тривиально) или оператор
switch
(когда вычисление более сложное, например, при определении, какой компонент React вернуть):
// 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/if-else/else
, который изолирован от отдельной чистой функции:
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;
}
}
Будьте осторожны с комментариями, которые вы добавляете в кодовую базу. Комментарии не следует использовать в качестве напоминаний или задач — запишите их, создав новую задачу на Github. В идеале код должен быть написан таким образом, чтобы он ясно объяснял сам себя. Если это не так, вы должны сначала попробовать переписать код. Если по какой-либо причине вы не можете написать что-то четко, добавьте комментарий, объясняющий «почему».
// 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;
}
Избегайте сложных логических выражений внутри операторов if.
// 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) {
// ...
}
Используйте ALL_CAPS для констант
// 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, () => ...);
Написать декларативный код¶
Вам следует писать код, думая о других инженерах, поскольку другие инженеры будут тратить больше времени на чтение, чем вы тратите на написание (и переписывание). Код более удобочитаем, когда он говорит компьютеру «что делать», а не «как делать». Избегайте императивных шаблонов, таких как циклы for:
// 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);
Имея дело с бизнес-логикой, вы не хотите беспокоиться о специфике языка. Вместо того, чтобы писать const query = new Question(card).query();
, что влечет за собой создание экземпляра нового экземпляра Question
и вызов метода query
для указанного экземпляра, вы должны ввести функцию, подобную getQueryFromCard(card)
чтобы разработчики могли не думать о том, что нужно для получения значения «запроса» с карты.
Стили компонентов Tree Rings¶
Классический/глобальный CSS с селекторами стилей BEM (устарело)¶
.Button.Button--primary {
color: -var(--color-brand);
}
Atomic/utility CSS (не рекомендуется)¶
.text-brand {
color: -var(--color-brand);
}
const Foo = () => <div className="text-brand" />;
Встроенный стиль (не рекомендуется)¶
const Foo = ({ color ) =>
<div style={%raw%}{{ color: color }}{%endraw%} />
CSS-модули (устаревшие)¶
:local(.primary) {
color: -var(--color-brand);
}
import style from "./Foo.css";
const Foo = () => <div className={style.primary} />;
Эмоции¶
import styled from "@emotion/styled";
const Foo = styled.div`
color: ${props => props.color};
`;
const Bar = ({ color }) => <Foo color={color} />;
Popovers¶
Popovers — это всплывающие окна или модальные окна.
В ядре Glarus BI они появляются над или под элементом, который инициирует их появление. Их высота рассчитывается автоматически, чтобы они поместились на экране.
Где найти Popovers в пользовательском интерфейсе¶
При создании пользовательских запросов¶
Войдя в систему, нажмите
Новый
, а потомЗапрос
.👀 Окно выбора, которое автоматически откроется рядом с
Выберите исходные данные
- это<Popover />
.Выберите
Sample Database
, если база данных ещё не выбрана.Выберите любую из таблиц, например,
Люди
.
Здесь нажатие на следующее откроет компоненты <Popover />
:
Выбрать столбцы
(стрелка справа от выбора столбца в разделеДанные
).Серый значок сетки с + под разделом с надписью
Данные
.Добавить фильтры, чтобы сузить круг ответов
.Выберите показатель, который вы хотите видеть
.Выберите столбец для группировки
.Значок
Сортировка
со стрелками вверх и вниз над кнопкойВизуализация
.
Модульное тестирование (unit testing)¶
Определение шаблона¶
Мы используем следующий шаблон для модульного тестирования компонентов:
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,
});
userEvent.clear(screen.getByDisplayValue("Old name"));
userEvent.type(screen.getByPlaceholderText("Add title"), "New title");
userEvent.tab();
expect(onUpdateCollection).toHaveBeenCalledWith({
...collection,
name: "New name",
});
});
});
Ключевые моменты:
Функция
setup
.renderWithProviders
добавляет провайдеров, которые используются приложением, включаяredux
.
Mock запросов¶
Мы используем fetch-mock
для mock запросов:
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();
});
});
Ключевые моменты:
Функция
setup
.Вызвать helpers из
__support__/server-mocks
для определения методов для ваших данных.Вызвать
nock.clearAll
для удаления всех mocks.