Dashboard & Widgets
The admin dashboard is the home page of the admin panel. It displays a collection of widgets — stats cards, charts, and activity feeds — assembled from your resource definitions and custom API endpoints.
Dashboard Page
When an admin user opens the admin panel, the first page they see is the dashboard (apps/admin/app/page.tsx). It aggregates widgets from all registered resources that define a dashboard section, plus any global widgets you configure.
The dashboard layout uses a responsive grid that adapts to the screen size:
- Desktop (lg+) — 4-column grid for stats cards, 2-column grid for charts.
- Tablet (md) — 2-column grid for stats, full-width charts.
- Mobile (sm) — single column, all widgets stacked vertically.
Widgets load their data independently using React Query, so the dashboard renders progressively — fast widgets appear immediately while slower ones show skeleton loaders.
Widget Types
Stats Card
A compact card that displays a single metric. Stats cards are the most common widget type and typically appear in a row of 3-4 across the top of the dashboard.
interface StatsCardWidget {
type: 'stat'
label: string // Display label (e.g. "Total Revenue")
query: string // Server query (e.g. "sum:amount")
format?: 'number' | 'currency' | 'percent'
color?: string // Accent color: 'purple' | 'green' | 'blue' | 'yellow' | 'red'
icon?: string // Lucide icon name
changeQuery?: string // Query for period-over-period change %
}Each stats card renders:
- Value — the main metric, formatted according to the
formatproperty (plain number, currency with$prefix, or percentage). - Label — descriptive text below the value.
- Change indicator — if
changeQueryis set, a green upward arrow or red downward arrow with the percentage change compared to the previous period. - Icon — a Lucide icon on the right side of the card, rendered in the accent color.
- Color — a subtle colored accent on the left border or background of the card.
dashboard: {
widgets: [
{
type: 'stat',
label: 'Total Revenue',
query: 'sum:amount',
format: 'currency',
icon: 'DollarSign',
color: 'green',
changeQuery: 'sum:amount:change:month',
},
{
type: 'stat',
label: 'Total Users',
query: 'count',
format: 'number',
icon: 'Users',
color: 'blue',
},
{
type: 'stat',
label: 'Pending Orders',
query: 'count:status=pending',
format: 'number',
icon: 'Clock',
color: 'yellow',
},
{
type: 'stat',
label: 'Conversion Rate',
query: 'custom:conversion-rate',
format: 'percent',
icon: 'TrendingUp',
color: 'purple',
},
],
}Line Chart
Line charts display time-series data using Recharts. They are ideal for showing trends over time — revenue per month, new users per week, or page views per day.
interface LineChartWidget {
type: 'chart'
chartType: 'line'
label: string // Chart title
query: string // e.g. "sum:amount:by:month" or "count:by:week"
format?: 'number' | 'currency'
color?: string // Line color
height?: number // Chart height in px (default: 300)
}The chart component renders a smooth line with a gradient fill area below it. Hovering over a data point shows a tooltip with the exact value and date. The X-axis shows time labels (months, weeks, or days depending on the query granularity) and the Y-axis auto-scales to fit the data range.
{
type: 'chart',
chartType: 'line',
label: 'Revenue Over Time',
query: 'sum:amount:by:month',
format: 'currency',
color: 'purple',
height: 320,
}Bar Chart
Bar charts display categorical data — comparisons between categories, status breakdowns, or grouped counts. They use the same Recharts library and share the same tooltip and axis styling as line charts.
interface BarChartWidget {
type: 'chart'
chartType: 'bar'
label: string // Chart title
query: string // e.g. "count:by:category" or "sum:amount:by:status"
format?: 'number' | 'currency'
color?: string // Bar color
height?: number // Chart height in px (default: 300)
}{
type: 'chart',
chartType: 'bar',
label: 'Posts by Category',
query: 'count:by:category',
format: 'number',
color: 'blue',
}Recent Activity
The activity widget displays a chronological list of recent events — new records created, records updated, users logged in, jobs completed, etc. Each event shows a timestamp, an icon, a description, and optionally a link to the related resource.
interface ActivityWidget {
type: 'activity'
label: string // Widget title (e.g. "Recent Activity")
query: string // e.g. "recent:10" (last 10 events)
height?: number // Max height in px (scrollable)
}
// API response format for activity events
interface ActivityEvent {
id: string
type: 'created' | 'updated' | 'deleted' | 'login' | 'custom'
resource: string // Resource name (e.g. "Post")
description: string // "John created a new post"
user?: { name: string; avatar?: string }
timestamp: string // ISO 8601
link?: string // Optional link to the resource
}{
type: 'activity',
label: 'Recent Activity',
query: 'recent:15',
height: 400,
}Widget Grid Layout
The dashboard arranges widgets in a responsive grid. The layout logic follows these rules:
- Stats cards are grouped together and rendered in a 4-column row (lg), 2-column (md), or 1-column (sm).
- Charts take half the grid width on desktop (2 charts side by side) and full width on smaller screens.
- Activity widgets take half the grid width on desktop and full width on smaller screens.
Widgets from different resources are merged and grouped by type. All stats cards from all resources appear in one row at the top, followed by charts, then activity feeds. This creates a cohesive dashboard rather than resource-isolated sections.
Widget Data Fetching
Each widget fetches its data independently from the Go API using React Query. The query string in the widget definition is translated to an API call:
| Widget Query | API Call |
|---|---|
| count | GET /api/posts/stats?metric=count |
| sum:amount | GET /api/invoices/stats?metric=sum&field=amount |
| count:status=pending | GET /api/orders/stats?metric=count&status=pending |
| sum:amount:by:month | GET /api/invoices/stats?metric=sum&field=amount&group=month |
| count:by:category | GET /api/posts/stats?metric=count&group=category |
| recent:10 | GET /api/activity?limit=10 |
| custom:conversion-rate | GET /api/stats/conversion-rate |
Widget data is cached by React Query with a default stale time of 30 seconds. Stats cards poll for fresh data every 60 seconds in the background so the dashboard stays reasonably current without excessive API calls.
Custom Widget Endpoints
For metrics that cannot be expressed as simple aggregations, use thecustom: prefix in the query string. This tells the widget to call a custom API endpoint that you implement in Go:
// Custom endpoint for conversion rate widget
func (h *StatsHandler) GetConversionRate(c *gin.Context) {
totalVisitors, err := h.service.CountVisitors(c)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
totalSignups, err := h.service.CountSignups(c)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
rate := float64(totalSignups) / float64(totalVisitors) * 100
c.JSON(200, gin.H{
"data": gin.H{
"value": rate,
"label": "Conversion Rate",
},
})
}Register the custom endpoint in your routes file:
// Stats endpoints
stats := api.Group("/stats")
stats.Use(middleware.AuthMiddleware(), middleware.RequireRole("admin"))
{
stats.GET("/conversion-rate", statsHandler.GetConversionRate)
stats.GET("/monthly-mrr", statsHandler.GetMonthlyMRR)
}Adding Widgets to Resource Definitions
Each resource can define its own widgets in the dashboard section. These widgets are automatically included on the admin dashboard alongside widgets from other resources.
import { defineResource } from '@grit/admin'
export default defineResource({
name: 'Order',
endpoint: '/api/orders',
icon: 'ShoppingCart',
table: { /* ... columns and filters ... */ },
form: { /* ... fields ... */ },
dashboard: {
widgets: [
// Stats cards
{
type: 'stat',
label: 'Total Orders',
query: 'count',
icon: 'ShoppingCart',
color: 'purple',
},
{
type: 'stat',
label: 'Revenue',
query: 'sum:total',
format: 'currency',
icon: 'DollarSign',
color: 'green',
changeQuery: 'sum:total:change:month',
},
{
type: 'stat',
label: 'Pending Orders',
query: 'count:status=pending',
icon: 'Clock',
color: 'yellow',
},
// Charts
{
type: 'chart',
chartType: 'line',
label: 'Revenue Over Time',
query: 'sum:total:by:month',
format: 'currency',
color: 'purple',
},
{
type: 'chart',
chartType: 'bar',
label: 'Orders by Status',
query: 'count:by:status',
color: 'blue',
},
// Activity feed
{
type: 'activity',
label: 'Recent Orders',
query: 'recent:10',
},
],
},
})Widget API Response Format
The Go API must return widget data in these formats:
Stats Response
// GET /api/orders/stats?metric=count
{
"data": {
"value": 1247,
"change": 12.5, // +12.5% vs previous period (optional)
"previous": 1108 // previous period value (optional)
}
}
// GET /api/orders/stats?metric=sum&field=total
{
"data": {
"value": 84350.00,
"change": -3.2
}
}Chart Response
// GET /api/orders/stats?metric=sum&field=total&group=month
{
"data": [
{ "label": "Sep 2025", "value": 12400 },
{ "label": "Oct 2025", "value": 15800 },
{ "label": "Nov 2025", "value": 13200 },
{ "label": "Dec 2025", "value": 19500 },
{ "label": "Jan 2026", "value": 17800 },
{ "label": "Feb 2026", "value": 21300 }
]
}
// GET /api/posts/stats?metric=count&group=category
{
"data": [
{ "label": "Tech", "value": 42 },
{ "label": "Design", "value": 28 },
{ "label": "Business", "value": 15 }
]
}Activity Response
// GET /api/activity?limit=10
{
"data": [
{
"id": "act_001",
"type": "created",
"resource": "Order",
"description": "New order #1247 placed by John Doe",
"user": { "name": "John Doe", "avatar": "https://..." },
"timestamp": "2026-02-11T14:30:00Z",
"link": "/resources/orders/1247"
},
{
"id": "act_002",
"type": "updated",
"resource": "Invoice",
"description": "Invoice INV-089 marked as paid",
"user": { "name": "Admin" },
"timestamp": "2026-02-11T14:15:00Z"
}
]
}Widget Styling
All widgets follow the Grit dark theme aesthetic. Cards have subtle borders (border-border/40), slightly elevated backgrounds (bg-card/80), and consistent padding. Charts use the purple accent color by default with gradient fills. Stats cards have a thin colored left border matching their color property.
Skeleton loaders match the exact dimensions of each widget type, preventing layout shift during initial load. Error states display a subtle error message inside the widget area without breaking the grid layout.