Admin Panel

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.

StatsCard properties
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 format property (plain number, currency with $ prefix, or percentage).
  • Label — descriptive text below the value.
  • Change indicator — if changeQuery is 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.
Stats card examples
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.

LineChart properties
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.

Line chart example
{
  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.

BarChart properties
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)
}
Bar chart example
{
  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.

Activity widget properties
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
}
Activity widget example
{
  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 QueryAPI Call
countGET /api/posts/stats?metric=count
sum:amountGET /api/invoices/stats?metric=sum&field=amount
count:status=pendingGET /api/orders/stats?metric=count&status=pending
sum:amount:by:monthGET /api/invoices/stats?metric=sum&field=amount&group=month
count:by:categoryGET /api/posts/stats?metric=count&group=category
recent:10GET /api/activity?limit=10
custom:conversion-rateGET /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:

apps/api/internal/handlers/stats.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:

apps/api/internal/routes/routes.go
// 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.

apps/admin/resources/orders.ts
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

Stats API 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

Chart API 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

Activity API 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.