#refineweek: Creating an Admin Dashboard with refine

#refineweek: Creating an Admin Dashboard with refine

This post is the first part of an admin dashboard app built using refine. The dashboard is an admin backend for the Pixels client that we built previously in the refineWeek series. We are using the same Supabase database for this app and have Ant Design as the UI framework.

This is Day 5, and refineWeek is a seven-part tutorial series that aims to help developers learn the ins-and-outs of refine's powerful capabilities within a week.

  • You can find the complete source code for the Pixels Admin app on GitHub
  • Also Pixel Client app source code from previous days can be found here

Overview

In this episode, we implement user authentication and CRUD functionalities of the dashboard. As it was in the case of the Pixels client app, for this app also, we implement an email-based authenticaiton along with social logins with Google and GitHub.

We use the same Supabase client for connecting to the database we already have in place for the Pixels app.

The dashboard shows a list of all users. It also has a list for canvases.

The user list is read only and the canvas list will eventually allow editors and admins - particular to their roles - to manipulate their subject data. We will implement proper authorization for editor and admin roles on Day 6, but for now we will implement relevant CRUD operations that will apply to any authenticated user.

For the API requests, we will be using the dataProvider object refine gave us for Supabase. Since we covered CRUD related concepts and architecture in depth on Day 3, in this post, we'll focus more on the Ant Design components side.

Let's begin with the project set up.

Project Setup

As done previously in the client app, let's initialize our admin app with create refine-app. We will choose the interactive option by answering necessary questions. Let's run the following command:

npm create refine-app@latest pixels-admin

We will use Supabase for our backend, and Ant Design for our UI. We want to be able to customize the Ant Design theme and layout. So, we have the below answers related to Supabase and Ant Design:

✔ Choose a project template · refine-react
✔ What would you like to name your project?: · pixels-admin
✔ Choose your backend service to connect: · Supabase
✔ Do you want to use a UI Framework?: · Ant Design
✔ Do you want to add example pages?: · no
✔ Do you want to add dark mode support?: · no
✔ Do you want to customize the Ant Design layout?: · antd-custom-layout
✔ Do you need i18n (Internationalization) support?: · no
✔ Do you want to add kbar command interface support?: · no

After completion of the initialization process, we should have the same refine, Supabase and Ant Design boilerplate code generated for us like before.

We'll start tweaking the relevant code straight away as we add features to our app - since we have already explored the boilerplate code in significant depth on Day 2 in Setting Up the Client App and on Day 3 in Adding CRUD Actions and Authentication. This will give us more time to focus on related Ant Design components and what they handle for us in the background.

Prior to that, let's just navigate to the project folder and run the dev server:

yarn dev

And prepare ourselves to the call-to-action at http://localhost/3000:

react admin dashboard


The App.tsx file should be familiar from Day 2. It looks like this:

// src/App.tsx

import React from 'react';

import { Refine } from '@pankod/refine-core';
import {
    AuthPage,
    notificationProvider,
    ReadyPage,
    ErrorComponent,
} from '@pankod/refine-antd';
import '@pankod/refine-antd/dist/reset.css';

import { dataProvider, liveProvider } from '@pankod/refine-supabase';
import routerProvider from '@pankod/refine-react-router-v6';
import { supabaseClient } from 'utility';
import { Title } from 'components/layout';
import authProvider from './authProvider';

function App() {
    return (
        <Refine
            dataProvider={dataProvider(supabaseClient)}
            liveProvider={liveProvider(supabaseClient)}
            authProvider={authProvider}
            routerProvider={{
                ...routerProvider,
                routes: [
                    {
                        path: '/register',
                        element: <AuthPage type="register" />,
                    },
                    {
                        path: '/forgot-password',
                        element: <AuthPage type="forgotPassword" />,
                    },
                    {
                        path: '/update-password',
                        element: <AuthPage type="updatePassword" />,
                    },
                ],
            }}
            LoginPage={() => (
                <AuthPage
                    type="login"
                    providers={[
                        {
                            name: 'google',
                            label: 'Sign in with Google',
                        },
                    ]}
                    formProps={{
                        initialValues: {
                            email: 'info@refine.dev',
                            password: 'refine-supabase',
                        },
                    }}
                />
            )}
            notificationProvider={notificationProvider}
            ReadyPage={ReadyPage}
            catchAll={<ErrorComponent />}
            Title={Title}
        />
    );
}

