Skip to content

Commit 7b774ac

Browse files
committed
Fix review issues
1 parent bf3716e commit 7b774ac

3 files changed

Lines changed: 186 additions & 5 deletions

File tree

packages/dashboard-core-plugins/src/WidgetLoaderPlugin.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,11 @@ export function WidgetLoaderPlugin(
232232
supportedTypes.forEach((info, _type) => {
233233
// Use the base plugin name as the key to get unique plugins
234234
if (!uniquePluginInfos.has(info.basePlugin.name)) {
235-
uniquePluginInfos.set(info.basePlugin.name, info);
235+
// Clone to avoid mutating the useMemo result
236+
uniquePluginInfos.set(info.basePlugin.name, {
237+
basePlugin: info.basePlugin,
238+
middleware: [...info.middleware],
239+
});
236240
} else {
237241
// Merge middleware from multiple type registrations for the same base plugin
238242
const existingInfo = uniquePluginInfos.get(info.basePlugin.name);
@@ -266,7 +270,9 @@ export function WidgetLoaderPlugin(
266270
);
267271
}
268272

269-
// Has panel component - chain both component and panel
273+
// Has panel component - chain middleware around the panel.
274+
// Middleware with panelComponent wraps at the panel level directly.
275+
// Middleware with only component is auto-promoted to a panel wrapper.
270276
const chainedPanelComponent = createChainedPanelComponent(
271277
panelComponent,
272278
middleware
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Middleware Plugin Architecture
2+
3+
## Overview
4+
5+
Middleware plugins allow you to wrap and enhance existing widget plugins without modifying them. Multiple middleware plugins can target the same widget type and are chained in registration order, with the first registered middleware as the outermost wrapper.
6+
7+
```
8+
┌─────────────────────────────┐
9+
│ Middleware A (outermost) │
10+
│ ┌────────────────────────┐ │
11+
│ │ Middleware B │ │
12+
│ │ ┌───────────────────┐ │ │
13+
│ │ │ Base Widget │ │ │
14+
│ │ └───────────────────┘ │ │
15+
│ └────────────────────────┘ │
16+
└─────────────────────────────┘
17+
```
18+
19+
## Key Concepts
20+
21+
- **Base plugin**: A standard `WidgetPlugin` that renders a widget type. Exactly one base plugin handles each type (last registered wins if duplicates exist).
22+
- **Middleware plugin**: A `WidgetMiddlewarePlugin` that wraps the next component in the chain. It receives a `Component` prop and must render it to continue the chain.
23+
- **Chaining**: Middleware is applied in registration order. The first middleware registered becomes the outermost wrapper. Each middleware renders the next via its `Component` prop.
24+
25+
## Types
26+
27+
### `WidgetMiddlewarePlugin`
28+
29+
```tsx
30+
interface WidgetMiddlewarePlugin<T = unknown> {
31+
name: string;
32+
type: PluginType.WIDGET_PLUGIN;
33+
supportedTypes: string | string[];
34+
isMiddleware: true;
35+
36+
// Required: wraps the widget component
37+
component: React.ComponentType<WidgetMiddlewareComponentProps<T>>;
38+
39+
// Optional: wraps the panel component (only needed if the base plugin defines panelComponent)
40+
panelComponent?: React.ComponentType<WidgetMiddlewarePanelProps<T>>;
41+
}
42+
```
43+
44+
### `WidgetMiddlewareComponentProps`
45+
46+
```tsx
47+
interface WidgetMiddlewareComponentProps<T = unknown> extends WidgetComponentProps<T> {
48+
// The next component in the chain — render this to continue
49+
Component: React.ComponentType<WidgetComponentProps<T>>;
50+
}
51+
```
52+
53+
## Rendering Paths
54+
55+
The middleware is applied in two different contexts:
56+
57+
| Context | File | When Used |
58+
|---|---|---|
59+
| Dashboard panels | `WidgetLoaderPlugin.tsx` | Widget opened as a panel via `PanelEvent.OPEN` |
60+
| Inline/embedded | `WidgetView.tsx` | Widget rendered inline (e.g., inside a document) |
61+
62+
Both paths collect middleware for the widget type and use `createChainedComponent` to build the wrapper chain.
63+
64+
## Usage Examples
65+
66+
### Basic Middleware — Add a Toolbar
67+
68+
```tsx
69+
import { PluginType, type WidgetMiddlewareComponentProps } from '@deephaven/plugin';
70+
71+
function MyToolbar({ Component, ...props }: WidgetMiddlewareComponentProps) {
72+
return (
73+
<div className="my-toolbar-wrapper">
74+
<div className="toolbar">
75+
<button onClick={() => console.log('Action!')}>Do Something</button>
76+
</div>
77+
<Component {...props} />
78+
</div>
79+
);
80+
}
81+
82+
const myToolbarPlugin = {
83+
name: 'my-toolbar-middleware',
84+
type: PluginType.WIDGET_PLUGIN,
85+
component: MyToolbar,
86+
supportedTypes: 'deephaven.plot.express.DeephavenFigure',
87+
isMiddleware: true,
88+
} satisfies WidgetMiddlewarePlugin;
89+
```
90+
91+
### Intercepting Props
92+
93+
```tsx
94+
function PropsInterceptor({ Component, ...props }: WidgetMiddlewareComponentProps) {
95+
// Modify or augment props before passing them to the wrapped component
96+
const enhancedFetch = useCallback(async () => {
97+
const widget = await props.fetch();
98+
// Transform or cache the widget data
99+
return widget;
100+
}, [props.fetch]);
101+
102+
return <Component {...props} fetch={enhancedFetch} />;
103+
}
104+
```
105+
106+
### Adding Context
107+
108+
```tsx
109+
const MyFeatureContext = createContext<MyFeatureState | null>(null);
110+
111+
function FeatureProvider({ Component, ...props }: WidgetMiddlewareComponentProps) {
112+
const [state, setState] = useState<MyFeatureState>({ enabled: true });
113+
114+
return (
115+
<MyFeatureContext.Provider value={state}>
116+
<Component {...props} />
117+
</MyFeatureContext.Provider>
118+
);
119+
}
120+
```
121+
122+
### Conditional Wrapping
123+
124+
```tsx
125+
function ConditionalMiddleware({ Component, ...props }: WidgetMiddlewareComponentProps) {
126+
const shouldWrap = useSomeCondition();
127+
128+
if (!shouldWrap) {
129+
// Pass through without wrapping
130+
return <Component {...props} />;
131+
}
132+
133+
return (
134+
<div className="conditional-wrapper">
135+
<Component {...props} />
136+
</div>
137+
);
138+
}
139+
```
140+
141+
### Targeting Multiple Widget Types
142+
143+
```tsx
144+
const multiTypeMiddleware = {
145+
name: 'multi-type-middleware',
146+
type: PluginType.WIDGET_PLUGIN,
147+
component: MyMiddlewareComponent,
148+
supportedTypes: ['deephaven.plot.express.DeephavenFigure', 'deephaven.ui.Element'],
149+
isMiddleware: true,
150+
} satisfies WidgetMiddlewarePlugin;
151+
```
152+
153+
## Registration
154+
155+
Middleware plugins are registered the same way as regular plugins — they are included in the plugin map passed to `PluginsContext`. Registration order determines chaining order.
156+
157+
```tsx
158+
// In the plugin map, order matters:
159+
const plugins = new Map([
160+
['base-widget', baseWidgetPlugin], // Base plugin
161+
['middleware-a', middlewarePluginA], // Outermost wrapper
162+
['middleware-b', middlewarePluginB], // Inner wrapper (closer to base)
163+
]);
164+
```
165+
166+
## Rules
167+
168+
1. **A base plugin is required.** Middleware registered for a type with no base plugin has no effect and produces a warning.
169+
2. **Last base plugin wins.** If multiple non-middleware plugins register for the same type, the last one replaces earlier ones (with a warning).
170+
3. **Middleware must render `Component`.** If a middleware doesn't render the `Component` prop, the rest of the chain (including the base widget) will not appear.
171+
4. **Middleware must spread props.** Pass all received props to `Component` to ensure the base widget and other middleware receive them.
172+
5. **`panelComponent` middleware is separate.** If the base plugin defines a `panelComponent`, middleware targeting the panel layer must also define `panelComponent`.

packages/plugin/src/WidgetView.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,13 @@ export function WidgetView({ fetch, type }: WidgetViewProps): JSX.Element {
3434

3535
if (isWidgetMiddlewarePlugin(p)) {
3636
foundMiddleware.push(p);
37-
} else if (foundBasePlugin == null) {
38-
foundBasePlugin = p;
3937
} else {
40-
log.warn(`Multiple base plugins for type ${type}, ignoring ${p.name}`);
38+
if (foundBasePlugin != null) {
39+
log.warn(
40+
`Multiple base plugins for type ${type}. Replacing ${foundBasePlugin.name} with ${p.name}`
41+
);
42+
}
43+
foundBasePlugin = p;
4144
}
4245
});
4346

0 commit comments

Comments
 (0)