Docs
Developer Documentation

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

modules/example/resources/dashboard/pages/items/page.js
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:

PropertyTypeDefaultDescription
namestringRequiredUnique page identifier within the module.
labelstring""Display title shown in the dashboard navigation sidebar.
descriptionstring""Subtitle explanation displayed at the top of the dashboard page.
iconstring"puzzle"Tabler or Lucide icon identifier (e.g. "box", "ticket", "trophy", "shield").
groupstring"Modules"Sidebar navigation group under which this page is categorized.
ordernumber0Numerical sorting weight for sidebar item ordering (lower numbers appear first).
componentstring"records"Dashboard view component to render. Matches "records", "leaderboard", "stats", "audit", or custom view key <module>/<name>.
configobject{}Options passed to the client-side component, such as API endpoint bindings.

Page Methods

MethodSignatureDescription
getName()() => stringReturns the page identifier name.
getLabel()() => stringReturns the display label string.
getDescription()() => stringReturns the page description text.
getIcon()() => stringReturns the icon name.
getGroup()() => stringReturns the navigation sidebar group name.
getOrder()() => numberReturns the numeric display sort order.
getComponent()() => stringReturns the component identifier.
getConfig()() => objectReturns the component configuration map.
describe()() => objectReturns 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 component

Server Data Loader (loader.ts)

The loader runs on the server before rendering the page, fetching initial data from internal routes:

modules/example/resources/dashboard/pages/items/loader.ts
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/*:

modules/example/resources/dashboard/pages/items/view.tsx
"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:

ComponentPathProps & Description
Button@/components/ui/buttonButtons supporting variant ("default", "secondary", "outline", "destructive", "ghost", "link") and size ("sm", "default", "lg", "icon").
Select@/components/ui/selectStyled dropdown select menu with value, onValueChange, options array containing objects with value and label.
Input@/components/ui/inputForm text input supporting type ("text", "number", "password"), placeholders, and input styling.
Switch@/components/ui/switchBoolean toggle control accepting checked and onCheckedChange.
Badge@/components/ui/badgeTag badge for categories and statuses with custom color classes.
Card@/components/ui/cardStructured container card with CardContent, CardHeader, and CardTitle.
Modal@/components/ui/modalAccessible 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>/items

Your route method returns an array of objects to populate the data grid:

modules/example/src/routes/items/route.js
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"));
    }
}

On this page