GrDashboard

  • dashboard
  • dashboard
  • API · 15 props
import { GrDashboard } from '@feugene/granularity-dashboard/components/GrDashboard'

Overview

When to take it

  • the layout is determined by the user — an analytics panel, a workspace, a summary for a particular role: the set and the arrangement of the widgets belong to a person rather than to the designer of the screen;
  • the layout has to survive a reload — useDashboardLayout puts it into localStorage or onto the server through the storage interface;
  • a laptop and a tablet need different things — the layout is stored per breakpoint, and a missing one is derived from the neighbouring one above;
  • the widgets are of different sizes — minW/minH/maxW/maxH are declared by the widget itself, because only it knows that “this chart is unreadable below three rows”.

When to take something else

NeedTake
The layout is fixed by the designCSS Grid and GrCard
Two areas whose boundary is moved by the userGrSplitter
The order of the elements in a single listGrSortableList
Sections switched with tabsGrTabs
Table rows with a changeable order of columnsGrDataTable
Where a widget takes its data fromgranularity-datasource
What a widget draws its content withgranularity-charts / granularity-chrono

A dashboard is not a way to lay out a static screen. If the user moves nothing, a grid of widgets gives an extra layer of state, a storage and an edit mode where CSS Grid would have been enough.

Examples 2 each with a live demo and its code

  • A finance overview that keeps its layout

    Switch to «Edit», move and resize the widgets, then reload the page: useDashboardLayout saves the layout per breakpoint and restores it after mounting, so the server and the first render agree. The markup only names the widgets by item-id; minW/minH come from the widgets, and breakpoints are measured on the container — twelve columns from 540 px, two below. «Reset» goes back to the initial layout.

    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.
  • Moving widgets between two boards

    Drag a widget by its handle past the edge of one board into the other. transferable lets a widget leave and droppable lets one arrive — separate rights. The source removes the widget by itself; the receiver emits item-drop with the cell, the breakpoint and its layout options, and the page places it with addItem — so it lands where the drop shadow showed it.

    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 generated from the package · cannot drift

Props

PropTypedefaultDescription
breakpointsGrDashboardBreakpoints | undefinedGR_DASHBOARD_BREAKPOINTSThe container width thresholds at which a breakpoint begins.
colsnumber | GrDashboardCols | undefinedGR_DASHBOARD_COLSThe number of columns: a map by breakpoint or a single number for all of them.
initialBreakpointstring | undefined"lg"The breakpoint of the first render. There is no container width on the server, so both the server and the first client render take it — the refinement arrives after mounting, and hydration does not diverge.
rowHeightnumber | undefinedundefinedThe grid row height, px.
gapnumber | undefinedundefinedThe gap between widgets, px.
modeGrDashboardMode | undefinedundefined`view` — the grid only displays; `edit` — the handles appear.
draggableboolean | undefinedundefinedWidgets are moved by the handle in `edit` mode; a widget narrows the rule with its own prop. Not set — from `GrConfigProvider`, otherwise `true`.
resizableboolean | undefinedundefinedWidgets are resized by the corner in `edit` mode; a widget narrows the rule with its own prop. Not set — from `GrConfigProvider`, otherwise `true`.
compactGrDashboardCompaction | undefinedundefinedCompaction after a move: `vertical` pulls the widgets up, `horizontal` — to the left, `both` — in both directions, `none` leaves them where they were dropped. Not set — from `GrConfigProvider`, otherwise `vertical`.
preventCollisionboolean | undefinedundefinedA collision cancels the move instead of pushing the neighbours.
lazyboolean | undefinedundefinedThe widget content mounts once it enters the viewport.
droppableboolean | undefinedundefinedThe grid accepts widgets dragged from the catalog. Works only in `mode="edit"`. The prop is needed where a page has several grids but only one should accept.
transferableboolean | undefinedundefinedA widget can be dragged out of the grid into a neighbouring dashboard. Separate from `droppable`: accepting and giving away are different permissions. An archive dashboard accepts widgets but gives none of its own away.
layoutrequiredv-modelGrDashboardResponsiveLayout—The layout for each breakpoint. `v-model:layout`.

Slots

SlotTypeDescription
defaultany`GrDashboardItem` widgets — one per layout entry.
emptyanyWhat to show while the layout has no widgets at all.

Events

EventSignatureDescription
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

MethodsSignatureDescription
breakpointstringThe current breakpoint: before the first measurement of the container — `initialBreakpoint`.

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { GrDashboardConfigurableProps, GrDashboardMode, GrDashboardItemBounds } from '@feugene/granularity-dashboard/components/GrDashboard'

Filedist/types/components/GrDashboard/config.d.ts

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

Filedist/types/components/GrDashboard/defaults.d.ts

GrDashboardConfigurablePropsinterface
interface GrDashboardConfigurableProps {
    rowHeight: number;
    gap: number;
    mode: GrDashboardMode;
    draggable: boolean;
    resizable: boolean;
    compact: GrDashboardCompaction;
    preventCollision: boolean;
    lazy: boolean;
    droppable: boolean;
    transferable: boolean;
}

Filedist/types/components/GrDashboard/grDashboardStyles.d.ts

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

Filedist/types/components/shared/context.d.ts

GrDashboardItemBoundsinterface
interface GrDashboardItemBounds {
    minW?: number;
    minH?: number;
    maxW?: number;
    maxH?: number;
    static?: boolean;
    draggable?: boolean;
    resizable?: boolean;
    title?: string;
}
GrDashboardDropEventinterface
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
function useGrDashboardContext(): GrDashboardContext | null;

