From c031d33211e17dbad1db2b28f5a6f0ce1cd46204 Mon Sep 17 00:00:00 2001
From: alex
Date: Tue, 21 Oct 2025 08:08:32 -0400
Subject: [PATCH 1/6] feat: init widget dashboard
---
ARCHITECTURE.md | 381 ++++++++++++++++++
MIGRATION.md | 256 ++++++++++++
README.md | 145 ++++++-
packages/module/package.json | 11 +-
.../design-guidelines/design-guidelines.md | 23 --
.../content/examples/Basic.tsx | 4 -
.../content/examples/BasicExample.tsx | 110 +++++
.../content/examples/LockedLayoutExample.tsx | 87 ++++
.../content/examples/WithoutDrawerExample.tsx | 106 +++++
.../patternfly-docs/content/examples/basic.md | 75 +++-
.../src/ExtendedButton/ExtendedButton.tsx | 30 --
.../__tests__/ExtendedButton.test.tsx | 72 ----
.../ExtendedButton.test.tsx.snap | 19 -
packages/module/src/ExtendedButton/index.ts | 1 -
.../module/src/WidgetLayout/GridLayout.tsx | 290 +++++++++++++
packages/module/src/WidgetLayout/GridTile.tsx | 230 +++++++++++
.../module/src/WidgetLayout/WidgetDrawer.tsx | 166 ++++++++
.../module/src/WidgetLayout/WidgetLayout.tsx | 131 ++++++
.../__tests__/WidgetLayout.test.tsx | 123 ++++++
.../__snapshots__/WidgetLayout.test.tsx.snap | 12 +
.../src/WidgetLayout/__tests__/utils.test.ts | 114 ++++++
packages/module/src/WidgetLayout/images.d.ts | 5 +
packages/module/src/WidgetLayout/index.ts | 23 ++
packages/module/src/WidgetLayout/styles.ts | 134 ++++++
packages/module/src/WidgetLayout/types.ts | 82 ++++
packages/module/src/WidgetLayout/utils.ts | 71 ++++
packages/module/src/index.ts | 13 +-
yarn.lock | 186 ++++++++-
28 files changed, 2736 insertions(+), 164 deletions(-)
create mode 100644 ARCHITECTURE.md
create mode 100644 MIGRATION.md
delete mode 100644 packages/module/patternfly-docs/content/design-guidelines/design-guidelines.md
delete mode 100644 packages/module/patternfly-docs/content/examples/Basic.tsx
create mode 100644 packages/module/patternfly-docs/content/examples/BasicExample.tsx
create mode 100644 packages/module/patternfly-docs/content/examples/LockedLayoutExample.tsx
create mode 100644 packages/module/patternfly-docs/content/examples/WithoutDrawerExample.tsx
delete mode 100644 packages/module/src/ExtendedButton/ExtendedButton.tsx
delete mode 100644 packages/module/src/ExtendedButton/__tests__/ExtendedButton.test.tsx
delete mode 100644 packages/module/src/ExtendedButton/__tests__/__snapshots__/ExtendedButton.test.tsx.snap
delete mode 100644 packages/module/src/ExtendedButton/index.ts
create mode 100644 packages/module/src/WidgetLayout/GridLayout.tsx
create mode 100644 packages/module/src/WidgetLayout/GridTile.tsx
create mode 100644 packages/module/src/WidgetLayout/WidgetDrawer.tsx
create mode 100644 packages/module/src/WidgetLayout/WidgetLayout.tsx
create mode 100644 packages/module/src/WidgetLayout/__tests__/WidgetLayout.test.tsx
create mode 100644 packages/module/src/WidgetLayout/__tests__/__snapshots__/WidgetLayout.test.tsx.snap
create mode 100644 packages/module/src/WidgetLayout/__tests__/utils.test.ts
create mode 100644 packages/module/src/WidgetLayout/images.d.ts
create mode 100644 packages/module/src/WidgetLayout/index.ts
create mode 100644 packages/module/src/WidgetLayout/styles.ts
create mode 100644 packages/module/src/WidgetLayout/types.ts
create mode 100644 packages/module/src/WidgetLayout/utils.ts
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
new file mode 100644
index 0000000..e6e4b3f
--- /dev/null
+++ b/ARCHITECTURE.md
@@ -0,0 +1,381 @@
+# Architecture Overview
+
+## Component Structure
+
+The Widgetized Dashboard is composed of several key components that work together to provide a complete dashboard experience:
+
+```
+WidgetLayout (main component)
+├── WidgetDrawer (widget selection)
+│ └── WidgetWrapper (draggable widget cards)
+└── GridLayout (drag-and-drop grid)
+ └── GridTile (individual widget wrapper)
+ └── Widget Content (user-provided via renderWidget)
+```
+
+## Core Components
+
+### 1. WidgetLayout
+The top-level component that orchestrates the entire dashboard experience.
+
+**Responsibilities:**
+- Manages overall state (template, drawer open/close)
+- Coordinates between GridLayout and WidgetDrawer
+- Provides unified API for consumers
+
+**State:**
+- `template`: Current layout configuration
+- `drawerOpen`: Whether widget drawer is visible
+- `currentlyUsedWidgets`: List of widget types in use
+
+### 2. GridLayout
+The core layout engine powered by `react-grid-layout`.
+
+**Responsibilities:**
+- Rendering the responsive grid
+- Handling drag-and-drop operations
+- Managing widget positions and sizes
+- Responsive breakpoint handling
+- Widget CRUD operations
+
+**Key Features:**
+- 4 responsive breakpoints (xl, lg, md, sm)
+- Drag-and-drop from drawer
+- Resize with corner handles
+- Lock/unlock individual widgets
+- Empty state display
+
+**State:**
+- `isDragging`: Drag operation in progress
+- `isInitialRender`: Skip save on first render
+- `layoutVariant`: Current breakpoint
+- `layoutWidth`: Container width
+- `currentDropInItem`: Widget being dragged
+
+### 3. GridTile
+Wrapper component for individual widgets.
+
+**Responsibilities:**
+- Render widget content
+- Provide widget actions menu
+- Display widget header with icon and title
+- Handle widget-level interactions
+
+**Features:**
+- Lock/unlock toggle
+- Autosize to max height
+- Minimize to min height
+- Remove widget
+- Optional header link
+- Drag handle
+
+### 4. WidgetDrawer
+Side panel for selecting widgets to add.
+
+**Responsibilities:**
+- Display available widgets
+- Filter out currently used widgets
+- Provide drag source for new widgets
+- Instructions for users
+
+## Data Flow
+
+### Template Management
+
+```
+┌─────────────┐
+│ Parent │
+│ Component │
+└──────┬──────┘
+ │
+ │ initialTemplate (prop)
+ ▼
+┌─────────────┐
+│ WidgetLayout│
+│ (state) │
+└──────┬──────┘
+ │
+ │ template (prop)
+ ▼
+┌─────────────┐
+│ GridLayout │
+│ (renders) │
+└──────┬──────┘
+ │
+ │ onChange
+ ▼
+┌─────────────┐
+│onTemplateChange│
+│ (callback) │
+└──────┬──────┘
+ │
+ │ save to API/storage
+ ▼
+┌─────────────┐
+│ Parent │
+│ Component │
+└─────────────┘
+```
+
+### Widget Rendering
+
+```
+┌──────────────┐
+│WidgetMapping │
+│ (config) │
+└──────┬───────┘
+ │
+ │ widgetType → renderWidget
+ ▼
+┌──────────────┐
+│ GridTile │
+│ (wrapper) │
+└──────┬───────┘
+ │
+ │ renders children
+ ▼
+┌──────────────┐
+│Widget Content│
+│ (React node) │
+└──────────────┘
+```
+
+## State Management
+
+The component uses **local React state** for all state management:
+
+- No external state libraries required (no Jotai, Redux, etc.)
+- Parent controls template via `initialTemplate` and `onTemplateChange`
+- Internal state synchronized with props
+- Callbacks for notifications, analytics, etc.
+
+### State Flow
+
+```typescript
+// Parent manages template
+const [template, setTemplate] = useState(initialTemplate);
+
+// WidgetLayout receives and manages internally
+ {
+ setTemplate(newTemplate);
+ saveToAPI(newTemplate);
+ }}
+/>
+
+// Internal state stays in sync
+useEffect(() => {
+ setInternalTemplate(template);
+}, [template]);
+```
+
+## Responsive Design
+
+### Breakpoints
+
+| Breakpoint | Width | Columns | Use Case |
+|-----------|------------|---------|----------|
+| xl | ≥1550px | 4 | Large desktop |
+| lg | 1400-1549px| 3 | Desktop |
+| md | 1100-1399px| 2 | Tablet landscape |
+| sm | 800-1099px | 1 | Tablet portrait |
+
+### Grid System
+
+- **Row Height**: 56px (fixed)
+- **Container**: 100% width, min-height 200px
+- **Columns**: Responsive (4/3/2/1)
+- **Gutter**: Managed by react-grid-layout
+
+### Responsive Behavior
+
+```typescript
+// Auto-detect breakpoint on mount and resize
+const observer = new ResizeObserver((entries) => {
+ const width = entries[0].contentRect.width;
+ const variant = getGridDimensions(width);
+ setLayoutVariant(variant);
+});
+```
+
+Each template must define layouts for all breakpoints:
+
+```typescript
+const template: ExtendedTemplateConfig = {
+ xl: [...], // 4 columns
+ lg: [...], // 3 columns
+ md: [...], // 2 columns
+ sm: [...] // 1 column
+};
+```
+
+## Type System
+
+### Core Types
+
+```typescript
+// Widget definition
+type WidgetMapping = {
+ [widgetType: string]: {
+ defaults: WidgetDefaults;
+ config?: WidgetConfiguration;
+ renderWidget: (widgetId: string) => React.ReactNode;
+ };
+};
+
+// Layout configuration
+type ExtendedTemplateConfig = {
+ xl: ExtendedLayoutItem[];
+ lg: ExtendedLayoutItem[];
+ md: ExtendedLayoutItem[];
+ sm: ExtendedLayoutItem[];
+};
+
+// Individual widget placement
+type ExtendedLayoutItem = Layout & {
+ widgetType: string;
+ title: string;
+ config?: WidgetConfiguration;
+ locked?: boolean;
+};
+```
+
+## Performance Considerations
+
+### Optimization Strategies
+
+1. **Memoization**
+ - `useMemo` for dropping item template
+ - `useMemo` for dropdown items
+ - `useMemo` for header links
+
+2. **Conditional Rendering**
+ - Empty state only when needed
+ - Drawer only when `showDrawer={true}`
+ - Resize handles only on hover
+
+3. **Event Handling**
+ - No inline function creation in render
+ - Callbacks defined at component level
+ - Event handlers properly memoized
+
+4. **Grid Reset**
+ - Key prop forces remount on breakpoint change
+ - Necessary for proper react-grid-layout behavior
+ - `key={'grid-' + layoutVariant}`
+
+## Integration Points
+
+### With PatternFly
+
+- Uses PatternFly Card for widget containers
+- Uses PatternFly EmptyState for empty dashboard
+- Uses PatternFly Dropdown for actions menu
+- Uses PatternFly Icons throughout
+- Follows PatternFly design tokens
+
+### With react-grid-layout
+
+- Wraps ReactGridLayout component
+- Custom resize handles
+- Custom drag handles
+- Responsive breakpoints
+- Layout persistence
+
+### With React Router
+
+- Optional Link component in GridTile
+- Handles internal/external links
+- Target blank for external links
+
+## Extensibility
+
+### Custom Empty State
+
+```typescript
+}
+/>
+```
+
+### Custom Analytics
+
+```typescript
+ {
+ myAnalytics.track(event, data);
+ }}
+/>
+```
+
+### Custom Notifications
+
+```typescript
+ {
+ toast.show(notification.title, {
+ variant: notification.variant
+ });
+ }}
+/>
+```
+
+## File Structure
+
+```
+src/WidgetLayout/
+├── types.ts # TypeScript type definitions
+├── utils.ts # Utility functions
+├── GridLayout.tsx # Main grid component
+├── GridLayout.scss # Grid styles
+├── GridTile.tsx # Widget wrapper component
+├── GridTile.scss # Tile styles
+├── WidgetDrawer.tsx # Widget selection drawer
+├── WidgetDrawer.scss # Drawer styles
+├── WidgetLayout.tsx # Main component
+├── index.ts # Public exports
+├── resize-handle.svg # Resize handle icon
+└── __tests__/ # Test files
+ ├── WidgetLayout.test.tsx
+ ├── utils.test.ts
+ └── __snapshots__/
+```
+
+## Dependencies
+
+### Runtime Dependencies
+
+- `react` & `react-dom` (peer)
+- `react-router-dom` (peer)
+- `@patternfly/react-core`
+- `@patternfly/react-icons`
+- `react-grid-layout`
+- `clsx`
+
+### Development Dependencies
+
+- `@types/react-grid-layout`
+- TypeScript
+- Jest & Testing Library
+
+## Browser Support
+
+Modern browsers with ES6 support:
+- Chrome/Edge (Chromium)
+- Firefox
+- Safari
+- Mobile browsers
+
+## Future Enhancements
+
+Potential areas for extension:
+
+1. **Widget Templates** - Pre-defined layout templates
+2. **Export/Import** - JSON export/import of layouts
+3. **Undo/Redo** - Layout change history
+4. **Widget Groups** - Grouping related widgets
+5. **Themes** - Custom color schemes
+6. **Animations** - Smooth transitions
+7. **Touch Support** - Better mobile experience
+
diff --git a/MIGRATION.md b/MIGRATION.md
new file mode 100644
index 0000000..cc37c16
--- /dev/null
+++ b/MIGRATION.md
@@ -0,0 +1,256 @@
+# Migration Guide from widget-layout
+
+This document provides guidance for users migrating from the RedHatInsights/widget-layout repository to this generic PatternFly component.
+
+## What Changed
+
+### Removed Dependencies
+
+The following console-specific dependencies have been removed:
+
+1. **Scalprum** (`@scalprum/react-core`) - Federated module loading
+2. **Chrome Services APIs** - API calls for template persistence
+3. **useCurrentUser** - Console-specific authentication
+4. **useChrome** - Console-specific hooks and analytics
+5. **Jotai atoms** - External state management
+6. **Console icons and branding**
+
+### New Approach
+
+#### Widget Definition
+
+**Before (widget-layout):**
+```typescript
+// Widget loaded via Scalprum federated modules
+const widgetMapping = {
+ 'my-widget': {
+ scope: 'myApp',
+ module: './MyWidget',
+ importName: 'MyWidget',
+ defaults: { w: 2, h: 3, maxH: 6, minH: 2 },
+ config: { title: 'My Widget' }
+ }
+};
+```
+
+**After (widgetized-dashboard):**
+```typescript
+// Widget rendered directly via React component
+const widgetMapping: WidgetMapping = {
+ 'my-widget': {
+ defaults: { w: 2, h: 3, maxH: 6, minH: 2 },
+ config: {
+ title: 'My Widget',
+ icon:
+ },
+ renderWidget: (id) =>
+ }
+};
+```
+
+#### Template Management
+
+**Before (widget-layout):**
+```typescript
+// Templates loaded/saved automatically via Chrome Services API
+import { getDashboardTemplates, patchDashboardTemplate } from './api/dashboard-templates';
+
+// Component handles API calls internally
+
+```
+
+**After (widgetized-dashboard):**
+```typescript
+// Templates managed via props (bring your own state management)
+const [template, setTemplate] = useState(loadTemplate());
+
+const handleTemplateChange = async (newTemplate) => {
+ setTemplate(newTemplate);
+ await saveTemplateToAPI(newTemplate); // Your own API
+};
+
+
+```
+
+#### State Management
+
+**Before (widget-layout):**
+```typescript
+// State managed via Jotai atoms
+import { useAtom } from 'jotai';
+import { templateAtom } from './state/templateAtom';
+
+const [template, setTemplate] = useAtom(templateAtom);
+```
+
+**After (widgetized-dashboard):**
+```typescript
+// State managed via React state or your preferred solution
+const [template, setTemplate] = useState(initialTemplate);
+
+// Or use Redux, MobX, Zustand, etc.
+```
+
+#### Analytics
+
+**Before (widget-layout):**
+```typescript
+// Chrome analytics used internally
+import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
+
+const { analytics } = useChrome();
+analytics.track('widget-layout.widget-add', { data });
+```
+
+**After (widgetized-dashboard):**
+```typescript
+// Optional analytics via callback
+ {
+ yourAnalytics.track(event, data);
+ }}
+/>
+```
+
+## Step-by-Step Migration
+
+### 1. Install Dependencies
+
+```bash
+yarn add @patternfly/widgetized-dashboard
+```
+
+### 2. Update Widget Definitions
+
+Convert your Scalprum widget definitions to render functions:
+
+```typescript
+// Old
+{
+ 'insights-widget': {
+ scope: '@redhat-cloud-services/insights',
+ module: './InsightsWidget',
+ importName: 'default',
+ defaults: { w: 2, h: 3, maxH: 6, minH: 2 }
+ }
+}
+
+// New
+{
+ 'insights-widget': {
+ defaults: { w: 2, h: 3, maxH: 6, minH: 2 },
+ config: {
+ title: 'Insights',
+ icon:
+ },
+ renderWidget: (id) =>
+ }
+}
+```
+
+### 3. Implement Template Persistence
+
+Replace Chrome Services API calls with your own persistence layer:
+
+```typescript
+// Load from your backend
+const loadTemplate = async () => {
+ const response = await fetch('/api/dashboard/template');
+ return response.json();
+};
+
+// Save to your backend
+const saveTemplate = async (template) => {
+ await fetch('/api/dashboard/template', {
+ method: 'POST',
+ body: JSON.stringify(template)
+ });
+};
+
+// Use in component
+const MyDashboard = () => {
+ const [template, setTemplate] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ loadTemplate().then(t => {
+ setTemplate(t);
+ setLoading(false);
+ });
+ }, []);
+
+ if (loading) return ;
+
+ return (
+
+ );
+};
+```
+
+### 4. Update Imports
+
+```typescript
+// Old
+import GridLayout from './Components/DnDLayout/GridLayout';
+import { getDashboardTemplates } from './api/dashboard-templates';
+
+// New
+import { WidgetLayout, GridLayout, ExtendedTemplateConfig } from '@patternfly/widgetized-dashboard';
+```
+
+### 5. Remove State Atoms
+
+If you were using the Jotai atoms, replace them with your own state management:
+
+```typescript
+// Old
+import { useAtom } from 'jotai';
+import { templateAtom } from './state/templateAtom';
+
+// New - use React state, Redux, or your preferred solution
+import { useState } from 'react';
+const [template, setTemplate] = useState(initialTemplate);
+```
+
+## API Mapping
+
+| widget-layout | widgetized-dashboard | Notes |
+|--------------|---------------------|-------|
+| `getDashboardTemplates()` | `loadTemplate()` | Implement your own |
+| `patchDashboardTemplate()` | `saveTemplate()` | Implement your own |
+| `useCurrentUser()` | Your auth solution | Use your app's auth |
+| `useChrome()` | Props/callbacks | Pass as props |
+| Jotai atoms | React state/your store | Bring your own state |
+| Scalprum loading | `renderWidget` prop | Direct rendering |
+
+## Breaking Changes
+
+1. **No automatic persistence** - You must implement template loading/saving
+2. **No federated modules** - Use direct React components instead
+3. **No built-in auth** - Handle authentication in your app layer
+4. **State management** - Component is self-contained, no external atoms
+5. **Analytics** - Optional callback instead of automatic tracking
+
+## Benefits of Migration
+
+1. **No console dependencies** - Works in any PatternFly application
+2. **Simpler mental model** - Props in, callbacks out
+3. **Flexible persistence** - Use any backend or storage solution
+4. **Better TypeScript support** - Full type definitions
+5. **Lighter bundle** - Fewer dependencies
+6. **More control** - Explicit state management
+
+## Need Help?
+
+- [Full Documentation](./README.md)
+- [Examples](./packages/module/patternfly-docs/content/examples/)
+- [Design Guidelines](./packages/module/patternfly-docs/content/design-guidelines/)
+
diff --git a/README.md b/README.md
index 9cf774e..efe2ddd 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,144 @@
-# Widgetized dashboard
+# Widgetized Dashboard
+
+A generic, reusable PatternFly component library providing a customizable widget-based dashboard with drag-and-drop functionality. This library was created by lifting and adapting code from the [RedHatInsights/widget-layout](https://github.com/RedHatInsights/widget-layout) repository, removing console-specific dependencies to make it suitable for any PatternFly application.
+
+## Features
+
+- **Drag-and-Drop Grid Layout**: Powered by `react-grid-layout` with responsive breakpoints
+- **Widget Drawer**: Easy widget selection and management
+- **Lock/Unlock Widgets**: Prevent accidental changes to widget positions and sizes
+- **Resize Widgets**: Adjust widget dimensions with corner handles
+- **Responsive Design**: Automatic layout adjustments for xl, lg, md, and sm breakpoints
+- **Customizable**: Fully configurable widgets with custom icons, titles, and content
+- **TypeScript Support**: Full type definitions included
+- **No External Dependencies**: Self-contained state management (no Jotai, Redux, or other state libraries required)
+
+## Installation
+
+```bash
+yarn add @patternfly/widgetized-dashboard
+```
+
+Or with npm:
+
+```bash
+npm install @patternfly/widgetized-dashboard
+```
+
+### Peer Dependencies
+
+Make sure you have the required peer dependencies installed:
+
+```bash
+yarn add react react-dom react-router-dom @patternfly/react-core @patternfly/react-icons
+```
+
+## Quick Start
+
+```tsx
+import React from 'react';
+import { WidgetLayout, WidgetMapping, ExtendedTemplateConfig } from '@patternfly/widgetized-dashboard';
+import { CubeIcon } from '@patternfly/react-icons';
+
+// Define your widgets
+const widgetMapping: WidgetMapping = {
+ 'example-widget': {
+ defaults: { w: 2, h: 3, maxH: 6, minH: 2 },
+ config: {
+ title: 'Example Widget',
+ icon:
+ },
+ renderWidget: (id) => Widget content goes here
+ }
+};
+
+// Define initial layout (or load from API/localStorage)
+const initialTemplate: ExtendedTemplateConfig = {
+ xl: [
+ { i: 'example-widget#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'example-widget', title: 'Example Widget' }
+ ],
+ lg: [
+ { i: 'example-widget#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'example-widget', title: 'Example Widget' }
+ ],
+ md: [
+ { i: 'example-widget#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'example-widget', title: 'Example Widget' }
+ ],
+ sm: [
+ { i: 'example-widget#1', x: 0, y: 0, w: 1, h: 3, widgetType: 'example-widget', title: 'Example Widget' }
+ ]
+};
+
+function App() {
+ return (
+ {
+ // Save template to API or localStorage
+ console.log('Template changed:', template);
+ }}
+ />
+ );
+}
+```
+
+## Documentation
+
+- [Getting Started Guide](./packages/module/patternfly-docs/content/examples/basic.md)
+- [API Reference](./packages/module/patternfly-docs/content/design-guidelines/design-guidelines.md)
+
+## Key Components
+
+### WidgetLayout
+
+The main component that provides the complete dashboard experience with grid layout and widget drawer.
+
+### GridLayout
+
+The core layout engine with drag-and-drop functionality (can be used standalone).
+
+### WidgetDrawer
+
+The widget selection drawer (can be used standalone with GridLayout).
+
+### GridTile
+
+Individual widget tile wrapper with actions menu (used internally by GridLayout).
+
+## Differences from widget-layout
+
+This library is based on [RedHatInsights/widget-layout](https://github.com/RedHatInsights/widget-layout) but has been adapted to be a generic, reusable PatternFly component:
+
+### Removed
+- ❌ Scalprum federated module loading
+- ❌ Chrome Services API calls for template persistence
+- ❌ Console-specific authentication (useCurrentUser)
+- ❌ Console-specific analytics (useChrome)
+- ❌ Jotai state management atoms
+- ❌ Console-specific icons and branding
+
+### Added
+- ✅ Generic widget rendering via `renderWidget` prop
+- ✅ Prop-based template management (bring your own state management)
+- ✅ Optional analytics callback
+- ✅ Standalone component usage (no external state required)
+- ✅ Full TypeScript support
+- ✅ Simplified API without console dependencies
+
+## Browser Support
+
+Modern browsers (Chrome, Firefox, Safari, Edge) with ES6 support.
+
+## License
+
+MIT
+
+## Contributing
+
+### AI-assisted development guidelines
+
+Please reference [PatternFly's AI-assisted development guidelines](https://github.com/patternfly/.github/blob/main/CONTRIBUTING.md) if you'd like to contribute code generated using AI.
+
+## Credits
+
+This library is based on the [RedHatInsights/widget-layout](https://github.com/RedHatInsights/widget-layout) repository, adapted to be a generic PatternFly component.
diff --git a/packages/module/package.json b/packages/module/package.json
index 2be194f..96e801e 100644
--- a/packages/module/package.json
+++ b/packages/module/package.json
@@ -30,11 +30,15 @@
"access": "public"
},
"dependencies": {
- "@patternfly/react-core": "^6.3.1"
+ "@patternfly/react-core": "^6.3.1",
+ "@patternfly/react-icons": "^6.3.1",
+ "clsx": "^2.1.0",
+ "react-grid-layout": "^1.5.1"
},
"peerDependencies": {
"react": "^18",
- "react-dom": "^18"
+ "react-dom": "^18",
+ "react-router-dom": "^6"
},
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.18.6",
@@ -46,8 +50,11 @@
"@patternfly/patternfly-a11y": "^5.1.0",
"@patternfly/react-code-editor": "^6.3.1",
"@patternfly/react-table": "^6.3.1",
+ "@types/react-grid-layout": "^1.3.5",
"monaco-editor": "^0.53.0",
+ "nodemon": "^3.0.0",
"react-monaco-editor": "^0.59.0",
+ "react-router-dom": "^6.24.0",
"rimraf": "^6.0.1"
}
}
diff --git a/packages/module/patternfly-docs/content/design-guidelines/design-guidelines.md b/packages/module/patternfly-docs/content/design-guidelines/design-guidelines.md
deleted file mode 100644
index 4417c78..0000000
--- a/packages/module/patternfly-docs/content/design-guidelines/design-guidelines.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-# Sidenav top-level section
-# should be the same for all markdown files for each extension
-section: extensions
-# Sidenav secondary level section
-# should be the same for all markdown files for each extension
-id: Widgetized dashboard
-# Tab (react | react-demos | html | html-demos | design-guidelines | accessibility)
-source: design-guidelines
----
-
-Design guidelines intro
-
-## Header
-
-### Sub-header
-
-Guidelines:
-
-1. A
-1. list
-1. using
-1. markdown
\ No newline at end of file
diff --git a/packages/module/patternfly-docs/content/examples/Basic.tsx b/packages/module/patternfly-docs/content/examples/Basic.tsx
deleted file mode 100644
index ad7eec8..0000000
--- a/packages/module/patternfly-docs/content/examples/Basic.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-import React from 'react';
-import { ExtendedButton } from '@patternfly/widgetized-dashboard';
-
-export const BasicExample: React.FunctionComponent = () => My custom extension button;
diff --git a/packages/module/patternfly-docs/content/examples/BasicExample.tsx b/packages/module/patternfly-docs/content/examples/BasicExample.tsx
new file mode 100644
index 0000000..5bb3a7a
--- /dev/null
+++ b/packages/module/patternfly-docs/content/examples/BasicExample.tsx
@@ -0,0 +1,110 @@
+import React from 'react';
+import WidgetLayout from '@patternfly/widgetized-dashboard/dist/esm/WidgetLayout/WidgetLayout';
+import { WidgetMapping, ExtendedTemplateConfig } from '@patternfly/widgetized-dashboard/dist/esm/WidgetLayout/types';
+import { CubeIcon, ChartLineIcon, BellIcon } from '@patternfly/react-icons';
+import { Card, CardBody, CardTitle } from '@patternfly/react-core';
+
+// Example widget content components
+const ExampleWidget1 = () => (
+
+
+ Example Widget 1
+ This is the content of the first example widget.
+ You can put any React content here!
+
+
+);
+
+const ExampleWidget2 = () => (
+
+
+ Chart Widget
+
+
Chart content would go here
+
+
+
+);
+
+const ExampleWidget3 = () => (
+
+
+ Notifications
+
+ - Notification 1
+ - Notification 2
+ - Notification 3
+
+
+
+);
+
+// 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: 'Performance Chart',
+ icon:
+ },
+ renderWidget: () =>
+ },
+ 'notifications-widget': {
+ defaults: { w: 1, h: 3, maxH: 6, minH: 2 },
+ config: {
+ title: 'Recent Notifications',
+ 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: 'Performance Chart' }
+ ],
+ 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: 'Performance Chart' }
+ ],
+ 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: 'Performance Chart' }
+ ],
+ 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: 'Performance Chart' }
+ ]
+};
+
+export const BasicExample: React.FunctionComponent = () => {
+ const [template, setTemplate] = React.useState(initialTemplate);
+
+ return (
+
+ {
+ setTemplate(newTemplate);
+ }}
+ documentationLink="https://www.patternfly.org"
+ initialDrawerOpen={true}
+ />
+
+ );
+};
diff --git a/packages/module/patternfly-docs/content/examples/LockedLayoutExample.tsx b/packages/module/patternfly-docs/content/examples/LockedLayoutExample.tsx
new file mode 100644
index 0000000..dac0dcc
--- /dev/null
+++ b/packages/module/patternfly-docs/content/examples/LockedLayoutExample.tsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import WidgetLayout from '@patternfly/widgetized-dashboard/dist/esm/WidgetLayout/WidgetLayout';
+import { WidgetMapping, ExtendedTemplateConfig } from '../../../src/WidgetLayout/types';
+import { CubeIcon, ChartLineIcon } from '@patternfly/react-icons';
+import { Card, CardBody, CardTitle } from '@patternfly/react-core';
+
+// Example widget content components
+const ExampleWidget1 = () => (
+
+
+ Example Widget 1
+ This is the content of the first example widget.
+ You can put any React content here!
+
+
+);
+
+const ExampleWidget2 = () => (
+
+
+ Chart Widget
+
+
Chart content would go here
+
+
+
+);
+
+// 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: 'Performance Chart',
+ 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: 'Performance Chart' }
+ ],
+ 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: 'Performance Chart' }
+ ],
+ 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: 'Performance Chart' }
+ ],
+ 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: 'Performance Chart' }
+ ]
+};
+
+export const BasicExample: React.FunctionComponent = () => (
+
+
+
+);
+
diff --git a/packages/module/patternfly-docs/content/examples/WithoutDrawerExample.tsx b/packages/module/patternfly-docs/content/examples/WithoutDrawerExample.tsx
new file mode 100644
index 0000000..6f1782c
--- /dev/null
+++ b/packages/module/patternfly-docs/content/examples/WithoutDrawerExample.tsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import WidgetLayout from '@patternfly/widgetized-dashboard/dist/esm/WidgetLayout/WidgetLayout';
+import { WidgetMapping, ExtendedTemplateConfig } from '@patternfly/widgetized-dashboard/dist/esm/WidgetLayout/types';
+import { CubeIcon, ChartLineIcon, BellIcon } from '@patternfly/react-icons';
+import { Card, CardBody, CardTitle } from '@patternfly/react-core';
+
+// Example widget content components
+const ExampleWidget1 = () => (
+
+
+ Example Widget 1
+ This is the content of the first example widget.
+ You can put any React content here!
+
+
+);
+
+const ExampleWidget2 = () => (
+
+
+ Chart Widget
+
+
Chart content would go here
+
+
+
+);
+
+const ExampleWidget3 = () => (
+
+
+ Notifications
+
+ - Notification 1
+ - Notification 2
+ - Notification 3
+
+
+
+);
+
+// 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: 'Performance Chart',
+ icon:
+ },
+ renderWidget: () =>
+ },
+ 'notifications-widget': {
+ defaults: { w: 1, h: 3, maxH: 6, minH: 2 },
+ config: {
+ title: 'Recent Notifications',
+ 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: 'Performance Chart' }
+ ],
+ 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: 'Performance Chart' }
+ ],
+ 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: 'Performance Chart' }
+ ],
+ 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: 'Performance Chart' }
+ ]
+};
+
+export const BasicExample: 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 34a3ab6..0119db6 100644
--- a/packages/module/patternfly-docs/content/examples/basic.md
+++ b/packages/module/patternfly-docs/content/examples/basic.md
@@ -9,21 +9,82 @@ 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: ['ExtendedButton']
----
+propComponents: ['WidgetLayout', 'GridLayout', 'WidgetDrawer']
+sortValue: 1
+sourceLink: https://github.com/patternfly/widgetized-dashboard
+---
-import { ExtendedButton } from "@patternfly/widgetized-dashboard";
+import { FunctionComponent, useState } from 'react';
+import { CubeIcon, ChartLineIcon, BellIcon } from '@patternfly/react-icons';
+import { Card, CardBody, CardTitle } from '@patternfly/react-core';
+import WidgetLayout from '@patternfly/widgetized-dashboard/dist/esm/WidgetLayout/WidgetLayout';
+import GridLayout from '@patternfly/widgetized-dashboard/dist/esm/WidgetLayout/GridLayout';
+import WidgetDrawer from '@patternfly/widgetized-dashboard/dist/esm/WidgetLayout/WidgetDrawer';
## Basic usage
-### Example
+The WidgetLayout component provides a complete drag-and-drop dashboard experience with a widget drawer for adding and removing widgets.
+
+### Interactive example
+
+```js file="./BasicExample.tsx"
+
+```
+
+## Locked layout
+
+Use `isLayoutLocked` to prevent users from modifying the layout.
-```js file="./Basic.tsx"
+```js file="./LockedLayoutExample.tsx"
```
-### Fullscreen example
+## Without drawer
+
+You can hide the widget drawer by setting `showDrawer={false}`.
-```js file="./Basic.tsx" isFullscreen
+```js file="./WithoutDrawerExample.tsx"
```
+
+## Key features
+
+- **Drag and drop**: Drag widgets from the drawer to add them to the dashboard
+- **Resize**: Drag corner handles to resize widgets
+- **Lock/unlock**: Lock widgets to prevent accidental changes
+- **Responsive**: Automatically adjusts layout for different screen sizes (xl, lg, md, sm)
+- **Persistent**: Save and restore layouts using the `onTemplateChange` callback
+
+## Widget mapping
+
+Define your widgets using the `WidgetMapping` type:
+
+```typescript
+const widgetMapping: WidgetMapping = {
+ 'my-widget': {
+ defaults: { w: 2, h: 3, maxH: 6, minH: 2 },
+ config: {
+ title: 'My Widget',
+ icon:
+ },
+ renderWidget: (id) =>
+ }
+};
+```
+
+## Template configuration
+
+Define your initial layout using the `ExtendedTemplateConfig` type:
+
+```typescript
+const initialTemplate: ExtendedTemplateConfig = {
+ xl: [
+ { i: 'my-widget#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'my-widget', title: 'My Widget' }
+ ],
+ lg: [...],
+ md: [...],
+ sm: [...]
+};
+```
+
+Each breakpoint (xl, lg, md, sm) should have its own layout configuration to ensure proper responsive behavior.
diff --git a/packages/module/src/ExtendedButton/ExtendedButton.tsx b/packages/module/src/ExtendedButton/ExtendedButton.tsx
deleted file mode 100644
index b8119e9..0000000
--- a/packages/module/src/ExtendedButton/ExtendedButton.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react';
-import { Button, ButtonProps } from '@patternfly/react-core';
-
-export interface ExtendedButtonProps extends ButtonProps {
- /** Content to render inside the extended button component */
- children?: React.ReactNode;
-}
-
-export const ExtendedButton: React.FunctionComponent = ({
- children,
- ...props
-}: ExtendedButtonProps) => {
- const [currentVariantIndex, setCurrentVariantIndex] = React.useState(0);
-
- const buttonVariants: ButtonProps['variant'][] = [
- 'primary',
- 'secondary',
- 'tertiary'
- ];
-
- const handleClick = () => {
- setCurrentVariantIndex((previousVariantIndex) => (previousVariantIndex + 1) % buttonVariants.length);
- };
-
- return (
-
- );
-};
diff --git a/packages/module/src/ExtendedButton/__tests__/ExtendedButton.test.tsx b/packages/module/src/ExtendedButton/__tests__/ExtendedButton.test.tsx
deleted file mode 100644
index 3a8b357..0000000
--- a/packages/module/src/ExtendedButton/__tests__/ExtendedButton.test.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import React from 'react';
-import { render, screen, waitFor } from '@testing-library/react';
-import '@testing-library/jest-dom';
-import userEvent from '@testing-library/user-event';
-import { ExtendedButton } from '../ExtendedButton';
-
-test('Renders without children', () => {
- render(
-
-
-
- );
-
- expect(screen.getByTestId('container').firstChild).toBeVisible();
-});
-
-test('Renders children', () => {
- render(Test);
-
- expect(screen.getByRole('button', { name: 'Test' })).toBeVisible();
-});
-
-test('Passes inherited props to the returned component', () => {
- render(Test);
-
- expect(screen.getByRole('button')).toHaveAccessibleName('Test label');
-});
-
-test('Renders as a primary button initially', () => {
- render(Test);
-
- expect(screen.getByRole('button')).toHaveClass('pf-v6-c-button pf-m-primary', { exact: true });
-});
-
-test('Renders as a secondary button once it has been clicked once', () => {
- render(Test);
- const button = screen.getByRole('button');
- userEvent.click(screen.getByRole('button'));
-
- waitFor(() => {
- expect(button).toHaveClass('pf-v6-c-button pf-m-secondary', { exact: true });
- });
-});
-
-test('Renders as a tertiary button once it has been clicked twice', () => {
- render(Test);
-
- const button = screen.getByRole('button');
- userEvent.click(button);
- userEvent.click(button);
-
- waitFor(() => {
- expect(button).toHaveClass('pf-v6-c-button pf-m-tertiary', { exact: true });
- });
-});
-
-test('Loops back to rendering a primary button again after being clicked three times', () => {
- render(Test);
-
- const button = screen.getByRole('button');
- userEvent.click(button);
- userEvent.click(button);
- userEvent.click(button);
-
- expect(button).toHaveClass('pf-v6-c-button pf-m-primary', { exact: true });
-});
-
-test('Matches expected default snapshot', () => {
- const { asFragment } = render(Test);
-
- expect(asFragment()).toMatchSnapshot();
-});
diff --git a/packages/module/src/ExtendedButton/__tests__/__snapshots__/ExtendedButton.test.tsx.snap b/packages/module/src/ExtendedButton/__tests__/__snapshots__/ExtendedButton.test.tsx.snap
deleted file mode 100644
index 9911eeb..0000000
--- a/packages/module/src/ExtendedButton/__tests__/__snapshots__/ExtendedButton.test.tsx.snap
+++ /dev/null
@@ -1,19 +0,0 @@
-// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
-
-exports[`Matches expected default snapshot 1`] = `
-
-
-
-`;
diff --git a/packages/module/src/ExtendedButton/index.ts b/packages/module/src/ExtendedButton/index.ts
deleted file mode 100644
index 03c57e3..0000000
--- a/packages/module/src/ExtendedButton/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './ExtendedButton';
diff --git a/packages/module/src/WidgetLayout/GridLayout.tsx b/packages/module/src/WidgetLayout/GridLayout.tsx
new file mode 100644
index 0000000..a94bc5d
--- /dev/null
+++ b/packages/module/src/WidgetLayout/GridLayout.tsx
@@ -0,0 +1,290 @@
+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';
+import { isWidgetType } from './utils';
+import React from 'react';
+import {
+ ExtendedLayoutItem,
+ Variants,
+ WidgetMapping,
+ ExtendedTemplateConfig,
+ AnalyticsTracker,
+} from './types';
+import { Button, EmptyState, EmptyStateActions, EmptyStateBody, EmptyStateVariant, PageSection } from '@patternfly/react-core';
+import { ExternalLinkAltIcon, GripVerticalIcon, PlusCircleIcon } from '@patternfly/react-icons';
+import { columns, breakpoints, droppingElemId, getWidgetIdentifier, extendLayout, getGridDimensions } from './utils';
+
+export const defaultBreakpoints = breakpoints;
+
+// SVG resize handle as inline data URI
+const resizeHandleSvg = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE2IDEuMTQyODZMMTQuODU3MSAwTDAgMTQuODU3MVYxNkgxLjE0Mjg2TDE2IDEuMTQyODZaIiBmaWxsPSIjRDJEMkQyIi8+Cjwvc3ZnPgo=';
+
+const getResizeHandle = (resizeHandleAxis: string, ref: React.Ref) => (
+
+

+
+ );
+
+export interface GridLayoutProps {
+ /** Widget mapping definition */
+ widgetMapping: WidgetMapping;
+ /** Current template configuration */
+ template: ExtendedTemplateConfig;
+ /** Callback when template changes */
+ onTemplateChange?: (template: ExtendedTemplateConfig) => void;
+ /** Whether the layout is locked (disables drag, drop, resize) */
+ isLayoutLocked?: boolean;
+ /** Custom empty state component */
+ emptyStateComponent?: React.ReactNode;
+ /** Documentation link for empty state */
+ documentationLink?: string;
+ /** Analytics tracker (optional) */
+ analytics?: AnalyticsTracker;
+ /** Whether to show the empty state when no widgets exist */
+ showEmptyState?: boolean;
+ /** Callback when drawer should be expanded */
+ onDrawerExpandChange?: (expanded: boolean) => void;
+ /** Currently active widgets (for tracking) */
+ onActiveWidgetsChange?: (widgetTypes: string[]) => void;
+}
+
+const LayoutEmptyState = ({
+ onDrawerExpandChange,
+ documentationLink,
+}: {
+ onDrawerExpandChange?: (expanded: boolean) => void;
+ documentationLink?: string;
+}) => {
+ useEffect(() => {
+ onDrawerExpandChange?.(true);
+ }, [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 && (
+
+ } iconPosition="end" component="a" href={documentationLink} target="_blank">
+ Learn more about widget dashboard
+
+
+ )}
+
+
+ );
+};
+
+const GridLayout = ({
+ widgetMapping,
+ template,
+ onTemplateChange,
+ isLayoutLocked = false,
+ emptyStateComponent,
+ documentationLink,
+ analytics,
+ showEmptyState = true,
+ onDrawerExpandChange,
+ onActiveWidgetsChange,
+}: GridLayoutProps) => {
+ const [isDragging, setIsDragging] = useState(false);
+ const [isInitialRender, setIsInitialRender] = useState(true);
+ const [layoutVariant, setLayoutVariant] = useState('xl');
+ const [layoutWidth, setLayoutWidth] = useState(1200);
+ const layoutRef = useRef(null);
+
+ const [currentDropInItem, setCurrentDropInItem] = useState();
+ const [internalTemplate, setInternalTemplate] = useState(template);
+
+ // Sync external template changes to internal state
+ useEffect(() => {
+ setInternalTemplate(template);
+ }, [template]);
+
+ const droppingItemTemplate: ReactGridLayoutProps['droppingItem'] = useMemo(() => {
+ if (currentDropInItem && isWidgetType(widgetMapping, currentDropInItem)) {
+ const widget = widgetMapping[currentDropInItem];
+ if (!widget) {return undefined;}
+ return {
+ ...widget.defaults,
+ i: droppingElemId,
+ widgetType: currentDropInItem,
+ title: 'New title',
+ config: widget.config,
+ };
+ }
+ return undefined;
+ }, [currentDropInItem, widgetMapping]);
+
+ const setWidgetAttribute: SetWidgetAttribute = (id, attributeName, value) => {
+ const newTemplate = Object.entries(internalTemplate).reduce(
+ (acc, [size, layout]) => ({
+ ...acc,
+ [size]: layout.map((widget) => (widget.i === id ? { ...widget, [attributeName]: value } : widget)),
+ }),
+ {} as ExtendedTemplateConfig
+ );
+ setInternalTemplate(newTemplate);
+ onTemplateChange?.(newTemplate);
+ };
+
+ const removeWidget = (id: string) => {
+ const newTemplate = Object.entries(internalTemplate).reduce(
+ (acc, [size, layout]) => ({
+ ...acc,
+ [size]: layout.filter((widget) => widget.i !== id),
+ }),
+ {} as ExtendedTemplateConfig
+ );
+ setInternalTemplate(newTemplate);
+ onTemplateChange?.(newTemplate);
+ };
+
+ const onDrop: ReactGridLayoutProps['onDrop'] = (_layout: ExtendedLayoutItem[], layoutItem: ExtendedLayoutItem, event: DragEvent) => {
+ const data = event.dataTransfer?.getData('text') || '';
+ if (isWidgetType(widgetMapping, data)) {
+ setCurrentDropInItem(undefined);
+ const widget = widgetMapping[data];
+ if (!widget) {return;}
+ const newTemplate = Object.entries(internalTemplate).reduce((acc, [size, layout]) => {
+ const newWidget = {
+ ...layoutItem,
+ ...widget.defaults,
+ // make sure the configuration is valid for all layout sizes
+ w: size === layoutVariant ? layoutItem.w : Math.min(widget.defaults.w, columns[size as Variants]),
+ x: size === layoutVariant ? layoutItem.x : Math.min(layoutItem.x, columns[size as Variants]),
+ widgetType: data,
+ i: getWidgetIdentifier(data),
+ title: 'New title',
+ config: widget.config,
+ };
+ return {
+ ...acc,
+ [size]: layout.reduce(
+ (acc, curr) => {
+ if (curr.x + curr.w > newWidget.x && curr.y + curr.h <= newWidget.y) {
+ acc.push(curr);
+ } else {
+ // push the current items down on the Y axis if they are supposed to be below the new widget
+ acc.push({ ...curr, y: curr.y + curr.h });
+ }
+ return acc;
+ },
+ [newWidget]
+ ),
+ };
+ }, {} as ExtendedTemplateConfig);
+
+ setInternalTemplate(newTemplate);
+ onTemplateChange?.(newTemplate);
+ analytics?.('widget-layout.widget-add', { data });
+ }
+ event.preventDefault();
+ };
+
+ const onLayoutChange = (currentLayout: Layout[]) => {
+ if (isInitialRender) {
+ setIsInitialRender(false);
+ const activeWidgets = activeLayout.map((item) => item.widgetType);
+ onActiveWidgetsChange?.(activeWidgets);
+ return;
+ }
+ if (isLayoutLocked || currentDropInItem) {
+ return;
+ }
+
+ const newTemplate = extendLayout({ ...internalTemplate, [layoutVariant]: currentLayout });
+ const activeWidgets = activeLayout.map((item) => item.widgetType);
+ onActiveWidgetsChange?.(activeWidgets);
+
+ setInternalTemplate(newTemplate);
+ onTemplateChange?.(newTemplate);
+ };
+
+ useEffect(() => {
+ const currentWidth = layoutRef.current?.getBoundingClientRect().width ?? 1200;
+ const variant: Variants = getGridDimensions(currentWidth);
+ setLayoutVariant(variant);
+ setLayoutWidth(currentWidth);
+
+ const observer = new ResizeObserver((entries) => {
+ if (!entries[0]) {return;}
+
+ const currentWidth = entries[0].contentRect.width;
+ const variant: Variants = getGridDimensions(currentWidth);
+ setLayoutVariant(variant);
+ setLayoutWidth(currentWidth);
+ });
+
+ if (layoutRef.current) {
+ observer.observe(layoutRef.current);
+ }
+
+ return () => {
+ observer.disconnect();
+ };
+ }, []);
+
+ const activeLayout = internalTemplate[layoutVariant] || [];
+
+ return (
+
+ );
+};
+
+export default GridLayout;
+
diff --git a/packages/module/src/WidgetLayout/GridTile.tsx b/packages/module/src/WidgetLayout/GridTile.tsx
new file mode 100644
index 0000000..073b300
--- /dev/null
+++ b/packages/module/src/WidgetLayout/GridTile.tsx
@@ -0,0 +1,230 @@
+import {
+ Button,
+ Card,
+ CardBody,
+ CardHeader,
+ CardTitle,
+ Divider,
+ Dropdown,
+ DropdownItem,
+ DropdownList,
+ Flex,
+ FlexItem,
+ HelperText,
+ HelperTextItem,
+ Icon,
+ MenuToggle,
+ MenuToggleElement,
+ Skeleton,
+ Tooltip,
+} from '@patternfly/react-core';
+import { CompressIcon, EllipsisVIcon, ExpandIcon, GripVerticalIcon, LockIcon, MinusCircleIcon, UnlockIcon } from '@patternfly/react-icons';
+import React, { useMemo, useState } from 'react';
+import clsx from 'clsx';
+import { Link } from 'react-router-dom';
+import { Layout } from 'react-grid-layout';
+import { ExtendedLayoutItem, WidgetConfiguration, AnalyticsTracker } from './types';
+
+export type SetWidgetAttribute = (id: string, attributeName: keyof ExtendedLayoutItem, value: T) => void;
+
+export type GridTileProps = React.PropsWithChildren<{
+ widgetType: string;
+ icon?: React.ReactNode;
+ setIsDragging: (isDragging: boolean) => void;
+ isDragging: boolean;
+ setWidgetAttribute: SetWidgetAttribute;
+ widgetConfig: Layout & {
+ colWidth: number;
+ locked?: boolean;
+ config?: WidgetConfiguration;
+ };
+ removeWidget: (id: string) => void;
+ children: React.ReactNode;
+ isLoaded?: boolean;
+ analytics?: AnalyticsTracker;
+}>;
+
+const GridTile = ({
+ widgetType,
+ isDragging,
+ setIsDragging,
+ setWidgetAttribute,
+ widgetConfig,
+ removeWidget,
+ children,
+ isLoaded = true,
+ analytics,
+}: GridTileProps) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const { headerLink } = widgetConfig.config || {};
+ const hasHeader = headerLink && headerLink.href && headerLink.title;
+
+ const [linkHref, linkTarget] = useMemo(() => {
+ if (!headerLink?.href) {
+ return [];
+ }
+
+ try {
+ const url = new URL(headerLink.href);
+ return url.origin === window.location.origin ? [url.pathname, undefined] : [headerLink.href, '_blank'];
+ } catch {
+ return [headerLink.href];
+ }
+ }, [headerLink?.href]);
+
+ const dropdownItems = useMemo(() => {
+ const isMaximized = widgetConfig.h === widgetConfig.maxH;
+ const isMinimized = widgetConfig.h === widgetConfig.minH;
+ return (
+ <>
+ {
+ setIsOpen(false);
+ setWidgetAttribute(widgetConfig.i, 'static', !widgetConfig.static);
+ }}
+ icon={widgetConfig.static ? : }
+ >
+ {widgetConfig.static ? 'Unlock location and size' : 'Lock location and size'}
+
+ {
+ setWidgetAttribute(widgetConfig.i, 'h', widgetConfig.maxH ?? widgetConfig.h);
+ setIsOpen(false);
+ }}
+ icon={}
+ >
+ Autosize height to content
+
+ {
+ setWidgetAttribute(widgetConfig.i, 'h', widgetConfig.minH ?? widgetConfig.h);
+ setIsOpen(false);
+ }}
+ isDisabled={isMinimized || widgetConfig.static}
+ icon={}
+ >
+ Minimize height
+
+ {
+ removeWidget(widgetConfig.i);
+ analytics?.('widget-layout.widget-remove', { widgetType });
+ }}
+ icon={
+
+
+
+ }
+ isDisabled={widgetConfig.static}
+ >
+ Remove
+
+
+ {"All 'removed' widgets can be added back by clicking the 'Add widgets' button."}
+
+
+
+ >
+ );
+ }, [widgetConfig.h, widgetConfig.maxH, widgetConfig.minH, widgetConfig.static, widgetConfig.i, setWidgetAttribute, removeWidget, analytics, widgetType]);
+
+ const headerActions = (
+ <>
+ Actions
}>
+ ) => (
+ setIsOpen((prev) => !prev)}
+ variant="plain"
+ aria-label="widget actions menu toggle"
+ >
+
+
+ )}
+ isOpen={isOpen}
+ onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)}
+ >
+ {dropdownItems}
+
+
+ {widgetConfig.static ? 'Widget locked' : 'Move'}}>
+ {
+ setIsDragging(true);
+ analytics?.('widget-layout.widget-move', { widgetType });
+ }}
+ onMouseUp={() => setIsDragging(false)}
+ className={clsx('drag-handle', {
+ dragging: isDragging,
+ })}
+ >
+
+
+
+ >
+ );
+
+ return (
+
+
+
+
+ {widgetConfig?.config?.icon && (
+
+ {isLoaded ? widgetConfig.config.icon : }
+
+ )}
+
+ {isLoaded ? (
+
+ {widgetConfig?.config?.title || widgetType}
+
+ ) : (
+
+ )}
+ {hasHeader && isLoaded && (
+
+
+
+ )}
+
+
+
+
+
+ {children}
+
+ );
+};
+
+export default GridTile;
+
diff --git a/packages/module/src/WidgetLayout/WidgetDrawer.tsx b/packages/module/src/WidgetLayout/WidgetDrawer.tsx
new file mode 100644
index 0000000..2c35d16
--- /dev/null
+++ b/packages/module/src/WidgetLayout/WidgetDrawer.tsx
@@ -0,0 +1,166 @@
+import {
+ Button,
+ Card,
+ CardHeader,
+ CardTitle,
+ Flex,
+ Gallery,
+ GalleryItem,
+ Icon,
+ PageSection,
+ Split,
+ SplitItem,
+ Title,
+ Tooltip,
+} from '@patternfly/react-core';
+import React, { useState } from 'react';
+import { CloseIcon, GripVerticalIcon } from '@patternfly/react-icons';
+import { WidgetMapping, WidgetConfiguration } from './types';
+
+export type WidgetDrawerProps = React.PropsWithChildren<{
+ /** Widget mapping definition */
+ widgetMapping: WidgetMapping;
+ /** List of currently used widget types (to filter out from drawer) */
+ currentlyUsedWidgets?: string[];
+ /** Whether the drawer is expanded */
+ isOpen?: boolean;
+ /** Callback when drawer open state changes */
+ onOpenChange?: (isOpen: boolean) => void;
+ /** Custom instruction text */
+ instructionText?: string;
+}>;
+
+const WidgetWrapper = ({ widgetType, config, onDragStart, onDragEnd }: {
+ widgetType: string;
+ config?: WidgetConfiguration;
+ onDragStart: (widgetType: string) => void;
+ onDragEnd: () => void;
+}) => {
+ const headerActions = (
+ Drag to add widget}>
+
+
+
+
+ );
+
+ return (
+ {
+ const nodeRect = (e.target as HTMLElement).getBoundingClientRect();
+
+ if (e.dataTransfer) {
+ e.dataTransfer.setDragImage(
+ e.target as HTMLDivElement,
+ e.clientX - nodeRect.left,
+ e.clientY - nodeRect.top
+ );
+ }
+
+ if (e.dataTransfer) {
+ e.dataTransfer.setData('text', widgetType);
+ }
+
+ onDragStart(widgetType);
+ }}
+ onDragEnd={onDragEnd}
+
+ unselectable="on"
+ draggable={true}
+ className="grid-tile"
+ ouiaId={`add-widget-card-${config?.title || widgetType}`}
+ >
+
+
+ {config?.icon && (
+
+ {config.icon}
+
+ )}
+ {config?.title || widgetType}
+
+
+
+ );
+};
+
+const WidgetDrawer = ({
+ children,
+ widgetMapping,
+ currentlyUsedWidgets = [],
+ isOpen: controlledIsOpen,
+ onOpenChange,
+ instructionText,
+}: WidgetDrawerProps) => {
+ const [internalIsOpen, setInternalIsOpen] = useState(false);
+
+ // Use controlled state if provided, otherwise use internal state
+ const isOpen = controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen;
+ const setIsOpen = onOpenChange || setInternalIsOpen;
+
+ const filteredWidgetMapping = Object.entries(widgetMapping).filter(([type]) => !currentlyUsedWidgets.includes(type));
+
+ const defaultInstructionText = `Add new and previously removed widgets by clicking the icon, then drag and drop to a new location. Drag the corners of the cards to resize widgets.`;
+
+ const panelContent = (
+
+
+
+
+ {instructionText || (
+ <>
+ {defaultInstructionText.split('icon').map((part, i) =>
+ i === 0 ? part : (
+
+
+ {part}
+
+ )
+ )}
+ >
+ )}
+
+
+
+
+
+
+ {filteredWidgetMapping.map(([type, widget], i) => (
+
+ {}}
+ onDragEnd={() => {}}
+ />
+
+ ))}
+
+
+ );
+
+ return (
+ <>
+ {isOpen ? {panelContent}
: null}
+ {children}
+ >
+ );
+};
+
+export default WidgetDrawer;
+
diff --git a/packages/module/src/WidgetLayout/WidgetLayout.tsx b/packages/module/src/WidgetLayout/WidgetLayout.tsx
new file mode 100644
index 0000000..9ce839a
--- /dev/null
+++ b/packages/module/src/WidgetLayout/WidgetLayout.tsx
@@ -0,0 +1,131 @@
+import React, { useState } from 'react';
+import GridLayout from './GridLayout';
+import WidgetDrawer from './WidgetDrawer';
+import { ExtendedTemplateConfig, WidgetMapping, AnalyticsTracker } from './types';
+
+export interface WidgetLayoutProps {
+ /** Widget mapping definition */
+ widgetMapping: WidgetMapping;
+ /** Initial template configuration */
+ initialTemplate: ExtendedTemplateConfig;
+ /** Callback when template changes */
+ onTemplateChange?: (template: ExtendedTemplateConfig) => void;
+ /** Whether the layout is locked (disables drag, drop, resize) */
+ isLayoutLocked?: boolean;
+ /** Custom empty state component */
+ emptyStateComponent?: React.ReactNode;
+ /** Documentation link for empty state */
+ documentationLink?: string;
+ /** Analytics tracker (optional) */
+ analytics?: AnalyticsTracker;
+ /** Whether to show the widget drawer */
+ showDrawer?: boolean;
+ /** Whether to show the empty state when no widgets exist */
+ showEmptyState?: boolean;
+ /** Custom instruction text for the drawer */
+ drawerInstructionText?: string;
+ /** Whether the drawer is initially open */
+ initialDrawerOpen?: boolean;
+};
+
+/**
+ * WidgetLayout - A complete drag-and-drop dashboard layout component
+ *
+ * This component provides a full-featured dashboard experience with:
+ * - Responsive grid layout with drag-and-drop
+ * - Widget drawer for adding/removing widgets
+ * - Lock/unlock widgets
+ * - Resize widgets
+ * - Persistent layout configuration
+ *
+ * @example
+ * ```tsx
+ * const widgetMapping = {
+ * 'my-widget': {
+ * defaults: { w: 2, h: 3, maxH: 6, minH: 2 },
+ * config: {
+ * title: 'My Widget',
+ * icon:
+ * },
+ * renderWidget: (id) =>
+ * }
+ * };
+ *
+ * const template = {
+ * xl: [{ i: 'my-widget#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'my-widget', title: 'My Widget' }],
+ * lg: [...],
+ * md: [...],
+ * sm: [...]
+ * };
+ *
+ * saveTemplate(newTemplate)}
+ * />
+ * ```
+ */
+const WidgetLayout = ({
+ widgetMapping,
+ initialTemplate,
+ onTemplateChange,
+ isLayoutLocked = false,
+ emptyStateComponent,
+ documentationLink,
+ analytics,
+ showDrawer = true,
+ showEmptyState = true,
+ drawerInstructionText,
+ initialDrawerOpen = false,
+}: WidgetLayoutProps) => {
+ const [template, setTemplate] = useState(initialTemplate);
+ const [drawerOpen, setDrawerOpen] = useState(initialDrawerOpen);
+ const [currentlyUsedWidgets, setCurrentlyUsedWidgets] = useState([]);
+
+ const handleTemplateChange = (newTemplate: ExtendedTemplateConfig) => {
+ setTemplate(newTemplate);
+ onTemplateChange?.(newTemplate);
+ };
+
+ const handleDrawerExpandChange = (expanded: boolean) => {
+ setDrawerOpen(expanded);
+ };
+
+ const handleActiveWidgetsChange = (widgetTypes: string[]) => {
+ setCurrentlyUsedWidgets(widgetTypes);
+ };
+
+ const gridLayout = (
+
+ );
+
+ if (!showDrawer) {
+ return gridLayout;
+ }
+
+ return (
+
+ {gridLayout}
+
+ );
+};
+
+export default WidgetLayout;
+
diff --git a/packages/module/src/WidgetLayout/__tests__/WidgetLayout.test.tsx b/packages/module/src/WidgetLayout/__tests__/WidgetLayout.test.tsx
new file mode 100644
index 0000000..5adb835
--- /dev/null
+++ b/packages/module/src/WidgetLayout/__tests__/WidgetLayout.test.tsx
@@ -0,0 +1,123 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { WidgetLayout } from '../WidgetLayout';
+import { WidgetMapping, ExtendedTemplateConfig } from '../types';
+import { CubeIcon } from '@patternfly/react-icons';
+import { BrowserRouter } from 'react-router-dom';
+
+const mockWidgetMapping: WidgetMapping = {
+ 'test-widget': {
+ defaults: { w: 2, h: 3, maxH: 6, minH: 2 },
+ config: {
+ title: 'Test Widget',
+ icon:
+ },
+ renderWidget: (id) => Test Widget Content
+ }
+};
+
+const mockTemplate: ExtendedTemplateConfig = {
+ xl: [
+ { i: 'test-widget#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'test-widget', title: 'Test Widget' }
+ ],
+ lg: [
+ { i: 'test-widget#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'test-widget', title: 'Test Widget' }
+ ],
+ md: [
+ { i: 'test-widget#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'test-widget', title: 'Test Widget' }
+ ],
+ sm: [
+ { i: 'test-widget#1', x: 0, y: 0, w: 1, h: 3, widgetType: 'test-widget', title: 'Test Widget' }
+ ]
+};
+
+const emptyTemplate: ExtendedTemplateConfig = {
+ xl: [],
+ lg: [],
+ md: [],
+ sm: []
+};
+
+describe('WidgetLayout', () => {
+ it('renders without crashing', () => {
+ render(
+
+
+
+ );
+ expect(screen.getByTestId('widget-test-widget#1')).toBeInTheDocument();
+ });
+
+ it('shows empty state when no widgets are present', () => {
+ render(
+
+
+
+ );
+ expect(screen.getByText(/No dashboard content/i)).toBeInTheDocument();
+ });
+
+ it('hides empty state when showEmptyState is false', () => {
+ render(
+
+
+
+ );
+ expect(screen.queryByText(/No dashboard content/i)).not.toBeInTheDocument();
+ });
+
+ it('hides drawer when showDrawer is false', () => {
+ render(
+
+
+
+ );
+ // Drawer instructions should not be present
+ expect(screen.queryByText(/Add new and previously removed widgets/i)).not.toBeInTheDocument();
+ });
+
+ it('renders widget with custom title', () => {
+ render(
+
+
+
+ );
+ expect(screen.getByText('Test Widget')).toBeInTheDocument();
+ });
+
+ it('calls onTemplateChange when template changes', () => {
+ const handleChange = jest.fn();
+ render(
+
+
+
+ );
+ // Note: Testing actual drag-and-drop interactions would require more complex testing setup
+ // This test just verifies the callback prop is accepted
+ expect(handleChange).not.toHaveBeenCalled();
+ });
+});
+
diff --git a/packages/module/src/WidgetLayout/__tests__/__snapshots__/WidgetLayout.test.tsx.snap b/packages/module/src/WidgetLayout/__tests__/__snapshots__/WidgetLayout.test.tsx.snap
new file mode 100644
index 0000000..006e639
--- /dev/null
+++ b/packages/module/src/WidgetLayout/__tests__/__snapshots__/WidgetLayout.test.tsx.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`WidgetLayout renders without crashing 1`] = `
+
+
+ Test Widget Content
+
+
+`;
+
diff --git a/packages/module/src/WidgetLayout/__tests__/utils.test.ts b/packages/module/src/WidgetLayout/__tests__/utils.test.ts
new file mode 100644
index 0000000..605a48a
--- /dev/null
+++ b/packages/module/src/WidgetLayout/__tests__/utils.test.ts
@@ -0,0 +1,114 @@
+import {
+ getWidgetIdentifier,
+ mapWidgetDefaults,
+ getGridDimensions,
+ isWidgetType,
+ extendLayout,
+ mapTemplateConfigToExtendedTemplateConfig,
+} from '../utils';
+import { ExtendedTemplateConfig, TemplateConfig } from '../types';
+
+describe('utils', () => {
+ describe('getWidgetIdentifier', () => {
+ it('creates identifier with widgetType and uuid', () => {
+ const result = getWidgetIdentifier('my-widget', 'test-uuid');
+ expect(result).toBe('my-widget#test-uuid');
+ });
+
+ it('generates random uuid when not provided', () => {
+ const result1 = getWidgetIdentifier('my-widget');
+ const result2 = getWidgetIdentifier('my-widget');
+ expect(result1).toMatch(/^my-widget#/);
+ expect(result2).toMatch(/^my-widget#/);
+ expect(result1).not.toBe(result2);
+ });
+ });
+
+ describe('mapWidgetDefaults', () => {
+ it('splits identifier into widgetType and uuid', () => {
+ const [widgetType, uuid] = mapWidgetDefaults('my-widget#test-uuid');
+ expect(widgetType).toBe('my-widget');
+ expect(uuid).toBe('test-uuid');
+ });
+ });
+
+ describe('getGridDimensions', () => {
+ it('returns xl for width >= 1550', () => {
+ expect(getGridDimensions(1550)).toBe('xl');
+ expect(getGridDimensions(2000)).toBe('xl');
+ });
+
+ it('returns lg for width 1400-1549', () => {
+ expect(getGridDimensions(1400)).toBe('lg');
+ expect(getGridDimensions(1549)).toBe('lg');
+ });
+
+ it('returns md for width 1100-1399', () => {
+ expect(getGridDimensions(1100)).toBe('md');
+ expect(getGridDimensions(1399)).toBe('md');
+ });
+
+ it('returns sm for width < 1100', () => {
+ expect(getGridDimensions(800)).toBe('sm');
+ expect(getGridDimensions(1099)).toBe('sm');
+ });
+ });
+
+ describe('isWidgetType', () => {
+ it('returns true if type exists in mapping', () => {
+ const mapping = { 'widget-1': {}, 'widget-2': {} };
+ expect(isWidgetType(mapping, 'widget-1')).toBe(true);
+ });
+
+ it('returns false if type does not exist in mapping', () => {
+ const mapping = { 'widget-1': {} };
+ expect(isWidgetType(mapping, 'widget-2')).toBe(false);
+ });
+ });
+
+ describe('extendLayout', () => {
+ it('filters out dropping element', () => {
+ const config: ExtendedTemplateConfig = {
+ xl: [
+ { i: 'widget-1#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'widget-1', title: 'Widget 1' },
+ { i: '__dropping-elem__', x: 2, y: 0, w: 2, h: 3, widgetType: 'widget-2', title: 'Dropping' }
+ ],
+ lg: [],
+ md: [],
+ sm: []
+ };
+
+ const result = extendLayout(config);
+ expect(result.xl).toHaveLength(1);
+ expect(result.xl[0].i).toBe('widget-1#1');
+ });
+
+ it('adds widgetType from identifier', () => {
+ const config: ExtendedTemplateConfig = {
+ xl: [{ i: 'my-widget#123', x: 0, y: 0, w: 2, h: 3, widgetType: '', title: 'Widget' }],
+ lg: [],
+ md: [],
+ sm: []
+ };
+
+ const result = extendLayout(config);
+ expect(result.xl[0].widgetType).toBe('my-widget');
+ });
+ });
+
+ describe('mapTemplateConfigToExtendedTemplateConfig', () => {
+ it('converts TemplateConfig to ExtendedTemplateConfig', () => {
+ const config: TemplateConfig = {
+ xl: [{ i: 'widget-1#1', x: 0, y: 0, w: 2, h: 3, title: 'Widget 1' }],
+ lg: [],
+ md: [],
+ sm: []
+ };
+
+ const result = mapTemplateConfigToExtendedTemplateConfig(config);
+ expect(result.xl[0].widgetType).toBe('widget-1');
+ expect(result.xl[0].i).toBe('widget-1#1');
+ });
+ });
+});
+
diff --git a/packages/module/src/WidgetLayout/images.d.ts b/packages/module/src/WidgetLayout/images.d.ts
new file mode 100644
index 0000000..6bebf14
--- /dev/null
+++ b/packages/module/src/WidgetLayout/images.d.ts
@@ -0,0 +1,5 @@
+declare module '*.svg' {
+ const content: string;
+ export default content;
+}
+
diff --git a/packages/module/src/WidgetLayout/index.ts b/packages/module/src/WidgetLayout/index.ts
new file mode 100644
index 0000000..0f7b92d
--- /dev/null
+++ b/packages/module/src/WidgetLayout/index.ts
@@ -0,0 +1,23 @@
+export { default as WidgetLayout } from './WidgetLayout';
+export { default as GridLayout } from './GridLayout';
+export { default as GridTile } from './GridTile';
+export { default as WidgetDrawer } from './WidgetDrawer';
+
+export * from './types';
+export {
+ columns,
+ breakpoints,
+ droppingElemId,
+ getWidgetIdentifier,
+ mapWidgetDefaults,
+ getGridDimensions,
+ isWidgetType,
+ extendLayout,
+ mapTemplateConfigToExtendedTemplateConfig,
+} from './utils';
+
+export type { WidgetLayoutProps } from './WidgetLayout';
+export type { GridLayoutProps } from './GridLayout';
+export type { GridTileProps, SetWidgetAttribute } from './GridTile';
+export type { WidgetDrawerProps } from './WidgetDrawer';
+
diff --git a/packages/module/src/WidgetLayout/styles.ts b/packages/module/src/WidgetLayout/styles.ts
new file mode 100644
index 0000000..5695ed5
--- /dev/null
+++ b/packages/module/src/WidgetLayout/styles.ts
@@ -0,0 +1,134 @@
+/**
+ * Styles for WidgetLayout components using PatternFly tokens
+ * This replaces the SCSS files with CSS-in-JS approach
+ */
+
+export const gridLayoutStyles = `
+ .react-grid-item .react-resizable-handle-nw,
+ .react-grid-item .react-resizable-handle-sw,
+ .react-grid-item .react-resizable-handle-se {
+ display: none;
+ }
+
+ .react-grid-item .react-resizable-handle-nw,
+ .react-grid-item .react-resizable-handle-se {
+ cursor: nwse-resize;
+ }
+
+ .react-grid-item .react-resizable-handle-ne,
+ .react-grid-item .react-resizable-handle-sw {
+ cursor: nesw-resize;
+ }
+
+ .react-grid-item:hover:not(.static) .react-resizable-handle-nw,
+ .react-grid-item:hover:not(.static) .react-resizable-handle-sw,
+ .react-grid-item:hover:not(.static) .react-resizable-handle-se,
+ .react-grid-item:active:not(.static) .react-resizable-handle-nw,
+ .react-grid-item:active:not(.static) .react-resizable-handle-sw,
+ .react-grid-item:active:not(.static) .react-resizable-handle-se {
+ display: inherit;
+ }
+
+ .react-grid-item.react-grid-placeholder {
+ background-color: var(--pf-t--color--gray--60);
+ border-radius: 12px;
+ }
+
+ .react-grid-item .react-resizable-handle::after {
+ display: none;
+ }
+
+ .react-grid-item .react-resizable-handle img {
+ padding: 3px;
+ }
+
+ #widget-layout-container {
+ width: 100%;
+ min-height: 200px;
+ }
+
+ .grid-tile::before {
+ z-index: 1;
+ }
+
+ .grid-tile {
+ background: var(--pf-t--global--background--color--100);
+ position: relative;
+ height: 100%;
+ overflow: hidden;
+ }
+
+ .grid-tile .drag-handle {
+ cursor: grab;
+ }
+
+ .grid-tile .drag-handle.dragging {
+ cursor: grabbing;
+ }
+
+ .grid-tile.static .drag-handle,
+ .grid-tile.static .drag-handle.dragging {
+ cursor: not-allowed;
+ }
+
+ .grid-tile .pf-v6-c-card__header {
+ padding: var(--pf-t--global--spacer--md);
+ padding-bottom: var(--pf-t--global--spacer--sm);
+ background: var(--pf-t--global--background--color--200);
+ }
+
+ .grid-tile .widg-c-icon--header .service-icon {
+ height: 28px;
+ width: 28px;
+ }
+
+ .grid-tile .widg-c-icon--header .pf-v6-svg {
+ color: var(--pf-t--color--blue--50);
+ height: 22px;
+ width: 22px;
+ margin-bottom: var(--pf-t--global--spacer--sm);
+ }
+
+ .grid-tile .widg-card-header-text {
+ gap: 0;
+ line-height: 0;
+ padding-top: 2px;
+ }
+
+ .grid-tile .pf-v6-c-card__header .pf-v6-c-menu-toggle {
+ padding-left: 0;
+ padding-right: 0;
+ }
+
+ .grid-tile .pf-v6-c-card__actions {
+ padding-left: var(--pf-t--global--spacer--xs);
+ }
+
+ .grid-tile .pf-v6-c-card__body {
+ background: var(--pf-t--global--background--color--100);
+ }
+
+ .widg-c-drawer__header {
+ height: 100%;
+ }
+
+ .widg-c-drawer__drag-handle {
+ cursor: grab;
+ }
+
+ .widg-l-gallery {
+ --pf-v6-l-gallery--m-gutter--GridGap: 8px;
+ }
+`;
+
+// Inject styles into document head
+if (typeof document !== 'undefined') {
+ const styleId = 'widget-layout-styles';
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = gridLayoutStyles;
+ document.head.appendChild(style);
+ }
+}
+
diff --git a/packages/module/src/WidgetLayout/types.ts b/packages/module/src/WidgetLayout/types.ts
new file mode 100644
index 0000000..5b45383
--- /dev/null
+++ b/packages/module/src/WidgetLayout/types.ts
@@ -0,0 +1,82 @@
+import { Layout } from 'react-grid-layout';
+
+export const widgetIdSeparator = '#';
+
+export type Variants = 'sm' | 'md' | 'lg' | 'xl';
+
+export type LayoutWithTitle = Layout & { title: string };
+
+export type TemplateConfig = {
+ [k in Variants]: LayoutWithTitle[];
+};
+
+export type PartialTemplateConfig = Partial;
+
+// Extended type the UI tracks
+export type ExtendedLayoutItem = LayoutWithTitle & {
+ widgetType: string;
+ config?: WidgetConfiguration;
+ locked?: boolean;
+};
+
+// Extended type the UI tracks
+export type ExtendedTemplateConfig = {
+ [k in Variants]: ExtendedLayoutItem[];
+};
+
+// Extended type the UI tracks
+export type PartialExtendedTemplateConfig = Partial;
+
+export type WidgetDefaults = {
+ w: number;
+ h: number;
+ maxH: number;
+ minH: number;
+};
+
+export type WidgetHeaderLink = {
+ title?: string;
+ href?: string;
+};
+
+export type WidgetConfiguration = {
+ icon?: React.ReactNode;
+ headerLink?: WidgetHeaderLink;
+ title?: string;
+};
+
+/**
+ * Widget definition with rendering function
+ */
+export type WidgetDefinition = {
+ /** Unique widget type identifier */
+ widgetType: string;
+ /** Default dimensions for the widget */
+ defaults: WidgetDefaults;
+ /** Widget configuration (title, icon, header link) */
+ config?: WidgetConfiguration;
+ /** Function that renders the widget content */
+ renderWidget: (widgetId: string) => React.ReactNode;
+};
+
+/**
+ * Widget mapping keyed by widget type
+ */
+export type WidgetMapping = {
+ [widgetType: string]: Omit;
+};
+
+/**
+ * Notification type for user feedback
+ */
+export type Notification = {
+ variant: 'success' | 'danger' | 'warning' | 'info' | 'default';
+ title: string;
+ description?: string;
+};
+
+/**
+ * Analytics tracking function (optional)
+ */
+export type AnalyticsTracker = (event: string, data?: Record) => void;
+
diff --git a/packages/module/src/WidgetLayout/utils.ts b/packages/module/src/WidgetLayout/utils.ts
new file mode 100644
index 0000000..cd0f57a
--- /dev/null
+++ b/packages/module/src/WidgetLayout/utils.ts
@@ -0,0 +1,71 @@
+import { ExtendedTemplateConfig, TemplateConfig, Variants, widgetIdSeparator } from './types';
+
+export const droppingElemId = '__dropping-elem__';
+
+export const columns: Record = { xl: 4, lg: 3, md: 2, sm: 1 };
+
+export const breakpoints: Record = { xl: 1550, lg: 1400, md: 1100, sm: 800 };
+
+/**
+ * Generate a unique widget identifier
+ */
+export const getWidgetIdentifier = (widgetType: string, uniqueId: string = crypto.randomUUID()): string => `${widgetType}${widgetIdSeparator}${uniqueId}`;
+
+/**
+ * Parse widget identifier into type and unique ID
+ */
+export const mapWidgetDefaults = (id: string): [string, string] => {
+ const [widgetType, i] = id.split(widgetIdSeparator);
+ return [widgetType ?? '', i ?? ''];
+};
+
+/**
+ * Map template config to extended template config with widget types
+ */
+export const mapTemplateConfigToExtendedTemplateConfig = (templateConfig: TemplateConfig): ExtendedTemplateConfig => {
+ const result: ExtendedTemplateConfig = { sm: [], md: [], lg: [], xl: [] };
+ (Object.keys(templateConfig) as Variants[]).forEach((key) => {
+ result[key] = templateConfig[key].map((layoutWithTitle) => ({
+ ...layoutWithTitle,
+ widgetType: mapWidgetDefaults(layoutWithTitle.i)[0],
+ }));
+ });
+ return result;
+};
+
+/**
+ * Extend layout by filtering out dropping elements and adding widget types
+ */
+export const extendLayout = (extendedTemplateConfig: ExtendedTemplateConfig): ExtendedTemplateConfig => {
+ const result: ExtendedTemplateConfig = { sm: [], md: [], lg: [], xl: [] };
+ (Object.keys(extendedTemplateConfig) as Variants[]).forEach((key) => {
+ result[key] = extendedTemplateConfig[key]
+ .filter(({ i }) => i !== droppingElemId)
+ .map((item) => ({
+ ...item,
+ widgetType: mapWidgetDefaults(item.i)[0],
+ }));
+ });
+ return result;
+};
+
+/**
+ * Get grid dimensions based on container width
+ */
+export function getGridDimensions(currentWidth: number): Variants {
+ let variant: Variants = 'xl';
+ Object.entries(breakpoints).forEach(([breakpoint, value]) => {
+ if (value >= currentWidth) {
+ variant = breakpoint as Variants;
+ }
+ });
+ return variant;
+}
+
+/**
+ * Check if a type exists in the widget mapping
+ */
+export function isWidgetType(widgetMapping: Record, type: string): boolean {
+ return Object.keys(widgetMapping).includes(type);
+}
+
diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts
index 03c57e3..a0c5e97 100644
--- a/packages/module/src/index.ts
+++ b/packages/module/src/index.ts
@@ -1 +1,12 @@
-export * from './ExtendedButton';
+export { default as WidgetLayout } from './WidgetLayout/WidgetLayout';
+export { default as GridLayout } from './WidgetLayout/GridLayout';
+export { default as GridTile } from './WidgetLayout/GridTile';
+export { default as WidgetDrawer } from './WidgetLayout/WidgetDrawer';
+
+export * from './WidgetLayout/types';
+export * from './WidgetLayout/utils';
+
+export type { WidgetLayoutProps } from './WidgetLayout/WidgetLayout';
+export type { GridLayoutProps } from './WidgetLayout/GridLayout';
+export type { GridTileProps, SetWidgetAttribute } from './WidgetLayout/GridTile';
+export type { WidgetDrawerProps } from './WidgetLayout/WidgetDrawer';
diff --git a/yarn.lock b/yarn.lock
index 580c4b0..4483811 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3319,13 +3319,20 @@ __metadata:
"@patternfly/patternfly-a11y": "npm:^5.1.0"
"@patternfly/react-code-editor": "npm:^6.3.1"
"@patternfly/react-core": "npm:^6.3.1"
+ "@patternfly/react-icons": "npm:^6.3.1"
"@patternfly/react-table": "npm:^6.3.1"
+ "@types/react-grid-layout": "npm:^1.3.5"
+ clsx: "npm:^2.1.0"
monaco-editor: "npm:^0.53.0"
+ nodemon: "npm:^3.0.0"
+ react-grid-layout: "npm:^1.5.1"
react-monaco-editor: "npm:^0.59.0"
+ react-router-dom: "npm:^6.24.0"
rimraf: "npm:^6.0.1"
peerDependencies:
react: ^18
react-dom: ^18
+ react-router-dom: ^6
languageName: unknown
linkType: soft
@@ -3399,6 +3406,13 @@ __metadata:
languageName: node
linkType: hard
+"@remix-run/router@npm:1.23.0":
+ version: 1.23.0
+ resolution: "@remix-run/router@npm:1.23.0"
+ checksum: 10c0/eaef5cb46a1e413f7d1019a75990808307e08e53a39d4cf69c339432ddc03143d725decef3d6b9b5071b898da07f72a4a57c4e73f787005fcf10162973d8d7d7
+ languageName: node
+ linkType: hard
+
"@rtsao/scc@npm:^1.1.0":
version: 1.1.0
resolution: "@rtsao/scc@npm:1.1.0"
@@ -3836,6 +3850,24 @@ __metadata:
languageName: node
linkType: hard
+"@types/react-grid-layout@npm:^1.3.5":
+ version: 1.3.5
+ resolution: "@types/react-grid-layout@npm:1.3.5"
+ dependencies:
+ "@types/react": "npm:*"
+ checksum: 10c0/abd2a1dda9625c753ff2571a10b69740b2fb9ed1d3141755d54d5814cc12a9701c7c5cd78e8797e945486b441303b82543be71043a32d6a988b57a14237f93c6
+ languageName: node
+ linkType: hard
+
+"@types/react@npm:*":
+ version: 19.2.2
+ resolution: "@types/react@npm:19.2.2"
+ dependencies:
+ csstype: "npm:^3.0.2"
+ checksum: 10c0/f830b1204aca4634ce3c6cb3477b5d3d066b80a4dd832a4ee0069acb504b6debd2416548a43a11c1407c12bc60e2dc6cf362934a18fe75fe06a69c0a98cba8ab
+ languageName: node
+ linkType: hard
+
"@types/react@npm:^19.2.0":
version: 19.2.1
resolution: "@types/react@npm:19.2.1"
@@ -5928,7 +5960,7 @@ __metadata:
languageName: node
linkType: hard
-"chokidar@npm:^3.6.0":
+"chokidar@npm:^3.5.2, chokidar@npm:^3.6.0":
version: 3.6.0
resolution: "chokidar@npm:3.6.0"
dependencies:
@@ -6120,6 +6152,13 @@ __metadata:
languageName: node
linkType: hard
+"clsx@npm:^2.1.0, clsx@npm:^2.1.1":
+ version: 2.1.1
+ resolution: "clsx@npm:2.1.1"
+ checksum: 10c0/c4c8eb865f8c82baab07e71bfa8897c73454881c4f99d6bc81585aecd7c441746c1399d08363dc096c550cceaf97bd4ce1e8854e1771e9998d9f94c4fe075839
+ languageName: node
+ linkType: hard
+
"co@npm:^4.6.0":
version: 4.6.0
resolution: "co@npm:4.6.0"
@@ -6841,7 +6880,7 @@ __metadata:
languageName: node
linkType: hard
-"debug@npm:^4.4.0, debug@npm:^4.4.1, debug@npm:^4.4.3":
+"debug@npm:^4, debug@npm:^4.4.0, debug@npm:^4.4.1, debug@npm:^4.4.3":
version: 4.4.3
resolution: "debug@npm:4.4.3"
dependencies:
@@ -8226,6 +8265,13 @@ __metadata:
languageName: node
linkType: hard
+"fast-equals@npm:^4.0.3":
+ version: 4.0.3
+ resolution: "fast-equals@npm:4.0.3"
+ checksum: 10c0/87fd2609c945ee61e9ed4d041eb2a8f92723fc02884115f67e429dd858d880279e962334894f116b3e9b223f387d246e3db5424ae779287849015ddadbf5ff27
+ languageName: node
+ linkType: hard
+
"fast-fifo@npm:^1.2.0, fast-fifo@npm:^1.3.2":
version: 1.3.2
resolution: "fast-fifo@npm:1.3.2"
@@ -9642,6 +9688,13 @@ __metadata:
languageName: node
linkType: hard
+"ignore-by-default@npm:^1.0.1":
+ version: 1.0.1
+ resolution: "ignore-by-default@npm:1.0.1"
+ checksum: 10c0/9ab6e70e80f7cc12735def7ecb5527cfa56ab4e1152cd64d294522827f2dcf1f6d85531241537dc3713544e88dd888f65cb3c49c7b2cddb9009087c75274e533
+ languageName: node
+ linkType: hard
+
"ignore@npm:^5.2.0, ignore@npm:^5.3.2":
version: 5.3.2
resolution: "ignore@npm:5.3.2"
@@ -12343,6 +12396,26 @@ __metadata:
languageName: node
linkType: hard
+"nodemon@npm:^3.0.0":
+ version: 3.1.10
+ resolution: "nodemon@npm:3.1.10"
+ dependencies:
+ chokidar: "npm:^3.5.2"
+ debug: "npm:^4"
+ ignore-by-default: "npm:^1.0.1"
+ minimatch: "npm:^3.1.2"
+ pstree.remy: "npm:^1.1.8"
+ semver: "npm:^7.5.3"
+ simple-update-notifier: "npm:^2.0.0"
+ supports-color: "npm:^5.5.0"
+ touch: "npm:^3.1.0"
+ undefsafe: "npm:^2.0.5"
+ bin:
+ nodemon: bin/nodemon.js
+ checksum: 10c0/95b64d647f2c22e85e375b250517b0a4b32c2d2392ad898444e331f70d6b1ab43b17f53a8a1d68d5879ab8401fc6cd6e26f0d2a8736240984f6b5a8435b407c0
+ languageName: node
+ linkType: hard
+
"nopt@npm:^8.0.0":
version: 8.1.0
resolution: "nopt@npm:8.1.0"
@@ -13411,7 +13484,7 @@ __metadata:
languageName: node
linkType: hard
-"prop-types@npm:^15.6.1, prop-types@npm:^15.8.1":
+"prop-types@npm:15.x, prop-types@npm:^15.6.1, prop-types@npm:^15.8.1":
version: 15.8.1
resolution: "prop-types@npm:15.8.1"
dependencies:
@@ -13480,6 +13553,13 @@ __metadata:
languageName: node
linkType: hard
+"pstree.remy@npm:^1.1.8":
+ version: 1.1.8
+ resolution: "pstree.remy@npm:1.1.8"
+ checksum: 10c0/30f78c88ce6393cb3f7834216cb6e282eb83c92ccb227430d4590298ab2811bc4a4745f850a27c5178e79a8f3e316591de0fec87abc19da648c2b3c6eb766d14
+ languageName: node
+ linkType: hard
+
"pump@npm:^1.0.0":
version: 1.0.3
resolution: "pump@npm:1.0.3"
@@ -13718,6 +13798,19 @@ __metadata:
languageName: node
linkType: hard
+"react-draggable@npm:^4.0.3, react-draggable@npm:^4.4.6":
+ version: 4.5.0
+ resolution: "react-draggable@npm:4.5.0"
+ dependencies:
+ clsx: "npm:^2.1.1"
+ prop-types: "npm:^15.8.1"
+ peerDependencies:
+ react: ">= 16.3.0"
+ react-dom: ">= 16.3.0"
+ checksum: 10c0/6f7591fe450555218bf0d9e31984be02451bf3f678fb121f51ac0a0a645d01a1b5ea8248ef9afddcd24239028911fd88032194b9c00b30ad5ece76ea13397fc3
+ languageName: node
+ linkType: hard
+
"react-dropzone@npm:14.3.5":
version: 14.3.5
resolution: "react-dropzone@npm:14.3.5"
@@ -13744,6 +13837,23 @@ __metadata:
languageName: node
linkType: hard
+"react-grid-layout@npm:^1.5.1":
+ version: 1.5.2
+ resolution: "react-grid-layout@npm:1.5.2"
+ dependencies:
+ clsx: "npm:^2.1.1"
+ fast-equals: "npm:^4.0.3"
+ prop-types: "npm:^15.8.1"
+ react-draggable: "npm:^4.4.6"
+ react-resizable: "npm:^3.0.5"
+ resize-observer-polyfill: "npm:^1.5.1"
+ peerDependencies:
+ react: ">= 16.3.0"
+ react-dom: ">= 16.3.0"
+ checksum: 10c0/b6605d1435fe116c3720d168100a5a08da924c6905686fe8a486c33b82abbde8ccacbb59e5c6243fa52f5e808ad393a7bdf0c09a3446ebf76efe43f29d9f13ee
+ languageName: node
+ linkType: hard
+
"react-is@npm:^16.13.1":
version: 16.13.1
resolution: "react-is@npm:16.13.1"
@@ -13783,6 +13893,42 @@ __metadata:
languageName: node
linkType: hard
+"react-resizable@npm:^3.0.5":
+ version: 3.0.5
+ resolution: "react-resizable@npm:3.0.5"
+ dependencies:
+ prop-types: "npm:15.x"
+ react-draggable: "npm:^4.0.3"
+ peerDependencies:
+ react: ">= 16.3"
+ checksum: 10c0/cfe50aa6efb79e0aa09bd681a5beab2fcd1186737c4952eb4c3974ed9395d5d263ccd1130961d06b8f5e24c8f544dd2967b5c740ce68719962d1771de7bdb350
+ languageName: node
+ linkType: hard
+
+"react-router-dom@npm:^6.24.0":
+ version: 6.30.1
+ resolution: "react-router-dom@npm:6.30.1"
+ dependencies:
+ "@remix-run/router": "npm:1.23.0"
+ react-router: "npm:6.30.1"
+ peerDependencies:
+ react: ">=16.8"
+ react-dom: ">=16.8"
+ checksum: 10c0/e9e1297236b0faa864424ad7d51c392fc6e118595d4dad4cd542fd217c479a81601a81c6266d5801f04f9e154de02d3b094fc22ccb544e755c2eb448fab4ec6b
+ languageName: node
+ linkType: hard
+
+"react-router@npm:6.30.1":
+ version: 6.30.1
+ resolution: "react-router@npm:6.30.1"
+ dependencies:
+ "@remix-run/router": "npm:1.23.0"
+ peerDependencies:
+ react: ">=16.8"
+ checksum: 10c0/0414326f2d8e0c107fb4603cf4066dacba6a1f6f025c6e273f003e177b2f18888aca3de06d9b5522908f0e41de93be1754c37e82aa97b3a269c4742c08e82539
+ languageName: node
+ linkType: hard
+
"react-ssr-prepass@npm:1.5.0":
version: 1.5.0
resolution: "react-ssr-prepass@npm:1.5.0"
@@ -14185,6 +14331,13 @@ __metadata:
languageName: node
linkType: hard
+"resize-observer-polyfill@npm:^1.5.1":
+ version: 1.5.1
+ resolution: "resize-observer-polyfill@npm:1.5.1"
+ checksum: 10c0/5e882475067f0b97dc07e0f37c3e335ac5bc3520d463f777cec7e894bb273eddbfecb857ae668e6fb6881fd6f6bb7148246967172139302da50fa12ea3a15d95
+ languageName: node
+ linkType: hard
+
"resolve-cwd@npm:^3.0.0":
version: 3.0.0
resolution: "resolve-cwd@npm:3.0.0"
@@ -15017,6 +15170,15 @@ __metadata:
languageName: node
linkType: hard
+"simple-update-notifier@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "simple-update-notifier@npm:2.0.0"
+ dependencies:
+ semver: "npm:^7.5.3"
+ checksum: 10c0/2a00bd03bfbcbf8a737c47ab230d7920f8bfb92d1159d421bdd194479f6d01ebc995d13fbe13d45dace23066a78a3dc6642999b4e3b38b847e6664191575b20c
+ languageName: node
+ linkType: hard
+
"sirv@npm:^2.0.3":
version: 2.0.4
resolution: "sirv@npm:2.0.4"
@@ -15602,7 +15764,7 @@ __metadata:
languageName: node
linkType: hard
-"supports-color@npm:^5.3.0":
+"supports-color@npm:^5.3.0, supports-color@npm:^5.5.0":
version: 5.5.0
resolution: "supports-color@npm:5.5.0"
dependencies:
@@ -15961,6 +16123,15 @@ __metadata:
languageName: node
linkType: hard
+"touch@npm:^3.1.0":
+ version: 3.1.1
+ resolution: "touch@npm:3.1.1"
+ bin:
+ nodetouch: bin/nodetouch.js
+ checksum: 10c0/d2e4d269a42c846a22a29065b9af0b263de58effc85a1764bb7a2e8fc4b47700e9e2fcbd7eb1f5bffbb7c73d860f93600cef282b93ddac8f0b62321cb498b36e
+ languageName: node
+ linkType: hard
+
"tough-cookie@npm:^5.1.1":
version: 5.1.2
resolution: "tough-cookie@npm:5.1.2"
@@ -16344,6 +16515,13 @@ __metadata:
languageName: node
linkType: hard
+"undefsafe@npm:^2.0.5":
+ version: 2.0.5
+ resolution: "undefsafe@npm:2.0.5"
+ checksum: 10c0/96c0466a5fbf395917974a921d5d4eee67bca4b30d3a31ce7e621e0228c479cf893e783a109af6e14329b52fe2f0cb4108665fad2b87b0018c0df6ac771261d5
+ languageName: node
+ linkType: hard
+
"undici-types@npm:~5.26.4":
version: 5.26.5
resolution: "undici-types@npm:5.26.5"
From 978718729a10ea383f9775c866e41e4eda8bc311 Mon Sep 17 00:00:00 2001
From: alex
Date: Tue, 21 Oct 2025 08:30:16 -0400
Subject: [PATCH 2/6] chore:fix rendering
---
packages/module/package.json | 4 +---
.../content/examples/BasicExample.tsx | 4 ++--
.../content/examples/LockedLayoutExample.tsx | 12 ++++------
.../content/examples/WithoutDrawerExample.tsx | 6 ++---
.../patternfly-docs/content/examples/basic.md | 10 ++++----
.../module/src/WidgetLayout/GridLayout.tsx | 1 -
packages/module/src/WidgetLayout/GridTile.tsx | 5 ++--
packages/module/src/WidgetLayout/types.ts | 24 +++++++++----------
8 files changed, 29 insertions(+), 37 deletions(-)
diff --git a/packages/module/package.json b/packages/module/package.json
index 96e801e..185e56c 100644
--- a/packages/module/package.json
+++ b/packages/module/package.json
@@ -37,8 +37,7 @@
},
"peerDependencies": {
"react": "^18",
- "react-dom": "^18",
- "react-router-dom": "^6"
+ "react-dom": "^18"
},
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.18.6",
@@ -54,7 +53,6 @@
"monaco-editor": "^0.53.0",
"nodemon": "^3.0.0",
"react-monaco-editor": "^0.59.0",
- "react-router-dom": "^6.24.0",
"rimraf": "^6.0.1"
}
}
diff --git a/packages/module/patternfly-docs/content/examples/BasicExample.tsx b/packages/module/patternfly-docs/content/examples/BasicExample.tsx
index 5bb3a7a..ac8ec54 100644
--- a/packages/module/patternfly-docs/content/examples/BasicExample.tsx
+++ b/packages/module/patternfly-docs/content/examples/BasicExample.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import WidgetLayout from '@patternfly/widgetized-dashboard/dist/esm/WidgetLayout/WidgetLayout';
-import { WidgetMapping, ExtendedTemplateConfig } from '@patternfly/widgetized-dashboard/dist/esm/WidgetLayout/types';
+import WidgetLayout from '../../../src/WidgetLayout/WidgetLayout';
+import { WidgetMapping, ExtendedTemplateConfig } from '../../../src/WidgetLayout/types';
import { CubeIcon, ChartLineIcon, BellIcon } from '@patternfly/react-icons';
import { Card, CardBody, CardTitle } from '@patternfly/react-core';
diff --git a/packages/module/patternfly-docs/content/examples/LockedLayoutExample.tsx b/packages/module/patternfly-docs/content/examples/LockedLayoutExample.tsx
index dac0dcc..02df4fc 100644
--- a/packages/module/patternfly-docs/content/examples/LockedLayoutExample.tsx
+++ b/packages/module/patternfly-docs/content/examples/LockedLayoutExample.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import WidgetLayout from '@patternfly/widgetized-dashboard/dist/esm/WidgetLayout/WidgetLayout';
+import WidgetLayout from '../../../src/WidgetLayout/WidgetLayout';
import { WidgetMapping, ExtendedTemplateConfig } from '../../../src/WidgetLayout/types';
import { CubeIcon, ChartLineIcon } from '@patternfly/react-icons';
import { Card, CardBody, CardTitle } from '@patternfly/react-core';
@@ -70,18 +70,14 @@ const initialTemplate: ExtendedTemplateConfig = {
]
};
-export const BasicExample: React.FunctionComponent = () => (
+export const LockedLayoutExample: React.FunctionComponent = () => (
+ showDrawer={false}
+ />
);
diff --git a/packages/module/patternfly-docs/content/examples/WithoutDrawerExample.tsx b/packages/module/patternfly-docs/content/examples/WithoutDrawerExample.tsx
index 6f1782c..9f0baef 100644
--- a/packages/module/patternfly-docs/content/examples/WithoutDrawerExample.tsx
+++ b/packages/module/patternfly-docs/content/examples/WithoutDrawerExample.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import WidgetLayout from '@patternfly/widgetized-dashboard/dist/esm/WidgetLayout/WidgetLayout';
-import { WidgetMapping, ExtendedTemplateConfig } from '@patternfly/widgetized-dashboard/dist/esm/WidgetLayout/types';
+import WidgetLayout from '../../../src/WidgetLayout/WidgetLayout';
+import { WidgetMapping, ExtendedTemplateConfig } from '../../../src/WidgetLayout/types';
import { CubeIcon, ChartLineIcon, BellIcon } from '@patternfly/react-icons';
import { Card, CardBody, CardTitle } from '@patternfly/react-core';
@@ -91,7 +91,7 @@ const initialTemplate: ExtendedTemplateConfig = {
]
};
-export const BasicExample: React.FunctionComponent = () => (
+export const WithoutDrawerExample: React.FunctionComponent = () => (
{activeLayout
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
.map(({ widgetType, title, ...rest }, index) => {
const widget = widgetMapping[widgetType];
if (!widget) {
diff --git a/packages/module/src/WidgetLayout/GridTile.tsx b/packages/module/src/WidgetLayout/GridTile.tsx
index 073b300..3e2d3b2 100644
--- a/packages/module/src/WidgetLayout/GridTile.tsx
+++ b/packages/module/src/WidgetLayout/GridTile.tsx
@@ -21,7 +21,6 @@ import {
import { CompressIcon, EllipsisVIcon, ExpandIcon, GripVerticalIcon, LockIcon, MinusCircleIcon, UnlockIcon } from '@patternfly/react-icons';
import React, { useMemo, useState } from 'react';
import clsx from 'clsx';
-import { Link } from 'react-router-dom';
import { Layout } from 'react-grid-layout';
import { ExtendedLayoutItem, WidgetConfiguration, AnalyticsTracker } from './types';
@@ -210,7 +209,9 @@ const GridTile = ({
diff --git a/packages/module/src/WidgetLayout/types.ts b/packages/module/src/WidgetLayout/types.ts
index 5b45383..741741b 100644
--- a/packages/module/src/WidgetLayout/types.ts
+++ b/packages/module/src/WidgetLayout/types.ts
@@ -27,28 +27,28 @@ export type ExtendedTemplateConfig = {
// Extended type the UI tracks
export type PartialExtendedTemplateConfig = Partial;
-export type WidgetDefaults = {
+export interface WidgetDefaults {
w: number;
h: number;
maxH: number;
minH: number;
-};
+}
-export type WidgetHeaderLink = {
+export interface WidgetHeaderLink {
title?: string;
href?: string;
-};
+}
-export type WidgetConfiguration = {
+export interface WidgetConfiguration {
icon?: React.ReactNode;
headerLink?: WidgetHeaderLink;
title?: string;
-};
+}
/**
* Widget definition with rendering function
*/
-export type WidgetDefinition = {
+export interface WidgetDefinition {
/** Unique widget type identifier */
widgetType: string;
/** Default dimensions for the widget */
@@ -57,23 +57,23 @@ export type WidgetDefinition = {
config?: WidgetConfiguration;
/** Function that renders the widget content */
renderWidget: (widgetId: string) => React.ReactNode;
-};
+}
/**
* Widget mapping keyed by widget type
*/
-export type WidgetMapping = {
+export interface WidgetMapping {
[widgetType: string]: Omit;
-};
+}
/**
* Notification type for user feedback
*/
-export type Notification = {
+export interface Notification {
variant: 'success' | 'danger' | 'warning' | 'info' | 'default';
title: string;
description?: string;
-};
+}
/**
* Analytics tracking function (optional)
From 21359fbbac239b246a4247d630bbb0a4793733dc Mon Sep 17 00:00:00 2001
From: alex
Date: Tue, 21 Oct 2025 09:15:03 -0400
Subject: [PATCH 3/6] fix: be sure locked tooltip is showing in locked layout
example
---
.../content/examples/BasicExample.tsx | 104 ++++++++++++------
.../content/examples/LockedLayoutExample.tsx | 93 +++++++++++-----
.../content/examples/WithoutDrawerExample.tsx | 102 +++++++++++------
.../patternfly-docs/content/examples/basic.md | 4 +-
4 files changed, 207 insertions(+), 96 deletions(-)
diff --git a/packages/module/patternfly-docs/content/examples/BasicExample.tsx b/packages/module/patternfly-docs/content/examples/BasicExample.tsx
index ac8ec54..7181069 100644
--- a/packages/module/patternfly-docs/content/examples/BasicExample.tsx
+++ b/packages/module/patternfly-docs/content/examples/BasicExample.tsx
@@ -1,42 +1,78 @@
import React from 'react';
import WidgetLayout from '../../../src/WidgetLayout/WidgetLayout';
import { WidgetMapping, ExtendedTemplateConfig } from '../../../src/WidgetLayout/types';
-import { CubeIcon, ChartLineIcon, BellIcon } from '@patternfly/react-icons';
-import { Card, CardBody, CardTitle } from '@patternfly/react-core';
+import { CubeIcon, ChartLineIcon, BellIcon, ExternalLinkAltIcon, ArrowRightIcon } from '@patternfly/react-icons';
+import { Card, CardBody, CardFooter, Content, Icon } from '@patternfly/react-core';
-// Example widget content components
-const ExampleWidget1 = () => (
-
-
- Example Widget 1
- This is the content of the first example widget.
- You can put any React content here!
+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 = () => (
-
-
- Chart Widget
-
-
Chart content would go here
-
-
-
+
);
const ExampleWidget3 = () => (
-
-
- Notifications
-
- - Notification 1
- - Notification 2
- - Notification 3
-
-
-
+
);
// Define widget mapping
@@ -56,7 +92,7 @@ const widgetMapping: WidgetMapping = {
'chart-widget': {
defaults: { w: 2, h: 4, maxH: 8, minH: 3 },
config: {
- title: 'Performance Chart',
+ title: 'Chart Widget',
icon:
},
renderWidget: () =>
@@ -64,7 +100,7 @@ const widgetMapping: WidgetMapping = {
'notifications-widget': {
defaults: { w: 1, h: 3, maxH: 6, minH: 2 },
config: {
- title: 'Recent Notifications',
+ title: 'Notification Widget',
icon:
},
renderWidget: () =>
@@ -75,19 +111,19 @@ const widgetMapping: WidgetMapping = {
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: 'Performance Chart' }
+ { 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: 'Performance Chart' }
+ { 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: 'Performance Chart' }
+ { 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: 'Performance Chart' }
+ { i: 'chart-widget#1', x: 0, y: 3, w: 1, h: 4, widgetType: 'chart-widget', title: 'Chart Widget' }
]
};
@@ -95,7 +131,7 @@ export const BasicExample: React.FunctionComponent = () => {
const [template, setTemplate] = React.useState(initialTemplate);
return (
-
+
(
+interface SimpleWidgetProps {
+ id: number;
+ body: string;
+ linkTitle: string;
+ url: string;
+ isExternal?: boolean;
+}
+
+const CardExample: React.FunctionComponent = (props) => (
-
- Example Widget 1
- This is the content of the first example widget.
- You can put any React content here!
+
+
+
+ {props.body}
+
+
+
+ {props.isExternal ? (
+
+ {props.linkTitle}
+
+
+
+
+ ) : (
+
+ {props.linkTitle}
+
+
+
+
+ )}
+
);
+// Example widget content components
+const ExampleWidget1 = () => (
+
+);
+
const ExampleWidget2 = () => (
-
-
- Chart Widget
-
-
Chart content would go here
-
-
-
+
);
// Define widget mapping
@@ -43,35 +82,35 @@ const widgetMapping: WidgetMapping = {
'chart-widget': {
defaults: { w: 2, h: 4, maxH: 8, minH: 3 },
config: {
- title: 'Performance Chart',
+ title: 'Chart Widget',
icon:
},
renderWidget: () =>
}
};
-// Define initial template
+// 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' },
- { i: 'chart-widget#1', x: 2, y: 0, w: 2, h: 4, widgetType: 'chart-widget', title: 'Performance Chart' }
+ { 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 }
],
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: 'Performance Chart' }
+ { 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 }
],
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: 'Performance Chart' }
+ { 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 }
],
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: 'Performance Chart' }
+ { 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 }
]
};
export const LockedLayoutExample: React.FunctionComponent = () => (
-
+
(
+interface SimpleWidgetProps {
+ id: number;
+ body: string;
+ linkTitle: string;
+ url: string;
+ isExternal?: boolean;
+}
+
+const CardExample: React.FunctionComponent = (props) => (
-
- Example Widget 1
- This is the content of the first example widget.
- You can put any React content here!
+
+
+
+ {props.body}
+
+
+
+ {props.isExternal ? (
+
+ {props.linkTitle}
+
+
+
+
+ ) : (
+
+ {props.linkTitle}
+
+
+
+
+ )}
+
);
+// Example widget content components
+const ExampleWidget1 = () => (
+
+);
+
const ExampleWidget2 = () => (
-
-
- Chart Widget
-
-
Chart content would go here
-
-
-
+
);
const ExampleWidget3 = () => (
-
-
- Notifications
-
- - Notification 1
- - Notification 2
- - Notification 3
-
-
-
+
);
// Define widget mapping
@@ -56,7 +92,7 @@ const widgetMapping: WidgetMapping = {
'chart-widget': {
defaults: { w: 2, h: 4, maxH: 8, minH: 3 },
config: {
- title: 'Performance Chart',
+ title: 'Chart Widget',
icon:
},
renderWidget: () =>
@@ -64,7 +100,7 @@ const widgetMapping: WidgetMapping = {
'notifications-widget': {
defaults: { w: 1, h: 3, maxH: 6, minH: 2 },
config: {
- title: 'Recent Notifications',
+ title: 'Notification Widget',
icon:
},
renderWidget: () =>
@@ -75,24 +111,24 @@ const widgetMapping: WidgetMapping = {
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: 'Performance Chart' }
+ { 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: 'Performance Chart' }
+ { 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: 'Performance Chart' }
+ { 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: 'Performance Chart' }
+ { i: 'chart-widget#1', x: 0, y: 3, w: 1, h: 4, widgetType: 'chart-widget', title: 'Chart Widget' }
]
};
export const WithoutDrawerExample: React.FunctionComponent = () => (
-
+
Date: Tue, 21 Oct 2025 10:15:19 -0400
Subject: [PATCH 4/6] chore: add button to open drawer
---
.../module/src/WidgetLayout/WidgetDrawer.tsx | 20 ++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/packages/module/src/WidgetLayout/WidgetDrawer.tsx b/packages/module/src/WidgetLayout/WidgetDrawer.tsx
index 2c35d16..c97eb03 100644
--- a/packages/module/src/WidgetLayout/WidgetDrawer.tsx
+++ b/packages/module/src/WidgetLayout/WidgetDrawer.tsx
@@ -14,7 +14,7 @@ import {
Tooltip,
} from '@patternfly/react-core';
import React, { useState } from 'react';
-import { CloseIcon, GripVerticalIcon } from '@patternfly/react-icons';
+import { CloseIcon, GripVerticalIcon, PlusCircleIcon } from '@patternfly/react-icons';
import { WidgetMapping, WidgetConfiguration } from './types';
export type WidgetDrawerProps = React.PropsWithChildren<{
@@ -105,10 +105,7 @@ const WidgetDrawer = ({
const panelContent = (
@@ -139,7 +136,7 @@ const WidgetDrawer = ({
/>
-
+
{filteredWidgetMapping.map(([type, widget], i) => (
- {isOpen ? {panelContent}
: null}
+
+
+
+ {isOpen && {panelContent}
}
{children}
>
);
From 110ae57b0f631721228d48036e77e28d547b8cd2 Mon Sep 17 00:00:00 2001
From: alex
Date: Tue, 21 Oct 2025 10:22:39 -0400
Subject: [PATCH 5/6] fix:remove commented code
---
.../module/src/WidgetLayout/WidgetLayout.tsx | 37 -------------------
1 file changed, 37 deletions(-)
diff --git a/packages/module/src/WidgetLayout/WidgetLayout.tsx b/packages/module/src/WidgetLayout/WidgetLayout.tsx
index 9ce839a..b2069c3 100644
--- a/packages/module/src/WidgetLayout/WidgetLayout.tsx
+++ b/packages/module/src/WidgetLayout/WidgetLayout.tsx
@@ -28,43 +28,6 @@ export interface WidgetLayoutProps {
initialDrawerOpen?: boolean;
};
-/**
- * WidgetLayout - A complete drag-and-drop dashboard layout component
- *
- * This component provides a full-featured dashboard experience with:
- * - Responsive grid layout with drag-and-drop
- * - Widget drawer for adding/removing widgets
- * - Lock/unlock widgets
- * - Resize widgets
- * - Persistent layout configuration
- *
- * @example
- * ```tsx
- * const widgetMapping = {
- * 'my-widget': {
- * defaults: { w: 2, h: 3, maxH: 6, minH: 2 },
- * config: {
- * title: 'My Widget',
- * icon:
- * },
- * renderWidget: (id) =>
- * }
- * };
- *
- * const template = {
- * xl: [{ i: 'my-widget#1', x: 0, y: 0, w: 2, h: 3, widgetType: 'my-widget', title: 'My Widget' }],
- * lg: [...],
- * md: [...],
- * sm: [...]
- * };
- *
- * saveTemplate(newTemplate)}
- * />
- * ```
- */
const WidgetLayout = ({
widgetMapping,
initialTemplate,
From 65b74bffec7c8c3f3f70f9110bf6591b34c22da1 Mon Sep 17 00:00:00 2001
From: alex
Date: Thu, 23 Oct 2025 08:39:31 -0400
Subject: [PATCH 6/6] remove files/text
---
ARCHITECTURE.md | 381 -------------------
MIGRATION.md | 256 -------------
packages/module/src/WidgetLayout/images.d.ts | 5 -
3 files changed, 642 deletions(-)
delete mode 100644 ARCHITECTURE.md
delete mode 100644 MIGRATION.md
delete mode 100644 packages/module/src/WidgetLayout/images.d.ts
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
deleted file mode 100644
index e6e4b3f..0000000
--- a/ARCHITECTURE.md
+++ /dev/null
@@ -1,381 +0,0 @@
-# Architecture Overview
-
-## Component Structure
-
-The Widgetized Dashboard is composed of several key components that work together to provide a complete dashboard experience:
-
-```
-WidgetLayout (main component)
-├── WidgetDrawer (widget selection)
-│ └── WidgetWrapper (draggable widget cards)
-└── GridLayout (drag-and-drop grid)
- └── GridTile (individual widget wrapper)
- └── Widget Content (user-provided via renderWidget)
-```
-
-## Core Components
-
-### 1. WidgetLayout
-The top-level component that orchestrates the entire dashboard experience.
-
-**Responsibilities:**
-- Manages overall state (template, drawer open/close)
-- Coordinates between GridLayout and WidgetDrawer
-- Provides unified API for consumers
-
-**State:**
-- `template`: Current layout configuration
-- `drawerOpen`: Whether widget drawer is visible
-- `currentlyUsedWidgets`: List of widget types in use
-
-### 2. GridLayout
-The core layout engine powered by `react-grid-layout`.
-
-**Responsibilities:**
-- Rendering the responsive grid
-- Handling drag-and-drop operations
-- Managing widget positions and sizes
-- Responsive breakpoint handling
-- Widget CRUD operations
-
-**Key Features:**
-- 4 responsive breakpoints (xl, lg, md, sm)
-- Drag-and-drop from drawer
-- Resize with corner handles
-- Lock/unlock individual widgets
-- Empty state display
-
-**State:**
-- `isDragging`: Drag operation in progress
-- `isInitialRender`: Skip save on first render
-- `layoutVariant`: Current breakpoint
-- `layoutWidth`: Container width
-- `currentDropInItem`: Widget being dragged
-
-### 3. GridTile
-Wrapper component for individual widgets.
-
-**Responsibilities:**
-- Render widget content
-- Provide widget actions menu
-- Display widget header with icon and title
-- Handle widget-level interactions
-
-**Features:**
-- Lock/unlock toggle
-- Autosize to max height
-- Minimize to min height
-- Remove widget
-- Optional header link
-- Drag handle
-
-### 4. WidgetDrawer
-Side panel for selecting widgets to add.
-
-**Responsibilities:**
-- Display available widgets
-- Filter out currently used widgets
-- Provide drag source for new widgets
-- Instructions for users
-
-## Data Flow
-
-### Template Management
-
-```
-┌─────────────┐
-│ Parent │
-│ Component │
-└──────┬──────┘
- │
- │ initialTemplate (prop)
- ▼
-┌─────────────┐
-│ WidgetLayout│
-│ (state) │
-└──────┬──────┘
- │
- │ template (prop)
- ▼
-┌─────────────┐
-│ GridLayout │
-│ (renders) │
-└──────┬──────┘
- │
- │ onChange
- ▼
-┌─────────────┐
-│onTemplateChange│
-│ (callback) │
-└──────┬──────┘
- │
- │ save to API/storage
- ▼
-┌─────────────┐
-│ Parent │
-│ Component │
-└─────────────┘
-```
-
-### Widget Rendering
-
-```
-┌──────────────┐
-│WidgetMapping │
-│ (config) │
-└──────┬───────┘
- │
- │ widgetType → renderWidget
- ▼
-┌──────────────┐
-│ GridTile │
-│ (wrapper) │
-└──────┬───────┘
- │
- │ renders children
- ▼
-┌──────────────┐
-│Widget Content│
-│ (React node) │
-└──────────────┘
-```
-
-## State Management
-
-The component uses **local React state** for all state management:
-
-- No external state libraries required (no Jotai, Redux, etc.)
-- Parent controls template via `initialTemplate` and `onTemplateChange`
-- Internal state synchronized with props
-- Callbacks for notifications, analytics, etc.
-
-### State Flow
-
-```typescript
-// Parent manages template
-const [template, setTemplate] = useState(initialTemplate);
-
-// WidgetLayout receives and manages internally
- {
- setTemplate(newTemplate);
- saveToAPI(newTemplate);
- }}
-/>
-
-// Internal state stays in sync
-useEffect(() => {
- setInternalTemplate(template);
-}, [template]);
-```
-
-## Responsive Design
-
-### Breakpoints
-
-| Breakpoint | Width | Columns | Use Case |
-|-----------|------------|---------|----------|
-| xl | ≥1550px | 4 | Large desktop |
-| lg | 1400-1549px| 3 | Desktop |
-| md | 1100-1399px| 2 | Tablet landscape |
-| sm | 800-1099px | 1 | Tablet portrait |
-
-### Grid System
-
-- **Row Height**: 56px (fixed)
-- **Container**: 100% width, min-height 200px
-- **Columns**: Responsive (4/3/2/1)
-- **Gutter**: Managed by react-grid-layout
-
-### Responsive Behavior
-
-```typescript
-// Auto-detect breakpoint on mount and resize
-const observer = new ResizeObserver((entries) => {
- const width = entries[0].contentRect.width;
- const variant = getGridDimensions(width);
- setLayoutVariant(variant);
-});
-```
-
-Each template must define layouts for all breakpoints:
-
-```typescript
-const template: ExtendedTemplateConfig = {
- xl: [...], // 4 columns
- lg: [...], // 3 columns
- md: [...], // 2 columns
- sm: [...] // 1 column
-};
-```
-
-## Type System
-
-### Core Types
-
-```typescript
-// Widget definition
-type WidgetMapping = {
- [widgetType: string]: {
- defaults: WidgetDefaults;
- config?: WidgetConfiguration;
- renderWidget: (widgetId: string) => React.ReactNode;
- };
-};
-
-// Layout configuration
-type ExtendedTemplateConfig = {
- xl: ExtendedLayoutItem[];
- lg: ExtendedLayoutItem[];
- md: ExtendedLayoutItem[];
- sm: ExtendedLayoutItem[];
-};
-
-// Individual widget placement
-type ExtendedLayoutItem = Layout & {
- widgetType: string;
- title: string;
- config?: WidgetConfiguration;
- locked?: boolean;
-};
-```
-
-## Performance Considerations
-
-### Optimization Strategies
-
-1. **Memoization**
- - `useMemo` for dropping item template
- - `useMemo` for dropdown items
- - `useMemo` for header links
-
-2. **Conditional Rendering**
- - Empty state only when needed
- - Drawer only when `showDrawer={true}`
- - Resize handles only on hover
-
-3. **Event Handling**
- - No inline function creation in render
- - Callbacks defined at component level
- - Event handlers properly memoized
-
-4. **Grid Reset**
- - Key prop forces remount on breakpoint change
- - Necessary for proper react-grid-layout behavior
- - `key={'grid-' + layoutVariant}`
-
-## Integration Points
-
-### With PatternFly
-
-- Uses PatternFly Card for widget containers
-- Uses PatternFly EmptyState for empty dashboard
-- Uses PatternFly Dropdown for actions menu
-- Uses PatternFly Icons throughout
-- Follows PatternFly design tokens
-
-### With react-grid-layout
-
-- Wraps ReactGridLayout component
-- Custom resize handles
-- Custom drag handles
-- Responsive breakpoints
-- Layout persistence
-
-### With React Router
-
-- Optional Link component in GridTile
-- Handles internal/external links
-- Target blank for external links
-
-## Extensibility
-
-### Custom Empty State
-
-```typescript
-}
-/>
-```
-
-### Custom Analytics
-
-```typescript
- {
- myAnalytics.track(event, data);
- }}
-/>
-```
-
-### Custom Notifications
-
-```typescript
- {
- toast.show(notification.title, {
- variant: notification.variant
- });
- }}
-/>
-```
-
-## File Structure
-
-```
-src/WidgetLayout/
-├── types.ts # TypeScript type definitions
-├── utils.ts # Utility functions
-├── GridLayout.tsx # Main grid component
-├── GridLayout.scss # Grid styles
-├── GridTile.tsx # Widget wrapper component
-├── GridTile.scss # Tile styles
-├── WidgetDrawer.tsx # Widget selection drawer
-├── WidgetDrawer.scss # Drawer styles
-├── WidgetLayout.tsx # Main component
-├── index.ts # Public exports
-├── resize-handle.svg # Resize handle icon
-└── __tests__/ # Test files
- ├── WidgetLayout.test.tsx
- ├── utils.test.ts
- └── __snapshots__/
-```
-
-## Dependencies
-
-### Runtime Dependencies
-
-- `react` & `react-dom` (peer)
-- `react-router-dom` (peer)
-- `@patternfly/react-core`
-- `@patternfly/react-icons`
-- `react-grid-layout`
-- `clsx`
-
-### Development Dependencies
-
-- `@types/react-grid-layout`
-- TypeScript
-- Jest & Testing Library
-
-## Browser Support
-
-Modern browsers with ES6 support:
-- Chrome/Edge (Chromium)
-- Firefox
-- Safari
-- Mobile browsers
-
-## Future Enhancements
-
-Potential areas for extension:
-
-1. **Widget Templates** - Pre-defined layout templates
-2. **Export/Import** - JSON export/import of layouts
-3. **Undo/Redo** - Layout change history
-4. **Widget Groups** - Grouping related widgets
-5. **Themes** - Custom color schemes
-6. **Animations** - Smooth transitions
-7. **Touch Support** - Better mobile experience
-
diff --git a/MIGRATION.md b/MIGRATION.md
deleted file mode 100644
index cc37c16..0000000
--- a/MIGRATION.md
+++ /dev/null
@@ -1,256 +0,0 @@
-# Migration Guide from widget-layout
-
-This document provides guidance for users migrating from the RedHatInsights/widget-layout repository to this generic PatternFly component.
-
-## What Changed
-
-### Removed Dependencies
-
-The following console-specific dependencies have been removed:
-
-1. **Scalprum** (`@scalprum/react-core`) - Federated module loading
-2. **Chrome Services APIs** - API calls for template persistence
-3. **useCurrentUser** - Console-specific authentication
-4. **useChrome** - Console-specific hooks and analytics
-5. **Jotai atoms** - External state management
-6. **Console icons and branding**
-
-### New Approach
-
-#### Widget Definition
-
-**Before (widget-layout):**
-```typescript
-// Widget loaded via Scalprum federated modules
-const widgetMapping = {
- 'my-widget': {
- scope: 'myApp',
- module: './MyWidget',
- importName: 'MyWidget',
- defaults: { w: 2, h: 3, maxH: 6, minH: 2 },
- config: { title: 'My Widget' }
- }
-};
-```
-
-**After (widgetized-dashboard):**
-```typescript
-// Widget rendered directly via React component
-const widgetMapping: WidgetMapping = {
- 'my-widget': {
- defaults: { w: 2, h: 3, maxH: 6, minH: 2 },
- config: {
- title: 'My Widget',
- icon:
- },
- renderWidget: (id) =>
- }
-};
-```
-
-#### Template Management
-
-**Before (widget-layout):**
-```typescript
-// Templates loaded/saved automatically via Chrome Services API
-import { getDashboardTemplates, patchDashboardTemplate } from './api/dashboard-templates';
-
-// Component handles API calls internally
-
-```
-
-**After (widgetized-dashboard):**
-```typescript
-// Templates managed via props (bring your own state management)
-const [template, setTemplate] = useState(loadTemplate());
-
-const handleTemplateChange = async (newTemplate) => {
- setTemplate(newTemplate);
- await saveTemplateToAPI(newTemplate); // Your own API
-};
-
-
-```
-
-#### State Management
-
-**Before (widget-layout):**
-```typescript
-// State managed via Jotai atoms
-import { useAtom } from 'jotai';
-import { templateAtom } from './state/templateAtom';
-
-const [template, setTemplate] = useAtom(templateAtom);
-```
-
-**After (widgetized-dashboard):**
-```typescript
-// State managed via React state or your preferred solution
-const [template, setTemplate] = useState(initialTemplate);
-
-// Or use Redux, MobX, Zustand, etc.
-```
-
-#### Analytics
-
-**Before (widget-layout):**
-```typescript
-// Chrome analytics used internally
-import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
-
-const { analytics } = useChrome();
-analytics.track('widget-layout.widget-add', { data });
-```
-
-**After (widgetized-dashboard):**
-```typescript
-// Optional analytics via callback
- {
- yourAnalytics.track(event, data);
- }}
-/>
-```
-
-## Step-by-Step Migration
-
-### 1. Install Dependencies
-
-```bash
-yarn add @patternfly/widgetized-dashboard
-```
-
-### 2. Update Widget Definitions
-
-Convert your Scalprum widget definitions to render functions:
-
-```typescript
-// Old
-{
- 'insights-widget': {
- scope: '@redhat-cloud-services/insights',
- module: './InsightsWidget',
- importName: 'default',
- defaults: { w: 2, h: 3, maxH: 6, minH: 2 }
- }
-}
-
-// New
-{
- 'insights-widget': {
- defaults: { w: 2, h: 3, maxH: 6, minH: 2 },
- config: {
- title: 'Insights',
- icon:
- },
- renderWidget: (id) =>
- }
-}
-```
-
-### 3. Implement Template Persistence
-
-Replace Chrome Services API calls with your own persistence layer:
-
-```typescript
-// Load from your backend
-const loadTemplate = async () => {
- const response = await fetch('/api/dashboard/template');
- return response.json();
-};
-
-// Save to your backend
-const saveTemplate = async (template) => {
- await fetch('/api/dashboard/template', {
- method: 'POST',
- body: JSON.stringify(template)
- });
-};
-
-// Use in component
-const MyDashboard = () => {
- const [template, setTemplate] = useState(null);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- loadTemplate().then(t => {
- setTemplate(t);
- setLoading(false);
- });
- }, []);
-
- if (loading) return ;
-
- return (
-
- );
-};
-```
-
-### 4. Update Imports
-
-```typescript
-// Old
-import GridLayout from './Components/DnDLayout/GridLayout';
-import { getDashboardTemplates } from './api/dashboard-templates';
-
-// New
-import { WidgetLayout, GridLayout, ExtendedTemplateConfig } from '@patternfly/widgetized-dashboard';
-```
-
-### 5. Remove State Atoms
-
-If you were using the Jotai atoms, replace them with your own state management:
-
-```typescript
-// Old
-import { useAtom } from 'jotai';
-import { templateAtom } from './state/templateAtom';
-
-// New - use React state, Redux, or your preferred solution
-import { useState } from 'react';
-const [template, setTemplate] = useState(initialTemplate);
-```
-
-## API Mapping
-
-| widget-layout | widgetized-dashboard | Notes |
-|--------------|---------------------|-------|
-| `getDashboardTemplates()` | `loadTemplate()` | Implement your own |
-| `patchDashboardTemplate()` | `saveTemplate()` | Implement your own |
-| `useCurrentUser()` | Your auth solution | Use your app's auth |
-| `useChrome()` | Props/callbacks | Pass as props |
-| Jotai atoms | React state/your store | Bring your own state |
-| Scalprum loading | `renderWidget` prop | Direct rendering |
-
-## Breaking Changes
-
-1. **No automatic persistence** - You must implement template loading/saving
-2. **No federated modules** - Use direct React components instead
-3. **No built-in auth** - Handle authentication in your app layer
-4. **State management** - Component is self-contained, no external atoms
-5. **Analytics** - Optional callback instead of automatic tracking
-
-## Benefits of Migration
-
-1. **No console dependencies** - Works in any PatternFly application
-2. **Simpler mental model** - Props in, callbacks out
-3. **Flexible persistence** - Use any backend or storage solution
-4. **Better TypeScript support** - Full type definitions
-5. **Lighter bundle** - Fewer dependencies
-6. **More control** - Explicit state management
-
-## Need Help?
-
-- [Full Documentation](./README.md)
-- [Examples](./packages/module/patternfly-docs/content/examples/)
-- [Design Guidelines](./packages/module/patternfly-docs/content/design-guidelines/)
-
diff --git a/packages/module/src/WidgetLayout/images.d.ts b/packages/module/src/WidgetLayout/images.d.ts
deleted file mode 100644
index 6bebf14..0000000
--- a/packages/module/src/WidgetLayout/images.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-declare module '*.svg' {
- const content: string;
- export default content;
-}
-