From 1fba4bf36f395de48f068c5508bd465c7db3a643 Mon Sep 17 00:00:00 2001 From: nicolethoen Date: Fri, 13 Feb 2026 13:36:28 -0500 Subject: [PATCH 1/4] refactor: PatternFly compliance cleanup and component architecture improvements - Replace CSS-in-JS (styles.ts) with static stylesheet (styles.css) - Standardize all class names to pf-v6-widget- prefix with BEM modifiers - Remove PF utility classes, convert to custom CSS with logical properties - Switch all icon imports to dist/esm individual paths for tree-shaking - Extract toolbar from WidgetDrawer into WidgetLayout with AddWidgetsButton - Fix maxH/minH spread-overwrite bug, remove unnecessary Divider - Replace hardcoded SVG color with currentColor + PF token - Fix resize handle, drag handle, and dropdown accessibility issues - Add rel="noopener noreferrer" to target="_blank" links - Remove anti-pattern tabIndex={index} from grid items - Rewrite examples with richer PF component content - Replace WithoutDrawerExample with CustomToolbarExample - Add transformIgnorePatterns to Jest config for dist/esm imports - Add build:watch CSS copying via nodemon - Add test coverage for GridTile actions, WidgetDrawer, AddWidgetsButton Co-Authored-By: Claude Opus 4.6 --- README.md | 15 +- jest.config.js | 3 + packages/module/package.json | 8 +- .../content/examples/BasicExample.tsx | 193 ++++++++------ .../content/examples/CustomToolbarExample.tsx | 251 ++++++++++++++++++ .../content/examples/LockedLayoutExample.tsx | 154 ++++++----- .../content/examples/WithoutDrawerExample.tsx | 142 ---------- .../patternfly-docs/content/examples/basic.md | 41 ++- .../patternfly-docs/patternfly-docs.css.js | 1 + .../src/WidgetLayout/AddWidgetsButton.tsx | 27 ++ .../module/src/WidgetLayout/GridLayout.tsx | 34 ++- packages/module/src/WidgetLayout/GridTile.tsx | 59 ++-- .../module/src/WidgetLayout/WidgetDrawer.tsx | 49 ++-- .../module/src/WidgetLayout/WidgetLayout.tsx | 29 +- .../__tests__/AddWidgetsButton.test.tsx | 34 +++ .../WidgetLayout/__tests__/GridTile.test.tsx | 185 ++++++++++++- .../__tests__/WidgetDrawer.test.tsx | 107 ++++++++ .../__tests__/WidgetLayout.test.tsx | 2 +- packages/module/src/WidgetLayout/styles.ts | 134 ---------- packages/module/src/index.ts | 2 + packages/module/src/styles.css | 166 ++++++++++++ 21 files changed, 1104 insertions(+), 532 deletions(-) create mode 100644 packages/module/patternfly-docs/content/examples/CustomToolbarExample.tsx delete mode 100644 packages/module/patternfly-docs/content/examples/WithoutDrawerExample.tsx create mode 100644 packages/module/src/WidgetLayout/AddWidgetsButton.tsx create mode 100644 packages/module/src/WidgetLayout/__tests__/AddWidgetsButton.test.tsx create mode 100644 packages/module/src/WidgetLayout/__tests__/WidgetDrawer.test.tsx delete mode 100644 packages/module/src/WidgetLayout/styles.ts create mode 100644 packages/module/src/styles.css diff --git a/README.md b/README.md index 5572d1d..e425093 100644 --- a/README.md +++ b/README.md @@ -29,12 +29,23 @@ This library requires React 18+ and React DOM 18+ as peer dependencies. Make sur yarn add react@^18 react-dom@^18 ``` +## Styles + +Import the required stylesheet in your application entry point: + +```ts +import '@patternfly/widgetized-dashboard/dist/esm/styles.css'; +``` + +This stylesheet is required for the dashboard layout, drag-and-drop, and widget tile styling. You will also need PatternFly's base styles — see the [PatternFly getting started guide](https://www.patternfly.org/get-started/develop/) for details. + ## Quick Start ```tsx import React from 'react'; import { WidgetLayout, WidgetMapping, ExtendedTemplateConfig } from '@patternfly/widgetized-dashboard'; -import { CubeIcon } from '@patternfly/react-icons'; +import '@patternfly/widgetized-dashboard/dist/esm/styles.css'; +import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon'; // Define your widgets const widgetMapping: WidgetMapping = { @@ -86,7 +97,7 @@ function App() { - [Basic Example](./packages/module/patternfly-docs/content/examples/BasicExample.tsx) - Complete dashboard with drawer - [Locked Layout Example](./packages/module/patternfly-docs/content/examples/LockedLayoutExample.tsx) - Dashboard with locked widgets -- [Without Drawer Example](./packages/module/patternfly-docs/content/examples/WithoutDrawerExample.tsx) - Grid layout without widget drawer +- [Custom Toolbar Example](./packages/module/patternfly-docs/content/examples/CustomToolbarExample.tsx) - Dashboard with custom toolbar controls ## Key Components diff --git a/jest.config.js b/jest.config.js index 708e296..2082387 100644 --- a/jest.config.js +++ b/jest.config.js @@ -10,6 +10,9 @@ module.exports = { transform: { '^.+\\.[jt]sx?$': 'babel-jest' }, + transformIgnorePatterns: [ + 'node_modules/(?!@patternfly)' + ], moduleNameMapper: { '\\.(css|less)$': '/styleMock.js' }, diff --git a/packages/module/package.json b/packages/module/package.json index 185e56c..88e4f92 100644 --- a/packages/module/package.json +++ b/packages/module/package.json @@ -5,8 +5,8 @@ "main": "dist/esm/index.js", "module": "dist/esm/index.js", "scripts": { - "build": "tsc --build --verbose ./tsconfig.json", - "build:watch": "tsc --build --verbose --watch ./tsconfig.json", + "build": "tsc --build --verbose ./tsconfig.json && cp src/styles.css dist/esm/styles.css", + "build:watch": "tsc --build --verbose --watch ./tsconfig.json & nodemon --watch src/styles.css --exec 'cp src/styles.css dist/esm/styles.css'", "build:fed:packages": "node generate-fed-package-json.js", "clean": "rimraf dist", "docs:develop": "pf-docs-framework start", @@ -36,8 +36,8 @@ "react-grid-layout": "^1.5.1" }, "peerDependencies": { - "react": "^18", - "react-dom": "^18" + "react": "^18 || ^19", + "react-dom": "^18 || ^19" }, "devDependencies": { "@babel/plugin-proposal-class-properties": "^7.18.6", diff --git a/packages/module/patternfly-docs/content/examples/BasicExample.tsx b/packages/module/patternfly-docs/content/examples/BasicExample.tsx index dec63a1..a0b3022 100644 --- a/packages/module/patternfly-docs/content/examples/BasicExample.tsx +++ b/packages/module/patternfly-docs/content/examples/BasicExample.tsx @@ -1,129 +1,158 @@ import React from 'react'; import WidgetLayout from '../../../src/WidgetLayout/WidgetLayout'; import { WidgetMapping, ExtendedTemplateConfig } from '../../../src/WidgetLayout/types'; -import { CubeIcon, ChartLineIcon, BellIcon, ExternalLinkAltIcon, ArrowRightIcon } from '@patternfly/react-icons'; -import { Card, CardBody, CardFooter, Content, Icon } from '@patternfly/react-core'; +import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon'; +import ChartLineIcon from '@patternfly/react-icons/dist/esm/icons/chart-line-icon'; +import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon'; +import ExternalLinkAltIcon from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon'; +import ArrowRightIcon from '@patternfly/react-icons/dist/esm/icons/arrow-right-icon'; +import { + Card, + CardBody, + CardFooter, + Content, + ContentVariants, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Icon, + List, + ListItem, +} from '@patternfly/react-core'; -interface SimpleWidgetProps { - id: number; - body: string; - linkTitle: string; - url: string; - isExternal?: boolean; -} - -const CardExample: React.FunctionComponent = (props) => ( - - - - - {props.body} - - +// A simple overview widget with key stats +const OverviewWidget = () => ( + + + + + Clusters + 12 + + + Running + 10 + + + Degraded + 1 + + + Stopped + 1 + + - - {props.isExternal ? ( - - {props.linkTitle} - - - - - ) : ( - - {props.linkTitle} - - - - - )} + + + View all clusters + + + + ); -// Example widget content components -const ExampleWidget1 = () => ( - -); - -const ExampleWidget2 = () => ( - +// A widget showing recent activity +const ActivityWidget = () => ( + + + Recent deployments and changes across your infrastructure. + + Production deploy completed — 2 min ago + Config update applied — 15 min ago + New node added to cluster-03 — 1 hr ago + Certificate renewed — 3 hr ago + Scaling policy triggered — 5 hr ago + + + + + View full report + + + + + + ); -const ExampleWidget3 = () => ( - +// A notifications widget +const NotificationsWidget = () => ( + + + + 3 alerts require attention + Maintenance window scheduled for Saturday + 2 patches available + + + + + View all notifications + + + + + + ); // Define widget mapping const widgetMapping: WidgetMapping = { - 'example-widget-1': { + 'overview-widget': { defaults: { w: 2, h: 3, maxH: 6, minH: 2 }, config: { - title: 'Example Widget', + title: 'Cluster Overview', icon: , headerLink: { title: 'View details', href: '#' } }, - renderWidget: () => + renderWidget: () => }, - 'chart-widget': { + 'activity-widget': { defaults: { w: 2, h: 4, maxH: 8, minH: 3 }, config: { - title: 'Chart Widget', - icon: + title: 'Recent Activity', + icon: , + headerLink: { + title: 'View full report', + href: '#' + } }, - renderWidget: () => + renderWidget: () => }, 'notifications-widget': { defaults: { w: 1, h: 3, maxH: 6, minH: 2 }, config: { - title: 'Notification Widget', + title: 'Notifications', icon: }, - renderWidget: () => + renderWidget: () => } }; // Define initial template const initialTemplate: ExtendedTemplateConfig = { xl: [ - { i: 'example-widget-1#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'example-widget-1', title: 'Example Widget' }, - { i: 'chart-widget#1', x: 2, y: 0, w: 2, h: 4, widgetType: 'chart-widget', title: 'Chart Widget' } + { i: 'overview-widget#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'overview-widget', title: 'Cluster Overview' }, + { i: 'activity-widget#1', x: 2, y: 0, w: 2, h: 4, widgetType: 'activity-widget', title: 'Recent Activity' }, ], lg: [ - { i: 'example-widget-1#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'example-widget-1', title: 'Example Widget' }, - { i: 'chart-widget#1', x: 0, y: 3, w: 2, h: 4, widgetType: 'chart-widget', title: 'Chart Widget' } + { i: 'overview-widget#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'overview-widget', title: 'Cluster Overview' }, + { i: 'activity-widget#1', x: 0, y: 3, w: 2, h: 4, widgetType: 'activity-widget', title: 'Recent Activity' }, ], md: [ - { i: 'example-widget-1#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'example-widget-1', title: 'Example Widget' }, - { i: 'chart-widget#1', x: 0, y: 3, w: 2, h: 4, widgetType: 'chart-widget', title: 'Chart Widget' } + { i: 'overview-widget#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'overview-widget', title: 'Cluster Overview' }, + { i: 'activity-widget#1', x: 0, y: 3, w: 2, h: 4, widgetType: 'activity-widget', title: 'Recent Activity' }, ], sm: [ - { i: 'example-widget-1#1', x: 0, y: 0, w: 1, h: 3, widgetType: 'example-widget-1', title: 'Example Widget' }, - { i: 'chart-widget#1', x: 0, y: 3, w: 1, h: 4, widgetType: 'chart-widget', title: 'Chart Widget' } + { i: 'overview-widget#1', x: 0, y: 0, w: 1, h: 3, widgetType: 'overview-widget', title: 'Cluster Overview' }, + { i: 'activity-widget#1', x: 0, y: 3, w: 1, h: 4, widgetType: 'activity-widget', title: 'Recent Activity' }, ] }; @@ -131,7 +160,7 @@ export const BasicExample: React.FunctionComponent = () => { const [template, setTemplate] = React.useState(initialTemplate); return ( -
+
( + + + + + Clusters + 12 + + + Running + 10 + + + Degraded + 1 + + + Stopped + 1 + + + + + + View all clusters + + + + +); + +const RecentActivityWidget = () => ( + + + Recent deployments and changes across your infrastructure. + + Production deploy completed — 2 min ago + Config update applied — 15 min ago + New node added to cluster-03 — 1 hr ago + Certificate renewed — 3 hr ago + + + + + View full report + + + + +); + +const NotificationsWidget = () => ( + + + + 3 alerts require attention + Maintenance window scheduled for Saturday + 2 patches available + + + + + View all notifications + + + + +); + +const ComplianceWidget = () => ( + + + + + Compliant + 87% + + + Policies + 24 active + + + Violations + 3 open + + + + + + View compliance report + + + + +); + +// Widget mapping with four widgets +const widgetMapping: WidgetMapping = { + 'cluster-overview': { + defaults: { w: 2, h: 3, maxH: 6, minH: 2 }, + config: { + title: 'Cluster Overview', + icon: , + headerLink: { title: 'View details', href: '#' }, + }, + renderWidget: () => , + }, + 'recent-activity': { + defaults: { w: 2, h: 4, maxH: 8, minH: 3 }, + config: { + title: 'Recent Activity', + icon: , + headerLink: { title: 'View full report', href: '#' }, + }, + renderWidget: () => , + }, + 'notifications': { + defaults: { w: 1, h: 3, maxH: 6, minH: 2 }, + config: { + title: 'Notifications', + icon: , + }, + renderWidget: () => , + }, + 'compliance': { + defaults: { w: 2, h: 3, maxH: 6, minH: 2 }, + config: { + title: 'Compliance', + icon: , + headerLink: { title: 'View report', href: '#' }, + }, + renderWidget: () => , + }, +}; + +// Initial template — only 2 of 4 widgets placed, leaving 2 for the drawer +const initialTemplate: ExtendedTemplateConfig = { + xl: [ + { i: 'cluster-overview#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'cluster-overview', title: 'Cluster Overview' }, + { i: 'recent-activity#1', x: 2, y: 0, w: 2, h: 4, widgetType: 'recent-activity', title: 'Recent Activity' }, + ], + lg: [ + { i: 'cluster-overview#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'cluster-overview', title: 'Cluster Overview' }, + { i: 'recent-activity#1', x: 0, y: 3, w: 2, h: 4, widgetType: 'recent-activity', title: 'Recent Activity' }, + ], + md: [ + { i: 'cluster-overview#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'cluster-overview', title: 'Cluster Overview' }, + { i: 'recent-activity#1', x: 0, y: 3, w: 2, h: 4, widgetType: 'recent-activity', title: 'Recent Activity' }, + ], + sm: [ + { i: 'cluster-overview#1', x: 0, y: 0, w: 1, h: 3, widgetType: 'cluster-overview', title: 'Cluster Overview' }, + { i: 'recent-activity#1', x: 0, y: 3, w: 1, h: 4, widgetType: 'recent-activity', title: 'Recent Activity' }, + ], +}; + +export const CustomToolbarExample: React.FunctionComponent = () => { + const [template, setTemplate] = useState(initialTemplate); + const [drawerOpen, setDrawerOpen] = useState(false); + const [isLocked, setIsLocked] = useState(false); + const [currentlyUsedWidgets, setCurrentlyUsedWidgets] = useState([]); + + const handleReset = () => { + setTemplate(initialTemplate); + setDrawerOpen(false); + }; + + return ( +
+ + + + + +
+ ); +}; diff --git a/packages/module/patternfly-docs/content/examples/LockedLayoutExample.tsx b/packages/module/patternfly-docs/content/examples/LockedLayoutExample.tsx index 1df063d..600a7ea 100644 --- a/packages/module/patternfly-docs/content/examples/LockedLayoutExample.tsx +++ b/packages/module/patternfly-docs/content/examples/LockedLayoutExample.tsx @@ -1,116 +1,125 @@ import React from 'react'; import WidgetLayout from '../../../src/WidgetLayout/WidgetLayout'; import { WidgetMapping, ExtendedTemplateConfig } from '../../../src/WidgetLayout/types'; -import { CubeIcon, ChartLineIcon, ExternalLinkAltIcon, ArrowRightIcon } from '@patternfly/react-icons'; -import { Card, CardBody, CardFooter, Content, Icon } from '@patternfly/react-core'; +import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon'; +import ChartLineIcon from '@patternfly/react-icons/dist/esm/icons/chart-line-icon'; +import ArrowRightIcon from '@patternfly/react-icons/dist/esm/icons/arrow-right-icon'; +import { + Card, + CardBody, + CardFooter, + Content, + ContentVariants, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Icon, + List, + ListItem, +} from '@patternfly/react-core'; -interface SimpleWidgetProps { - id: number; - body: string; - linkTitle: string; - url: string; - isExternal?: boolean; -} - -const CardExample: React.FunctionComponent = (props) => ( - - - - - {props.body} - - +const ClusterOverviewWidget = () => ( + + + + + Clusters + 12 + + + Running + 10 + + + Degraded + 1 + + + Stopped + 1 + + - - {props.isExternal ? ( - - {props.linkTitle} - - - - - ) : ( - - {props.linkTitle} - - - - - )} + + + View all clusters + + ); -// Example widget content components -const ExampleWidget1 = () => ( - -); - -const ExampleWidget2 = () => ( - +const RecentActivityWidget = () => ( + + + Recent deployments and changes across your infrastructure. + + Production deploy completed — 2 min ago + Config update applied — 15 min ago + New node added to cluster-03 — 1 hr ago + Certificate renewed — 3 hr ago + + + + + View full report + + + + ); // Define widget mapping const widgetMapping: WidgetMapping = { - 'example-widget-1': { + 'cluster-overview': { defaults: { w: 2, h: 3, maxH: 6, minH: 2 }, config: { - title: 'Example Widget', + title: 'Cluster Overview', icon: , headerLink: { title: 'View details', href: '#' } }, - renderWidget: () => + renderWidget: () => }, - 'chart-widget': { + 'recent-activity': { defaults: { w: 2, h: 4, maxH: 8, minH: 3 }, config: { - title: 'Chart Widget', - icon: + title: 'Recent Activity', + icon: , + headerLink: { + title: 'View full report', + href: '#' + } }, - renderWidget: () => + renderWidget: () => } }; // Define initial template with locked widgets const initialTemplate: ExtendedTemplateConfig = { xl: [ - { i: 'example-widget-1#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'example-widget-1', title: 'Example Widget', static: true }, - { i: 'chart-widget#1', x: 2, y: 0, w: 2, h: 4, widgetType: 'chart-widget', title: 'Chart Widget', static: true } + { i: 'cluster-overview#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'cluster-overview', title: 'Cluster Overview', static: true }, + { i: 'recent-activity#1', x: 2, y: 0, w: 2, h: 4, widgetType: 'recent-activity', title: 'Recent Activity', static: true } ], lg: [ - { i: 'example-widget-1#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'example-widget-1', title: 'Example Widget', static: true }, - { i: 'chart-widget#1', x: 0, y: 3, w: 2, h: 4, widgetType: 'chart-widget', title: 'Chart Widget', static: true } + { i: 'cluster-overview#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'cluster-overview', title: 'Cluster Overview', static: true }, + { i: 'recent-activity#1', x: 0, y: 3, w: 2, h: 4, widgetType: 'recent-activity', title: 'Recent Activity', static: true } ], md: [ - { i: 'example-widget-1#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'example-widget-1', title: 'Example Widget', static: true }, - { i: 'chart-widget#1', x: 0, y: 3, w: 2, h: 4, widgetType: 'chart-widget', title: 'Chart Widget', static: true } + { i: 'cluster-overview#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'cluster-overview', title: 'Cluster Overview', static: true }, + { i: 'recent-activity#1', x: 0, y: 3, w: 2, h: 4, widgetType: 'recent-activity', title: 'Recent Activity', static: true } ], sm: [ - { i: 'example-widget-1#1', x: 0, y: 0, w: 1, h: 3, widgetType: 'example-widget-1', title: 'Example Widget', static: true }, - { i: 'chart-widget#1', x: 0, y: 3, w: 1, h: 4, widgetType: 'chart-widget', title: 'Chart Widget', static: true } + { i: 'cluster-overview#1', x: 0, y: 0, w: 1, h: 3, widgetType: 'cluster-overview', title: 'Cluster Overview', static: true }, + { i: 'recent-activity#1', x: 0, y: 3, w: 1, h: 4, widgetType: 'recent-activity', title: 'Recent Activity', static: true } ] }; export const LockedLayoutExample: React.FunctionComponent = () => ( -
+
( />
); - diff --git a/packages/module/patternfly-docs/content/examples/WithoutDrawerExample.tsx b/packages/module/patternfly-docs/content/examples/WithoutDrawerExample.tsx deleted file mode 100644 index a6d96b4..0000000 --- a/packages/module/patternfly-docs/content/examples/WithoutDrawerExample.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import React from 'react'; -import WidgetLayout from '../../../src/WidgetLayout/WidgetLayout'; -import { WidgetMapping, ExtendedTemplateConfig } from '../../../src/WidgetLayout/types'; -import { CubeIcon, ChartLineIcon, BellIcon, ExternalLinkAltIcon, ArrowRightIcon } from '@patternfly/react-icons'; -import { Card, CardBody, CardFooter, Content, Icon } from '@patternfly/react-core'; - -interface SimpleWidgetProps { - id: number; - body: string; - linkTitle: string; - url: string; - isExternal?: boolean; -} - -const CardExample: React.FunctionComponent = (props) => ( - - - - - {props.body} - - - - - {props.isExternal ? ( - - {props.linkTitle} - - - - - ) : ( - - {props.linkTitle} - - - - - )} - - -); - -// Example widget content components -const ExampleWidget1 = () => ( - -); - -const ExampleWidget2 = () => ( - -); - -const ExampleWidget3 = () => ( - -); - -// Define widget mapping -const widgetMapping: WidgetMapping = { - 'example-widget-1': { - defaults: { w: 2, h: 3, maxH: 6, minH: 2 }, - config: { - title: 'Example Widget', - icon: , - headerLink: { - title: 'View details', - href: '#' - } - }, - renderWidget: () => - }, - 'chart-widget': { - defaults: { w: 2, h: 4, maxH: 8, minH: 3 }, - config: { - title: 'Chart Widget', - icon: - }, - renderWidget: () => - }, - 'notifications-widget': { - defaults: { w: 1, h: 3, maxH: 6, minH: 2 }, - config: { - title: 'Notification Widget', - icon: - }, - renderWidget: () => - } -}; - -// Define initial template -const initialTemplate: ExtendedTemplateConfig = { - xl: [ - { i: 'example-widget-1#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'example-widget-1', title: 'Example Widget' }, - { i: 'chart-widget#1', x: 2, y: 0, w: 2, h: 4, widgetType: 'chart-widget', title: 'Chart Widget' } - ], - lg: [ - { i: 'example-widget-1#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'example-widget-1', title: 'Example Widget' }, - { i: 'chart-widget#1', x: 0, y: 3, w: 2, h: 4, widgetType: 'chart-widget', title: 'Chart Widget' } - ], - md: [ - { i: 'example-widget-1#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'example-widget-1', title: 'Example Widget' }, - { i: 'chart-widget#1', x: 0, y: 3, w: 2, h: 4, widgetType: 'chart-widget', title: 'Chart Widget' } - ], - sm: [ - { i: 'example-widget-1#1', x: 0, y: 0, w: 1, h: 3, widgetType: 'example-widget-1', title: 'Example Widget' }, - { i: 'chart-widget#1', x: 0, y: 3, w: 1, h: 4, widgetType: 'chart-widget', title: 'Chart Widget' } - ] -}; - -export const WithoutDrawerExample: React.FunctionComponent = () => ( -
- { - // Template changes can be saved here - }} - /> -
-); - diff --git a/packages/module/patternfly-docs/content/examples/basic.md b/packages/module/patternfly-docs/content/examples/basic.md index 75eaf5b..f219fcf 100644 --- a/packages/module/patternfly-docs/content/examples/basic.md +++ b/packages/module/patternfly-docs/content/examples/basic.md @@ -9,15 +9,42 @@ id: Widgetized dashboard source: react # If you use typescript, the name of the interface to display props for # These are found through the sourceProps function provided in patternfly-docs.source.js -propComponents: ['WidgetLayout', 'GridLayout', 'WidgetDrawer'] +propComponents: ['WidgetLayout', 'GridLayout', 'WidgetDrawer', 'AddWidgetsButton'] sortValue: 1 sourceLink: https://github.com/patternfly/widgetized-dashboard --- import { FunctionComponent, useState } from 'react'; -import { ExternalLinkAltIcon, ArrowRightIcon, CubeIcon, ChartLineIcon, BellIcon } from '@patternfly/react-icons'; -import { Card, CardBody, CardFooter, Content, Icon } from '@patternfly/react-core'; -import { WidgetLayout, GridLayout, WidgetDrawer } from '@patternfly/widgetized-dashboard'; +import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon'; +import ChartLineIcon from '@patternfly/react-icons/dist/esm/icons/chart-line-icon'; +import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon'; +import ShieldAltIcon from '@patternfly/react-icons/dist/esm/icons/shield-alt-icon'; +import LockIcon from '@patternfly/react-icons/dist/esm/icons/lock-icon'; +import LockOpenIcon from '@patternfly/react-icons/dist/esm/icons/lock-open-icon'; +import UndoIcon from '@patternfly/react-icons/dist/esm/icons/undo-icon'; +import ArrowRightIcon from '@patternfly/react-icons/dist/esm/icons/arrow-right-icon'; +import ExternalLinkAltIcon from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon'; +import { + Button, + Card, + CardBody, + CardFooter, + Content, + ContentVariants, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Icon, + List, + ListItem, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, + Tooltip, +} from '@patternfly/react-core'; +import { WidgetLayout, GridLayout, WidgetDrawer, AddWidgetsButton } from '@patternfly/widgetized-dashboard'; ### Basic usage @@ -37,11 +64,11 @@ Use `isLayoutLocked` to prevent users from modifying the layout. ``` -### Without drawer +### Custom toolbar -You can hide the widget drawer by setting `showDrawer={false}`. +Use `GridLayout`, `WidgetDrawer`, and `AddWidgetsButton` directly to build a custom toolbar with lock/unlock, reset, and other controls. -```js file="./WithoutDrawerExample.tsx" +```js file="./CustomToolbarExample.tsx" isFullscreen ``` diff --git a/packages/module/patternfly-docs/patternfly-docs.css.js b/packages/module/patternfly-docs/patternfly-docs.css.js index 240ac3b..c715f81 100644 --- a/packages/module/patternfly-docs/patternfly-docs.css.js +++ b/packages/module/patternfly-docs/patternfly-docs.css.js @@ -6,3 +6,4 @@ import '@patternfly/patternfly/patternfly-addons.css'; import '@patternfly/documentation-framework/global.css'; // Add your extension CSS below +import '../src/styles.css'; diff --git a/packages/module/src/WidgetLayout/AddWidgetsButton.tsx b/packages/module/src/WidgetLayout/AddWidgetsButton.tsx new file mode 100644 index 0000000..c10f4dc --- /dev/null +++ b/packages/module/src/WidgetLayout/AddWidgetsButton.tsx @@ -0,0 +1,27 @@ +import { Button, ButtonProps } from '@patternfly/react-core'; +import PlusCircleIcon from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon'; +import React from 'react'; + +export interface AddWidgetsButtonProps extends Omit { + /** Callback when the button is clicked */ + onClick: () => void; +} + +const AddWidgetsButton: React.FunctionComponent = ({ + onClick, + children = 'Add widgets', + variant = 'secondary', + icon = , + ...rest +}) => ( + +); + +export default AddWidgetsButton; diff --git a/packages/module/src/WidgetLayout/GridLayout.tsx b/packages/module/src/WidgetLayout/GridLayout.tsx index d8991b4..651f8eb 100644 --- a/packages/module/src/WidgetLayout/GridLayout.tsx +++ b/packages/module/src/WidgetLayout/GridLayout.tsx @@ -1,5 +1,4 @@ import 'react-grid-layout/css/styles.css'; -import './styles'; import ReactGridLayout, { Layout, ReactGridLayoutProps } from 'react-grid-layout'; import GridTile, { SetWidgetAttribute } from './GridTile'; import { useEffect, useMemo, useRef, useState } from 'react'; @@ -14,7 +13,9 @@ import { WidgetConfiguration, } from './types'; import { Button, EmptyState, EmptyStateActions, EmptyStateBody, EmptyStateVariant, PageSection } from '@patternfly/react-core'; -import { ExternalLinkAltIcon, GripVerticalIcon, PlusCircleIcon } from '@patternfly/react-icons'; +import ExternalLinkAltIcon from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon'; +import GripVerticalIcon from '@patternfly/react-icons/dist/esm/icons/grip-vertical-icon'; +import PlusCircleIcon from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon'; import { columns, breakpoints, droppingElemId, getWidgetIdentifier, extendLayout, getGridDimensions } from './utils'; export const defaultBreakpoints = breakpoints; @@ -27,12 +28,11 @@ const createSerializableConfig = (config?: WidgetConfiguration) => { }; }; -// SVG resize handle as inline data URI -const resizeHandleSvg = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE2IDEuMTQyODZMMTQuODU3MSAwTDAgMTQuODU3MVYxNkgxLjE0Mjg2TDE2IDEuMTQyODZaIiBmaWxsPSIjRDJEMkQyIi8+Cjwvc3ZnPgo='; - const getResizeHandle = (resizeHandleAxis: string, ref: React.Ref) => (
- Resize handle +
); @@ -71,15 +71,15 @@ const LayoutEmptyState = ({ }, [onDrawerExpandChange]); return ( - - + + You don't have any widgets on your dashboard. To populate your dashboard, drag items from the widget drawer to this dashboard. {documentationLink && ( - @@ -242,13 +242,13 @@ const GridLayout = ({ const activeLayout = internalTemplate[layoutVariant] || []; return ( -
+
{activeLayout.length === 0 && !currentDropInItem && showEmptyState && ( emptyStateComponent || )} {activeLayout - .map((layoutItem, index) => { + .map((layoutItem) => { const { widgetType } = layoutItem; const widget = widgetMapping[widgetType]; if (!widget) { @@ -274,12 +274,18 @@ const GridLayout = ({ } const config = widgetMapping[widgetType]?.config; return ( -
+
{ setWidgetAttribute(widgetConfig.i, 'h', widgetConfig.maxH ?? widgetConfig.h); @@ -95,7 +100,7 @@ const GridTile = ({ }} icon={} > - Autosize height to content + Maximize height + } @@ -123,7 +128,7 @@ const GridTile = ({ > Remove - + {"All 'removed' widgets can be added back by clicking the 'Add widgets' button."} @@ -134,8 +139,7 @@ const GridTile = ({ const headerActions = ( <> - Actions

}> - ) => ( setIsOpen((prev) => !prev)} variant="plain" - aria-label="widget actions menu toggle" + aria-label="Widget actions" > @@ -158,19 +163,18 @@ const GridTile = ({ > {dropdownItems} -
- {widgetConfig.static ? 'Widget locked' : 'Move'}

}> + {widgetConfig.static ? 'Widget locked' : 'Move'}

}> { setIsDragging(true); analytics?.('widget-layout.widget-move', { widgetType }); }} onMouseUp={() => setIsDragging(false)} - className={clsx('drag-handle', { - dragging: isDragging, + className={clsx('pf-v6-widget-drag-handle', { + 'pf-v6-widget-drag-handle--dragging': isDragging, })} > - +
@@ -180,25 +184,24 @@ const GridTile = ({ - + - + {widgetConfig?.config?.icon && ( -
+ {isLoaded ? widgetConfig.config.icon : } -
+ )} - + {isLoaded ? ( {widgetConfig?.config?.title || widgetType} @@ -208,7 +211,7 @@ const GridTile = ({ {hasHeader && isLoaded && (