GrDashboard

  • dashboard
  • Дашборд
  • API · 15 props

Берут, когда раскладку определяет пользователь.

import { GrDashboard } from '@feugene/granularity-dashboard/components/GrDashboard'

Обзор

Когда брать

  • раскладку определяет пользователь — аналитическая панель, рабочий стол, сводка под конкретную роль: набор и расположение виджетов принадлежат человеку, а не дизайнеру экрана;
  • раскладка обязана пережить перезагрузку — useDashboardLayout кладёт её в localStorage или на сервер через интерфейс хранилища;
  • на ноутбуке и планшете нужно разное — раскладка хранится на каждый брейкпоинт, недостающая выводится из соседней сверху;
  • виджеты разного размера — minW/minH/maxW/maxH объявляет сам виджет, потому что «этот график ниже трёх строк не читается» знает только он.

Когда взять другое

НужноБерите
Раскладка фиксирована дизайномCSS Grid и GrCard
Две области, границу двигает пользовательGrSplitter
Порядок элементов в одном спискеGrSortableList
Разделы, переключаемые вкладкамиGrTabs
Строки таблицы с изменяемым порядком колонокGrDataTable
Откуда виджет берёт данныеgranularity-datasource
Чем виджет рисует содержимоеgranularity-charts / granularity-chrono

Дашборд — не способ разложить статичный экран. Если пользователь ничего не двигает, сетка виджетов даёт лишний слой состояния, хранилище и режим редактирования там, где хватило бы CSS Grid.

Примеры 2 у каждого — живое демо и его код

  • Финансовая сводка, которая помнит раскладку

    Включите «Edit», подвигайте и растяните виджеты, затем перезагрузите страницу: useDashboardLayout сохраняет раскладку на каждый брейкпоинт и восстанавливает её после монтирования, так что сервер и первый рендер совпадают. Разметка лишь называет виджеты по item-id; minW/minH задают сами виджеты, а брейкпоинты меряются по контейнеру — двенадцать колонок от 540 px, две ниже. «Reset» возвращает исходную раскладку.

    Revenue, October
    €48,210+12,4%
    Overdue
    €6,940-18,2%
    Cash in bank
    €182,400+3,1%
    Overdue invoices
    • Northwind TradersINV-0418 · 6 days late€1,284.50
    • Contoso LtdINV-0415 · 9 days late€432.00
    • Fabrikam InteriorsINV-0409 · 17 days late€3,210.00
    • Litware IncINV-0402 · 24 days late€2,013.50
    Cash runway
    14 monthsAt September's burn of €13,000 a month, before new contracts.
  • Перенос виджетов между двумя досками

    Перетащите виджет за ручку через край одной доски в другую. transferable разрешает виджету уйти, droppable — прийти: это разные права. Источник убирает виджет сам, приёмник эмитит item-drop с ячейкой, брейкпоинтом и своими опциями раскладки, а страница кладёт его через addItem — туда, где его показывала подложка.

    My board
    My queue
    12 open · 3 waiting on customer
    SLA breaches
    2 today · both on chat
    Satisfaction
    94% · 312 ratings this week
    Team board
    Team backlog
    87 open · oldest 4 days
    On call
    Lena until 18:00, then Omar

API порождается из пакета · разойтись не может

Props