export default App;

Let's start modifying the existing code to meet our requirements.

Setting Up Supabase Config

For the admin app, we'll connect to the same PostgreSQL database already up and running on Supabase for the Pixels client app.

So, we need to get the access credentials for our server from the Supabase dashboard. We can avail them by following this section in the Supabase quickstart guide. Let's store them in an .env file.

We'll go ahead and update the supabaseClient.ts file:

// src/utility/supabaseClient.ts

import { createClient } from "@pankod/refine-supabase";

const SUPABASE_URL = process.env.SUPABASE_URL;
const SUPABASE_KEY = process.env.SUPABASE_KEY;

export const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY);

Now we have enabled authProvider and dataProvider methods to connect to our Supabase database with supabaseClient().

Adding required files

Here is the finalized version of what we’ll be building in this article:

Before we move on, you need to add required page and components to the project if you want build the app by following the article . Please add the following components and files into src folder in the project:

Creating a Table View With Refine and Ant Design

Our <UserList /> component looks like this:

 // pages/users/list.tsx

import React from 'react';
import {
  useTable,
  List,
  Table,
  Avatar,
  Icons
} from '@pankod/refine-antd';
import { TUser } from 'types/user';

const { UserOutlined } = Icons;

export const UserList = () => {
  const { tableProps } = useTable<TUser>();

  return (
    <List>
      <Table {...tableProps} rowKey={"id"}>
        <Table.Column
          dataIndex="avatar_url"
          title={
            <h4 style={{ textAlign: "center", fontWeight: "bold" }}>Avatar</h4>
          }
          render={(_, record: TUser) => (
            <Avatar
              icon={<UserOutlined />}
              src={record.avatar_url}
              size={{ xs: 24, sm: 32, md: 40 }}
            />
          )}
        />
        <Table.Column
          dataIndex="id"
          title={
            <h4 style={{ textAlign: "center", fontWeight: "bold" }}>ID</h4>
          }
        />
        <Table.Column
          dataIndex="email"
          title={
            <h4 style={{ textAlign: "center", fontWeight: "bold" }}>Email</h4>
          }
        />
        <Table.Column
          dataIndex="full_name"
          title={
            <h4 style={{ textAlign: "center", fontWeight: "bold" }}>
              Full Name
            </h4>
          }
          render={(_, record: TUser) =>
            record.full_name ? (
              record.full_name
            ) : (
              <p style={{ textAlign: "center", fontWeight: "bold" }}>--</p>
            )
          }
        />
        <Table.Column
          dataIndex="username"
          title={
            <h4 style={{ textAlign: "center", fontWeight: "bold" }}>
              Username
            </h4>
          }
          render={(_, record: TUser) =>
            record.username ? (
              record.username
            ) : (
              <p style={{ textAlign: "center", fontWeight: "bold" }}>--</p>
            )
          }
        />
      </Table>
    </List>
  );
};

The components tree looks very plain, but there is plenty going on in there. Firstly, the useTable() hook that handles all the data fetching stuff with React Query in the background. The <List /> and <Table /> components also do intense secret service for us. We'll go over them one by one below.

refine Ant Design useTable() Hook

useTable() is a refine Ant Design hook served to us from the @pankod/refine-antd package. As we can see above, it returns us a tableProps object:

const { tableProps } = useTable<TUser>();

useTable() is built on top of refine core's useMany() data hook. useMany(), in turn, invokes the getMany() data provider method.

Here, we did not need to set any configuration for our API request and the returned response. The resource.name was figured by useTable from the resources prop that was passed to <Refine />. It is possible to set options for sorting, filtering, pagination, etc. with an object passed to useTable().

