Skip to content

Commit 1fba4bf

Browse files
nicolethoenclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 85c6957 commit 1fba4bf

21 files changed

Lines changed: 1104 additions & 532 deletions

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,23 @@ This library requires React 18+ and React DOM 18+ as peer dependencies. Make sur
2929
yarn add react@^18 react-dom@^18
3030
```
3131

32+
## Styles
33+
34+
Import the required stylesheet in your application entry point:
35+
36+
```ts
37+
import '@patternfly/widgetized-dashboard/dist/esm/styles.css';
38+
```
39+
40+
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.
41+
3242
## Quick Start
3343

3444
```tsx
3545
import React from 'react';
3646
import { WidgetLayout, WidgetMapping, ExtendedTemplateConfig } from '@patternfly/widgetized-dashboard';
37-
import { CubeIcon } from '@patternfly/react-icons';
47+
import '@patternfly/widgetized-dashboard/dist/esm/styles.css';
48+
import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon';
3849

3950
// Define your widgets
4051
const widgetMapping: WidgetMapping = {
@@ -86,7 +97,7 @@ function App() {
8697

8798
- [Basic Example](./packages/module/patternfly-docs/content/examples/BasicExample.tsx) - Complete dashboard with drawer
8899
- [Locked Layout Example](./packages/module/patternfly-docs/content/examples/LockedLayoutExample.tsx) - Dashboard with locked widgets
89-
- [Without Drawer Example](./packages/module/patternfly-docs/content/examples/WithoutDrawerExample.tsx) - Grid layout without widget drawer
100+
- [Custom Toolbar Example](./packages/module/patternfly-docs/content/examples/CustomToolbarExample.tsx) - Dashboard with custom toolbar controls
90101

91102
## Key Components
92103

jest.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ module.exports = {
1010
transform: {
1111
'^.+\\.[jt]sx?$': 'babel-jest'
1212
},
13+
transformIgnorePatterns: [
14+
'node_modules/(?!@patternfly)'
15+
],
1316
moduleNameMapper: {
1417
'\\.(css|less)$': '<rootDir>/styleMock.js'
1518
},

packages/module/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
"main": "dist/esm/index.js",
66
"module": "dist/esm/index.js",
77
"scripts": {
8-
"build": "tsc --build --verbose ./tsconfig.json",
9-
"build:watch": "tsc --build --verbose --watch ./tsconfig.json",
8+
"build": "tsc --build --verbose ./tsconfig.json && cp src/styles.css dist/esm/styles.css",
9+
"build:watch": "tsc --build --verbose --watch ./tsconfig.json & nodemon --watch src/styles.css --exec 'cp src/styles.css dist/esm/styles.css'",
1010
"build:fed:packages": "node generate-fed-package-json.js",
1111
"clean": "rimraf dist",
1212
"docs:develop": "pf-docs-framework start",
@@ -36,8 +36,8 @@
3636
"react-grid-layout": "^1.5.1"
3737
},
3838
"peerDependencies": {
39-
"react": "^18",
40-
"react-dom": "^18"
39+
"react": "^18 || ^19",
40+
"react-dom": "^18 || ^19"
4141
},
4242
"devDependencies": {
4343
"@babel/plugin-proposal-class-properties": "^7.18.6",

packages/module/patternfly-docs/content/examples/BasicExample.tsx

Lines changed: 111 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,137 +1,166 @@
11
import React from 'react';
22
import WidgetLayout from '../../../src/WidgetLayout/WidgetLayout';
33
import { WidgetMapping, ExtendedTemplateConfig } from '../../../src/WidgetLayout/types';
4-
import { CubeIcon, ChartLineIcon, BellIcon, ExternalLinkAltIcon, ArrowRightIcon } from '@patternfly/react-icons';
5-
import { Card, CardBody, CardFooter, Content, Icon } from '@patternfly/react-core';
4+
import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon';
5+
import ChartLineIcon from '@patternfly/react-icons/dist/esm/icons/chart-line-icon';
6+
import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon';
7+
import ExternalLinkAltIcon from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon';
8+
import ArrowRightIcon from '@patternfly/react-icons/dist/esm/icons/arrow-right-icon';
9+
import {
10+
Card,
11+
CardBody,
12+
CardFooter,
13+
Content,
14+
ContentVariants,
15+
DescriptionList,
16+
DescriptionListDescription,
17+
DescriptionListGroup,
18+
DescriptionListTerm,
19+
Icon,
20+
List,
21+
ListItem,
22+
} from '@patternfly/react-core';
623

7-
interface SimpleWidgetProps {
8-
id: number;
9-
body: string;
10-
linkTitle: string;
11-
url: string;
12-
isExternal?: boolean;
13-
}
14-
15-
const CardExample: React.FunctionComponent<SimpleWidgetProps> = (props) => (
16-
<Card isPlain>
17-
<CardBody className="pf-v6-u-p-md pf-v6-u-pb-0">
18-
<Content
19-
key={props.id}
20-
className="pf-v6-u-display-flex pf-v6-u-flex-direction-column"
21-
>
22-
<Content component="p" className="pf-v6-u-flex-grow-1">
23-
{props.body}
24-
</Content>
25-
</Content>
24+
// A simple overview widget with key stats
25+
const OverviewWidget = () => (
26+
<Card isPlain isFullHeight>
27+
<CardBody>
28+
<DescriptionList isHorizontal isCompact>
29+
<DescriptionListGroup>
30+
<DescriptionListTerm>Clusters</DescriptionListTerm>
31+
<DescriptionListDescription>12</DescriptionListDescription>
32+
</DescriptionListGroup>
33+
<DescriptionListGroup>
34+
<DescriptionListTerm>Running</DescriptionListTerm>
35+
<DescriptionListDescription>10</DescriptionListDescription>
36+
</DescriptionListGroup>
37+
<DescriptionListGroup>
38+
<DescriptionListTerm>Degraded</DescriptionListTerm>
39+
<DescriptionListDescription>1</DescriptionListDescription>
40+
</DescriptionListGroup>
41+
<DescriptionListGroup>
42+
<DescriptionListTerm>Stopped</DescriptionListTerm>
43+
<DescriptionListDescription>1</DescriptionListDescription>
44+
</DescriptionListGroup>
45+
</DescriptionList>
2646
</CardBody>
27-
<CardFooter className="pf-v6-u-p-md">
28-
{props.isExternal ? (
29-
<a href={props.url}>
30-
{props.linkTitle}
31-
<Icon className="pf-v6-u-ml-sm" isInline>
32-
<ExternalLinkAltIcon />
33-
</Icon>
34-
</a>
35-
) : (
36-
<a href={props.url}>
37-
{props.linkTitle}
38-
<Icon className="pf-v6-u-ml-sm" isInline>
39-
<ArrowRightIcon />
40-
</Icon>
41-
</a>
42-
)}
47+
<CardFooter>
48+
<a href="#">
49+
View all clusters
50+
<Icon isInline>
51+
<ArrowRightIcon />
52+
</Icon>
53+
</a>
4354
</CardFooter>
4455
</Card>
4556
);
4657

47-
// Example widget content components
48-
const ExampleWidget1 = () => (
49-
<CardExample
50-
id={1}
51-
body="This is the content of the first example widget. You can put any React content here!"
52-
linkTitle="View details"
53-
url="https://www.patternfly.org"
54-
isExternal={true}
55-
/>
56-
);
57-
58-
const ExampleWidget2 = () => (
59-
<CardExample
60-
id={2}
61-
body="Chart and visualization content would be displayed here."
62-
linkTitle="View full report"
63-
url="#"
64-
isExternal={false}
65-
/>
58+
// A widget showing recent activity
59+
const ActivityWidget = () => (
60+
<Card isPlain isFullHeight>
61+
<CardBody>
62+
<Content component={ContentVariants.p}>Recent deployments and changes across your infrastructure.</Content>
63+
<List isPlain>
64+
<ListItem>Production deploy completed — 2 min ago</ListItem>
65+
<ListItem>Config update applied — 15 min ago</ListItem>
66+
<ListItem>New node added to cluster-03 — 1 hr ago</ListItem>
67+
<ListItem>Certificate renewed — 3 hr ago</ListItem>
68+
<ListItem>Scaling policy triggered — 5 hr ago</ListItem>
69+
</List>
70+
</CardBody>
71+
<CardFooter>
72+
<a href="#">
73+
View full report
74+
<Icon isInline>
75+
<ArrowRightIcon />
76+
</Icon>
77+
</a>
78+
</CardFooter>
79+
</Card>
6680
);
6781

68-
const ExampleWidget3 = () => (
69-
<CardExample
70-
id={3}
71-
body="Recent notifications and updates will appear in this widget."
72-
linkTitle="View all notifications"
73-
url="#"
74-
isExternal={false}
75-
/>
82+
// A notifications widget
83+
const NotificationsWidget = () => (
84+
<Card isPlain isFullHeight>
85+
<CardBody>
86+
<List isPlain>
87+
<ListItem>3 alerts require attention</ListItem>
88+
<ListItem>Maintenance window scheduled for Saturday</ListItem>
89+
<ListItem>2 patches available</ListItem>
90+
</List>
91+
</CardBody>
92+
<CardFooter>
93+
<a href="https://www.patternfly.org">
94+
View all notifications
95+
<Icon isInline>
96+
<ExternalLinkAltIcon />
97+
</Icon>
98+
</a>
99+
</CardFooter>
100+
</Card>
76101
);
77102

78103
// Define widget mapping
79104
const widgetMapping: WidgetMapping = {
80-
'example-widget-1': {
105+
'overview-widget': {
81106
defaults: { w: 2, h: 3, maxH: 6, minH: 2 },
82107
config: {
83-
title: 'Example Widget',
108+
title: 'Cluster Overview',
84109
icon: <CubeIcon />,
85110
headerLink: {
86111
title: 'View details',
87112
href: '#'
88113
}
89114
},
90-
renderWidget: () => <ExampleWidget1 />
115+
renderWidget: () => <OverviewWidget />
91116
},
92-
'chart-widget': {
117+
'activity-widget': {
93118
defaults: { w: 2, h: 4, maxH: 8, minH: 3 },
94119
config: {
95-
title: 'Chart Widget',
96-
icon: <ChartLineIcon />
120+
title: 'Recent Activity',
121+
icon: <ChartLineIcon />,
122+
headerLink: {
123+
title: 'View full report',
124+
href: '#'
125+
}
97126
},
98-
renderWidget: () => <ExampleWidget2 />
127+
renderWidget: () => <ActivityWidget />
99128
},
100129
'notifications-widget': {
101130
defaults: { w: 1, h: 3, maxH: 6, minH: 2 },
102131
config: {
103-
title: 'Notification Widget',
132+
title: 'Notifications',
104133
icon: <BellIcon />
105134
},
106-
renderWidget: () => <ExampleWidget3 />
135+
renderWidget: () => <NotificationsWidget />
107136
}
108137
};
109138

110139
// Define initial template
111140
const initialTemplate: ExtendedTemplateConfig = {
112141
xl: [
113-
{ i: 'example-widget-1#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'example-widget-1', title: 'Example Widget' },
114-
{ i: 'chart-widget#1', x: 2, y: 0, w: 2, h: 4, widgetType: 'chart-widget', title: 'Chart Widget' }
142+
{ i: 'overview-widget#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'overview-widget', title: 'Cluster Overview' },
143+
{ i: 'activity-widget#1', x: 2, y: 0, w: 2, h: 4, widgetType: 'activity-widget', title: 'Recent Activity' },
115144
],
116145
lg: [
117-
{ i: 'example-widget-1#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'example-widget-1', title: 'Example Widget' },
118-
{ i: 'chart-widget#1', x: 0, y: 3, w: 2, h: 4, widgetType: 'chart-widget', title: 'Chart Widget' }
146+
{ i: 'overview-widget#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'overview-widget', title: 'Cluster Overview' },
147+
{ i: 'activity-widget#1', x: 0, y: 3, w: 2, h: 4, widgetType: 'activity-widget', title: 'Recent Activity' },
119148
],
120149
md: [
121-
{ i: 'example-widget-1#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'example-widget-1', title: 'Example Widget' },
122-
{ i: 'chart-widget#1', x: 0, y: 3, w: 2, h: 4, widgetType: 'chart-widget', title: 'Chart Widget' }
150+
{ i: 'overview-widget#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'overview-widget', title: 'Cluster Overview' },
151+
{ i: 'activity-widget#1', x: 0, y: 3, w: 2, h: 4, widgetType: 'activity-widget', title: 'Recent Activity' },
123152
],
124153
sm: [
125-
{ i: 'example-widget-1#1', x: 0, y: 0, w: 1, h: 3, widgetType: 'example-widget-1', title: 'Example Widget' },
126-
{ i: 'chart-widget#1', x: 0, y: 3, w: 1, h: 4, widgetType: 'chart-widget', title: 'Chart Widget' }
154+
{ i: 'overview-widget#1', x: 0, y: 0, w: 1, h: 3, widgetType: 'overview-widget', title: 'Cluster Overview' },
155+
{ i: 'activity-widget#1', x: 0, y: 3, w: 1, h: 4, widgetType: 'activity-widget', title: 'Recent Activity' },
127156
]
128157
};
129158

130159
export const BasicExample: React.FunctionComponent = () => {
131160
const [template, setTemplate] = React.useState(initialTemplate);
132161

133162
return (
134-
<div style={{ height: '600px', width: '100%', overflow: 'auto', position: 'relative', isolation: 'isolate' }}>
163+
<div style={{ height: '800px', overflowY: 'scroll' }}>
135164
<WidgetLayout
136165
widgetMapping={widgetMapping}
137166
initialTemplate={template}

0 commit comments

Comments
 (0)