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:

src/api/admin/brands/route.ts
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 the data array.
  • skip: The number of items skipped before retrieving the returned items.

You return in the response the retrieved brands and the pagination configurations.

NoteLearn more about pagination with Query in this chapter.

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:

src/api/middlewares.ts
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.

NoteLearn more about using the 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:

The directory structure of the Medusa application after adding the file

src/admin/lib/sdk.ts
1import Medusa from "@medusajs/js-sdk"2
3export const sdk = new Medusa({4  baseUrl: "http://localhost:9000",5  debug: process.env.NODE_ENV === "development",6  auth: {7    type: "session",8  },9})

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 is session in the Medusa Admin dashboard.

You can now use the SDK to send requests to the Medusa server.

NoteLearn more about the JS SDK and its options in this reference.

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.

NoteLearn more about UI routes in this chapter.

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:

Directory structure of the Medusa application after adding the UI route.

src/admin/routes/brands/page.tsx
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:

Directory structure of the Medusa application after adding the table component.

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:

src/admin/routes/brands/page.tsx
1type BrandsResponse = {2  brands: {3    id: string4    name: string5  }[]6  count: number7  limit: number8  offset: number9}

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:

src/admin/routes/brands/page.tsx
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 the currentPage and limit 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:

src/admin/routes/brands/page.tsx
import { Table } from "../../components/table"

Then, replace the {/* TODO show brands */} in the return statement with the following:

src/admin/routes/brands/page.tsx
1<Table2  columns={[3    {4      key: "id",5      label: "#",6    },7    {8      key: "name",9      label: "Name",10    },11  ]}12  data={data?.brands || []}13  pageSize={data?.limit || limit}14  count={data?.count || 0}15  currentPage={currentPage}16  setCurrentPage={setCurrentPage}17/>

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.

A new sidebar item is added for the new brands UI route. The UI route shows the table of brands with pagination.


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.

Was this chapter helpful?
Edit this page