Kora/developer documentation/Pages

Pages

Add pages to the staff or client dashboard, from a ready-made table to your own React view.

A page class adds a page to the dashboard sidebar. Most pages describe a table and a form and let Kora build them. For anything else, add a React view next to the page.

Put each page in its own folder under resources/dashboard/pages/, with the class in page.js.

modules/example/resources/dashboard/pages/items/page.js
import { Pages } from "#kora/modules";

export class ItemsPage extends Pages {
    constructor(kora) {
        super(kora, {
            name: "items",
            label: "Items",
            description: "Everything the example module syncs.",
            icon: "box",
            group: "licensing",
            order: 7,
            component: "resource",
            permission: "items.view",
            config: {
                endpoint: "/items",
                title: "name",
                columns: [
                    { key: "name" },
                    { key: "sku", label: "SKU", format: "code" },
                    { key: "price", format: "money" },
                    { key: "status", format: "badge", tones: { active: "success", paused: "warning" } },
                    { key: "updated_at", label: "Updated", format: "relative" }
                ],
                fields: [
                    { key: "name", required: true },
                    { key: "sku", label: "SKU", required: true, edit: false },
                    { key: "price", type: "number", default: 0 },
                    { key: "status", type: "select", options: ["active", "paused"] }
                ],
                remove: false
            }
        });
    }
}
OptionDefaultWhat it does
nameRequiredThe page ID, used in its URL.
labelnameThe sidebar label and page title.
description""A line under the title.
icon"puzzle"A Lucide icon name.
group"Modules"The sidebar group: general, licensing, sales, security, automation, team or system.
order0Sort order inside the group. Lower comes first.
component"records"resource, stats, records, or the name of your own view.
audience"staff"staff or customer. See Client Pages.
permission<module>.viewThe permission needed to see the page.
config{}Options for the component.

Pages are only listed when their module is turned on and the member has the permission.

Resource Pages

component: "resource" builds a searchable, paged table with create, edit, detail and delete, all backed by your routes:

RequestWhen
GET <endpoint>?limit=&offset=&search=Listing. Return { rows, total }. Filters are added as more query values.
POST <endpoint>Create.
PATCH <endpoint>/<key>Save an edit.
DELETE <endpoint>/<key>Delete.

Requests go to your module's routes as the signed-in member, so your route permissions still apply.

ConfigDefaultWhat it does
endpointRequiredThe route the table uses.
moduleThis moduleUse another module's routes.
key"id"The column that identifies a row.
titlekeyThe column used as the detail view's title.
columnsRequiredThe table columns.
fields[]The create and edit form.
filters[]Dropdown filters, as { key, label, options }.
actions[]Extra buttons. See below.
sections[]Related tables in the detail view.
create, edit, detail, removetrueSet to false to turn that part off.
searchtrueShow the search box.
exporttrueShow a CSV export button.
pageSize25Rows per page.
pollOffRefresh every this many seconds.
empty{ title, hint } for an empty table.

Columns

OptionWhat it does
keyThe value to show. Dots read nested values, such as product.name.
labelThe heading. Defaults to the key in title case.
formatHow to show it. See below.
tonesBadge colours by value: default, success, warning, danger, info or muted.
alignleft, right or center.
hiddenOnly show it in the detail view.

Formats: text, code, date, datetime, relative, badge, boolean, number, tags, json, link, bytes, money and duration.

Fields

OptionWhat it does
key, labelThe value and its label.
typestring, text, number, boolean, select, multiselect, tags, date, datetime, json, password, reference, references, file or hidden.
optionsChoices for select, as strings or { label, value, image }. image is a URL or data URI drawn at 16px before the label with no frame, such as a store logo.
sourceChoices for reference and references, loaded from a route: { endpoint, label, value, hint }. Use :field in the endpoint to filter by another field.
required, default, placeholderAs you would expect.
create, editSet to false to hide the field on that form.
generateA route that returns a new value, such as a random key, shown as a button.
emptyText for a source that returns nothing.
visibleWhen{ field, value }, { field, in } or { field, not }. Checked against the form values and the record being edited, so a field can depend on one that cannot be edited.
statesLabels for a boolean's two states.
acceptAllowed file types for file. A form with a file is sent as multipart.

