feat: allow specifying static widgets to customizable home

this feature allows app developers to make the default layout so that
there can be widgets that cannot be deleted, resized or moved by the
users when they go to the custom homepage edit mode.

Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2023-08-03 08:35:06 +03:00
parent a9f1417fd6
commit 1853ffa09b
8 changed files with 156 additions and 98 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-home': patch
---
Allow specifying static widgets to custom home page
@@ -15,14 +15,14 @@
*/
import {
HomePageRandomJoke,
WelcomeTitle,
HeaderWorldClock,
ClockConfig,
HomePageStarredEntities,
CustomHomepageGrid,
HomePageToolkit,
HeaderWorldClock,
HomePageCompanyLogo,
HomePageRandomJoke,
HomePageStarredEntities,
HomePageToolkit,
WelcomeTitle,
} from '@backstage/plugin-home';
import { Content, Header, Page } from '@backstage/core-components';
import { HomePageSearchBar } from '@backstage/plugin-search';
@@ -64,6 +64,9 @@ const defaultConfig = [
y: 0,
width: 12,
height: 1,
movable: false,
resizable: false,
deletable: false,
},
{
component: 'WelcomeTitle',
@@ -77,7 +80,7 @@ const defaultConfig = [
x: 0,
y: 2,
width: 12,
height: 1,
height: 2,
},
];
+3
View File
@@ -233,6 +233,9 @@ const defaultConfig = [
y: 0,
width: 12,
height: 1,
movable: true,
resizable: false,
deletable: false,
},
];
+3
View File
@@ -147,6 +147,9 @@ export type LayoutConfiguration = {
y: number;
width: number;
height: number;
movable?: boolean;
deletable?: boolean;
resizable?: boolean;
};
// @public @deprecated (undocumented)
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { ReactNode, useCallback, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Layout, Layouts, Responsive, WidthProvider } from 'react-grid-layout';
import {
ElementCollection,
@@ -40,6 +40,7 @@ import { WidgetSettingsOverlay } from './WidgetSettingsOverlay';
import { AddWidgetDialog } from './AddWidgetDialog';
import { CustomHomepageButtons } from './CustomHomepageButtons';
import {
CustomHomepageGridProps,
CustomHomepageGridStateV1,
CustomHomepageGridStateV1Schema,
GridWidget,
@@ -158,6 +159,9 @@ const convertConfigToDefaultWidgets = (
isResizable: false,
},
settings: {},
movable: conf.movable,
deletable: conf.deletable,
resizable: conf.resizable,
};
});
return compact(ret);
@@ -190,82 +194,6 @@ const availableWidgetsFilter = (elements: ElementCollection) => {
});
};
/**
* Breakpoint options for <CustomHomepageGridProps/>
*
* @public
*/
export type Breakpoint = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
/**
* Props customizing the <CustomHomepageGrid/> component.
*
* @public
*/
export type CustomHomepageGridProps = {
/**
* Children contain all widgets user can configure on their own homepage.
*/
children?: ReactNode;
/**
* Default layout for the homepage before users have modified it.
*/
config?: LayoutConfiguration[];
/**
* Height of grid row in pixels.
* @defaultValue 60
*/
rowHeight?: number;
/**
* Screen width in pixels for different breakpoints.
* @defaultValue theme breakpoints
*/
breakpoints?: Record<Breakpoint, number>;
/**
* Number of grid columns for different breakpoints.
* @defaultValue \{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 \}
*/
cols?: Record<Breakpoint, number>;
/**
* Grid container padding (x, y) in pixels for all or specific breakpoints.
* @defaultValue [0, 0]
* @example [10, 10]
* @example \{ lg: [10, 10] \}
*/
containerPadding?: [number, number] | Record<Breakpoint, [number, number]>;
/**
* Grid container margin (x, y) in pixels for all or specific breakpoints.
* @defaultValue [0, 0]
* @example [10, 10]
* @example \{ lg: [10, 10] \}
*/
containerMargin?: [number, number] | Record<Breakpoint, [number, number]>;
/**
* Maximum number of rows user can have in the grid.
* @defaultValue unlimited
*/
maxRows?: number;
/**
* Custom style for grid.
*/
style?: React.CSSProperties;
/**
* Compaction type of widgets in the grid. This controls where widgets are moved in case
* they are overlapping in the grid.
*/
compactType?: 'vertical' | 'horizontal' | null;
/**
* Controls if widgets can overlap in the grid. If true, grid can be placed one over the other.
* @defaultValue false
*/
allowOverlap?: boolean;
/**
* Controls if widgets can collide with each other. If true, grid items won't change position when being dragged over.
* @defaultValue false
*/
preventCollision?: boolean;
};
/**
* A component that allows customizing components in home grid layout.
*
@@ -318,6 +246,9 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => {
isDraggable: editMode,
},
settings: {},
movable: widget.movable,
deletable: widget.deletable,
resizable: widget.resizable,
},
]);
setAddWidgetDialogOpen(false);
@@ -348,9 +279,11 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => {
setEditMode(mode);
setWidgets(
widgets.map(w => {
const resizable = w.resizable === false ? false : mode;
const movable = w.movable === false ? false : mode;
return {
...w,
layout: { ...w.layout, isDraggable: mode, isResizable: mode },
layout: { ...w.layout, isDraggable: movable, isResizable: resizable },
};
}),
);
@@ -370,7 +303,20 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => {
};
const handleRestoreDefaultConfig = () => {
setWidgets(defaultLayout);
setWidgets(
defaultLayout.map(w => {
const resizable = w.resizable === false ? false : editMode;
const movable = w.movable === false ? false : editMode;
return {
...w,
layout: {
...w.layout,
isDraggable: movable,
isResizable: resizable,
},
};
}),
);
};
return (
@@ -404,7 +350,7 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => {
style={props.style}
allowOverlap={props.allowOverlap}
preventCollision={props.preventCollision}
draggableCancel=".overlayGridItem,.widgetSettingsDialog"
draggableCancel=".overlayGridItem,.widgetSettingsDialog,.disabled"
containerPadding={props.containerPadding}
margin={props.containerMargin}
breakpoints={
@@ -435,7 +381,9 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => {
return (
<div
key={l.i}
className={`${styles.widgetWrapper} ${editMode && 'edit'}`}
className={`${styles.widgetWrapper} ${editMode && 'edit'} ${
w.movable === false && 'disabled'
}`}
>
<ErrorBoundary>
<widget.component.type {...widgetProps} />
@@ -447,6 +395,7 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => {
handleRemove={handleRemove}
handleSettingsSave={handleSettingsSave}
settings={w.settings}
deletable={w.deletable}
/>
)}
</div>
@@ -58,10 +58,12 @@ interface WidgetSettingsOverlayProps {
handleRemove: (id: string) => void;
handleSettingsSave: (id: string, settings: Record<string, any>) => void;
settings?: Record<string, any>;
deletable?: boolean;
}
export const WidgetSettingsOverlay = (props: WidgetSettingsOverlayProps) => {
const { id, widget, settings, handleRemove, handleSettingsSave } = props;
const { id, widget, settings, handleRemove, handleSettingsSave, deletable } =
props;
const [settingsDialogOpen, setSettingsDialogOpen] = React.useState(false);
const styles = useStyles();
@@ -110,13 +112,15 @@ export const WidgetSettingsOverlay = (props: WidgetSettingsOverlayProps) => {
</Tooltip>
</Grid>
)}
<Grid item className="overlayGridItem">
<Tooltip title="Delete widget">
<IconButton color="secondary" onClick={() => handleRemove(id)}>
<DeleteIcon fontSize="large" />
</IconButton>
</Tooltip>
</Grid>
{deletable !== false && (
<Grid item className="overlayGridItem">
<Tooltip title="Delete widget">
<IconButton color="secondary" onClick={() => handleRemove(id)}>
<DeleteIcon fontSize="large" />
</IconButton>
</Tooltip>
</Grid>
)}
</Grid>
</div>
);
@@ -14,5 +14,8 @@
* limitations under the License.
*/
export { CustomHomepageGrid } from './CustomHomepageGrid';
export type { CustomHomepageGridProps, Breakpoint } from './CustomHomepageGrid';
export type { LayoutConfiguration } from './types';
export type {
CustomHomepageGridProps,
Breakpoint,
LayoutConfiguration,
} from './types';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { ReactElement } from 'react';
import React, { ReactElement, ReactNode } from 'react';
import { Layout } from 'react-grid-layout';
import { z } from 'zod';
import { RJSFSchema, UiSchema } from '@rjsf/utils';
@@ -24,12 +24,91 @@ const RSJFTypeUiSchema: z.ZodType<UiSchema> = z.any();
const ReactElementSchema: z.ZodType<ReactElement> = z.any();
const LayoutSchema: z.ZodType<Layout> = z.any();
/**
* Breakpoint options for <CustomHomepageGridProps/>
*
* @public
*/
export type Breakpoint = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
/**
* Props customizing the <CustomHomepageGrid/> component.
*
* @public
*/
export type CustomHomepageGridProps = {
/**
* Children contain all widgets user can configure on their own homepage.
*/
children?: ReactNode;
/**
* Default layout for the homepage before users have modified it.
*/
config?: LayoutConfiguration[];
/**
* Height of grid row in pixels.
* @defaultValue 60
*/
rowHeight?: number;
/**
* Screen width in pixels for different breakpoints.
* @defaultValue theme breakpoints
*/
breakpoints?: Record<Breakpoint, number>;
/**
* Number of grid columns for different breakpoints.
* @defaultValue \{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 \}
*/
cols?: Record<Breakpoint, number>;
/**
* Grid container padding (x, y) in pixels for all or specific breakpoints.
* @defaultValue [0, 0]
* @example [10, 10]
* @example \{ lg: [10, 10] \}
*/
containerPadding?: [number, number] | Record<Breakpoint, [number, number]>;
/**
* Grid container margin (x, y) in pixels for all or specific breakpoints.
* @defaultValue [0, 0]
* @example [10, 10]
* @example \{ lg: [10, 10] \}
*/
containerMargin?: [number, number] | Record<Breakpoint, [number, number]>;
/**
* Maximum number of rows user can have in the grid.
* @defaultValue unlimited
*/
maxRows?: number;
/**
* Custom style for grid.
*/
style?: React.CSSProperties;
/**
* Compaction type of widgets in the grid. This controls where widgets are moved in case
* they are overlapping in the grid.
*/
compactType?: 'vertical' | 'horizontal' | null;
/**
* Controls if widgets can overlap in the grid. If true, grid can be placed one over the other.
* @defaultValue false
*/
allowOverlap?: boolean;
/**
* Controls if widgets can collide with each other. If true, grid items won't change position when being dragged over.
* @defaultValue false
*/
preventCollision?: boolean;
};
export const LayoutConfigurationSchema = z.object({
component: ReactElementSchema,
x: z.number().nonnegative('x must be positive number'),
y: z.number().nonnegative('y must be positive number'),
width: z.number().positive('width must be positive number'),
height: z.number().positive('height must be positive number'),
movable: z.boolean().optional(),
deletable: z.boolean().optional(),
resizable: z.boolean().optional(),
});
/**
@@ -43,6 +122,9 @@ export type LayoutConfiguration = {
y: number;
width: number;
height: number;
movable?: boolean;
deletable?: boolean;
resizable?: boolean;
};
export const WidgetSchema = z.object({
@@ -64,6 +146,9 @@ export const WidgetSchema = z.object({
.optional(),
settingsSchema: RSJFTypeSchema.optional(),
uiSchema: RSJFTypeUiSchema.optional(),
movable: z.boolean().optional(),
deletable: z.boolean().optional(),
resizable: z.boolean().optional(),
});
export type Widget = z.infer<typeof WidgetSchema>;
@@ -72,6 +157,9 @@ const GridWidgetSchema = z.object({
id: z.string(),
layout: LayoutSchema,
settings: z.record(z.string(), z.any()),
movable: z.boolean().optional(),
deletable: z.boolean().optional(),
resizable: z.boolean().optional(),
});
export type GridWidget = z.infer<typeof GridWidgetSchema>;