# Dashboard Pages

URL: https://zerodev.ca/docs/medusa/developer-documentation/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

```js title="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:

| 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"`  | Lucide icon identifier (e.g. `"box"`, `"ticket"`, `"trophy"`, `"shield"`). Each page should use a distinct icon for clear sidebar navigation. |
| `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.                                              |
| `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:

```js title="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 };
    }
}
```

| Context   | Description                                                                                                                                              |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client`  | Internal API client. `send(method, path, body)` calls any `/api/...` route with the API secret and the selected server and bot already attached.         |
| `session` | The signed-in dashboard session. `current()` returns `{ user, discordId, permissions }`, `guild()` the selected server id, `bot()` the selected bot key. |
| `params`  | Path 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:

```text
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/*`:

```tsx title="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:

| 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 search filtering, supporting `value`, `onValueChange`, and `options` with `value`, `label`, `icon`, `color`, and `group`.       |
| `MultiSelect` | `@/components/ui/select` | Multi-choice dropdown with search filtering, tag badges, and `values`, `onValuesChange`, and `options`.                                                          |
| `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:

```text
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:

```json title="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:

```text
GET /api/modules/<moduleName>/items
```

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

```js title="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"));
    }
}
```