PropTypeпо умолчаниюОписание
breakpointsGrDashboardBreakpoints | undefinedGR_DASHBOARD_BREAKPOINTSПороги ширины контейнера, от которых начинается брейкпоинт.
colsnumber | GrDashboardCols | undefinedGR_DASHBOARD_COLSЧисло колонок: мапой по брейкпоинтам или одним числом на всё.
initialBreakpointstring | undefined"lg"Брейкпоинт первого рендера. Ширины контейнера на сервере нет, поэтому и сервер, и первый клиентский рендер берут его — уточнение приходит после монтирования, и гидрация не расходится.
rowHeightnumber | undefinedundefinedВысота строки сетки, px.
gapnumber | undefinedundefinedЗазор между виджетами, px.
modeGrDashboardMode | undefinedundefined`view` — сетка только показывает; `edit` — появляются ручки.
draggableboolean | undefinedundefinedВиджеты переносятся за ручку в режиме `edit`; виджет сужает правило своим пропом. Не задан — из `GrConfigProvider`, иначе `true`.
resizableboolean | undefinedundefinedВиджеты растягиваются уголком в режиме `edit`; виджет сужает правило своим пропом. Не задан — из `GrConfigProvider`, иначе `true`.
compactGrDashboardCompaction | undefinedundefinedУплотнение после переноса: `vertical` тянет виджеты вверх, `horizontal` — влево, `both` — в обе стороны, `none` оставляет там, куда положили. Не задан — из `GrConfigProvider`, иначе `vertical`.
preventCollisionboolean | undefinedundefinedСтолкновение отменяет перемещение, а не толкает соседей.
lazyboolean | undefinedundefinedСодержимое виджета монтируется по попаданию в окно.
droppableboolean | undefinedundefinedСетка принимает виджеты, перетаскиваемые из каталога. Работает только в `mode="edit"`. Проп нужен там, где сеток на странице несколько, а принимать должна одна.
transferableboolean | undefinedundefinedИз сетки можно утащить виджет в соседний дашборд. Отдельно от `droppable`: принимать и отдавать — разные разрешения. Архивный дашборд принимает виджеты, но своих не отдаёт.
layoutобязательныйv-modelGrDashboardResponsiveLayout—Раскладка на каждый брейкпоинт. `v-model:layout`.

Слоты

SlotTypeОписание
defaultanyВиджеты `GrDashboardItem` — по одному на запись раскладки.
emptyanyЧто показать, пока в раскладке нет ни одного виджета.

События