Actions

actions: [
    { name: "resync", label: "Resync", endpoint: "/items/:id/resync", method: "POST" },
    { name: "archive", label: "Archive", endpoint: "/items/archive", method: "POST", scope: "bulk", tone: "danger", confirm: "Archive the selected items?" },
    { name: "import", label: "Import", endpoint: "/items/import", scope: "page", fields: [{ key: "file", type: "file", accept: ".csv", required: true }] }
]
OptionWhat it does
scoperow on each row and in the detail view, bulk for selected rows (sent as ids), page at the top. Defaults to row.
endpoint, methodThe request. :field is replaced from the row. method defaults to POST.
fieldsAsk for values first.
bodyFixed values to send.
confirmAsk before running.
tonedanger for a red button.
whenOnly show it when { field, in: [...] } matches the row.

The route's response can drive what happens next:

ReturnResult
{ message }A toast.
{ secret } or { reveal: [{ label, value }] }A one-time dialog with copy buttons, for keys and tokens.
{ download: { filename, content, type } }A file download.
{ url }Opens the link in a new tab.

Sections

Sections are tables inside the detail view, for related records:

sections: [
    { name: "history", label: "History", endpoint: "/items/:id/history", columns: [{ key: "created_at", format: "datetime" }, { key: "event", format: "badge" }], empty: "Nothing yet" }
]

Sections take their own columns, actions and empty.

Stats and Records

component: "stats" shows a grid of cards from { stats: [{ label, value, hint }] }. records shows a plain table from { rows }, with optional columns. Both get their data from the page's load().

Loading Data

load() runs on the server when the page opens. By default it calls GET on config.endpoint in your module. Override it to return anything:

async load({ client, session, params }) {
    const stats = await client.send("GET", "/api/modules/example/stats");
    return { available: true, data: { stats: [{ label: "Synced", value: stats.count }] } };
}
ContextWhat it is
client.send(method, path, body)Calls Kora's API as the signed-in member, so their permissions apply.
sessionThe signed-in member.
paramsURL segments after the page name, such as ["42"] for /m/example/items/42.

Return { available: false } to show an error state, or { notFound: true } for a 404.

Your Own View

Add a view.tsx next to page.js. Kora compiles it when the module loads and uses it in place of the built-in component:

modules/example/resources/dashboard/pages/items/view.tsx
import { Card, CardContent } from "@/components/ui/card";
import { useT } from "@/components/i18nProvider";
import type { ViewProps } from "@/components/pages/types";

export default function ItemsView({ data, params, config, module }: ViewProps) {
    const { t } = useT();
    const rows = data && typeof data === "object" && "rows" in data ? (data as { rows: unknown[] }).rows : [];

    return (
        <Card>
            <CardContent className="p-4 text-sm">{t("page.example.items.count", { count: rows.length }, `${rows.length} items`)}</CardContent>
        </Card>
    );
}
PropWhat it is
dataWhat load() returned.
paramsThe URL segments after the page name.
configThe page's config.
moduleYour module ID.

Views can import react, react-router, anything from the dashboard under @/, such as @/components/ui/*, @/lib/api and @/components/pages/resourceForm, and packages installed in the dashboard such as lucide-react. Tailwind classes work. Call your routes with request(module, path, init) from @/lib/api.

Client Pages

Set audience: "customer" to add a page to the client dashboard that buyers sign in to. Its permission defaults to customer, and its routes should only return the signed-in customer's own records. Client pages are hidden when Enable Client Dashboard is turned off in Settings.

Translating Labels

Page text can be translated in your module's common.json with keys under page.<module>.<name>:

KeyTranslates
page.example.items.column.skuA column heading.
page.example.items.field.skuA form label.
page.example.items.filter.statusA filter.
page.example.items.value.status.activeA filter option.
page.example.items.action.resyncAn action button.
page.example.items.action.resync.confirmIts confirm text.
page.example.items.action.resync.doneIts success toast.
page.example.items.section.historyA section heading.