For all the features that come with the useTable() hook, visit the API reference here.

The properties of the tableProps object produced are intended to be passed to a <Table /> component, which we'll consider after <List />.

refine Ant Design <List /> Component

The <List /> component represents a list view. It is a wrapper around the contents of the list. It accepts a number of relevant props and comes with their sensible defaults, such as for resource name and title of the page.

In our case, we don't have to pass in any prop because refine figures the resource name and title from the resources prop. In other words, the <List /> component above is conveniently equivalent to this:

<List resource="users" title="Users">
  // Content here...
</List>

For more on the usage of <List />, look into the details here.

refine Ant Design <Table /> Component

useTable() hook's tableProps is specifically configured to match the props of Ant Design's native <Table /> component. refine makes <Table /> available to us with the @pankod/refine-antd module.

Besides passing in the tableProps object to <Table />, we are required to provide a unique rowKey prop to identify each row in the table:

<Table {...tableProps} rowKey="id">
  // React nodes here...
</Table>

The records inside tableProps are placed inside <Table.Column />s of a row of the table - one record per row. If you're new to this, feel free to dive into the Ant Design docs for <Table />.

refine Ant Design <Table.Column /> Children

<Table.Column />s represent individual columns in the table. A column header is set by <Table.Column />'s title prop. The value of a field in a record is set by the dataIndex prop. For example, for the following column, dataIndex="email" tells our app to fill the Email column of a row associated with a particular record with the value of the record's email property:

<Table.Column dataIndex="email" title="Email" />

We can also customize what content to render inside a table cell. We'll examine two such instances in the next section on <CanvasList /> view, which involves an editable table.

Adding <UserList> to the resources

We have covered adding CRUD operations on Day 3 in significant depth. So, here we'll quickly add both users and canvases resources to <Refine /> component:

// App.tsx

...
import { UserList } from "pages";


return (
  <Refine
    ...
    authProvider={authProvider}
    //highlight-start
    resources={[
      {
        name: "users",
        list: UserList,
      },
    ]}
    //highlight-end
    ...
  />
)
...

<AuthPage /> Customization

At this point, it is helpful that we customize our Ant Design theme, the content of the <AuthPage /> and implement GitHub authentication. We won't cover these here, as they are relatively straight forward and were covered on Day 3.

Simply you change your App.tsx code like this:

// App.tsx

import React from "react";

import { Refine } from "@pankod/refine-core";
import {
    Layout,
    notificationProvider,
    ReadyPage,
    ErrorComponent,
    ConfigProvider,
} from "@pankod/refine-antd";
import { Title } from "./components/layout";
import { dataProvider, liveProvider } from "@pankod/refine-supabase";
import routerProvider from "@pankod/refine-react-router-v6";
import { supabaseClient } from "utility";
import "@pankod/refine-antd/dist/reset.css";

import authProvider from 'authProvider';
import { UserList } from "pages";
import { AuthPage } from "pages/auth";

function App() {
    return (
        <ConfigProvider
            theme={{
                token: {
                    colorPrimary: "#3ecf8e",
                    colorText: "#80808a",
                    colorError: "#fa541c",
                    colorBgLayout: "#f0f2f5",
                    colorLink: "#3ecf8e",
                    colorLinkActive: "#3ecf8e",
                    colorLinkHover: "#3ecf8e",
                },
            }}
        >
            <Refine
                dataProvider={dataProvider(supabaseClient)}
                liveProvider={liveProvider(supabaseClient)}
                authProvider={authProvider}
                routerProvider={{
                    ...routerProvider,
                    routes: [
                        {
                            path: "/forgot-password",
                            element: <AuthPage type="forgotPassword" />,
                        },
                        {
                            path: "/update-password",
                            element: <AuthPage type="updatePassword" />,
                        },
                    ],
                }}
                resources={[
                    {
                        name: "users",
                        list: UserList,
                    },

                ]}
                LoginPage={() => (
                    <AuthPage
                        type="login"
                        registerLink={false}
                    />
                )}
                notificationProvider={notificationProvider}
                ReadyPage={ReadyPage}
                catchAll={<ErrorComponent />}
                Layout={Layout}
                Title={Title}
            />
        </ConfigProvider>
    );
}

