|
| 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`. |
0 commit comments