From 1853ffa09b503bfb826d9291bc4d6de9def4ec45 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Thu, 3 Aug 2023 08:35:06 +0300 Subject: [PATCH] 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 --- .changeset/witty-ants-cover.md | 5 + packages/app/src/components/home/HomePage.tsx | 15 ++- plugins/home/README.md | 3 + plugins/home/api-report.md | 3 + .../CustomHomepage/CustomHomepageGrid.tsx | 111 +++++------------- .../CustomHomepage/WidgetSettingsOverlay.tsx | 20 ++-- .../src/components/CustomHomepage/index.ts | 7 +- .../src/components/CustomHomepage/types.ts | 90 +++++++++++++- 8 files changed, 156 insertions(+), 98 deletions(-) create mode 100644 .changeset/witty-ants-cover.md diff --git a/.changeset/witty-ants-cover.md b/.changeset/witty-ants-cover.md new file mode 100644 index 0000000000..7f499a88ae --- /dev/null +++ b/.changeset/witty-ants-cover.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +Allow specifying static widgets to custom home page diff --git a/packages/app/src/components/home/HomePage.tsx b/packages/app/src/components/home/HomePage.tsx index aa3739728e..a8ddc0e0e2 100644 --- a/packages/app/src/components/home/HomePage.tsx +++ b/packages/app/src/components/home/HomePage.tsx @@ -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, }, ]; diff --git a/plugins/home/README.md b/plugins/home/README.md index 57c2a4d243..e147cdfbdf 100644 --- a/plugins/home/README.md +++ b/plugins/home/README.md @@ -233,6 +233,9 @@ const defaultConfig = [ y: 0, width: 12, height: 1, + movable: true, + resizable: false, + deletable: false, }, ]; diff --git a/plugins/home/api-report.md b/plugins/home/api-report.md index 32c1c0eb9d..0499209013 100644 --- a/plugins/home/api-report.md +++ b/plugins/home/api-report.md @@ -147,6 +147,9 @@ export type LayoutConfiguration = { y: number; width: number; height: number; + movable?: boolean; + deletable?: boolean; + resizable?: boolean; }; // @public @deprecated (undocumented) diff --git a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx index f5ee8f3ddc..88c3273ff3 100644 --- a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx +++ b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx @@ -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 - * - * @public - */ -export type Breakpoint = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; - -/** - * Props customizing the 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; - /** - * Number of grid columns for different breakpoints. - * @defaultValue \{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 \} - */ - cols?: Record; - /** - * 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; - /** - * 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; - /** - * 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 (
@@ -447,6 +395,7 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => { handleRemove={handleRemove} handleSettingsSave={handleSettingsSave} settings={w.settings} + deletable={w.deletable} /> )}
diff --git a/plugins/home/src/components/CustomHomepage/WidgetSettingsOverlay.tsx b/plugins/home/src/components/CustomHomepage/WidgetSettingsOverlay.tsx index 261dabb833..2c3a1d9ab9 100644 --- a/plugins/home/src/components/CustomHomepage/WidgetSettingsOverlay.tsx +++ b/plugins/home/src/components/CustomHomepage/WidgetSettingsOverlay.tsx @@ -58,10 +58,12 @@ interface WidgetSettingsOverlayProps { handleRemove: (id: string) => void; handleSettingsSave: (id: string, settings: Record) => void; settings?: Record; + 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) => { )} - - - handleRemove(id)}> - - - - + {deletable !== false && ( + + + handleRemove(id)}> + + + + + )} ); diff --git a/plugins/home/src/components/CustomHomepage/index.ts b/plugins/home/src/components/CustomHomepage/index.ts index eed597cca3..380fdb4b25 100644 --- a/plugins/home/src/components/CustomHomepage/index.ts +++ b/plugins/home/src/components/CustomHomepage/index.ts @@ -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'; diff --git a/plugins/home/src/components/CustomHomepage/types.ts b/plugins/home/src/components/CustomHomepage/types.ts index 8e9c6f403e..79e94c5f03 100644 --- a/plugins/home/src/components/CustomHomepage/types.ts +++ b/plugins/home/src/components/CustomHomepage/types.ts @@ -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 = z.any(); const ReactElementSchema: z.ZodType = z.any(); const LayoutSchema: z.ZodType = z.any(); +/** + * Breakpoint options for + * + * @public + */ +export type Breakpoint = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + +/** + * Props customizing the 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; + /** + * Number of grid columns for different breakpoints. + * @defaultValue \{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 \} + */ + cols?: Record; + /** + * 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; + /** + * 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; + /** + * 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; @@ -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;