SvGrid Studio · dashboard

A schema-driven dashboard above the grid. SvSchemaDashboard renders a declarative spec of KPI tiles (count / sum / avg) + charts (SvSchemaChart) over the same EntitySchema - a data view, not a page builder. Click a chart category to drill the grid; create / edit / delete and the KPIs and charts update. (requires @svgrid/enterprise)

A live, editable Svelte 5 data grid example from the SvGrid gallery (Studio). See the SvGrid documentation for the full API.

About this example

A schema-driven dashboard above the grid in the SvGrid Studio stack. SvSchemaDashboard renders a declarative DashboardSpec of KPI tiles with count, sum and avg and charts through SvSchemaChart, all over the same EntitySchema and data source as the grid. It is a data view rather than a page builder: click a chart category to drill the grid, and creating, editing or deleting a row updates the KPIs and charts.

Data-app Studio · dashboard - a schema-driven dashboard above the grid.

SvSchemaDashboard renders a declarative DashboardSpec (KPI tiles + charts) over the same EntitySchema + rows as the grid. It's a data VIEW, not a page builder: KPIs reduce a measure to one number, charts reuse SvSchemaChart. Click a chart category to drill the grid; create / edit / delete updates both.

Imports, features and API used

Imports: @svgrid/grid, @svgrid/enterprise, ../shared/SvGridStudio.svelte

Frequently asked questions

What goes in a DashboardSpec?

An array of tiles, each a KPI with a measure and a reducer or a chart with a group field and a measure. The dashboard lays them out and asks the data source's getAggregate for the numbers.

Does the dashboard need its own data?

No. It receives the same schema and getAggregate as the grid, so KPIs and charts describe the same source; refreshKey re-runs the aggregates after a write.

Can users edit the dashboard?

Not in this component; the spec is authored in code. It is meant as a data view over an entity, and the Studio designer is where layouts are edited visually.

Related documentation

Related articles

Source code (200-studio-dashboard.svelte)

<script lang="ts">
  /**
   * Data-app Studio · dashboard - a schema-driven dashboard above the grid.
   *
   * `SvSchemaDashboard` renders a declarative `DashboardSpec` (KPI tiles + charts)
   * over the same `EntitySchema` + rows as the grid. It's a data VIEW, not a page
   * builder: KPIs reduce a measure to one number, charts reuse `SvSchemaChart`.
   * Click a chart category to drill the grid; create / edit / delete updates both.
   */
  import { SvGrid, createServerDataSource, type ServerState } from '@svgrid/grid'
  import {
    SvSchemaDashboard,
    SvGridEditPanel,
    createInMemoryDataSource,
    schemaToColumns,
    type EntitySchema,
    type DashboardSpec,
  } from '@svgrid/enterprise'
  import SvGridStudio from '../shared/SvGridStudio.svelte'

  type Customer = { id: string; name: string; email: string; tier: string; mrr: number; active: boolean }

  const schema: EntitySchema<Customer> = {
    name: 'customers', label: 'Customer', idField: 'id',
    fields: [
      { field: 'id', type: 'text', primaryKey: true, readonly: true, hidden: { form: true } },
      { field: 'name', type: 'text', required: true, minLength: 2 },
      { field: 'email', type: 'text', label: 'Email', required: true, format: 'email' },
      { field: 'tier', type: 'enum', options: [
        { value: 'free', label: 'Free' }, { value: 'pro', label: 'Pro' }, { value: 'enterprise', label: 'Enterprise' },
      ] },
      { field: 'mrr', type: 'number', label: 'MRR ($)', min: 0 },
      { field: 'active', type: 'boolean' },
    ],
  }

  const seed: Customer[] = [
    { id: 'c1', name: 'Ada Lovelace', email: '[email protected]', tier: 'enterprise', mrr: 1200, active: true },
    { id: 'c2', name: 'Alan Turing', email: '[email protected]', tier: 'pro', mrr: 240, active: true },
    { id: 'c3', name: 'Grace Hopper', email: '[email protected]', tier: 'enterprise', mrr: 980, active: true },
    { id: 'c4', name: 'Edsger Dijkstra', email: '[email protected]', tier: 'free', mrr: 0, active: false },
    { id: 'c5', name: 'Barbara Liskov', email: '[email protected]', tier: 'pro', mrr: 300, active: true },
    { id: 'c6', name: 'Donald Knuth', email: '[email protected]', tier: 'enterprise', mrr: 1500, active: true },
    { id: 'c7', name: 'Margaret Hamilton', email: '[email protected]', tier: 'pro', mrr: 420, active: true },
    { id: 'c8', name: 'Tim Berners-Lee', email: '[email protected]', tier: 'enterprise', mrr: 1100, active: true },
    { id: 'c9', name: 'Linus Torvalds', email: '[email protected]', tier: 'pro', mrr: 360, active: true },
    { id: 'c10', name: 'Katherine Johnson', email: '[email protected]', tier: 'enterprise', mrr: 890, active: true },
    { id: 'c11', name: 'Dennis Ritchie', email: '[email protected]', tier: 'pro', mrr: 275, active: false },
    { id: 'c12', name: 'Frances Allen', email: '[email protected]', tier: 'free', mrr: 0, active: false },
  ]

  const columns = schemaToColumns(schema)
  const source = createInMemoryDataSource(seed, schema)

  // KPIs + charts are computed SERVER-SIDE via getAggregate, so they're correct
  // over the whole table (not just the grid's current page). Bump `rev` to
  // re-aggregate after a mutation.
  let rev = $state(0)

  const dashboard: DashboardSpec = {
    widgets: [
      { kind: 'kpi', label: 'Customers', reduce: 'count' },
      { kind: 'kpi', label: 'Total MRR', measure: 'mrr', reduce: 'sum', format: (v) => '$' + v.toLocaleString() },
      { kind: 'kpi', label: 'Avg MRR', measure: 'mrr', reduce: 'avg', format: (v) => '$' + Math.round(v) },
      { kind: 'chart', label: 'MRR by tier', dimension: 'tier', measure: 'mrr', reduce: 'sum', type: 'bar', span: 2 },
      { kind: 'chart', label: 'Customers by status', dimension: 'active', reduce: 'count', type: 'pie', span: 1 },
    ],
  }

  let view = $state<ServerState<Customer>>({
    rows: [], total: 0, loading: false, saving: false, error: null,
    pageIndex: 0, pageSize: 8, pageCount: 1, sortModel: [], filterModel: {},
  })
  const controller = createServerDataSource(source, {
    pageSize: 8, optimistic: true, getRowId: (r) => r.id, onChange: (s) => (view = s),
  })
  controller.refresh()

  // Click a chart category -> filter the grid to it.
  let activeFilter = $state<{ dim: string; value: string } | null>(null)
  function drill(category: string, dimension: string) {
    activeFilter = { dim: dimension, value: category }
    controller.setFilter({ columns: { [dimension]: { operator: 'equals', value: category } } })
  }
  function clearFilter() {
    activeFilter = null
    controller.setFilter({})
  }

  let editing = $state<Customer | null | undefined>(undefined)
  let genId = 13
  async function save({ mode, id, values }: { mode: 'create' | 'edit'; id: string | null; values: Partial<Customer> }) {
    if (mode === 'create') { await controller.createRow({ id: `c${genId++}`, ...values }); controller.setPage(view.pageCount - 1) }
    else if (id) { await controller.updateRow(id, values) }
    editing = undefined
    rev++ // re-aggregate the dashboard (server-side)
  }