EventСигнатураОписание
itemMove
(event: "itemMove", id: string, from: GrDashboardItemLayout,(event: "itemMove", id: string, from: GrDashboardItemLayout, to: GrDashboardItemLayout): void
—
itemResize
(event: "itemResize", id: string, from: GrDashboardItemLayou(event: "itemResize", id: string, from: GrDashboardItemLayout, to: GrDashboardItemLayout): void
—
itemAutoResize
(event: "itemAutoResize", id: string, from: GrDashboardItemL(event: "itemAutoResize", id: string, from: GrDashboardItemLayout, to: GrDashboardItemLayout): void
—
layoutChange(event: "layoutChange", value: GrDashboardLayout, breakpoint: string): void—
breakpointChange(event: "breakpointChange", breakpoint: string, cols: number): void—
update:layout(event: "update:layout", value: GrDashboardResponsiveLayout): void—
itemSettings(event: "itemSettings", id: string): void—
itemDrop(event: "itemDrop", event: GrDashboardDropEvent): void—
itemTransferOut(event: "itemTransferOut", id: string, transfer: GrDashboardTransfer): void—

Методы

MethodsСигнатураОписание
breakpointstringТекущий брейкпоинт: до первого замера контейнера — `initialBreakpoint`.

Типы

Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.

Импорт
import type { GrDashboardConfigurableProps, GrDashboardMode, GrDashboardItemBounds } from '@feugene/granularity-dashboard/components/GrDashboard'

Файлdist/types/components/GrDashboard/config.d.ts

grDashboardConfigconst

group — раскладка общих чанков: модули из components/shared/ сборка кладёт в dist/groups/GrDashboardFrame/shared/, а не в безымянный dist/chunks/, и по имени группы видно, чьё это общее. На состав CSS это не влияет: классы извлекаются по графу бандла, и общий чанк достаётся каждому компоненту, который до него дотягивается.

const grDashboardConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrDashboard">;

Файлdist/types/components/GrDashboard/defaults.d.ts

GrDashboardConfigurablePropsinterface

Пропы GrDashboard, настраиваемые глобально через <GrConfigProvider :component-defaults="{ GrDashboard: { … } }">. Только оформление раскладки и её правила. Сама раскладка глобальной настройкой быть не может по определению — это состояние экземпляра. mode исключение осмысленное: «вся админка только для просмотра» — ровно та настройка, которую задают один раз на приложение.

interface GrDashboardConfigurableProps {
    rowHeight: number;
    gap: number;
    mode: GrDashboardMode;
    draggable: boolean;
    resizable: boolean;
    compact: GrDashboardCompaction;
    preventCollision: boolean;
    lazy: boolean;
    droppable: boolean;
    /** Из сетки можно утащить виджет в соседний дашборд. */
    transferable: boolean;
}

Файлdist/types/components/GrDashboard/grDashboardStyles.d.ts

GrDashboardModetype
type GrDashboardMode = 'view' | 'edit';

Файлdist/types/components/shared/context.d.ts

GrDashboardItemBoundsinterface

Границы, объявленные самим виджетом. Раскладка приходит из приложения и про виджет знает только координаты; «этот график ниже трёх строк не читается» знает виджет. Поэтому границы едут не в раскладке, а регистрацией — и применяются к записи поверх неё.

interface GrDashboardItemBounds {
    minW?: number;
    minH?: number;
    maxW?: number;
    maxH?: number;
    static?: boolean;
    /**
     * Запрет переноса и растягивания для одного виджета.
     *
     * Не задан — действует правило сетки. Отличие от `static`: тот про раскладку
     * («не двигается и соседям не даёт себя двигать»), а эти два — про
     * интерфейс, и каждый снимается по отдельности.
     */
    draggable?: boolean;
    resizable?: boolean;
    /** Имя виджета для ручек и объявлений. */
    title?: string;
}
GrDashboardDropEventinterface

Всё, что нужно, чтобы положить брошенный виджет ровно туда, где стояла подложка. breakpoint и options — не полнота ради полноты: без первого приложение пишет в lg, когда пользователь на sm, а со своими опциями считает раскладку не так, как её посчитала сетка, и получает не то, что было показано.

interface GrDashboardDropEvent {
    transfer: GrDashboardTransfer;
    /** Ячейка левого верхнего угла — та же, что показывала подложка. */
    cell: GrDashboardCell;
    breakpoint: GrDashboardBreakpoint;
    options: GrDashboardMoveOptions;
}
GrDashboardContextinterface
interface GrDashboardContext {
    mode: ComputedRef<GrDashboardMode>;
    cols: ComputedRef<number>;
    draggable: ComputedRef<boolean>;
    resizable: ComputedRef<boolean>;
    lazy: ComputedRef<boolean>;
    layout: ComputedRef<GrDashboardLayout>;
    order: ComputedRef<string[]>;
    itemFor: (id: string) => GrDashboardItemLayout | undefined;
GR_DASHBOARD_KEYconst
const GR_DASHBOARD_KEY: InjectionKey<GrDashboardContext>;
useGrDashboardContextfunction

Контекст дашборда, если виджет стоит внутри него. null — легальный ответ: GrDashboardPalette и GrDashboardToolbar работают и снаружи, через собственные пропы и эмиты.

function useGrDashboardContext(): GrDashboardContext | null;

Файлdist/types/components/GrDashboard/safelist.d.ts

grDashboardSafelistconst
const grDashboardSafelist: string[];

Файлdist/types/components/GrDashboard/GrDashboard.vue.d.ts

GrDashboardPropsinterface
interface GrDashboardProps {
    /** Раскладка на каждый брейкпоинт. `v-model:layout`. */
    layout: GrDashboardResponsiveLayout;
    /** Пороги ширины контейнера, от которых начинается брейкпоинт. */
    breakpoints?: GrDashboardBreakpoints;
    /** Число колонок: мапой по брейкпоинтам или одним числом на всё. */
    cols?: GrDashboardCols | number;
    /**
     * Брейкпоинт первого рендера. Ширины контейнера на сервере нет, поэтому и
     * сервер, и первый клиентский рендер берут его — уточнение приходит после
     * монтирования, и гидрация не расходится.
     */
    initialBreakpoint?: GrDashboardBreakpoint;
    /** Высота строки сетки, px. */
    rowHeight?: number;
    /** Зазор между виджетами, px. */
    gap?: number;
    /** `view` — сетка только показывает; `edit` — появляются ручки. */
    mode?: GrDashboardMode;
    /**
     * Виджеты переносятся за ручку в режиме `edit`; виджет сужает правило своим пропом. Не задан —
     * из `GrConfigProvider`, иначе `true`.
     */
    draggable?: boolean;
    /**
     * Виджеты растягиваются уголком в режиме `edit`; виджет сужает правило своим пропом. Не задан —
     * из `GrConfigProvider`, иначе `true`.
     */
    resizable?: boolean;
    /**
     * Уплотнение после переноса: `vertical` тянет виджеты вверх, `horizontal` — влево, `both` —
     * в обе стороны, `none` оставляет там, куда положили. Не задан — из `GrConfigProvider`, иначе
     * `vertical`.
     */
    compact?: GrDashboardCompaction;
    /** Столкновение отменяет перемещение, а не толкает соседей. */
    preventCollision?: boolean;
    /** Содержимое виджета монтируется по попаданию в окно. */
    lazy?: boolean;
    /**
     * Сетка принимает виджеты, перетаскиваемые из каталога.
     *
     * Работает только в `mode="edit"`. Проп нужен там, где сеток на странице
     * несколько, а принимать должна одна.
     */
    droppable?: boolean;
    /**
     * Из сетки можно утащить виджет в соседний дашборд.
     *
     * Отдельно от `droppable`: принимать и отдавать — разные разрешения. Архивный
     * дашборд принимает виджеты, но своих не отдаёт.
     */
    transferable?: boolean;
    /** Имя группы-сетки. Не задано — из локали («Dashboard»). */
    ariaLabel?: string;
}
GrDashboardEmitsinterface
interface GrDashboardEmits {
    /**
     * Раскладка изменилась (`v-model:layout`): приходит целиком, с новой записью текущего
     * брейкпоинта.
     */
    (e: 'update:layout', value: GrDashboardResponsiveLayout): void;
    /**
     * Раскладка текущего брейкпоинта изменилась — вместе с самим брейкпоинтом; приходит следом
     * за `update:layout`.
     */
    (e: 'layoutChange', value: GrDashboardLayout, breakpoint: GrDashboardBreakpoint): void;
    /** Виджет переехал — указателем или стрелками: прежняя и новая запись раскладки. */
    (e: 'itemMove', id: string, from: GrDashboardItemLayout, to: GrDashboardItemLayout): void;
    /**
     * Пользователь изменил размер виджета — уголком, стрелками или в окне настроек: прежняя и новая
     * запись раскладки.
     */
    (e: 'itemResize', id: string, from: GrDashboardItemLayout, to: GrDashboardItemLayout): void;
    /**
     * Виджет с `auto-height` подстроился под содержимое.
     *
     * Отдельно от `itemResize` потому, что это не действие пользователя:
     * приложение, которое считает раскладку грязной по правкам, иначе спрашивало
     * бы «сохранить изменения?» после загрузки данных в виджет.
     */
    (e: 'itemAutoResize', id: string, from: GrDashboardItemLayout, to: GrDashboardItemLayout): void;
    /** Брейкпоинт сменился по ширине контейнера — новый брейкпоинт и число его колонок. */
    (e: 'breakpointChange', breakpoint: GrDashboardBreakpoint, cols: number): void;
    /** Виджет попросил открыть свои настройки: нажата встроенная кнопка-шестерёнка. */
    (e: 'itemSettings', id: string): void;
    /**
     * В сетку бросили виджет из каталога.
     *
     * Сетка сообщает **что и куда**, а кладёт приложение — тем же `addItem`, что
     * и по кнопке каталога. Иначе в раскладке появился бы виджет, для которого
     * приложение не рисовало разметки.
     */
    (e: 'itemDrop', event: GrDashboardDropEvent): void;
    /**
     * Виджет уехал в другую сетку.
     *
     * Из своей раскладки сетка убирает его сама — удаление однозначно, разметки
     * для него не нужно, в отличие от вставки. Событие говорит приложению, что
     * произошло, и несёт ту же нагрузку, что уехала в `itemDrop` приёмника.
     */
    (e: 'itemTransferOut', id: string, transfer: GrDashboardTransfer): void;
}

Использование из прозы библиотеки

Сдвинуть виджет в пустоту по оси уплотнения нельзя

compact знает четыре режима: 'vertical' (по умолчанию) тянет виджеты вверх, 'horizontal' — влево, 'both' — в обе стороны, 'none' оставляет их там, куда положили. Отпущенный в пустоту виджет уезжает обратно — вниз при 'vertical', вправо при 'horizontal', в обоих направлениях при 'both'. Это не дефект, а определение компактизации. Нужна свободная сетка с дырами — compact: 'none'.

Толчок при столкновении распространяется по цепочке и всегда вниз, в любом режиме: строк бесконечно много, а вправо мешает правый край сетки. В горизонтальных режимах это даёт сочетание, которое стоит увидеть один раз: толкнутый вниз сосед тут же придвигается влево.

Куда бросили — говорит сетка, кладёт приложение

Виджет, перетащенный из каталога, в раскладку сама сетка не пишет: она эмитит itemDrop с ячейкой, брейкпоинтом и своими опциями раскладки, а addItem вызывает потребитель — тем же способом, что и по кнопке каталога. Иначе в раскладке появился бы виджет, для которого приложение не рисовало разметки.

Пока указатель над сеткой, подложка показывает место по-настоящему: предпросмотр считается тем же addItem с теми же опциями, что уедут в событие. Поэтому опции и едут в событие: посчитай приложение раскладку своими, результат разошёлся бы с тем, что человек только что видел.

Столкновение со статикой отменяет перенос целиком

Подвинуть static-виджет нельзя по определению, а положить один поверх другого значило бы молча испортить раскладку. То же делает preventCollision для любого столкновения. Половинчатого исхода — «подвинулось насколько влезло» — здесь нет: пользователь не увидел бы, что именно не поместилось.

itemAutoResize — не то же, что itemResize

Виджет с auto-height подстраивается под содержимое сам, и его высота меняется без участия человека: приехали данные, раскрыли секцию, сменился язык. Такое изменение уезжает в update:layout наравне с остальными — раскладка остаётся единственной правдой, — но сопровождается отдельным событием.

СобытиеКогда
itemResizeпользователь потянул уголок или нажал стрелку
itemAutoResizeсодержимое виджета попросило другую высоту

Различать их приходится всякому, кто по правкам решает, грязна ли раскладка: без этого «сохранить изменения?» всплывало бы после загрузки данных в виджет. Подробности замера — ./GrDashboardItem.md.

itemTransferOut — виджет уехал к соседям

Два дашборда на странице обмениваются виджетами перетаскиванием: жест за ручку, ушедший за край своей сетки в чужую, становится переносом между ними.

СобытиеУ когоКогда
itemDropприёмникв сетку что-то положили — из каталога или из другого дашборда
itemTransferOutисточниквиджет уехал; из своей раскладки сетка убрала его сама

Отличать источники приёмник может по transfer.source ('palette' или 'dashboard') и transfer.from — идентификатору сетки, из которой виджет приехал. Разбор — ../model.md.

Отдавать и принимать разрешается по отдельности: transferable у источника, droppable у приёмника.

Пока виджет несут, источник держит его место: виджет прячется, на его ячейке стоит подложка, соседи не уплотняются. Уплотнись источник сразу — всё ниже него, вместе с приёмником, подъехало бы под указателем, и бросок пришёлся бы мимо. Убирается виджет только после того, как приёмник его принял; решает это одна точка — тот приёмник, что под указателем в момент отпускания. Отпускание мимо всех сеток, Esc и обрыв указателя (pointercancel) сворачивают перенос, и виджет возвращается на место.

Нулевая ширина — это не самый узкий экран

Скрытая вкладка, свёрнутая панель, display: none дают нулевую ширину контейнера, и брейкпоинт от этого не переключается. Принять одно за другое значило бы перевести дашборд на две колонки и записать эту раскладку в модель — то есть потерять настройку пользователя из-за того, что он открыл соседнюю вкладку.

Границы

Чего в пакете нет и не будет — записано, чтобы вопрос не всплывал заново.

  • виртуализации сетки — виджетов на дашборде десятки, а не тысячи, и цена сложности не окупается;
  • вложенных дашбордов — сетка в сетке даёт неразрешимый вопрос, чей жест обрабатывать;
  • своей карточки вместо GrCard — копия разошлась бы с карточками остального приложения;
  • конструктора отчётов с полями и агрегатами — это продукт, а не компонент.