Filedist/types/components/GrDashboard/safelist.d.ts

grDashboardSafelistconst
const grDashboardSafelist: string[];

Filedist/types/components/GrDashboard/GrDashboard.vue.d.ts

GrDashboardPropsinterface
interface GrDashboardProps {
    layout: GrDashboardResponsiveLayout;
    breakpoints?: GrDashboardBreakpoints;
    cols?: GrDashboardCols | number;
    initialBreakpoint?: GrDashboardBreakpoint;
    rowHeight?: number;
    gap?: number;
    mode?: GrDashboardMode;
    draggable?: boolean;
    resizable?: boolean;
    compact?: GrDashboardCompaction;
    preventCollision?: boolean;
    lazy?: boolean;
    droppable?: boolean;
    transferable?: boolean;
    ariaLabel?: string;
}
GrDashboardEmitsinterface
interface GrDashboardEmits {
    (e: 'update:layout', value: GrDashboardResponsiveLayout): void;
    (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;
    (e: 'itemAutoResize', id: string, from: GrDashboardItemLayout, to: GrDashboardItemLayout): void;
    (e: 'breakpointChange', breakpoint: GrDashboardBreakpoint, cols: number): void;
    (e: 'itemSettings', id: string): void;
    (e: 'itemDrop', event: GrDashboardDropEvent): void;
    (e: 'itemTransferOut', id: string, transfer: GrDashboardTransfer): void;
}

Usage from the library prose

A widget cannot be moved into emptiness along the axis of compaction

compact knows four modes: 'vertical' (the default) pulls the widgets up, 'horizontal' to the left, 'both' in both directions, and 'none' leaves them where they were put. A widget released into emptiness moves back — down with 'vertical', to the right with 'horizontal', in both directions with 'both'. That is not a defect but the definition of compaction. If a free grid with holes is needed — compact: 'none'.

A push on a collision propagates along a chain and always downwards, in any mode: there are infinitely many rows, while to the right the right edge of the grid gets in the way. In the horizontal modes that gives a combination worth seeing once: a neighbour pushed downwards immediately moves to the left.

Where it was dropped is said by the grid, and the putting is done by the application

The grid does not write a widget dragged in from the catalogue into the layout itself: it emits itemDrop with the cell, the breakpoint and its own layout options, and addItem is called by the consumer — the same way as with a button in the catalogue. Otherwise a widget would appear in the layout for which the application drew no markup.

While the pointer is over the grid, the underlay shows the place for real: the preview is computed with the same addItem and the same options that will travel into the event. That is why the options travel into the event: were the application to compute the layout with its own, the result would diverge from what the person has just seen.

A collision with a static widget cancels the whole move

Moving a static widget is impossible by definition, and putting one on top of another would mean silently spoiling the layout. preventCollision does the same for any collision. There is no half-way outcome — “it moved as far as it fit” — here: the user would not see what exactly did not fit.

itemAutoResize is not the same as itemResize

A widget with auto-height adjusts to its content itself, and its height changes with no participation from a person: the data arrived, a section was expanded, the language changed. Such a change travels into update:layout on a par with the rest — the layout remains the only truth — but is accompanied by a separate event.

EventWhen
itemResizethe user pulled a corner or pressed an arrow
itemAutoResizethe content of the widget asked for a different height

Telling them apart is necessary for anyone who decides from the edits whether the layout is dirty: without that, “save the changes?” would pop up after data has loaded into a widget. The details of the measurement — ./GrDashboardItem.md.

itemTransferOut — the widget went to the neighbours

Two dashboards on a page exchange widgets by dragging: a gesture by the handle that left the edge of its own grid into another becomes a transfer between them.

EventWhoseWhen
itemDropthe receiversomething was put into the grid — from the catalogue or from another dashboard
itemTransferOutthe sourcethe widget left; the grid removed it from its own layout itself

The receiver can tell the sources apart by transfer.source ('palette' or 'dashboard') and transfer.from — the identifier of the grid the widget came from. The details — ../model.md.

Giving away and receiving are allowed separately: transferable on the source, droppable on the receiver.

While a widget is being carried, the source holds its place: the widget is hidden, an underlay stands on its cell, and the neighbours are not compacted. Were the source to compact at once, everything below it, the receiver included, would slide up under the pointer, and the drop would land wide of the mark. The widget is removed only after the receiver has accepted it; that is decided at a single point — the receiver that is under the pointer at the moment of release. A release outside all grids, Esc and a broken-off pointer (pointercancel) roll the transfer back, and the widget returns to its place.

A zero width is not the narrowest screen

A hidden tab, a collapsed panel, a display: none give a zero width of the container, and the breakpoint does not switch because of that. Taking one for the other would mean moving the dashboard to two columns and writing that layout into the model — that is, losing the user’s setting because they opened a neighbouring tab.

Limits

What is not in the package and will not be — written down so that the question does not come up again.

  • virtualisation of the grid — there are dozens of widgets on a dashboard rather than thousands, and the price of the complexity does not pay off;
  • nested dashboards — a grid inside a grid poses an unanswerable question about whose gesture to handle;
  • a card of its own instead of GrCard — a copy would diverge from the cards of the rest of the application;
  • a report builder with fields and aggregates — that is a product rather than a component.