Get started
Customize the UI
Now that we've got our backend sorted out (without writing any code), let's take a look at the frontend.
If you've clicked around the UI, you already saw that you have complete CRUD functionality for the todos
and comments
tables, all the lists, views and forms respect the permissions defined in the database (the user does not see the user_id
column and they can't change the id
columns when creating/editing items). The filtering and sorting capabilities are also automatically generated based on the database schema.
You can add any new table or column to the database and it will automatically be available in the UI (with the appropriate permissions).
This interface is immediately usable, but it's not very intuitive and user friendly, so let's customize it a bit.
The UI stack
The UI is built as a React Admin application, which is a frontend framework for creating admin applications running in the browser on top of REST/GraphQL APIs, it uses Material UI as a component library.
In addition to that we pre-configured Tailwind CSS to allow for easy customization of the UI.
To have the deep integration between the UI and the backend, subZero provides a few components that take care of auto-generating the lists and forms based on the database schema and permissions.
Let's open the file src/App.tsx
and take start customizing the UI.
Customizing the menu
The first obvious thing that we would like to change is the menu. While we want to interact with the comments
table, we don't want to see it in the menu, but rather on the individual todo
page.
Find the MyMenu
component and change it to look like this:
const MyMenu = () => (
<Menu>
<Menu.DashboardItem />
<Menu.ResourceItem name="todos" />
</Menu>
);
Custom Show components
Start by navigating to Todos
page and create a new todo item. You'll notice that the list and create forms look rather nice, are user configurable (click gear icon in the top right corner) and are fully functional, however the Show
page (when you click on a todo) is not very useful. It just shows all the properties of the todo item in a rather simple unappealing way, plus we'd like this page to also show the comments for the todo item.
While on a todo item page, open your browser's developer tools and take a look at the Console tab (you might need to refresh the page). You'll notice the text Guess resources:
and Guessed Show:
. These messages are printed by subZero components to help you customize the autogenerated parts of the UI.
First, let's manually specify the Resource
definition for the todos
. Grab the text from the console that starts with Guess resources:
and it to the customizedResources
array in the App
component. It should look like this:
// Add customized resources here (use custom List, Edit, Show components)
const customizedResources: ReactElement[] = [
<Resource
key="todos"
name="todos"
create={canAccess('create','todos')?<CreateGuesser canAccess={canAccess} />:undefined}
list={canAccess('list','todos')?<ListGuesser canAccess={canAccess} />:undefined}
edit={canAccess('edit','todos')?<EditGuesser canAccess={canAccess} />:undefined}
show={canAccess('show','todos')?<ShowGuesser canAccess={canAccess} />:undefined}
options={{ label: 'Todos', model: schema?.todos }}
/>
];
For other customizations you can take a look at the Resource component documentation.
After you save the file, while nothing changed in the UI, we can now use a custom Show
component for the todos
resource instead of the ShowGuesser
component.
Create a file named src/components/TodoShow.tsx
and add the following code (which you can also grab from the console output):
import { BooleanField, NumberField, Show, SimpleShowLayout, TextField }
from 'react-admin';
export const TodoShow = () => (
<Show>
<SimpleShowLayout>
<BooleanField source="done" />
<NumberField source="id" />
<TextField source="title" />
</SimpleShowLayout>
</Show>
);
Import the TodoShow
component in src/App.tsx
import { TodoShow } from './components/TodoShow';
And change the resource definition to use the TodoShow
component:
// Add customized resources here (use custom List, Edit, Show components)
const customizedResources: ReactElement[] = [
<Resource
key="todos"
name="todos"
create={canAccess('create','todos')?<CreateGuesser canAccess={canAccess} />:undefined}
list={canAccess('list','todos')?<ListGuesser canAccess={canAccess} />:undefined}
edit={canAccess('edit','todos')?<EditGuesser canAccess={canAccess} />:undefined}
show={canAccess('show','todos')?<TodoShow />:undefined}
options={{ label: 'Todos', model: schema?.todos }}
/>
];
Save the files, and if you make any change in the TodoShow component, you'll see the changes reflected in the UI.
Let's go a bit further and really use our own layout for the Show
page. We'll also use some Tailwind CSS classes to make it look nicer.
Change the TodoShow
component to look like this:
import { Show, SimpleShowLayout, useRecordContext } from 'react-admin';
import CheckIcon from '@mui/icons-material/Check';
export const TodoShow = () => (
<Show>
<SimpleShowLayout>
<TodoDetails />
</SimpleShowLayout>
</Show>
);
const TodoDetails = () => {
const todo = useRecordContext();
if (!todo) return null;
return (
<>
<div className="text-xl flex">
<div className='w-10'>{todo.done && <CheckIcon />}</div>
<div>{todo.title}</div>
</div>
</>
)
};
Adding lists and data grids
One of the most common tasks in an admin application is to display a list of items and allow the user to filter and sort the list and to edit the items in the list. While subZero components take care of auto-generating the main list for the resources, you'll often want to display lists of related items on the Show
page of a resource.
In our tutorial, we'll add a list of comments.
Import the necessary components:
import { ResourceContextProvider, List, Datagrid, TextField, CreateButton}
from 'react-admin';
In the TodoDetails component, add the following code below the div
with the flex
class:
<div className='mt-10'>
<ResourceContextProvider value="comments">
<List
title={' '}
empty={false}
component="div"
disableSyncWithLocation
actions={false}
filter={{ 'todo_id@eq': todo.id }}
>
<Datagrid>
<TextField source="text" />
</Datagrid>
<CreateButton label="Add Comment" resource="comments" state={{ record: { todo_id: todo.id } }} />
</List>
</ResourceContextProvider>
</div>
In the code above, first we say we want to work with the comments
(ResourceContextProvider
) and we want to display a list of comments (List
) filtered by the current todo item (filter={{ 'todo_id@eq': todo.id }}
).
Then we specify the columns to be displayed in the list (Datagrid
) and finally we add a button to create a new comment (CreateButton
).
Now we'll have a list of comments for each todo item.
Click the + Add Comment
button. You'll be taken to a form to create a new comment. That form contains a select for the todo_id
column, but it's just a list of ids which is not very useful. Let's fix that.
Let's go back to src/App.txt
where we defined the todos
resource and change it to this:
<Resource
key="todos"
name="todos"
create={canAccess('create','todos')?<CreateGuesser canAccess={canAccess} />:undefined}
list={canAccess('list','todos')?<ListGuesser canAccess={canAccess} />:undefined}
edit={canAccess('edit','todos')?<EditGuesser canAccess={canAccess} />:undefined}
show={canAccess('show','todos')?<TodoShow />:undefined}
recordRepresentation={(r) => `${r.title}`}
options={{
label: 'Todos',
model: schema?.todos,
filterToQuery: s => ({ 'title@ilike': `%${s}%` }),
}}
/>
Save and refresh the page.
Notice the recordRepresentation
and filterToQuery
, this will make the dropdown display the actual todo title and you can also start typing the title to filter the list.
Once you save the new comment, you'll be redirected to a list displaying all the comments, however we would like to be redirected back to the todo item page. Let's fix that.
We do that by specifying the redirect
parameter (which will be a function) on the CreateGuesser
component used for the comments
resource.
So again, in the src/App.tsx
file, add a custom definition for the comments
resource:
<Resource
key="comments"
name="comments"
create={
canAccess('create', 'comments') ?
<CreateGuesser canAccess={canAccess} redirect={(resource, id, data) => `todos/${data.todo_id}/show`} />
: undefined
}
list={canAccess('list','comments')?<ListGuesser canAccess={canAccess} />:undefined}
edit={canAccess('edit','comments')?<EditGuesser canAccess={canAccess} />:undefined}
show={canAccess('show','comments')?<ShowGuesser canAccess={canAccess} />:undefined}
options={{ label: 'Comments', model: schema?.comments }}
/>
Aggregate data and charts
Another common task in admin applications is to display statistics and charts based on the data in the database. While using the dataProvider
functionality from ReactAdmin when working with simple lists is enough, it does not provide the necessary flexibility to execute analytical queries.
For these types of requests we will be using the underlying client
object that is also used by the dataProvider
to talk to the backend REST api which has much more powerful capabilities.
Let's first open src/components/Dashboard.tsx
:
Notice the line where we use a hook to get the client
object, which we will be using:
const client = useClient();
Let's display the number of completed vs uncompleted todos in an info panel.
First we need some state variables to hold the data:
const [completedTodos, setCompletedTodos] = useState(0);
const [uncompletedTodos, setUncompletedTodos] = useState(0);
Then we need to fetch the data from the backend:
useEffect(() => {
client
.from('todos')
.select(`
total:$count(id),
done
`)
// @ts-ignore
.groupby('done')
.then(({data, error}:PostgrestResponse<any>) => {
const completed = data?.find((i:any) => i.done === true);
const uncompleted = data?.find((i:any) => i.done === false);
setCompletedTodos(completed?.total || 0);
setUncompletedTodos(uncompleted?.total || 0);
}
);
}, []);
Finally we can display the data either as a info panel or as a donut chart.
<TremorCard className='max-w-lg' decoration="top" decorationColor="blue">
<Text>Completed Todos</Text>
<Metric>
{completedTodos} / {completedTodos + uncompletedTodos}
({Math.round(
completedTodos / (completedTodos + uncompletedTodos) * 100
)}%)
</Metric>
</TremorCard>
TremorCard is a component from Tremor which provides a set of pre-made React UI components for building dashboards. It also includes components for charts, icons, lists, etc.