# Фронтенд
## Загрузчики сущностей
Если вы разрабатываете новую функцию или просто хотите получить некоторые данные приложения во фронтенде, вам помогут загрузчики сущностей. Они абстрагируются от вызова API, обработки состояния загрузки и ошибки, кэширования ранее загруженных объектов, аннулирования кэша (в некоторых случаях) и позволяют легко выполнять обновления или создавать новые элементы.
### Хорошее применение загрузчиков сущностей
- Мне нужно получить определённый X (пользователь, база данных и т. д.) и отобразить его.
- Мне нужно получить список X (базы данных, вопросы и т. д.) и отобразить его.
### В настоящее время доступны объекты:
- запросы, дашборды, пульсы;
- коллекции;
- базы данных, таблицы, поля, сегменты, метрики;
- пользователи, группы.
Полный текущий список сущностей перечислен в исходном коде Metabase: [frontend/src/metabase/entities](https://github.com/metabase/metabase/tree/master/frontend/src/metabase/entities).
Есть два способа использования загрузчиков: либо в качестве компонентов React "render prop", либо в качестве декораторов классов компонентов React ("компоненты более высокого уровня").
### Загрузка объекта
В этом примере мы собираемся загрузить информацию о конкретной базе данных для новой страницы.
```js
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 (
{database.name}
);
}
}
```
В этом примере используется декоратор класса для запроса и последующего отображения базы данных с идентификатором 4. Если вместо этого вы хотите использовать компонент рендеринга, ваш код будет выглядеть следующим образом.
```js
import React from "react";
import Databases from "metabase/entities/databases";
class MyNewPage extends React.Component {
render() {
const { database } = this.props;
return (
{({ database }) =>
{database.name}
}
);
}
}
```
Теперь вы, скорее всего, не хотите просто отображать только один статический элемент. Поэтому в случаях, когда некоторые из значений, которые вам могут понадобиться, будут динамическими, вы можете использовать функцию, чтобы вернуть нужное вам значение. Если вы используете компонентный подход, вы можете просто передавать свойства, как обычно для динамических значений.
```js
@Databases.load({
id: (state, props) => props.params.databaseId
}))
```
## Загрузка списка
Загрузить список элементов так же просто, как применить декоратор `loadList`:
```js
import React from "react";
import Users from "metabase/entities/users";
@Users.loadList()
class MyList extends React.Component {
render() {
const { users } = this.props;
return
{users.map(u => u.first_name)}
;
}
}
```
Подобно аргументу `id` загрузчика объектов, вы также можете передать объект `query` (если API поддерживает):
```js
@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](https://prettier.io/) для форматирования нашего кода JavaScript, и это обеспечивается CI. Мы рекомендуем настроить ваш редактор на «форматирование при сохранении». Вы также можете форматировать код, используя `yarn prettier`, и проверять правильность его форматирования, используя `yarn lint-prettier`.
Мы используем ESLint для обеспечения соблюдения дополнительных правил. Он интегрирован в сборку Webpack, также вы можете вручную запустить `yarn lint-eslint` для проверки.
### Руководство по стилю React и JSX
По большей части мы следуем [Руководству по стилю Airbnb React/JSX](https://github.com/airbnb/javascript/tree/master/react). ESLint и Prettier должны позаботиться о большинстве правил руководства по стилю Airbnb. Исключения будут отмечены в этом документе.
- Предпочитайте [функциональные компоненты над компонентами класса](https://reactjs.org/docs/components-and-props.html#function-and-class-components)
- Избегайте создания новых компонентов в папке `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)
```javascript
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 ;
}
}
```
- Для стилизации компонентов мы в настоящее время используем смесь `styled-components` и [atomic/utility-first классы CSS](https://github.com/metabase/metabase/tree/master/frontend/src/metabase/css/core).
- Предпочитайте использование `grid-style` для компонентов Box и Flex, вместо div.
- Компоненты обычно должны передавать своё свойство `className` корневому элементу компонента. Его можно объединить с дополнительными классами, используя функцию `cx` из пакета `classnames`.
- Чтобы сделать компоненты более пригодными для повторного использования, компонент должен применять классы или стили только к корневому элементу компонента, что влияет на макет/стиль его собственного содержимого, но _не_ на макет самого себя в его родительском контейнере. Например, он может включать отступы или класс `flex`, но не должен включать margin или `flex-full`, `full`, `absolute`, `spread` и т. д. Они должны передаваться через `className` или свойства `style` потребителем компонента, который знает, как компонент должен быть расположен внутри себя.
- Избегайте разбиения JSX на отдельные вызовы методов внутри одного компонента. Предпочитайте встраивание JSX, чтобы вы могли лучше видеть, какое отношение имеет JSX, метод `render` возвращает к тому, что находится в `state` или `props` компонента. Встраивая JSX, вы также лучше понимаете, какие части должны и не должны быть отдельными компонентами.
```javascript
// don't do this
render () {
return (
);
}
```
### Соглашения JavaScript
- `import`ы должны быть упорядочены по типу, как правило:
1. внешние библиотеки (на первом месте часто стоят `react`, а также такие вещи, как `ttags`, `underscore`, `classnames` и т. д.)
2. Компоненты и контейнеры React верхнего уровня Glarus BI (`metabase/components/*`, `metabase/containers/*` и т. д.)
3. Компоненты и контейнеры React Glarus BI, специфичные для этой части приложения (`metabase/*/components/*` и т. д.)
4. Библиотеки Glarus BI, сущности, сервисы, файлы Redux и т. д.
- Предпочитайте `const` вместо `let` (и никогда не используйте `var`). Используйте `let` только в том случае, если у вас есть конкретная причина для переназначения идентификатора (примечание: теперь это применяется ESLint)
- Предпочитайте [стрелочные функции](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) для встроенных функций, особенно если вам нужно ссылаться на `this` из родительской области. (почти никогда не должно быть необходимости делать `const self = this;` и т. д.), но обычно, даже если вы этого не делаете (например, `array.map(x => x * 2)`).
- Предпочитайте объявления `function` для функций верхнего уровня, включая компоненты функций React. Исключение составляют однострочные функции, которые возвращают значение
```javascript
// YES:
function MyComponent(props) {
return
...
;
}
// NO:
const MyComponent = props => {
return
...
;
};
// YES:
const double = n => n * 2;
// ALSO OK:
function double(n) {
return n * 2;
}
```
- Предпочитайте нативные методы `Array`, а не `underscore`. Мы полифилим все функции ES6. Используйте `underscore` для вещей, которые не реализованы изначально.
- Предпочитайте [`async`/`await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) вместо использования `promise.then(...)` и т.д. напрямую.
- Вы можете использовать деструктуризацию присваивания или деструктуризацию аргументов, но избегайте глубоко вложенной деструктуризации, поскольку она может быть трудной для чтения, а `prettier` иногда форматирует с дополнительными пробелами.
- избегать деструктуризации свойств объектов, подобных "сущностям", например. не делайте `const { display_name } = column;`
- не деструктурируйте `this` напрямую, например `const { foo } = this.props; const {bar} = this.state;` вместо `const {props: {foo}, state: {bar}} = this;`
- Избегайте вложенных тернарных кодов, так как они часто приводят к трудночитаемому коду. Если в вашем коде есть логические ветвления, которые зависят от значения строки, предпочтите использовать объект в качестве карты для нескольких значений (когда вычисление тривиально) или оператор `switch` (когда вычисление более сложное, например, при определении, какой компонент React вернуть):
```javascript
// 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 ;
case 'b':
return ;
case 'c':
return ;
case 'd':
default:
return ;
}
```
Если ваши вложенные тернарные операции имеют форму предикатов, оценивающих логические значения, предпочтите оператор `if/if-else/else`, который изолирован от отдельной чистой функции:
```javascript
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. В идеале код должен быть написан таким образом, чтобы он ясно объяснял сам себя. Если это не так, вы должны сначала попробовать переписать код. Если по какой-либо причине вы не можете написать что-то четко, добавьте комментарий, объясняющий «почему».
```javascript
// 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.
```javascript
// 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 для констант
```javascript
// do this
const MIN_HEIGHT = 200;
// also acceptable
const OBJECT_CONFIG_CONSTANT = {
camelCaseProps: "are OK",
abc: 123,
};
```
- Предпочитать именованный экспорт экспорту по умолчанию
```javascript
// this makes it harder to search for Widget
import Foo from "./Widget";
// do this to enforce using the proper name
import { Widget } from "./Widget";
```
- Избегайте магических строк и чисел
```javascript
// 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:
```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);
```
Имея дело с бизнес-логикой, вы не хотите беспокоиться о специфике языка. Вместо того, чтобы писать `const query = new Question(card).query();`, что влечет за собой создание экземпляра нового экземпляра `Question` и вызов метода `query` для указанного экземпляра, вы должны ввести функцию, подобную `getQueryFromCard(card)` чтобы разработчики могли не думать о том, что нужно для получения значения «запроса» с карты.
## Стили компонентов Tree Rings
### Классический/глобальный CSS с селекторами стилей BEM (устарело)
```css
.Button.Button--primary {
color: -var(--color-brand);
}
```
### Atomic/utility CSS (не рекомендуется)
```css
.text-brand {
color: -var(--color-brand);
}
```
```javascript
const Foo = () => ;
```
### Встроенный стиль (не рекомендуется)
```javascript
const Foo = ({ color ) =>
```
### CSS-модули (устаревшие)
```css
:local(.primary) {
color: -var(--color-brand);
}
```
```javascript
import style from "./Foo.css";
const Foo = () => ;
```
### [Эмоции](https://emotion.sh/)
```javascript
import styled from "@emotion/styled";
const Foo = styled.div`
color: ${props => props.color};
`;
const Bar = ({ color }) => ;
```
## Popovers
Popovers — это всплывающие окна или модальные окна.
В ядре Glarus BI они появляются над или под элементом, который инициирует их появление. Их высота рассчитывается автоматически, чтобы они поместились на экране.
### Где найти Popovers в пользовательском интерфейсе
#### При создании пользовательских запросов
1. Войдя в систему, нажмите `Новый`, а потом `Запрос`.
2. 👀 Окно выбора, которое автоматически откроется рядом с`Выберите исходные данные` - это ``.
3. Выберите `Sample Database`, если база данных ещё не выбрана.
4. Выберите любую из таблиц, например, `Люди`.
Здесь нажатие на следующее откроет компоненты ``:
- `Выбрать столбцы` (стрелка справа от выбора столбца в разделе `Данные`).
- Серый значок сетки с + под разделом с надписью `Данные`.
- `Добавить фильтры, чтобы сузить круг ответов`.
- `Выберите показатель, который вы хотите видеть`.
- `Выберите столбец для группировки`.
- Значок `Сортировка` со стрелками вверх и вниз над кнопкой `Визуализация`.
## Модульное тестирование (unit testing)
### Определение шаблона
Мы используем следующий шаблон для модульного тестирования компонентов:
```tsx
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(
,
);
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`](https://www.npmjs.com/package/fetch-mock) для mock запросов:
```tsx
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.