Skip to content

feat(dashboard): Add support for custom React providers in dashboard#4600

Open
alingabrieldm wants to merge 8 commits into
vendurehq:minorfrom
alingabrieldm:dashboard-custom-providers
Open

feat(dashboard): Add support for custom React providers in dashboard#4600
alingabrieldm wants to merge 8 commits into
vendurehq:minorfrom
alingabrieldm:dashboard-custom-providers

Conversation

@alingabrieldm
Copy link
Copy Markdown
Contributor

Description

This PR adds custom React provider support to Vendure Dashboard extensions. Extensions can now register customProviders via defineDashboardExtension(), which are rendered at the app or layout level (with deterministic ordering), enabling cross-cutting concerns like context, feature flags, error boundaries, etc.

import { defineDashboardExtension } from '@vendure/dashboard';
import type { ReactNode } from 'react';

function MyProvider({ children }: { children: ReactNode }) {
    return children;
}

export default defineDashboardExtension({
    customProviders: [
        {
            id: 'my-provider',
            component: MyProvider,
            location: 'app',
            order: 0,
        },
    ],
});

Adds a new Custom Providers guide under Extending the Dashboard and wires it into the docs navigation manifest.

Breaking changes

None.

Checklist

📌 Always:

  • I have set a clear title
  • My PR is small and contains a single feature
  • I have checked my own PR

👍 Most of the time:

  • I have added or updated test cases
  • I have updated the README if needed

@vendure-developer-hub
Copy link
Copy Markdown

vendure-developer-hub Bot commented Apr 1, 2026

Vendure CoreView preview

8483628

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vendure-storybook Ready Ready Preview, Comment Apr 22, 2026 7:14am

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 1, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9aea454f-5800-4671-b841-d9ec1920e7fe

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new "Custom Providers" dashboard feature and documentation. Introduces DashboardCustomProviderDefinition and registry-backed APIs (registerDashboardCustomProvider(s), getDashboardCustomProvidersRegistry), a renderProviders helper, and a CustomProviders React component. Hooks custom providers into the extension system via defineDashboardExtension and the global registry. AppProviders and AppLayout now render CustomProviders for app and layout locations respectively. Providers are filterable by location and ordered by an order field. Manifest updated with a docs nav entry.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly and concisely summarizes the main feature: adding custom React provider support to the dashboard.
Description check ✅ Passed The pull request description is comprehensive and covers the main requirements: it includes a clear summary, code example, breaking changes statement, and partially completed checklist.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/dashboard/src/lib/framework/extension-api/custom-providers.ts (1)

74-77: Use Readonly<> for component props type.

Please wrap the props object in Readonly<> for consistency with dashboard typing rules.

