Medusa/developer documentation/Dashboard Pages

Dashboard 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 with a view.tsx next to the page.js. Page data is loaded on the bot by the page's load() method.

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"Lucide icon identifier (e.g. "box", "ticket", "trophy", "shield"). Each page should use a distinct icon for clear sidebar navigation.
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.
load(context)({ client, session, params }) => Promise<{ available: boolean, data?: unknown, notFound?: boolean }>Loads the page data on the bot before the page renders. The default fetches config.endpoint from the module's routes.

Loading Page Data

When a page opens, the dashboard server calls the page's load() method inside the bot process and passes the result to the view as data. The default implementation requests GET /api/modules/<module><config.endpoint> and reports the page as unavailable when the request fails or no endpoint is set. Override it to combine several routes, reshape the response, or read the signed-in user:

modules/example/resources/dashboard/pages/items/page.js
async load({ client, session }) {
    try {
        const current = await session.current();
        const [items, stats] = await Promise.all([
            client.send("GET", `/api/modules/example/items?user=${encodeURIComponent(current.discordId)}`),
            client.send("GET", "/api/modules/example/stats").catch(() => ({ total: 0, active: 0 }))
        ]);
        return { available: true, data: { items, stats } };
    } catch {
        return { available: false };
    }
}
ContextDescription
clientInternal API client. send(method, path, body) calls any /api/... route with the API secret and the selected server and bot already attached.
sessionThe signed-in dashboard session. current() returns { user, discordId, permissions }, guild() the selected server id, bot() the selected bot key.
paramsPath segments after the page name, e.g. ["42"] for /m/example/items/42.

Return { notFound: true } to show the dashboard's 404 state for an unknown record.

Bespoke React Dashboard Views

To build a fully custom interface, place a view.tsx in the page folder:

resources/dashboard/pages/items/
├── page.js      # Page metadata and load()
└── view.tsx     # React view component

Views are compiled by the bot when the page is first opened, so adding, updating or removing a module never rebuilds the dashboard. The compiled view shares the dashboard's React instance and every @/components/... and @/lib/... module, so imports such as @/components/ui/card or useT from @/components/i18nProvider resolve to the same code the dashboard uses. Other npm packages are bundled into the view; declare them in the module manifest under dependencies.node so they install with the module. CSS files imported by the view (for example a package stylesheet or a view.css next to it) are bundled and injected with the view. Tailwind classes used in any file under resources/dashboard/ are generated at runtime, so views use the same utility classes and theme tokens as the dashboard.

View Component (view.tsx)

The default export receives ViewProps: the data returned by load(), the page params, the page config, and the selected guildId and server resources. Views are built with the shadcn/ui components (Base UI and Tailwind CSS) from @/components/ui/*:

modules/example/resources/dashboard/pages/items/view.tsx
import { useState } 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 { Modal } from "@/components/ui/modal";
import { medusa } from "@/lib/medusa";
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);

    async function create(name: string) {
        await medusa.moduleAction("example", "POST", "/items", { name });
        setModalOpen(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={() => create("Item")} className="h-9">
                        Save
                    </Button>
                </div>
            </Modal>
        </div>
    );
}

medusa.moduleData(module, path) and medusa.moduleAction(module, method, path, body) from @/lib/medusa call the module's routes through the dashboard's authenticated /api/m/<module>/... proxy, which adds the signed-in user, selected server and selected bot.

Configuration Previews

A configuration group with preview: "<name>" renders resources/dashboard/previews/<name>.tsx above its fields. Previews are compiled the same way as views and receive PreviewProps (values, user, module).

Available Dashboard UI Components

Custom views can import and utilize the built-in shadcn/ui 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 search filtering, supporting value, onValueChange, and options with value, label, icon, color, and group.
MultiSelect@/components/ui/selectMulti-choice dropdown with search filtering, tag badges, and values, onValuesChange, and options.
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.

Translating Records Pages

The records view looks up column headers, the empty state and plain string cell values in the module's common.json before falling back to the English text in page.js or the route response:

modules/appeals/resources/lang/de/common.json
{
    "page.appeals.appeals.column.status": "Status",
    "page.appeals.appeals.empty.title": "Keine Einsprüche gefunden",
    "page.appeals.appeals.value.status.pending": "Ausstehend"
}

Return stable machine values such as pending from the route and translate them with value.<column>.<value> keys instead of returning translated text.

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"));
    }
}