123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- import type {Layout} from 'react-grid-layout';
- import {compact} from 'react-grid-layout/build/utils';
- import pickBy from 'lodash/pickBy';
- import sortBy from 'lodash/sortBy';
- import zip from 'lodash/zip';
- import {defined} from 'sentry/utils';
- import {uniqueId} from 'sentry/utils/guid';
- import {NUM_DESKTOP_COLS} from './dashboard';
- import type {Widget, WidgetLayout} from './types';
- import {DisplayType} from './types';
- export const DEFAULT_WIDGET_WIDTH = 2;
- export const METRIC_WIDGET_MIN_SIZE = {minH: 2, h: 2, w: 3};
- const WIDGET_PREFIX = 'grid-item';
- // Keys for grid layout values we track in the server
- const STORE_KEYS = ['x', 'y', 'w', 'h', 'minW', 'maxW', 'minH', 'maxH'];
- export type Position = Pick<Layout, 'x' | 'y'>;
- type NextPosition = [position: Position, columnDepths: number[]];
- export function generateWidgetId(widget: Widget, index: number) {
- return widget.id ? `${widget.id}-index-${index}` : `index-${index}`;
- }
- export function constructGridItemKey(widget: {id?: string; tempId?: string}) {
- return `${WIDGET_PREFIX}-${widget.id ?? widget.tempId}`;
- }
- export function assignTempId(widget: Widget) {
- if (widget.id ?? widget.tempId) {
- return widget;
- }
- return {...widget, tempId: uniqueId()};
- }
- /**
- * Naive positioning for widgets assuming no resizes.
- */
- export function getDefaultPosition(index: number, displayType: DisplayType) {
- return {
- x: (DEFAULT_WIDGET_WIDTH * index) % NUM_DESKTOP_COLS,
- y: Number.MAX_SAFE_INTEGER,
- w: DEFAULT_WIDGET_WIDTH,
- h: displayType === DisplayType.BIG_NUMBER ? 1 : 2,
- minH: displayType === DisplayType.BIG_NUMBER ? 1 : 2,
- };
- }
- export function getMobileLayout(desktopLayout: Layout[], widgets: Widget[]) {
- if (desktopLayout.length === 0) {
- // Initial case where the user has no layout saved, but
- // dashboard has widgets
- return [];
- }
- const layoutWidgetPairs = zip(desktopLayout, widgets) as [Layout, Widget][];
- // Sort by y and then subsort by x
- const sorted = sortBy(layoutWidgetPairs, ['0.y', '0.x']);
- const mobileLayout = sorted.map(([layout, widget], index) => ({
- ...layout,
- x: 0,
- y: index * 2,
- w: 2,
- h: widget.displayType === DisplayType.BIG_NUMBER ? 1 : 2,
- }));
- return mobileLayout;
- }
- /**
- * Reads the layout from an array of widgets.
- */
- export function getDashboardLayout(widgets: Widget[]): Layout[] {
- type WidgetWithDefinedLayout = Omit<Widget, 'layout'> & {layout: WidgetLayout};
- return widgets
- .filter((widget): widget is WidgetWithDefinedLayout => defined(widget.layout))
- .map(({layout, ...widget}) => ({
- ...layout,
- i: constructGridItemKey(widget),
- }));
- }
- export function pickDefinedStoreKeys(layout: Layout): WidgetLayout {
- // TODO(nar): Fix the types here
- return pickBy(
- layout,
- (value, key) => defined(value) && STORE_KEYS.includes(key)
- ) as WidgetLayout;
- }
- export function getDefaultWidgetHeight(displayType: DisplayType): number {
- return displayType === DisplayType.BIG_NUMBER ? 1 : 2;
- }
- export function getInitialColumnDepths() {
- return Array(NUM_DESKTOP_COLS).fill(0);
- }
- /**
- * Creates an array from layouts where each column stores how deep it is.
- */
- export function calculateColumnDepths(
- layouts: Pick<Layout, 'h' | 'w' | 'x' | 'y'>[]
- ): number[] {
- const depths = getInitialColumnDepths();
- // For each layout's x, record the max depth
- layouts.forEach(({x, w, y, h}) => {
- // Adjust the column depths for each column the widget takes up
- for (let col = x; col < x + w; col++) {
- depths[col] = Math.max(y + h, depths[col]);
- }
- });
- return depths;
- }
- /**
- * Find the next place to place a widget and also returns the next
- * input when this operation needs to be called multiple times.
- *
- * @param columnDepths A profile of how deep the widgets in a column extend.
- * @param height The desired height of the next widget we want to place.
- * @returns An {x, y} positioning for the next available spot, as well as the
- * next columnDepths array if this position were used.
- */
- export function getNextAvailablePosition(
- initialColumnDepths: number[],
- height: number
- ): NextPosition {
- const columnDepths = [...initialColumnDepths];
- const maxColumnDepth = Math.max(...columnDepths);
- // Look for an opening at each depth by scanning from 0, 0
- // By scanning from 0 depth to the highest depth, we ensure
- // we get the top-most available spot
- for (let currDepth = 0; currDepth <= maxColumnDepth; currDepth++) {
- for (let start = 0; start <= columnDepths.length - DEFAULT_WIDGET_WIDTH; start++) {
- if (columnDepths[start] > currDepth) {
- // There are potentially widgets in the way here, so skip
- continue;
- }
- // If all of the columns from start to end (the size of the widget)
- // have at most the current depth, then we've found a valid positioning
- // No other widgets extend into the space we need
- const end = start + DEFAULT_WIDGET_WIDTH;
- if (columnDepths.slice(start, end).every(val => val <= currDepth)) {
- for (let col = start; col < start + DEFAULT_WIDGET_WIDTH; col++) {
- columnDepths[col] = currDepth + height;
- }
- return [{x: start, y: currDepth}, [...columnDepths]];
- }
- }
- }
- for (let col = 0; col < DEFAULT_WIDGET_WIDTH; col++) {
- columnDepths[col] = maxColumnDepth;
- }
- return [{x: 0, y: maxColumnDepth}, [...columnDepths]];
- }
- export function assignDefaultLayout<T extends Pick<Widget, 'displayType' | 'layout'>>(
- widgets: T[],
- initialColumnDepths: number[]
- ): T[] {
- let columnDepths = [...initialColumnDepths];
- const newWidgets = widgets.map(widget => {
- if (defined(widget.layout)) {
- return widget;
- }
- const height = getDefaultWidgetHeight(widget.displayType);
- const [nextPosition, nextColumnDepths] = getNextAvailablePosition(
- columnDepths,
- height
- );
- columnDepths = nextColumnDepths;
- return {
- ...widget,
- layout: {
- ...nextPosition,
- h: height,
- minH: height,
- w: DEFAULT_WIDGET_WIDTH,
- },
- };
- });
- return newWidgets;
- }
- export function enforceWidgetHeightValues(widget: Widget): Widget {
- const {displayType, layout} = widget;
- const nextWidget = {
- ...widget,
- };
- if (!defined(layout)) {
- return nextWidget;
- }
- const minH = getDefaultWidgetHeight(displayType);
- const nextLayout = {
- ...layout,
- h: Math.max(layout?.h ?? minH, minH),
- minH,
- };
- return {...nextWidget, layout: nextLayout};
- }
- export function generateWidgetsAfterCompaction(widgets: Widget[]) {
- // Resolves any potential compactions that need to occur after a
- // single widget change would affect other widget positions, e.g. deletion
- const nextLayout = compact(getDashboardLayout(widgets), 'vertical', NUM_DESKTOP_COLS);
- return widgets.map(widget => {
- const layout = nextLayout.find(({i}) => i === constructGridItemKey(widget));
- if (!layout) {
- return widget;
- }
- return {...widget, layout};
- });
- }
- export function isValidLayout(layout: Layout) {
- return !isNaN(layout.x) && !isNaN(layout.y) && layout.w > 0 && layout;
- }
|