</script>

<SvGridStudio
  title="Dashboard"
  subtitle="<code>SvSchemaDashboard</code> renders a declarative spec of KPI tiles + charts over the same schema. Click a chart category to drill the grid; create / edit updates the numbers."
>
  {#snippet toolbar()}
    <button class="st-btn st-btn--primary" onclick={() => (editing = null)}>+ New customer</button>
    {#if activeFilter}
      <button class="st-chip" onclick={clearFilter}>{activeFilter.dim} = {activeFilter.value} ✕</button>
    {/if}
    <span class="st-hint" style="margin-left:auto">Double-click a row to edit</span>
  {/snippet}

  <div class="dash-demo">
    <SvSchemaDashboard {schema} getAggregate={(req) => source.getAggregate(req)} refreshKey={rev} spec={dashboard} onDrill={drill} />

    <SvGrid responsive={true}
      columnResize
      data={view.rows}
      {columns}
      loading={view.loading}
      fitColumns
      sortable
      externalSort
      onSortingChange={(s) => controller.setSort(s)}
      onRowDoubleClick={(e) => (editing = e.row)}
      showPagination
      externalPagination
      rowCount={view.total}
      pageIndex={view.pageIndex}
      pageSize={view.pageSize}
      onPaginationChange={({ pageIndex, pageSize }) => (pageSize !== view.pageSize ? controller.setPageSize(pageSize) : controller.setPage(pageIndex))}
      containerHeight={320}
    />
  </div>

  {#if editing !== undefined}
    <SvGridEditPanel {schema} row={editing} presentation="modal" persistKey="studio" onSubmit={save} onCancel={() => (editing = undefined)} />
  {/if}
</SvGridStudio>

<style>
  .dash-demo { display: flex; flex-direction: column; gap: 16px; }
</style>

View this example on GitHub

More Studio examples

  • SvGrid Studio · live SQL - The Studio stack backed by a REAL Postgres running in the browser via PGlite (WASM), no server. createSqlDataSource turns the grid's sort / filter / page requests into parameterized SQL run through PGlite; the executed query is shown live under the toolbar. Full CRUD with optimistic updates against actual Postgres.
  • Northwind on PGlite - The classic Northwind sample DB (categories, customers, products, orders, order_details) seeded into a real in-browser Postgres (PGlite, WASM). Switch between a five-table JOIN exposed as a read-only VIEW and the editable base tables - each via createSqlDataSource. Sort / filter / page / edit all run as parameterized SQL; new rows get an auto id from a Postgres IDENTITY sequence.
  • SvGrid Studio · Supabase - The Studio stack over hosted Postgres on Supabase, straight from the browser via supabase-js (PostgREST) and your project's public anon key. createSupabaseDataSource maps the grid's sort / filter / page / CRUD onto the query builder, introspectSupabaseTable adapts to any table AND detects foreign keys (a FK column auto-becomes a searchable lookup in the form), and createSupabaseRealtime makes it LIVE - change a row in the Supabase dashboard and it flashes in the grid (toggle the Live pill). Paste your URL + anon key, run the one-time setup SQL, done. RLS keeps the anon key safe.
  • SvGrid Studio · relations - Foreign-key lookup fields end to end. Contacts belong to a Company: the contact form renders a searchable Company picker (SvLookupInput + createRelationLookup) that queries the Companies source and stores the id, while the grid shows the resolved company NAME. The lookup runs over the same ServerDataSource contract, so related options can come from Supabase / REST / SQL / in-memory (here both entities are in-memory).
  • SvGrid Studio · secured - A secured screen: SvAuthGate requires a signed-in user (Supabase Auth via createSupabaseAuth) before showing the Studio grid, with a login / sign-up form and a signed-in bar. Uses a mock auth client so any email + password works here; swap for your supabase-js client and Row-Level Security scopes each user to their own rows (auth = who, RLS = what).