♻️ Proposed refactor
+type CustomProvidersProps = Readonly<{
+    location: DashboardCustomProviderDefinition['location'];
+    children: ReactNode;
+}>;
+
 export const CustomProviders = ({
     location,
     children,
-}: {
-    location: DashboardCustomProviderDefinition['location'];
-    children: ReactNode;
-}) => {
+}: CustomProvidersProps) => {

As per coding guidelines, "Set React component props objects to Readonly<> type for type safety".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dashboard/src/lib/framework/extension-api/custom-providers.ts`
around lines 74 - 77, Wrap the component's props object type in Readonly<> to
follow dashboard typing rules: change the anonymous props type used in the arrow
component (the object containing location:
DashboardCustomProviderDefinition['location'] and children: ReactNode) to
Readonly<{ location: DashboardCustomProviderDefinition['location']; children:
ReactNode }>, updating the component signature where that props destructuring
occurs in custom-providers.ts so the props are immutable per the guideline.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/docs/guides/extending-the-dashboard/custom-providers/index.mdx`:
- Around line 16-17: The file imports ReactNode directly from 'react' (the
import statement for ReactNode at Lines ~16-17 and again at ~27-28); update the
example to import ReactNode via the dashboard package public API instead (i.e.,
re-export the missing ReactNode type from vendure/dashboard and then change the
import to pull ReactNode from that package), so all extension examples only
import from vendure/dashboard and no third-party modules directly.

In `@packages/dashboard/src/lib/framework/extension-api/custom-providers.ts`:
- Around line 78-84: The memoized providers list in CustomProviders
(providersToRender computed via useMemo) misses the extensions bootstrap signal,
causing an empty list on first render; update the dependency array of the
useMemo that references getDashboardCustomProvidersRegistry() to include
extensionsLoaded from useDashboardExtensions() (in addition to reloadCount and
location) so providersToRender recomputes after extensions load, and update the
component props/type declarations to use Readonly<...> per the file's coding
guidelines (touch the CustomProviders props/type definitions and any exported
prop interfaces).

In
`@packages/dashboard/src/lib/framework/extension-api/define-dashboard-extension.ts`:
- Around line 122-124: The call to
registerDashboardCustomProviders(extension.customProviders) can silently
overwrite providers with duplicate ids; update the registration logic (in
custom-providers.ts / the registerDashboardCustomProviders function) to validate
ids before inserting: detect duplicates within the incoming
extension.customProviders and against the existing global provider registry, and
then either throw an informative error or emit a logged warning including the
conflicting provider id and both provider sources; do not perform the overwrite
without explicit handling. Ensure the check references the provider id field and
include the extension identifier when reporting conflicts so callers can resolve
collisions.

---

Nitpick comments:
In `@packages/dashboard/src/lib/framework/extension-api/custom-providers.ts`:
- Around line 74-77: Wrap the component's props object type in Readonly<> to
follow dashboard typing rules: change the anonymous props type used in the arrow
component (the object containing location:
DashboardCustomProviderDefinition['location'] and children: ReactNode) to
Readonly<{ location: DashboardCustomProviderDefinition['location']; children:
ReactNode }>, updating the component signature where that props destructuring
occurs in custom-providers.ts so the props are immutable per the guideline.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d39ad373-e21b-401e-8a8d-0ccce7e0b439

📥 Commits

Reviewing files that changed from the base of the PR and between a8584ff and 50846eb.

📒 Files selected for processing (9)
  • docs/docs/guides/extending-the-dashboard/custom-providers/index.mdx
  • docs/src/manifest.ts
  • packages/dashboard/src/app/app-providers.tsx
  • packages/dashboard/src/lib/components/layout/app-layout.tsx
  • packages/dashboard/src/lib/framework/extension-api/custom-providers.ts
  • packages/dashboard/src/lib/framework/extension-api/define-dashboard-extension.ts
  • packages/dashboard/src/lib/framework/extension-api/extension-api-types.ts
  • packages/dashboard/src/lib/framework/registry/registry-types.ts
  • packages/dashboard/src/lib/index.ts

Comment on lines +16 to +17
import type { ReactNode } from 'react';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid direct react imports in extension examples.

The snippets at Line 16-Line 17 and Line 27-Line 28 import ReactNode from react. This conflicts with the extension import policy and should be rewritten to use the dashboard package public API (and re-export any missing type there first).

Based on learnings: Extensions must import everything from vendure/dashboard — never from third-party libraries directly. Always re-export new dependencies or components from this package's public API.

Also applies to: 27-28

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/docs/guides/extending-the-dashboard/custom-providers/index.mdx` around
lines 16 - 17, The file imports ReactNode directly from 'react' (the import
statement for ReactNode at Lines ~16-17 and again at ~27-28); update the example
to import ReactNode via the dashboard package public API instead (i.e.,
re-export the missing ReactNode type from vendure/dashboard and then change the
import to pull ReactNode from that package), so all extension examples only
import from vendure/dashboard and no third-party modules directly.

Comment thread packages/dashboard/src/lib/framework/extension-api/custom-providers.ts Outdated
Comment on lines +122 to +124
// Register dashboard custom providers
registerDashboardCustomProviders(extension.customProviders);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent silent custom provider ID collisions.

At Line 123, providers are registered without collision feedback. Since registration is keyed by id, duplicates can silently overwrite earlier providers, making extension composition order-dependent.

Suggested guard (in custom-providers.ts)
 export function registerDashboardCustomProvider(customProvider: DashboardCustomProviderDefinition) {
     globalRegistry.set('dashboardCustomProvidersRegistry', map => {
+        if (map.has(customProvider.id)) {
+            throw new Error(
+                `Duplicate dashboard custom provider id "${customProvider.id}". ` +
+                `Provider ids must be unique across extensions.`,
+            );
+        }
         map.set(customProvider.id, {
             ...customProvider,
             location: customProvider.location ?? 'app',
         });
         return map;
     });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/dashboard/src/lib/framework/extension-api/define-dashboard-extension.ts`
around lines 122 - 124, The call to
registerDashboardCustomProviders(extension.customProviders) can silently
overwrite providers with duplicate ids; update the registration logic (in
custom-providers.ts / the registerDashboardCustomProviders function) to validate
ids before inserting: detect duplicates within the incoming
extension.customProviders and against the existing global provider registry, and
then either throw an informative error or emit a logged warning including the
conflicting provider id and both provider sources; do not perform the overwrite
without explicit handling. Ensure the check references the provider id field and
include the extension identifier when reporting conflicts so callers can resolve
collisions.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/dashboard/src/lib/framework/extension-api/custom-providers.ts`:
- Around line 35-46: getDashboardCustomProvidersRegistry currently exposes the
live Map and registerDashboardCustomProvider silently overwrites existing
entries, allowing last-writer-wins; change getDashboardCustomProvidersRegistry
to return an immutable/read-only copy (e.g., new Map(map) or Object.freeze
wrapper) so callers cannot mutate the live registry, and update
registerDashboardCustomProvider to perform a collision check inside its updater
(use map.has(customProvider.id)) and throw an error if the id already exists
before calling map.set; reference getDashboardCustomProvidersRegistry and
registerDashboardCustomProvider and the 'dashboardCustomProvidersRegistry'
registry key when making the changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f5492638-7611-4fcb-8fdb-cd3f9fd9fcae

📥 Commits

Reviewing files that changed from the base of the PR and between 50846eb and 281e831.

📒 Files selected for processing (2)
  • docs/docs/guides/extending-the-dashboard/custom-providers/index.mdx
  • packages/dashboard/src/lib/framework/extension-api/custom-providers.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/docs/guides/extending-the-dashboard/custom-providers/index.mdx

Comment on lines +35 to +46
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;
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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 live Map, and registerDashboardCustomProvider() overwrites existing entries with map.set(). That makes the “globally unique” invariant depend on which helper the caller uses.

Suggested hardening
-export function getDashboardCustomProvidersRegistry() {
+function getDashboardCustomProvidersRegistry() {
     return globalRegistry.get('dashboardCustomProvidersRegistry');
 }
+
+export function getRegisteredDashboardCustomProviders(): ReadonlyMap<
+    string,
+    DashboardCustomProviderDefinition
+> {
+    return getDashboardCustomProvidersRegistry();
+}
 
 export function registerDashboardCustomProvider(customProvider: DashboardCustomProviderDefinition) {
+    if (getDashboardCustomProvidersRegistry().has(customProvider.id)) {
+        throw new Error(
+            `Duplicate dashboard custom provider ids detected: "${customProvider.id}". ` +
+                `Provider ids must be globally unique.`,
+        );
+    }
     globalRegistry.set('dashboardCustomProvidersRegistry', map => {
         map.set(customProvider.id, {
             ...customProvider,
             location: customProvider.location ?? 'app',
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dashboard/src/lib/framework/extension-api/custom-providers.ts`
around lines 35 - 46, getDashboardCustomProvidersRegistry currently exposes the
live Map and registerDashboardCustomProvider silently overwrites existing
entries, allowing last-writer-wins; change getDashboardCustomProvidersRegistry
to return an immutable/read-only copy (e.g., new Map(map) or Object.freeze
wrapper) so callers cannot mutate the live registry, and update
registerDashboardCustomProvider to perform a collision check inside its updater
(use map.has(customProvider.id)) and throw an error if the id already exists
before calling map.set; reference getDashboardCustomProvidersRegistry and
registerDashboardCustomProvider and the 'dashboardCustomProvidersRegistry'
registry key when making the changes.

Copy link
Copy Markdown
Member

@michaelbromley michaelbromley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this PR — the approach is sound and follows the established extension registry pattern well. A few things to address before we can merge:

Target branch

This PR adds a new feature, so it should target the minor branch rather than master. The @since tag should be 3.7.0.

Documentation / docgen

The DashboardCustomProviderDefinition type needs proper JSDoc annotations to be picked up by our documentation generator. Currently the type has plain comments but is missing the required tags. Following the pattern established by DashboardToolbarItemDefinition and DashboardAlertDefinition:

  1. Top-level doc block on the type itself needs @description, @docsCategory extensions-api, @docsPage Custom Providers (or similar), and @since 3.7.0.
  2. Every property needs a @description tag — without it, properties are invisible to docgen. e.g.:
    /**
     * @description
     * A unique identifier for this custom provider.
     */
    id: string;
  3. The customProviders field on DashboardExtension also needs @since 3.7.0.

Docs precision: location descriptions

The location: 'layout' description (both in the type JSDoc and in the docs page) should clarify that it wraps only the <Outlet /> — not the sidebar or header. This distinction matters for use cases like error boundaries. Something like:

Wraps the main content area of the authenticated layout. The sidebar and header are outside this wrapper.

Tests (nice to have)

There are no tests for renderProviders (recursive nesting, ordering) or the duplicate ID detection in registerDashboardCustomProviders. The existing define-dashboard-extension.spec.ts would be the natural home for these. Not a blocker, but would be good to have for completeness.

@alingabrieldm alingabrieldm changed the base branch from master to minor April 22, 2026 06:53
@alingabrieldm
Copy link
Copy Markdown
Contributor Author

Hi @michaelbromley, thanks for the feedback! I've added two more commits covering documentation and tests.

Also, I've updated the target branch to vendurehq:minor. If there's anything else you'd like me to adjust, please let me know.

@BibiSebi BibiSebi requested a review from michaelbromley June 2, 2026 08:15
@michaelbromley michaelbromley added this to the v3.7.0 milestone Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants