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
:
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:
- pages: https://github.com/refinedev/refine/tree/master/examples/pixels-admin/src/pages
- components: https://github.com/refinedev/refine/tree/master/examples/pixels/src/components
- utility: https://github.com/refinedev/refine/tree/master/examples/pixels-admin/src/utility
- types: https://github.com/refinedev/refine/tree/master/examples/pixels-admin/src/types
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:
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
.
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:
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.