export default App;

Since authProvider prop is already passed in by default, after we added the above resources and granted we are connected to the Internet, we will be redirected to the login page:

react admin dashboard


From the /login route, logging in should work perfectly with an account already created with the Pixels client app. If we log in, we should be directed to /users - the default root route of the admin app.

The name: "users" property in our first resource is used to define the /users route, and list: UserList property specifies that <UserList /> component should be rendered at /users.

react admin dashboard


Editable Table Using refine and Ant Design

For our <CanvasList /> view, we want to allow editors and admins to promote or delete a canvas item. This means, we need to be able to send POST, PUT/PATCH and DELETE requests. @pankod/refine-antd's useEditableTable() hook makes life beautiful for us.

We have a useEditableTable() hook in action inside our <CanvasList /> component:

// pages/canvases/list.tsx

import React from 'react';
import { useUpdate } from '@pankod/refine-core';
import {
    List,
    useEditableTable,
    Table,
    Form,
    Button,
    DeleteButton,
    Space,
    Tag,
} from '@pankod/refine-antd';
import { TCanvas } from 'types/canvas';

type TCanvasPromoteResult = {
    id: number;
    featured: boolean;
};

export const CanvasList = () => {
    const { tableProps, formProps } = useEditableTable<TCanvas>();
    const { mutate } = useUpdate<TCanvasPromoteResult>();

    return (
        <List>
            <Form {...formProps}>
                <Table {...tableProps} rowKey="id">
                    <Table.Column<TCanvas>
                        key="id"
                        dataIndex="id"
                        title={
                            <h4
                                style={{
                                    textAlign: 'center',
                                    fontWeight: 'bold',
                                }}
                            >
                                ID
                            </h4>
                        }
                    />
                    <Table.Column<TCanvas>
                        key="name"
                        dataIndex="name"
                        title={
                            <h4
                                style={{
                                    textAlign: 'center',
                                    fontWeight: 'bold',
                                }}
                            >
                                Name
                            </h4>
                        }
                    />
                    <Table.Column<TCanvas>
                        key="is_featured"
                        dataIndex="is_featured"
                        title={
                            <h4
                                style={{
                                    textAlign: 'center',
                                    fontWeight: 'bold',
                                }}
                            >
                                Featured
                            </h4>
                        }
                        render={(_, record) =>
                            record.is_featured ? (
                                <Tag
                                    color="success"
                                    style={{
                                        display: 'flex',
                                        justifyContent: 'center',
                                        alignItems: 'center',
                                    }}
                                >
                                    Yes
                                </Tag>
                            ) : (
                                <Tag
                                    color="warning"
                                    style={{
                                        display: 'flex',
                                        justifyContent: 'center',
                                        alignItems: 'center',
                                    }}
                                >
                                    No
                                </Tag>
                            )
                        }
                    />
                    <Table.Column<TCanvas>
                        title={
                            <h4
                                style={{
                                    textAlign: 'center',
                                    fontWeight: 'bold',
                                }}
                            >
                                Actions
                            </h4>
                        }
                        dataIndex="actions"
                        render={(_, record) => (
                            <Space
                                style={{
                                    display: 'flex',
                                    justifyContent: 'center',
                                }}
                            >
                                <Button
                                    size="small"
                                    style={{ width: '100px' }}
                                    type={
                                        record.is_featured ? 'ghost' : 'primary'
                                    }
                                    onClick={() =>
                                        mutate({
                                            resource: 'canvases',
                                            id: record.id,
                                            values: {
                                                is_featured:
                                                    !record.is_featured,
                                            },
                                            metaData: {
                                                canvas: record,
                                            },
                                        })
                                    }
                                >
                                    {record.is_featured
                                        ? 'Unpromote'
                                        : 'Promote'}
                                </Button>
                                <DeleteButton
                                    size="small"
                                    recordItemId={record.id}
                                />
                            </Space>
                        )}
                    />
                </Table>
            </Form>
        </List>
    );
};

