-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat(dashboard): Add support for custom React providers in dashboard #4600
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
alingabrieldm
wants to merge
8
commits into
vendurehq:minor
Choose a base branch
from
alingabrieldm:dashboard-custom-providers
base: minor
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+302
−2
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
50846eb
feat(dashboard): Add support for custom React providers in dashboard …
alingabrieldm 5083412
chore(dashboard): Refactor CustomProviders to use explicit interface …
alingabrieldm a74f838
chore(dashboard): Add validation for duplicate custom provider IDs
alingabrieldm 281e831
chore(dashboard): Remove `ReactNode` import from custom provider docu…
alingabrieldm 8a94fda
Merge branch 'master' into dashboard-custom-providers
michaelbromley a0074bb
Merge branch 'vendurehq:master' into dashboard-custom-providers
alingabrieldm 0820d42
chore(dashboard): Documented customProviders API
alingabrieldm 8483628
chore(dashboard): Add tests for custom dashboard providers
alingabrieldm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
66 changes: 66 additions & 0 deletions
66
docs/docs/guides/extending-the-dashboard/custom-providers/index.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| --- | ||
| title: 'Custom Providers' | ||
| metaTitle: 'Vendure Dashboard Custom Providers — Wrap the App & Layout' | ||
| metaDescription: 'Learn how to register custom React providers in Vendure Dashboard extensions, where they render (app vs layout) and how ordering works.' | ||
| --- | ||
|
|
||
| Custom Providers let your extension **wrap parts of the Dashboard UI in your own React provider components**. | ||
| This is useful when you need to inject cross-cutting concerns like custom context, feature flags, error boundaries, | ||
| telemetry, or theming that should apply to dashboard pages. | ||
|
|
||
| ## What is a "Custom Provider"? | ||
|
|
||
| A custom provider is a React component that receives `children` and returns a wrapped subtree: | ||
|
|
||
| ```tsx | ||
| export function MyProvider({ children }: { children: ReactNode }) { | ||
| return children; | ||
| } | ||
| ``` | ||
|
|
||
| You register it via `defineDashboardExtension()`: | ||
|
|
||
| ```tsx | ||
| import { defineDashboardExtension } from '@vendure/dashboard'; | ||
|
|
||
| function MyProvider({ children }: { children: ReactNode }) { | ||
| return children; | ||
| } | ||
|
|
||
| export default defineDashboardExtension({ | ||
| customProviders: [ | ||
| { | ||
| id: 'my-provider', | ||
| component: MyProvider, | ||
| location: 'app', | ||
| order: 0, | ||
| }, | ||
| ], | ||
| }); | ||
| ``` | ||
|
|
||
| ## Where providers render: `location` | ||
|
|
||
| Each provider can target one of two places: | ||
|
|
||
| - **`location: 'app'`**: wraps the whole application (the highest level available to extensions). | ||
| - **`location: 'layout'`**: wraps the main content area of the authenticated layout (the `<Outlet />` subtree). The sidebar and header are outside this wrapper. | ||
|
|
||
| If `location` is omitted, it defaults to `'app'`. | ||
|
|
||
| ## Provider ordering: `order` | ||
|
|
||
| Providers at the same `location` are sorted by `order` (ascending). | ||
|
|
||
| - Lower `order` values are rendered **outermost / earlier**. | ||
| - Higher `order` values are rendered **innermost / later**. | ||
|
|
||
| If `order` is omitted, it defaults to `0`. | ||
|
|
||
| ## Common use cases | ||
|
|
||
| - Providing a custom React context your extension components can consume | ||
| - Adding an `ErrorBoundary` around parts of the dashboard | ||
| - Wiring up feature flags or experimentation frameworks | ||
| - Adding analytics/telemetry providers | ||
| - Providing localization or formatting helpers specific to your organization |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
130 changes: 130 additions & 0 deletions
130
packages/dashboard/src/lib/framework/extension-api/custom-providers.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| import { globalRegistry } from '@/vdb/framework/registry/global-registry.js'; | ||
| import { ComponentType, createElement, ReactNode, useMemo } from 'react'; | ||
|
|
||
| import { useDashboardExtensions } from './use-dashboard-extensions.js'; | ||
|
|
||
| /** | ||
| * @description | ||
| * Allows you to define custom React providers that wrap selected parts of the dashboard UI. | ||
| * This is useful for cross-cutting concerns such as custom context, error boundaries, | ||
| * feature flags, telemetry, or theming. | ||
| * | ||
| * Providers can be mounted at either the application root (`'app'`) or the authenticated | ||
| * layout main content area (`'layout'`, i.e. the `<Outlet />` subtree only; sidebar and | ||
| * header are outside this wrapper). | ||
| * | ||
| * @docsCategory extensions-api | ||
| * @docsPage Custom Providers | ||
| * @since 3.7.0 | ||
| */ | ||
| export type DashboardCustomProviderDefinition = { | ||
| /** | ||
| * @description | ||
| * A unique identifier for this custom provider. | ||
| */ | ||
| id: string; | ||
| /** | ||
| * @description | ||
| * The React provider component to render. It receives `children` and should | ||
| * return a wrapped subtree. | ||
| */ | ||
| component: ComponentType<{ children: ReactNode }>; | ||
| /** | ||
| * @description | ||
| * Optional. Controls render order relative to other providers at the same location. | ||
| * Lower numbers render first (outermost), higher numbers render later (innermost). | ||
| */ | ||
| order?: number; | ||
| /** | ||
| * @description | ||
| * Determines where this provider is mounted in the dashboard hierarchy. | ||
| * | ||
| * - `'app'`: Wraps the entire dashboard application at the root level. | ||
| * - `'layout'`: Wraps the main content area of the authenticated layout (the `<Outlet />` subtree). | ||
| * | ||
| * The sidebar and header are outside this wrapper. | ||
| * | ||
| * Optional. Defaults to 'app' if not specified. | ||
| */ | ||
| location?: 'app' | 'layout'; | ||
| }; | ||
|
|
||
| globalRegistry.register( | ||
| 'dashboardCustomProvidersRegistry', | ||
| new Map<string, DashboardCustomProviderDefinition>(), | ||
| ); | ||
|
|
||
| export function getDashboardCustomProvidersRegistry() { | ||
| return globalRegistry.get('dashboardCustomProvidersRegistry'); | ||
| } | ||
|
|
||
| export function registerDashboardCustomProvider(customProvider: DashboardCustomProviderDefinition) { | ||
| globalRegistry.set('dashboardCustomProvidersRegistry', map => { | ||
| map.set(customProvider.id, { | ||
| ...customProvider, | ||
| location: customProvider.location ?? 'app', | ||
| }); | ||
| return map; | ||
| }); | ||
| } | ||
|
|
||
| export function registerDashboardCustomProviders(providers: DashboardCustomProviderDefinition[] | undefined) { | ||
| if (!providers?.length) { | ||
| return; | ||
| } | ||
| const registry = getDashboardCustomProvidersRegistry(); | ||
| const allIds = [...registry.keys(), ...providers.map(p => p.id)]; | ||
| const seen = new Set<string>(); | ||
| const duplicateIds = new Set<string>(); | ||
| for (const id of allIds) { | ||
| if (seen.has(id)) { | ||
| duplicateIds.add(id); | ||
| } else { | ||
| seen.add(id); | ||
| } | ||
| } | ||
|
|
||
| if (duplicateIds.size) { | ||
| const duplicates = Array.from(duplicateIds).sort(); | ||
| throw new Error( | ||
| `Duplicate dashboard custom provider ids detected: ` + | ||
| `${duplicates.map(id => `"${id}"`).join(', ')}. ` + | ||
| `Provider ids must be globally unique.`, | ||
| ); | ||
| } | ||
|
|
||
| for (const provider of providers) { | ||
| registerDashboardCustomProvider(provider); | ||
| } | ||
| } | ||
|
|
||
| export const renderProviders = ( | ||
| providers: DashboardCustomProviderDefinition[], | ||
| children: ReactNode, | ||
| ): ReactNode => { | ||
| if (providers.length === 0) { | ||
| return children; | ||
| } | ||
|
|
||
| const [currentProvider, ...remainingProviders] = providers; | ||
| const ProviderComponent = currentProvider.component; | ||
|
|
||
| return createElement(ProviderComponent, null, renderProviders(remainingProviders, children)); | ||
| }; | ||
|
|
||
| export interface CustomProvidersProps { | ||
| location: DashboardCustomProviderDefinition['location']; | ||
| children: ReactNode; | ||
| } | ||
|
|
||
| export function CustomProviders({ location, children }: Readonly<CustomProvidersProps>) { | ||
| const { extensionsLoaded, reloadCount } = useDashboardExtensions(); | ||
| const providersToRender = useMemo(() => { | ||
| const customProviders = Array.from(getDashboardCustomProvidersRegistry().values()); | ||
| return customProviders | ||
| .filter(provider => provider.location === location) | ||
| .sort((a, b) => (a.order || 0) - (b.order || 0)); | ||
| }, [extensionsLoaded, reloadCount, location]); | ||
|
|
||
| return renderProviders(providersToRender, children); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { getNavMenuConfig, setNavMenuConfig } from '../nav-menu/nav-menu-extensions.js'; | ||
| import { globalRegistry } from '../registry/global-registry.js'; | ||
|
|
||
| import { registerDashboardCustomProviders } from './custom-providers.js'; | ||
| import { DashboardExtension } from './extension-api-types.js'; | ||
| import { | ||
| registerAlertExtensions, | ||
|
|
@@ -118,6 +119,9 @@ export function defineDashboardExtension(extension: DashboardExtension) { | |
| // Register custom history entry components | ||
| registerHistoryEntryComponents(extension.historyEntries); | ||
|
|
||
| // Register dashboard custom providers | ||
| registerDashboardCustomProviders(extension.customProviders); | ||
|
|
||
|
Comment on lines
+122
to
+124
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prevent silent custom provider ID collisions. At Line 123, providers are registered without collision feedback. Since registration is keyed by Suggested guard (in
|
||
| // Register toolbar extensions | ||
| registerToolbarExtensions(extension.toolbarItems); | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enforce duplicate-id checks on every public write path.
registerDashboardCustomProviders()rejects collisions, but the two exports here still allow silent last-writer-wins behavior:getDashboardCustomProvidersRegistry()returns the liveMap, andregisterDashboardCustomProvider()overwrites existing entries withmap.set(). That makes the “globally unique” invariant depend on which helper the caller uses.Suggested hardening
🤖 Prompt for AI Agents