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>
);

For other customizations you can take a look at the Menu component documentation.

You can also customize the Layout

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>

List and Datagrid are among the most used components in React Admin

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.

Previous
Configure permissions