Admin Panel Customization
Every Grit Triple project includes a fully-featured admin panel. In this course, you will learn how the admin panel works — the dashboard, DataTable, FormBuilder, resource definitions, multi-step forms, style variants, and system pages — and how to customize every part of it.
What is the Admin Panel?
Every Grit Triple project includes a fully-featured admin panel running at localhost:3001. It's a separate Next.js/TanStack app that provides a dashboard, data management, and system tools — similar to Laravel Nova, Django Admin, or WordPress Admin.
Regular users interact with your web app at localhost:3000. Administrators use the admin panel at localhost:3001 to manage users, content, orders, settings, and everything else behind the scenes. Think of it as the "control room" for your application.
The admin panel includes these major features out of the box:
- • Dashboard with stats cards and charts
- • DataTable for listing, sorting, filtering, and exporting data
- • FormBuilder for creating and editing records
- • System pages for jobs, files, cron, email preview, and security
apps/admin/. It shares types, schemas, and constants with the web app through the packages/shared/ package, but has its own routes, layouts, and components.Challenge: Explore the Admin Panel
Start your project with grit dev, then open localhost:3001 in your browser and log in. Click through every item in the sidebar. List 5 different pages you can see in the admin panel.
The Dashboard
The first thing you see after logging in is the dashboard. It gives you a high-level overview of your application's data and activity. The dashboard is divided into three sections:
- • Stats cards at the top — quick numbers like total users, total posts, revenue, etc.
- • Charts below — a line chart for signups over time, a bar chart for content by category
- • Activity feed — a list of recent actions (user registered, post created, order placed)
The dashboard is defined in the admin app's dashboard page. You can customize the stats, charts, and layout by editing this file. Here's what a stats card looks like:
<StatsCard
title="Total Users"
value={stats.totalUsers}
icon={<Users className="h-4 w-4" />}
change="+12% from last month"
/>Explained: The StatsCard component takes a title (what the stat is), a value (the number), an icon (a Lucide icon), and an optional change string showing the trend. You can add as many stats cards as you want by duplicating this pattern with different data.
Challenge: Inspect the Dashboard
Look at the dashboard in your admin panel. How many stats cards do you see? What data does each one show? Write down the title and value of each card.
Challenge: Find the Dashboard Code
Open the dashboard page in your code editor. Look for the file at apps/admin/app/(dashboard)/page.tsx or a similar path. Find the StatsCard components. Can you match each component in the code to what you see in the browser?
DataTable — Displaying Data
The DataTable is the core component for viewing data in the admin panel. Every resource page (Users, Posts, Products, etc.) uses a DataTable to display records. It handles all the complexity of data display so you don't have to build it from scratch:
- • Server-side pagination — load 20 records at a time, not all at once
- • Column sorting — click a header to sort ascending or descending
- • Text search — filter records by typing a keyword
- • Column visibility toggle — show or hide columns
- • Row selection — select multiple rows for bulk actions
- • Export to CSV or JSON — download data for offline use
Here's how a DataTable is configured in a resource definition. You define an array of columns, and the DataTable renders them automatically:
const columns = [
{ key: "title", label: "Title", sortable: true },
{ key: "author", label: "Author", sortable: true },
{ key: "published", label: "Published", type: "badge" },
{ key: "createdAt", label: "Created", type: "date" },
]Explained: Each object in the array describes one column. The key maps to the field name in your data. The label is what appears as the column header. Setting sortable: true lets users click the header to sort. The type controls how the value is displayed — "badge" renders a colored pill, "date" formats the timestamp into a readable date.
Challenge: Use Every DataTable Feature
Open the Users page in the admin panel. Try each of these features:
- Sort by name (click the Name column header)
- Search for a user (type in the search box)
- Toggle column visibility (click the columns dropdown)
- Export to CSV (click the export button)
Challenge: Generate a Resource and Test the DataTable
Generate a Product resource with grit generate resource Product --fields "name:string,price:float,active:bool". Open the admin panel — does a Products page appear automatically in the sidebar? Open it and try all DataTable features: sort, search, toggle columns, and export.
FormBuilder — Creating and Editing Records
The FormBuilder component generates complete forms from a field configuration. Instead of writing HTML form elements, validation logic, and submission handlers by hand, you define what fields you need, and FormBuilder creates the entire form — inputs, labels, validation messages, and the submit button.
FormBuilder supports these field types:
- •
text— Single line input - •
textarea— Multi-line text - •
select— Dropdown menu - •
date— Date picker - •
richtext— Rich text editor (Tiptap with bold, italic, headings, lists, links, code) - •
file— File upload with preview - •
checkbox— True/false toggle - •
relationship-select— Dropdown that loads related records from another table - •
multi-relationship-select— Multi-select with tags for many-to-many relations
Here's how you configure fields for FormBuilder:
const fields = [
{ name: "title", label: "Title", type: "text", required: true },
{ name: "content", label: "Content", type: "richtext" },
{ name: "category_id", label: "Category", type: "relationship-select", resource: "categories" },
{ name: "published", label: "Published", type: "checkbox" },
]Explained: Each object defines one form field. The name is the field key sent to the API. The label is shown above the input. The type determines which input component is rendered — a text input, a rich text editor, a relationship dropdown, or a checkbox. Setting required: true means Zod will reject the form if this field is empty. The relationship-select type automatically fetches records from the categories API endpoint to populate the dropdown.
Challenge: Identify Field Types
In the admin panel, click "Create" on any resource. Look at the form that appears. For each input, identify which FormBuilder field type it uses. Is it a text, textarea, richtext, checkbox, or something else?
Challenge: Test Form Validation
Try submitting a form with required fields left empty. What validation messages appear? Where do they appear — above the field, below it, or in a toast? Try submitting with an invalid email format. Does Zod catch it?
Resource Definitions
Every resource in the admin panel is defined using a defineResource() pattern. This is the central configuration that tells the admin panel how to display and manage each data type. It connects the DataTable columns, FormBuilder fields, sidebar icon, and filters all in one place.
Here's a complete resource definition:
defineResource({
name: "products",
label: "Products",
icon: Package,
columns: [
{ key: "name", label: "Name", sortable: true },
{ key: "price", label: "Price", type: "currency" },
{ key: "active", label: "Active", type: "badge" },
],
formFields: [
{ name: "name", label: "Name", type: "text", required: true },
{ name: "price", label: "Price", type: "text", required: true },
{ name: "active", label: "Active", type: "checkbox" },
],
filters: ["active"],
})Explained: The name is the API endpoint (the admin panel will call /api/products). The label is what appears in the sidebar and page header. The icon is a Lucide React icon component. The columns array defines what the DataTable shows — each column maps a data key to a display format. The formFields array defines the create/edit form. The filters array adds filter dropdowns above the table — here, a filter for the "active" field so you can show only active or only inactive products.
grit generate resource, the resource definition file is created automatically. You rarely need to write one from scratch — instead, you customize the generated one by adding columns, changing field types, or adding filters.Challenge: Read a Resource Definition
Find a resource definition file in your admin app (look in the admin app's resource or definitions folder). Read through it. Can you match each columns entry to what appears in the DataTable? Can you match each formFields entry to the create/edit form?
Multi-Step Forms
For complex resources with many fields, cramming everything into a single form can be overwhelming. Multi-step forms solve this by splitting the form into logical steps. Users complete one step at a time, with validation at each step before they can proceed.
Grit supports two variants:
- •
formView: "modal-steps"— Multi-step form inside a modal dialog - •
formView: "page-steps"— Full-page multi-step form with a progress bar
Here's how you configure a multi-step form in a resource definition:
formView: "page-steps",
steps: [
{
title: "Basic Info",
fields: [
{ name: "name", label: "Name", type: "text" },
{ name: "email", label: "Email", type: "text" },
]
},
{
title: "Details",
fields: [
{ name: "bio", label: "Bio", type: "richtext" },
{ name: "avatar", label: "Avatar", type: "file" },
]
},
{
title: "Review",
fields: [] // Read-only summary
}
]Explained: The formView property switches from a single form to a stepped layout. Each object in the steps array is one step with its own title (shown in the progress bar) and fields (the inputs for that step). Validation runs per-step — the user must fill in all required fields in step 1 before they can move to step 2. The final step with an empty fields array acts as a read-only summary where users can review everything before submitting.
"modal-steps" when you have 2-3 steps with a few fields each (keeps things quick). Use "page-steps" when you have 4+ steps or fields that need more space (like rich text editors or file uploads).Challenge: Design Multi-Step Form Steps
Imagine you have a "Customer" resource with these fields: name, email, phone, address_line1, address_line2, city, state, zip, country, preferred_language, newsletter_opt_in, notes. That's a lot of fields for one form. Design 3 logical steps by grouping related fields:
- Step 1: Basic Info (which fields?)
- Step 2: Address (which fields?)
- Step 3: Preferences (which fields?)
Style Variants
The admin panel comes in 4 visual styles that you choose during scaffolding. Each style applies different CSS to the same components, giving your admin panel a distinct look:
- • Default — Clean dark theme with subtle borders
- • Modern — Gradient accents and bolder colors
- • Minimal — Ultra clean, lots of whitespace
- • Glass — Glassmorphism effect (frosted glass backgrounds with blur)
You chose a style during grit new. To see another style, scaffold a new project with a different --style flag:
grit new test-modern --triple --next --style modern
grit new test-glass --triple --next --style glassExplained: The --style flag tells the scaffolder which CSS variant to use. Each style changes the background colors, border styles, shadow effects, and accent colors across the entire admin panel. The component structure stays the same — only the visual treatment changes.
--bg-elevated, --border, and --accent in your stylesheet.Challenge: Compare Admin Styles
Create a temporary project with grit new test-glass --triple --next --style glass. Start it with grit dev and open the admin panel. Compare the login page and dashboard with your default-style project. What visual differences do you notice? Look at backgrounds, borders, and shadows.
Standalone Usage
You can use DataTable and FormBuilder outside the admin resource system — on any page in either the web app or admin app. This is useful when you need a custom page that isn't a standard CRUD resource.
For example, you might want an analytics page that shows a table of aggregated data, a log viewer that lists system events, or a report page that combines data from multiple resources. None of these fit the standard "create, list, edit, delete" pattern, but they still need a good table component.
Here's how to use DataTable as a standalone component on any page:
import { DataTable } from "@/components/ui/data-table"
export default function CustomPage() {
return (
<DataTable
columns={[
{ key: "name", label: "Name" },
{ key: "status", label: "Status", type: "badge" },
]}
endpoint="/api/custom-data"
/>
)
}Explained: You import DataTable directly and pass it a columns array and an endpoint URL. The DataTable handles fetching data from that endpoint, pagination, sorting, and everything else automatically. You get all the same features (search, sort, export) without needing a full resource definition.
Challenge: Design a Custom Table
Think of a page in your app that would need a data table but isn't a standard CRUD resource. Examples: an analytics dashboard, a log viewer, a report, or a leaderboard. Write down the columns you would configure. What key, label, and type would each column have?
System Pages
The admin panel includes several built-in system pages that help you monitor and manage your application's infrastructure. These pages are pre-built and require no configuration — they work automatically as soon as you start using the corresponding features.
- •Jobs Dashboard — View background job status, retries, and failures. See which jobs are queued, processing, or completed. Retry failed jobs with one click.
- •Files — Manage uploaded files. See file names, sizes, types, and storage locations. Preview images directly in the browser.
- •Cron Tasks — View scheduled recurring tasks. See when each task last ran, when it will run next, and whether it succeeded or failed.
- •Mail Preview — Preview email templates before sending them. See how your welcome emails, password resets, and notification emails look.
- •Security (Sentinel) — View rate limits, blocked IPs, and security threats. Monitor who is hitting your API and whether any suspicious activity is occurring.
Summary
You now understand how the Grit admin panel works and how to customize every part of it. Here's what you learned:
- Admin panel overview — a separate Next.js app at localhost:3001 for managing your application's data
- Dashboard — stats cards, charts, and activity feed for a high-level overview
- DataTable — server-side pagination, sorting, searching, column visibility, row selection, and export
- FormBuilder — generates forms from configuration with 9 field types and Zod validation
- Resource definitions — the central configuration that connects DataTable columns, form fields, icons, and filters
- Multi-step forms — split complex forms into logical steps with per-step validation
- Style variants — default, modern, minimal, and glass visual themes
- Standalone usage — using DataTable and FormBuilder outside the resource system for custom pages
- System pages — built-in pages for jobs, files, cron, mail preview, and security monitoring
Challenge: Final Challenge: Build an Invoice System
Put everything together. Generate an Invoice resource:
grit generate resource Invoice --fields "customer_name:string,email:string,amount:float,status:string,due_date:date,notes:text:optional,paid:bool"Then complete these tasks:
- Open the admin panel and find the Invoices page
- Look at the generated resource definition file — match each
columnsentry to the DataTable and eachformFieldsentry to the create form - Create 5 sample invoices with different statuses ("draft", "sent", "paid", "overdue", "cancelled")
- Use the DataTable to sort invoices by amount, search by customer name, and filter by status
- Export all 5 invoices to CSV and open the file to verify the data
Enjoying the course?
Help us grow — star us on GitHub, subscribe on YouTube, and follow on LinkedIn.