Dashboard Custom Pages
Register custom views, interactive management tables, and dedicated dashboard tabs.
Modules can register dedicated views and interactive management tabs in Medusa's web dashboard. When you register a custom page, Medusa automatically displays it in the dashboard navigation sidebar, binds it to the permission management system, and connects it to your module's HTTP API routes.
Pages reside in resources/dashboard/pages/ and load from every page.js file located at any nested directory depth. Custom pages can use built-in views like "records" or provide completely bespoke React interfaces using view.tsx and loader.ts.
Base Class Definition
import { Pages } from "#medusa/modules";
export class ItemsPage extends Pages {
constructor(medusa) {
super(medusa, {
name: "items",
label: "Items Manager",
description: "View, inspect, and manage server stock items.",
icon: "box",
group: "Modules",
order: 1,
component: "items",
config: {
endpoint: "/items"
}
});
}
}Constructor Options
The configuration object passed to super(medusa, meta) accepts the following properties:
| Property | Type | Default | Description |
|---|---|---|---|
name | string | Required | Unique page identifier within the module. |
label | string | "" | Display title shown in the dashboard navigation sidebar. |
description | string | "" | Subtitle explanation displayed at the top of the dashboard page. |
icon | string | "puzzle" | Tabler or Lucide icon identifier (e.g. "box", "ticket", "trophy", "shield"). |
group | string | "Modules" | Sidebar navigation group under which this page is categorized. |
order | number | 0 | Numerical sorting weight for sidebar item ordering (lower numbers appear first). |
component | string | "records" | Dashboard view component to render. Matches "records", "leaderboard", "stats", "audit", or custom view key <module>/<name>. |
config | object | {} | Options passed to the client-side component, such as API endpoint bindings. |
Page Methods
| Method | Signature | Description |
|---|---|---|
getName() | () => string | Returns the page identifier name. |
getLabel() | () => string | Returns the display label string. |
getDescription() | () => string | Returns the page description text. |
getIcon() | () => string | Returns the icon name. |
getGroup() | () => string | Returns the navigation sidebar group name. |
getOrder() | () => number | Returns the numeric display sort order. |
getComponent() | () => string | Returns the component identifier. |
getConfig() | () => object | Returns the component configuration map. |
describe() | () => object | Returns the serialized descriptor payload sent to the dashboard front end. |
Bespoke React Dashboard Views
To build fully custom client-side interfaces, place a view.tsx and optional loader.ts in your page folder:
resources/dashboard/pages/items/
├── page.js # Page metadata and manifest declaration
├── loader.ts # Next.js server data loader
└── view.tsx # React client componentServer Data Loader (loader.ts)
The loader runs on the server before rendering the page, fetching initial data from internal routes:
import type { ModuleLoader } from "@/components/pages/types";
import { medusa } from "@/lib/medusa";
const loader: ModuleLoader = async (module) => {
try {
const data = await medusa.moduleData(module, "/items");
return { available: true, data };
} catch {
return {
available: true,
data: { items: [], stats: { total: 0, active: 0 } }
};
}
};
export default loader;Client View Component (view.tsx)
Custom views import official design system components directly from @/components/ui/*:
"use client";
import { useState, useMemo } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import { Modal } from "@/components/ui/modal";
import type { ViewProps } from "@/components/pages/types";
export default function ItemsView({ data }: ViewProps) {
const [searchQuery, setSearchQuery] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [modalOpen, setModalOpen] = useState(false);
return (
<div className="flex flex-col gap-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<Card>
<CardContent className="flex items-center gap-4 p-5">
<span className="text-2xl font-bold">150</span>
<span className="text-xs text-muted-foreground">Total Stock</span>
</CardContent>
</Card>
</div>
<div className="flex items-center gap-3">
<Input
value={searchQuery}
onChange={event => setSearchQuery(event.target.value)}
placeholder="Search items"
className="h-9"
/>
<Select
value={statusFilter}
onValueChange={setStatusFilter}
options={[
{ value: "all", label: "All Items" },
{ value: "active", label: "Active" }
]}
className="h-9 w-48"
/>
<Button variant="default" size="default" onClick={() => setModalOpen(true)} className="h-9">
Create Item
</Button>
</div>
<Modal open={modalOpen} onClose={() => setModalOpen(false)} title="Create Item">
<div className="flex flex-col gap-3 pt-2">
<Input placeholder="Item Name" className="h-9" />
<Button variant="default" size="default" onClick={() => setModalOpen(false)} className="h-9">
Save
</Button>
</div>
</Modal>
</div>
);
}Available Dashboard UI Components
Custom views can import and utilize the complete set of dashboard components:
| Component | Path | Props & Description |
|---|---|---|
Button | @/components/ui/button | Buttons supporting variant ("default", "secondary", "outline", "destructive", "ghost", "link") and size ("sm", "default", "lg", "icon"). |
Select | @/components/ui/select | Styled dropdown select menu with value, onValueChange, options array containing objects with value and label. |
Input | @/components/ui/input | Form text input supporting type ("text", "number", "password"), placeholders, and input styling. |
Switch | @/components/ui/switch | Boolean toggle control accepting checked and onCheckedChange. |
Badge | @/components/ui/badge | Tag badge for categories and statuses with custom color classes. |
Card | @/components/ui/card | Structured container card with CardContent, CardHeader, and CardTitle. |
Modal | @/components/ui/modal | Accessible modal dialog with open, onClose, title, description, and children. |
Permissions Integration
Medusa automatically generates a dashboard permission node for every custom page:
page:<moduleName>:<pageName>For example, ItemsPage in module example produces permission identifier page:example:items. Server administrators can grant or restrict access to this page per rank or user within the dashboard's Permissions view.
Connecting with Module Routes
The records dashboard component queries data directly from your module's HTTP routes. When pairing config: { endpoint: "/items" }, the dashboard makes requests to:
GET /api/modules/<moduleName>/itemsYour route method returns an array of objects to populate the data grid:
import { Routes } from "#medusa/modules";
export class ItemsRoute extends Routes {
constructor(medusa) {
super(medusa, {
name: "items",
path: "/items",
authentication: { key: "secret" }
});
}
async GET(request) {
const handler = this.medusa.modules.handlers.get(this.module, "items");
return handler.listForGuild(this.medusa.settings.get("GUILD_ID"));
}
}