refine Ant Design useEditableTable() Hook

The useEditableTable() hook is the extension of @pankod/refine-antd's useTable() hook. It returns a formProps object that we can pass to <Form /> components in order to handle form actions, loading and displaying success and error messages.

Like useTable(), the useEditableTable() hook also returns a tableProps object:

const { tableProps, formProps } = useEditableTable<TCanvas>();

The items of formProps object are passed to the <Form /> component:

<Form {...formProps } >
  // React nodes here...
</Form>

We can do much more with the useEditableTable() hook, like activating editing fields when a row is clicked . Here's the elaborate documentation for useEditableTable()

refine Ant Design <DeleteButton />

Thanks to the formProps being passed to <Form />, implementing delete action becomes a piece of cake:

<DeleteButton size="small" recordItemId={record.id} />

@pankod/refine-antd's <DeleteButton /> leverages Ant Design's <Button /> and <Popconfirm /> components. It invokes the delete() data provider method to send a DELETE request to the resource end point. The resource.name is inferred from the formProps passed to <Form /> component.

For more details, visit the <DeleteButton /> docs.

<Table.Column />'s render Prop

We can customize the content inside our table cell by passing a function to the render prop of <Table.Column />. In our example, we have a conditional rendering, where the component being rendered depends on record.is_featured:

<Table.Column<TCanvas>
  key="is_featured"
  dataIndex="is_featured"
  title={
    <h4 style={{ textAlign: "center", fontWeight: "bold" }}>
      Featured
    </h4>
  }
  render={(_, record) =>
    record.is_featured ? (
      <Tag
        color="success"
        style={{
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
        }}
      >
        Yes
      </Tag>
    ) : (
      <Tag
        color="warning"
        style={{
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
        }}
      >
        No
      </Tag>
    )
  }
/>

We also grouped content inside a cell, with <Buttom /> and <DeleteButton /> being sibling flex items:

<Table.Column<TCanvas>
  title={
    <h4 style={{ textAlign: "center", fontWeight: "bold" }}>
      Actions
    </h4>
  }
  dataIndex="actions"
  render={(_, record) => (
    <Space style={{ display: "flex", justifyContent: "center" }}>
      <Button
        size="small"
        style={{ width: "100px" }}
        type={record.is_featured ? "ghost" : "primary"}
        onClick={() =>
          mutate({
            resource: "canvases",
            id: record.id,
            values: {
              is_featured: !record.is_featured,
            },
            metaData: {
              canvas: record,
            },
          })
        }
      >
        {record.is_featured ? "Unpromote" : "Promote"}
      </Button>
      <DeleteButton size="small" recordItemId={record.id} />
    </Space>
  )}
/>

Adding <CanvasList> to the resources

We have covered adding CRUD operations on Day 3 in significant depth. So, here we'll quickly add canvases resources to <Refine /> component:

// src/App.tsx

import { CanvasList } from "pages";
...

return (
  <Refine
    ...
    authProvider={authProvider}
    // highlight-start
    resources={[
      {
          name: "canvases",
          list: CanvasList,
        },
    ]}
    // highlight-end
  />
)

With these additions, /canvases looks like this:

react admin dashboard


Summary

In this post, we initialized an admin dashboard app for our Pixels client app which we built in the previous episodes in the refineWeek series. We implemented list views for two resources: users and canvases.

Inside the lists, we fetched data from these resources and rendered them inside tables. We implemented two types of tables using two distinct @pankod/refine-antd hooks: useTable() for regular tables and useEditableTable() that allows data in the table to be mutated.

These hooks are supported by refine core's useMany() hook, which uses the getMany() data provider method to interact with external API.

In the UI side, these hooks automatically make available appropriate props to be passed to <Table /> and <Form /> components.

In the next post, we will focus on implementing Role Based Access Control to our dashboard based on editor and admin roles.