@mantajs/dashboard
v0.1.17
Published
B2B Admin Dashboard for Medusa - Fork of @medusajs/dashboard
Maintainers
Readme
@mantajs/dashboard
A customizable fork of @medusajs/dashboard that lets you override components, override routes, and define your own sidebar menu — without modifying the original dashboard source code.
Drop-in replacement for @medusajs/dashboard via package manager resolutions/overrides.
Installation
# Yarn
yarn add @mantajs/dashboard
# npm
npm install @mantajs/dashboard
# pnpm
pnpm add @mantajs/dashboardUsing as a dashboard replacement
In your Medusa backend's package.json, add a resolution/override to swap @medusajs/dashboard with @mantajs/dashboard. The syntax depends on your package manager:
Yarn (v1 & v4+)
{
"resolutions": {
"@medusajs/dashboard": "npm:@mantajs/dashboard@^0.1.13"
}
}npm (v8.3+)
{
"overrides": {
"@medusajs/dashboard": "npm:@mantajs/dashboard@^0.1.13"
}
}pnpm
{
"pnpm": {
"overrides": {
"@medusajs/dashboard": "npm:@mantajs/dashboard@^0.1.13"
}
}
}Then register the Vite plugin in your medusa-config.ts:
import { defineConfig } from "@medusajs/framework/config"
import { customDashboardPlugin } from "@mantajs/dashboard/vite-plugin"
export default defineConfig({
// ...
admin: {
vite: () => ({
plugins: [customDashboardPlugin()],
}),
},
})Run medusa build (or medusa develop) and the custom dashboard will be compiled in place of the stock one.
Features
1. Component Overrides
Replace any dashboard component by placing a file with the same name in your project's src/admin/components/ directory. The plugin recursively scans the entire components/ tree, so you can organize overrides in subdirectories.
your-project/
└── src/
└── admin/
└── components/
├── product-general-section.tsx ← flat override (works)
├── orders/
│ └── order-activity-section.tsx ← nested override (works too)
└── forms/
└── shipping-address-form.tsx ← deeply nested (works too)How it works:
During Vite's pre-bundling phase, the plugin redirects the dashboard's dist/app.mjs entry to source files. It then intercepts individual source file loads and swaps any component whose filename matches one of your overrides.
Matching is done by file name (without extension), regardless of subdirectory depth. For example:
| Your file | Overrides |
|-----------|-----------|
| product-general-section.tsx | src/routes/products/.../product-general-section.tsx |
| orders/order-list.tsx | src/routes/orders/order-list/order-list.tsx |
| layout/main-layout.tsx | src/components/layout/main-layout/main-layout.tsx |
Your override component must export a default export:
// src/admin/components/orders/order-activity-section.tsx
const OrderActivitySection = () => {
return <div>My custom order activity section</div>
}
export default OrderActivitySectionDeveloper experience (HMR):
| Action | Behavior | Details | |--------|----------|---------| | Modify an override | HMR (Hot Module Replacement) | The component is swapped in-place — no page reload, no React state loss. Instant feedback. | | Create a new override | Automatic full reload (~2-3s) | The Vite server restarts to rebuild the pre-bundle, then the browser reloads automatically. No manual refresh needed. | | Delete an override | Automatic full reload (~2-3s) | Same as creation — the original dashboard component is restored automatically. |
Under the hood, override files are kept as separate Vite modules (not inlined into the pre-bundled chunk). This allows React Fast Refresh to handle modifications via standard HMR. Creation and deletion require a server restart because the esbuild chunk structure changes.
Important notes:
- Override files are discovered recursively in
src/admin/components/and all its subdirectories. - Matching is based on file name only — the subdirectory structure is for your own organization and does not affect matching.
- If two files in different subdirectories share the same name, the plugin logs a warning in development and uses the one that comes last alphabetically by full path.
- Index/barrel files (
index.ts) are never overridden to preserve re-exports. - The plugin forces Vite to re-optimize dependencies when overrides are present, so changes are always picked up.
2. Route Overrides
Add new pages or replace existing ones using Medusa's standard admin extension system.
your-project/
└── src/
└── admin/
└── routes/
└── custom-page/
└── page.tsx ← adds /custom-page to the dashboard
└── orders/
└── page.tsx ← overrides the /orders pageHow merging works:
- If your extension route has a path that doesn't exist in the dashboard, it's added as a new route.
- If your extension route has the same path as a built-in route, your component replaces the original.
- Children of the original route that you don't redefine are preserved. For example, overriding
/orderskeeps/orders/:idintact.
This uses Medusa's @medusajs/admin-vite-plugin to discover routes in src/admin/routes/, combined with a custom merge function that ensures backward compatibility.
Route page example:
// src/admin/routes/orders/page.tsx
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ShoppingCart } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"
const OrdersPage = () => {
return (
<Container>
<Heading level="h1">My Custom Orders Page</Heading>
{/* Your custom orders list */}
</Container>
)
}
export const config = defineRouteConfig({
label: "Orders",
icon: ShoppingCart,
})
export default OrdersPageDeveloper experience:
Route overrides use Medusa's standard admin extension system (@medusajs/admin-vite-plugin), which provides its own HMR. Modifying a route page triggers a standard Vite HMR update — the page refreshes instantly without a full reload.
Component overrides vs Route overrides — when to use which:
| Use case | Approach |
|----------|----------|
| Replace a full page (e.g., the orders list) | Route override (src/admin/routes/orders/page.tsx) |
| Replace a section inside a page (e.g., the customer info block in order detail) | Component override (src/admin/components/order-customer-section.tsx) |
| Add a new page that doesn't exist in the dashboard | Route override (src/admin/routes/my-page/page.tsx) |
| Tweak a reusable UI element used across multiple pages | Component override |
3. Custom Menu Configuration
Define your own sidebar menu by creating a src/admin/menu.config.tsx file:
your-project/
└── src/
└── admin/
└── menu.config.tsx// src/admin/menu.config.tsx
import { ShoppingCart, Users, BuildingStorefront } from "@medusajs/icons"
import type { MenuConfig } from "@mantajs/dashboard/vite-plugin"
const config: MenuConfig = {
items: [
{
icon: <ShoppingCart />,
label: "orders.domain",
to: "/orders",
useTranslation: true,
},
{
icon: <Users />,
label: "customers.domain",
to: "/customers",
useTranslation: true,
},
{
icon: <BuildingStorefront />,
label: "My Custom Page",
to: "/custom-page",
items: [
{ label: "Sub Page", to: "/custom-page/sub" },
],
},
],
}
export default configMenu item properties:
| Property | Type | Description |
|----------|------|-------------|
| icon | ReactNode | Icon component (use @medusajs/icons) |
| label | string | Display label or i18n translation key |
| to | string | Route path |
| useTranslation | boolean | If true, label is treated as an i18n key |
| items | MenuNestedItem[] | Optional nested items |
Types:
import type { MenuConfig, MenuItem, MenuNestedItem } from "@mantajs/dashboard/vite-plugin"When no menu.config.ts is found, the dashboard falls back to its built-in sidebar menu.
Menu, Nested Routes, and Modules: How They Interact
Understanding how the sidebar menu is built is critical to avoid duplicate or missing entries. There are three sources that can add items to the sidebar:
- Your
menu.config.tsx— the custom menu you define - Route configs with
nested— pages that declarenested: "/parent"indefineRouteConfig() - Plugin modules — modules like
@medusajs/draft-orderthat register their own routes and menu entries
How nested works
When a route page exports a config with nested, Medusa automatically injects it as a sub-item under the specified parent in the sidebar:
// src/admin/routes/draft-orders/page.tsx
export const config = defineRouteConfig({
label: "Drafts",
nested: "/orders", // ← auto-injected under Orders in the sidebar
})This happens regardless of your menu.config.tsx. Even if you define a custom menu, any route with nested will still be injected as a child of its parent entry.
To prevent a route from appearing in the sidebar, remove the nested property:
export const config = defineRouteConfig({
label: "Drafts Test",
// no `nested` → not auto-injected in the menu
})The page remains accessible via its URL (/app/draft-orders) but won't appear in the sidebar unless you explicitly add it to your menu config.
Controlling sub-items via menu.config.tsx
If you want full control over which sub-items appear under a menu entry, define them explicitly in items:
{
icon: <ShoppingCart />,
label: "orders.domain",
useTranslation: true,
to: "/orders",
items: [
{ label: "Draft Orders", to: "/draft-orders" },
],
}Important: Nested routes (nested: "/orders") are still injected even if you define items manually. To avoid duplicates, either:
- Remove
nestedfrom the route config, or - Don't list the route in
items(letnestedhandle it)
Never do both — you'll get a duplicate entry.
Plugin modules and the Extensions section
Medusa plugin modules (e.g., @medusajs/draft-order) register their own sidebar entries. By default, these appear in the Extensions section at the bottom of the sidebar.
When you include a module's route in your menu.config.tsx, the module's entry is absorbed into your custom menu and no longer appears separately in Extensions:
// Including /draft-orders in the custom menu prevents it from
// appearing again under Extensions
{
icon: <ShoppingCart />,
label: "Orders",
to: "/orders",
items: [
{ label: "Draft Orders", to: "/draft-orders" }, // ← module route
],
}If you don't include a module's route in your menu config, it will appear in the Extensions section as usual.
Summary
| Scenario | Result |
|----------|--------|
| Route has nested: "/orders" | Auto-injected under Orders in sidebar |
| Route has no nested | Not in sidebar (unless in menu.config.tsx) |
| Module route listed in menu.config.tsx | Appears in your menu, not in Extensions |
| Module route not in menu.config.tsx | Appears in Extensions section |
| Route has nested and listed in items | Duplicate entry (avoid this!) |
Exports
| Import | Description |
|--------|-------------|
| @mantajs/dashboard | Main dashboard app (DashboardPlugin type, render function) |
| @mantajs/dashboard/css | Dashboard stylesheet |
| @mantajs/dashboard/vite-plugin | Vite plugin + menu types |
Development
# Install dependencies
yarn install
# Start dev server (standalone dashboard)
yarn dev
# Build for distribution
yarn build
# Run tests
yarn test
# Validate i18n translations
yarn i18n:validateEnvironment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| VITE_MEDUSA_BACKEND_URL | http://localhost:9000 | Medusa backend URL |
| VITE_MEDUSA_STOREFRONT_URL | http://localhost:8000 | Storefront URL |
| VITE_MEDUSA_BASE | / | Base path for the dashboard |
| VITE_MEDUSA_PROJECT | — | Path to a Medusa project to load admin extensions from |
Compatibility
- Medusa v2 (2.13.x)
- React 18
- Vite 5
- TypeScript 5.6+
