- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
2.3.2. Create Brands UI Route in Admin
In this chapter, you'll add a UI route to the admin dashboard that shows the all brands in a new page. You'll retrieve the brands from the server and display them in a table with pagination.
1. Get Brands API Route#
In a previous chapter, you learned how to add an API route that retrieves brands and their products using Query. You'll expand that API route to support pagination, so that on the admin dashboard you can show the brands in a paginated table.
Replace or create the GET
API route at src/api/admin/brands/route.ts
with the following:
1// other imports...2import {3 MedusaRequest,4 MedusaResponse,5} from "@medusajs/framework/http"6 7export const GET = async (8 req: MedusaRequest,9 res: MedusaResponse10) => {11 const query = req.scope.resolve("query")12 13 const { 14 data: brands, 15 metadata: { count, take, skip },16 } = await query.graph({17 entity: "brand",18 ...req.remoteQueryConfig,19 })20 21 res.json({ 22 brands,23 count,24 limit: take,25 offset: skip,26 })27}
In the API route, you use Query's graph
method to retrieve the brands. In the method's object parameter, you spread the remoteQueryConfig
property of the request object. This property holds configurations for pagination and retrieved fields.
The query configurations are combined from default configurations, which you'll add next, and the request's query parameters:
fields
: The fields to retrieve in the brands.limit
: The maximum number of items to retrieve.offset
: The number of items to skip before retrieving the returned items.
When you pass pagination configurations to the graph
method, the returned object has the pagination's details in a metadata
property, whose value is an object having the following properties:
count
: The total count of items.take
: The maximum number of items returned in thedata
array.skip
: The number of items skipped before retrieving the returned items.
You return in the response the retrieved brands and the pagination configurations.
2. Add Default Query Configurations#
Next, you'll set the default query configurations of the above API route and allow passing query parameters to change the configurations.
Medusa provides a validateAndTransformQuery
middleware that validates the accepted query parameters for a request and sets the default Query configuration. So, in src/api/middlewares.ts
, add a new middleware configuration object:
1import { 2 defineMiddlewares,3 validateAndTransformQuery,4} from "@medusajs/framework/http"5import { createFindParams } from "@medusajs/medusa/api/utils/validators"6// other imports...7 8export const GetBrandsSchema = createFindParams()9 10export default defineMiddlewares({11 routes: [12 // ...13 {14 matcher: "/admin/brands",15 method: "GET",16 middlewares: [17 validateAndTransformQuery(18 GetBrandsSchema,19 {20 defaults: [21 "id",22 "name",23 "products.*",24 ],25 isList: true,26 }27 ),28 ],29 },30 31 ],32})
You apply the validateAndTransformQuery
middleware on the GET /admin/brands
API route. The middleware accepts two parameters:
- A Zod schema that a request's query parameters must satisfy. Medusa provides
createFindParams
that generates a Zod schema with the following properties:fields
: A comma-separated string indicating the fields to retrieve.limit
: The maximum number of items to retrieve.offset
: The number of items to skip before retrieving the returned items.order
: The name of the field to sort the items by. Learn more about sorting in the API reference
- An object of Query configurations having the following properties:
defaults
: An array of default fields and relations to retrieve.isList
: Whether the API route returns a list of items.
By applying the above middleware, you can pass pagination configurations to GET /admin/brands
, which will return a paginated list of brands. You'll see how it works when you create the UI route.
validateAndTransformQuery
middleware to configure Query in this chapter.3. Initialize JS SDK#
In your custom UI route, you'll retrieve the brands by sending a request to the Medusa server. Medusa has a JS SDK that simplifies sending requests to the core API route.
If you didn't follow the previous chapter, create the file src/admin/lib/sdk.ts
with the following content:
You initialize the SDK passing it the following options:
baseUrl
: The URL to the Medusa server.debug
: Whether to enable logging debug messages. This should only be enabled in development.auth.type
: The authentication method used in the client application, which issession
in the Medusa Admin dashboard.
You can now use the SDK to send requests to the Medusa server.
4. Add a UI Route to Show Brands#
You'll now add the UI route that shows the paginated list of brands. A UI route is a React component created in a page.tsx
file under a sub-directory of src/admin/routes
. The file's path relative to src/admin/routes determines its path in the dashboard.
So, to add the UI route at the localhost:9000/app/brands
path, create the file src/admin/routes/brands/page.tsx
with the following content:
1import { defineRouteConfig } from "@medusajs/admin-sdk"2import { TagSolid } from "@medusajs/icons"3import { Container, Heading } from "@medusajs/ui"4import { useQuery } from "@tanstack/react-query"5import { sdk } from "../../lib/sdk"6import { useMemo, useState } from "react"7 8const BrandsPage = () => {9 // TODO retrieve brands10 11 return (12 <Container className="divide-y p-0">13 <div className="flex items-center justify-between px-6 py-4">14 <div>15 <Heading level="h2">Brands</Heading>16 </div>17 </div>18 {/* TODO show brands */}19 </Container>20 )21}22 23export const config = defineRouteConfig({24 label: "Brands",25 icon: TagSolid,26})27 28export default BrandsPage
A route's file must export the React component that will be rendered in the new page. It must be the default export of the file. You can also export configurations that add a link in the sidebar for the UI route. You create these configurations using defineRouteConfig
from the Admin Extension SDK.
So far, you only show a "Brands" header. In admin customizations, use components from the Medusa UI package to maintain a consistent user interface and design in the dashboard.
Add Table Component#
To show the brands with pagination functionalities, you'll create a new Table
component that uses the UI package's Table component with some alterations to match the design of the Medusa Admin. This new component is taken from the Admin Components guide.
Create the Table
component in the file src/admin/components/table.tsx
:
1import { useMemo } from "react"2import { Table as UiTable } from "@medusajs/ui"3 4export type TableProps = {5 columns: {6 key: string7 label?: string8 render?: (value: unknown) => React.ReactNode9 }[]10 data: Record<string, unknown>[]11 pageSize: number12 count: number13 currentPage: number14 setCurrentPage: (value: number) => void15}16 17export const Table = ({18 columns,19 data,20 pageSize,21 count,22 currentPage,23 setCurrentPage,24}: TableProps) => {25 const pageCount = useMemo(() => {26 return Math.ceil(count / pageSize)27 }, [count, pageSize])28 29 const canNextPage = useMemo(() => {30 return currentPage < pageCount - 131 }, [currentPage, pageCount])32 const canPreviousPage = useMemo(() => {33 return currentPage - 1 >= 034 }, [currentPage])35 36 const nextPage = () => {37 if (canNextPage) {38 setCurrentPage(currentPage + 1)39 }40 }41 42 const previousPage = () => {43 if (canPreviousPage) {44 setCurrentPage(currentPage - 1)45 }46 }47 48 return (49 <div className="flex h-full flex-col overflow-hidden !border-t-0">50 <UiTable>51 <UiTable.Header>52 <UiTable.Row>53 {columns.map((column, index) => (54 <UiTable.HeaderCell key={index}>55 {column.label || column.key}56 </UiTable.HeaderCell>57 ))}58 </UiTable.Row>59 </UiTable.Header>60 <UiTable.Body>61 {data.map((item, index) => {62 const rowIndex = "id" in item ? item.id as string : index63 return (64 <UiTable.Row key={rowIndex}>65 {columns.map((column, index) => (66 <UiTable.Cell key={`${rowIndex}-${index}`}>67 <>68 {column.render && column.render(item[column.key])}69 {!column.render && (70 <>{item[column.key] as string}</>71 )}72 </>73 </UiTable.Cell>74 ))}75 </UiTable.Row>76 )77 })}78 </UiTable.Body>79 </UiTable>80 <UiTable.Pagination81 count={count}82 pageSize={pageSize}83 pageIndex={currentPage}84 pageCount={pageCount}85 canPreviousPage={canPreviousPage}86 canNextPage={canNextPage}87 previousPage={previousPage}88 nextPage={nextPage}89 />90 </div>91 )92}
This component accepts the following props:
columns
: An array of the table's columns.data
: The rows in the table.pageSize
: The maximum number of items shown in a page.count
: The total number of items.currentPage
: A zero-based index of the current page.setCurrentPage
: A function to change the current page.
In the component, you use the UI package's Table component to display the data received as a prop in a table that supports pagination.
You can learn more about this component's implementation and how it works in the Admin Components guide, which provides more examples of how to build common components in the Medusa Admin dashboard.
Retrieve Brands From API Route#
You'll now update the UI route to retrieve the brands from the API route you added earlier.
First, add the following type in src/admin/routes/brands/page.tsx
:
This is the type of expected response from the GET /admin/brands
API route.
Then, replace the // TODO retrieve brands
in the component with the following:
1const [currentPage, setCurrentPage] = useState(0)2const limit = 153const offset = useMemo(() => {4 return currentPage * limit5}, [currentPage])6 7const { data } = useQuery<BrandsResponse>({8 queryFn: () => sdk.client.fetch(`/admin/brands`, {9 query: {10 limit,11 offset,12 },13 }),14 queryKey: [["brands", limit, offset]],15})
You first define pagination-related variables:
currentPage
: A zero-based index of the current page of items.limit
: The maximum number of items per page.offset
: The number of items to skip before retrieving the page's items. This is calculated from thecurrentPage
andlimit
variables.
Then, you use useQuery
from Tanstack (React) Query to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching.
In the queryFn
function that executes the query, you use the JS SDK's client.fetch
method to send a request to your custom API route. The first parameter is the route's path, and the second is an object of request configuration and data. You pass the query parameters in the query
property.
This sends a request to the Get Brands API route, passing the pagination query parameters. Whenever currentPage
is updated, the offset
is also updated, which will send a new request to retrieve the brands for the current page.
Display Brands Table#
Finally, you'll display the brands in a table using the component you created earlier. Import the component at the top of src/admin/routes/brands/page.tsx
:
Then, replace the {/* TODO show brands */}
in the return statement with the following:
This renders a table that shows the ID and name of the brands.
Test it Out#
To test out the UI route, start the Medusa application:
Then, open the admin dashboard at http://localhost:9000/app
. After you log in, you'll find a new "Brands" sidebar item. Click on it to see the brands in your store. You can also go to http://localhost:9000/app/brands
to see the page.
Summary#
By following the previous chapters, you:
- Injected a widget into the product details page to show the product's brand.
- Created a UI route in the Medusa Admin that shows the list of brands.
Next Steps: Integrate Third-Party Systems#
Your customizations often span across systems, where you need to retrieve data or perform operations in a third-party system.
In the next chapters, you'll learn about the concepts that facilitate integrating third-party systems in your application. You'll integrate a dummy third-party system and sync the brands between it and the Medusa application.