www/apps/book/app/learn/fundamentals/admin/ui-routes/page.mdx
import { Prerequisites, CodeTab, CodeTabs } from "docs-ui"
export const metadata = {
title: ${pageNumber} Admin UI Routes,
}
In this chapter, you’ll learn how to create a UI route in the admin dashboard.
The Medusa Admin dashboard is customizable, allowing you to add new pages, called UI routes. You create a UI route as a React component showing custom content that allows admin users to perform custom actions.
For example, you can add a new page to show and manage product reviews, which aren't available natively in Medusa.
You can create a UI route directly in your Medusa application, or in a plugin if you want to share the UI route across multiple Medusa applications.
<Prerequisites items={[{ link: "/learn/installation", text: "Medusa application installed" }]} />
You create a UI route in a page.tsx file under a sub-directory of src/admin/routes directory. The file's path relative to src/admin/routes determines its path in the dashboard. The file’s default export must be the UI route’s React component.
For example, create the file src/admin/routes/custom/page.tsx with the following content:
import { Container, Heading } from "@medusajs/ui"
const CustomPage = () => {
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">This is my custom route</Heading>
</div>
</Container>
)
}
export default CustomPage
You add a new route at http://localhost:9000/app/custom. The CustomPage component holds the page's content, which currently only shows a heading.
In the route, you use Medusa UI, a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it.
<Note title="Important" type="warning">The UI route component must be created as an arrow function.
</Note>To test the UI route, start the Medusa application:
npm run dev
Then, after logging into the admin dashboard, open the page http://localhost:9000/app/custom to see your custom page.
To add a sidebar item for your custom UI route, export a configuration object in the UI route's file:
export const highlights = [ ["16", "label", "The label of the UI route's sidebar item."], ["17", "icon", "The icon of the UI route's sidebar item."] ]
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"
const CustomPage = () => {
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">This is my custom route</Heading>
</div>
</Container>
)
}
export const config = defineRouteConfig({
label: "Custom Route",
icon: ChatBubbleLeftRight,
})
export default CustomPage
The configuration object is created using defineRouteConfig from the Medusa Framework. It accepts the following properties:
label: the sidebar item's label.icon: an optional React component used as an icon in the sidebar.rank: an optional number to order the route among sibling routes. Learn more in the Specify UI Route Sidebar Rank section.The above example adds a new sidebar item with the label Custom Route and an icon from the Medusa UI Icons package.
UI route ranking is available starting Medusa v2.11.4.
</Note>By default, custom UI routes are added to the sidebar in the order their files are loaded. This applies to your custom UI routes, and UI routes defined in plugins.
You can specify the ranking of your UI route in the sidebar using the rank property passed to defineRouteConfig.
For example, consider you have the following UI routes:
<CodeTabs group="ui-routes"> <CodeTab label="UI Route 1" value="ui-route-1">import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChartBar } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"
const AnalyticsPage = () => {
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">Analytics Dashboard</Heading>
</div>
</Container>
)
}
export const config = defineRouteConfig({
label: "Analytics",
icon: ChartBar,
rank: 1,
})
export default AnalyticsPage
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { DocumentText } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"
const ReportsPage = () => {
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">Reports</Heading>
</div>
</Container>
)
}
export const config = defineRouteConfig({
label: "Reports",
icon: DocumentText,
rank: 2,
})
export default ReportsPage
In the sidebar, "Analytics" with the rank 1 will be added before "Reports" with the rank 2.
Medusa sorts custom UI routes based on their rank:
Medusa also applies the same sorting logic to UI routes at the nested level. Learn more in the Nested UI Routes Ranking section.
Consider that alongside the UI route above at src/admin/routes/custom/page.tsx you create a nested UI route at src/admin/routes/custom/nested/page.tsx that also exports route configurations:
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"
const NestedCustomPage = () => {
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">This is my nested custom route</Heading>
</div>
</Container>
)
}
export const config = defineRouteConfig({
label: "Nested Route",
})
export default NestedCustomPage
This UI route is shown in the sidebar as an item nested in the parent "Custom Route" item. Nested items are only shown when the parent sidebar items (in this case, "Custom Route") are clicked.
Some caveats for nested UI routes in the sidebar:
src/admin/routes/custom/[id]/page.tsx aren't added to the sidebar as it's not possible to link to a dynamic route. If the dynamic route exports route configurations, a warning is logged in the browser's console.icon configuration is ignored for the sidebar item of nested UI routes to follow the admin's design conventions.You can add a custom UI route under an existing route. For example, you can add a route under the orders route:
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"
const NestedOrdersPage = () => {
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h1">Nested Orders Page</Heading>
</div>
</Container>
)
}
export const config = defineRouteConfig({
label: "Nested Orders",
nested: "/orders",
})
export default NestedOrdersPage
The nested property passed to defineRouteConfig specifies which route this custom route is nested under. This route will now show in the sidebar under the existing "Orders" sidebar item.
Nested UI routes also accept the rank configuration. It allows you to specify the order that the nested UI routes are shown in the sidebar under the parent item.
For example:
// In nested UI route 1 at src/admin/routes/orders/insights/page.tsx
export const config = defineRouteConfig({
label: "Order Insights",
nested: "/orders",
rank: 1, // Will appear first
})
// In nested UI route 2 at src/admin/routes/orders/reports/page.tsx
export const config = defineRouteConfig({
label: "Order Reports",
nested: "/orders",
rank: 2, // Will appear second
})
In this example, the "Order Insights" item will appear before the "Order Reports" item under the parent "Orders" item in the sidebar.
To create a page under the settings section of the admin dashboard, create a UI route under the path src/admin/routes/settings.
For example, create a UI route at src/admin/routes/settings/custom/page.tsx:
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"
const CustomSettingPage = () => {
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h1">Custom Setting Page</Heading>
</div>
</Container>
)
}
export const config = defineRouteConfig({
label: "Custom",
})
export default CustomSettingPage
This adds a page under the path /app/settings/custom. An item is also added to the settings sidebar with the label Custom.
A UI route can accept path parameters if the name of any of the directories in its path is of the format [param].
For example, create the file src/admin/routes/custom/[id]/page.tsx with the following content:
import { useParams } from "react-router-dom"
import { Container, Heading } from "@medusajs/ui"
const CustomPage = () => {
const { id } = useParams()
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h1">Passed ID: {id}</Heading>
</div>
</Container>
)
}
export default CustomPage
You access the passed parameter using react-router-dom's useParams hook.
If you run the Medusa application and go to http://localhost:9000/app/custom/123, you'll see 123 printed in the page.
The Medusa Admin dashboard shows breadcrumbs at the top of each page, if specified. This allows users to navigate through your custom UI routes.
To set the breadcrumbs of a UI route, export a handle object with a breadcrumb property in the UI route's file:
import { Container, Heading } from "@medusajs/ui"
const CustomPage = () => {
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">This is my custom route</Heading>
</div>
</Container>
)
}
export default CustomPage
export const handle = {
breadcrumb: () => "Custom Route",
}
The breadcrumb's value is a function that returns the breadcrumb label as a string, or a React JSX element.
If you set a breadcrumb for a nested UI route, and you open the route in the Medusa Admin, you'll see the breadcrumbs starting from its parent route to the nested route.
For example, if you have the following UI route at src/admin/routes/custom/nested/page.tsx that's nested under the previous one:
import { Container, Heading } from "@medusajs/ui"
const NestedCustomPage = () => {
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">This is my nested custom route</Heading>
</div>
</Container>
)
}
export default NestedCustomPage
export const handle = {
breadcrumb: () => "Nested Custom Route",
}
Then, when you open the nested route at http://localhost:9000/app/custom/nested, you'll see the breadcrumbs as Custom Route > Nested Custom Route. Each breadcrumb is clickable, allowing users to navigate back to the parent route.
In some use cases, you may want to show a dynamic breadcrumb for a UI route. For example, if you have a UI route that displays a brand's details, you can set the breadcrumb to show the brand's name dynamically.
To do that, you can:
loader function in the UI route file that fetches the data needed for the breadcrumb.breadcrumb function and return the dynamic label.For example, create a UI route at src/admin/routes/brands/[id]/page.tsx with the following content:
export const dynamicBreadcrumbsHighlights = [ ["25", "loader", "Define a loader function to fetch the brand data."], ["35", "breadcrumb", "Set the breadcrumb using the fetched brand data."], ["36", "data", "The data returned by the loader function is passed as props to the breadcrumb function."], ]
import { Container, Heading } from "@medusajs/ui"
import { LoaderFunctionArgs, UIMatch, useLoaderData } from "react-router-dom"
import { sdk } from "../../../lib/sdk"
type BrandResponse = {
brand: {
name: string
}
}
const BrandPage = () => {
const { brand } = useLoaderData() as Awaited<BrandResponse>
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{brand.name}</Heading>
</div>
</Container>
)
}
export default BrandPage
export async function loader({ params }: LoaderFunctionArgs) {
const { id } = params
const { brand } = await sdk.client.fetch<BrandResponse>(`/admin/brands/${id}`)
return {
brand,
}
}
export const handle = {
breadcrumb: (
{ data }: UIMatch<BrandResponse>
) => data.brand.name || "Brand",
}
In the loader function, you retrieve the brands from a custom API route and return them.
Then, in the handle.breadcrumb function, you receive data prop containing the brand information returned by the loader function. You can use this data to return a dynamic breadcrumb label.
When you open the UI route at http://localhost:9000/app/brands/123, the breadcrumb will show the brand's name, such as Acme.
You also use the useLoaderData hook to access the data returned by the loader function in the UI route component. Learn more in the Routing Customizations chapter.
To build admin customizations that match the Medusa Admin's designs and layouts, refer to this guide to find common components.
For more customizations related to routes, refer to the Routing Customizations chapter.