Base dashboard view

A dashboard view over a sales base: the open pipeline and what was won as money, the share of deals won, charts by stage and by industry, the biggest open deals and the activities due this week. Each tile reads its own table with its own filter, and Edit dashboard adds, resizes, moves and removes tiles. (requires @svgrid/enterprise)

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

About this example

A dashboard view in an SvGrid Base, in Svelte 5. Tiles sum up every table of the base: a number (a count, or a field's sum, average, smallest or largest) with an optional caption, a chart grouped by a field, the top records by a sort, and how many records match a filter. Each tile has its own table and filter, and the tiles are saved on the view.

A sales base with three tables (Deals, Accounts, Activities) and a dashboard view on Deals that sums up all three. Each tile reads one table with its own filter: the open pipeline and what was won as money, the share of deals won, the pipeline by stage, accounts by industry, the biggest open deals, and the activities due in the next seven days.

"Edit dashboard" adds a tile (a number, a chart, a list of records or a progress bar), edits or removes one, sets its size and moves it by drag or with the arrows. The tiles are saved on the view, so they come back with tables. Change a deal in the "All deals" view and the tiles follow; a record in a list tile opens in its own table. Click a number or progress tile, or a bar or slice of a chart, to list the records behind it and open one.

Imports, features and API used

Imports: @svgrid/enterprise/base, ../shared/base-people

Columns: deal (Deal), account (Account), stage (Stage), owner (Owner), value (Value), close (Close date), value, value, value, stage, name (Account), industry (Industry)

Frequently asked questions

Can one dashboard read several tables?

Yes. A dashboard view lives on one table, but each tile picks the table it reads. The demo's tiles read Deals, Accounts and Activities from a dashboard on Deals.

What can a tile show?

A number (the records counted, or a number field's sum, average, smallest or largest, written in the field's own format), a chart (bar, line, pie or donut, grouped by a field and counted or summed), a list of the first records by a sort, or a progress bar of the records that match a filter out of all of them.

Where are the tiles saved?

On the view, as its dashboard setting. They change with the rest of the tables and come back through onTablesChange or bind:tables, like any other view setting.

Which license does it need?

SvGrid Base is part of the Suite edition of @svgrid/enterprise. Without a key it runs with a watermark, so it can be evaluated as is.

Related documentation

Related articles

Source code (721-base-dashboard.svelte)

<script lang="ts">
  /**
   * 721. SvGrid Base: dashboard view
   * --------------------------------
   * A sales base with three tables (Deals, Accounts, Activities) and a
   * dashboard view on Deals that sums up all three. Each tile reads one
   * table with its own filter: the open pipeline and what was won as money,
   * the share of deals won, the pipeline by stage, accounts by industry,
   * the biggest open deals, and the activities due in the next seven days.
   *
   * "Edit dashboard" adds a tile (a number, a chart, a list of records or a
   * progress bar), edits or removes one, sets its size and moves it by drag
   * or with the arrows. The tiles are saved on the view, so they come back
   * with `tables`. Change a deal in the "All deals" view and the tiles
   * follow; a record in a list tile opens in its own table. Click a number
   * or progress tile, or a bar or slice of a chart, to list the records
   * behind it and open one.
   */
  import { SvBase, enableBase, type BaseTable, type FieldFilter } from '@svgrid/enterprise/base'
  import { usePeople } from '../shared/base-people'

  enableBase()
  usePeople()

  const STAGES = [
    { value: 'Lead', color: '#94a3b8' },
    { value: 'Qualified', color: '#18bfff' },
    { value: 'Proposal', color: '#2d7ff9' },
    { value: 'Negotiation', color: '#fcb400' },
    { value: 'Won', color: '#20c933' },
    { value: 'Lost', color: '#f82b60' },
  ]
  const INDUSTRIES = [
    { value: 'Wholesale', color: '#ff6f2c' },
    { value: 'Manufacturing', color: '#2d7ff9' },
    { value: 'Software', color: '#8b46ff' },
    { value: 'Retail', color: '#fcb400' },
    { value: 'Healthcare', color: '#20c933' },
  ]
  const KINDS = [
    { value: 'Call', color: '#18bfff' },
    { value: 'Email', color: '#8b46ff' },
    { value: 'Meeting', color: '#ff6f2c' },
  ]
  // Dates around today, so "due this week" always has something in it.
  const day = (n: number) => {
    const d = new Date()
    d.setDate(d.getDate() + n)
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
  }

  // ---- Sample records -------------------------------------------------------

  const ACCOUNTS: Array<[string, string, number]> = [
    ['Northwind Traders', 'Wholesale', 420],
    ['Contoso', 'Manufacturing', 2600],
    ['Fabrikam', 'Software', 880],
    ['Adventure Works', 'Retail', 310],
    ['Tailspin Toys', 'Retail', 140],
    ['Litware', 'Healthcare', 560],
    ['Proseware', 'Software', 95],
  ]
  const PRODUCTS = ['Platform license', 'Team plan', 'Renewal', 'Expansion', 'Pilot']
  const STAGE_OF = [0, 1, 2, 3, 4, 2, 1, 3, 0, 5, 2, 4, 1, 3, 2, 0, 4, 3, 1, 2, 5, 4]
  const deals = STAGE_OF.map((s, i) => {
    const acct = i % ACCOUNTS.length
    return {
      id: `d${i + 1}`,
      deal: `${ACCOUNTS[acct]![0]}: ${PRODUCTS[(i + Math.floor(i / ACCOUNTS.length)) % PRODUCTS.length]}`,
      account: [`a${acct + 1}`],
      stage: STAGES[s]!.value,
      owner: ['u1', 'u2', 'u4', 'u7'][i % 4]!,
      value: 8000 + ((i * 7919) % 60) * 1500,
      close: day(((i * 5) % 70) - 20),
    }
  })
  const accounts = ACCOUNTS.map(([name, industry, employees], i) => ({
    id: `a${i + 1}`,
    name,
    industry,
    employees,
    deals: deals.filter((d) => d.account[0] === `a${i + 1}`).map((d) => d.id),
  }))
  const ACTIVITIES: Array<[string, string, number, boolean]> = [
    ['Call Contoso about the security review', 'Call', 0, false],
    ['Send the Fabrikam proposal', 'Email', 1, false],
    ['Pricing call with Northwind', 'Meeting', 2, false],
    ['Demo for Litware clinical team', 'Meeting', 3, false],
    ['Follow up on the Proseware pilot', 'Email', 5, false],
    ['Renewal check-in with Adventure Works', 'Call', 6, false],
    ['Quarterly review with Tailspin', 'Meeting', 12, false],
    ['Thank-you note to Contoso', 'Email', -2, true],
    ['Kickoff call with Litware', 'Call', -5, true],
  ]
  const activities = ACTIVITIES.map(([subject, kind, due, done], i) => ({ id: `act${i + 1}`, subject, kind, owner: ['u1', 'u2', 'u4', 'u7'][i % 4]!, due: day(due), done }))

  // ---- The dashboard's filters -------------------------------------------------

  const OPEN: FieldFilter = { kind: 'fieldFilter', conjunction: 'and', conditions: [{ column: 'stage', kind: 'select', op: 'isNoneOf', value: ['Won', 'Lost'] }] }
  const WON: FieldFilter = { kind: 'fieldFilter', conjunction: 'and', conditions: [{ column: 'stage', kind: 'select', op: 'is', value: 'Won' }] }
  const DUE_THIS_WEEK: FieldFilter = {
    kind: 'fieldFilter',
    conjunction: 'and',
    conditions: [
      { column: 'due', kind: 'date', op: 'isWithin', value: { span: 'nextWeek' } },
      { column: 'done', kind: 'checkbox', op: 'is', value: false },
    ],
  }

  // ---- The base ---------------------------------------------------------------

  let tables = $state.raw<BaseTable[]>([
    {
      id: 'dash-deals',
      name: 'Deals',
      primaryField: 'deal',
      data: deals,
      columns: [
        { id: 'deal', field: 'deal', header: 'Deal', width: 260, fieldType: 'text' },
        { id: 'account', field: 'account', header: 'Account', width: 180, fieldType: 'link', fieldProps: { table: 'dash-accounts', multiple: false, inverse: 'deals' } },
        { id: 'stage', field: 'stage', header: 'Stage', width: 130, fieldType: 'singleSelect', fieldProps: { options: STAGES } },
        { id: 'owner', field: 'owner', header: 'Owner', width: 160, fieldType: 'user' },
        { id: 'value', field: 'value', header: 'Value', width: 130, fieldType: 'number', fieldProps: { format: 'currency', symbol: '$', precision: 0, summary: 'sum' } },
        { id: 'close', field: 'close', header: 'Close date', width: 150, fieldType: 'date', fieldProps: { dateFormat: 'friendly' } },
      ],
      views: [
        {
          id: 'overview',
          name: 'Sales overview',
          type: 'dashboard',
          dashboard: [
            { id: 'pipeline', kind: 'number', title: 'Open pipeline', aggregation: 'sum', field: 'value', filter: OPEN, caption: 'Quarter target $600,000' },
            { id: 'won', kind: 'number', title: 'Won', aggregation: 'sum', field: 'value', filter: WON },
            { id: 'avg', kind: 'number', title: 'Average open deal', aggregation: 'avg', field: 'value', filter: OPEN },
            { id: 'rate', kind: 'progress', title: 'Deals won', filter: WON },
            { id: 'by-stage', kind: 'chart', title: 'Pipeline by stage', chart: { kind: 'bar', x: 'stage', y: 'value' } },
            { id: 'due', kind: 'list', title: 'Activities due this week', table: 'dash-activities', filter: DUE_THIS_WEEK, sortBy: 'due', limit: 6 },
            { id: 'industry', kind: 'chart', title: 'Accounts by industry', table: 'dash-accounts', chart: { kind: 'donut', x: 'industry' } },
            { id: 'top', kind: 'list', title: 'Biggest open deals', filter: OPEN, sortBy: 'value', sortDesc: true, limit: 6 },
          ],
        },
        { id: 'deals', name: 'All deals', type: 'grid', color: { mode: 'field', field: 'stage' }, state: { sorting: [{ id: 'value', desc: true }] } },
        { id: 'pipeline', name: 'Pipeline', type: 'kanban', stackBy: 'stage' },
      ],
      props: { rowAdder: { label: 'Add deal', defaults: () => ({ stage: 'Lead' }) } },
    },
    {
      id: 'dash-accounts',
      name: 'Accounts',
      primaryField: 'name',
      data: accounts,
      columns: [
        { id: 'name', field: 'name', header: 'Account', width: 220, fieldType: 'text' },
        { id: 'industry', field: 'industry', header: 'Industry', width: 150, fieldType: 'singleSelect', fieldProps: { options: INDUSTRIES } },
        { id: 'deals', field: 'deals', header: 'Deals', width: 280, fieldType: 'link', fieldProps: { table: 'dash-deals', multiple: true, inverse: 'account' } },
        { id: 'pipeline', field: 'pipeline', header: 'Deal value', width: 150, fieldType: 'rollup', fieldProps: { linkField: 'deals', field: 'value', aggregation: 'SUM' } },
        { id: 'employees', field: 'employees', header: 'Employees', width: 120, fieldType: 'number', fieldProps: { format: 'integer' } },
      ],
      views: [{ id: 'accounts', name: 'All accounts', type: 'grid' }],
      props: { rowAdder: { label: 'Add account' } },
    },
    {
      id: 'dash-activities',
      name: 'Activities',
      primaryField: 'subject',
      data: activities,
      columns: [
        { id: 'subject', field: 'subject', header: 'Activity', width: 300, fieldType: 'text' },
        { id: 'kind', field: 'kind', header: 'Type', width: 120, fieldType: 'singleSelect', fieldProps: { options: KINDS } },
        { id: 'owner', field: 'owner', header: 'Owner', width: 160, fieldType: 'user' },
        { id: 'due', field: 'due', header: 'Due', width: 150, fieldType: 'date', fieldProps: { dateFormat: 'friendly' } },
        { id: 'done', field: 'done', header: 'Done', width: 90, fieldType: 'checkbox' },
      ],
      views: [{ id: 'activities', name: 'All activities', type: 'grid', state: { sorting: [{ id: 'due', desc: false }] } }],
      props: { rowAdder: { label: 'Add activity', defaults: () => ({ done: false }) } },
    },
  ])
</script>

<section class="base-demo">
  <SvBase
    bind:tables
    editable
    sortable
    showRowNumbers
    expandRows
    rowNumberWidth={64}
    showRowSelection={false}
    rowHeight={40}
    containerHeight="100%"
    columnAdder
    fieldMenu
  />
</section>

<style>
  .base-demo { display: flex; flex-direction: column; flex: 1; min-height: 560px; height: 100%; }
</style>

View this example on GitHub

More Base examples

  • Project tracker - A project tracker, ready to use: Tasks and Milestones as two linked tables. Tasks have status, tags, assignee, dates, estimate, progress, budget and files; each milestone lists its tasks with their progress and budget. Views for all tasks, the board by status, due dates on a calendar, a gallery and a request form. Automations close finished tasks, start work from a button and flag overdue ones.
  • Content calendar - An editorial content calendar, ready to use: Posts and Campaigns as two linked tables. Posts have stage, channels, author, publish date, cover and brief. Views for the plan, the pipeline as a Kanban, the publishing calendar, a wall of covers and a pitch form.
  • Sales CRM - A sales CRM, ready to use: Deals, Accounts and Contacts as three linked tables. Link a deal to an account and the account lists it. Views for all deals, the pipeline as a Kanban, the closing calendar, open deals by owner and a new-lead form.
  • Product roadmap - A product roadmap, ready to use: Features and customer Feedback as two linked tables. Features have quarter, status, teams, lead, impact, effort and votes. Views for the roadmap grouped by quarter, a status board, a timeline, the work in progress now and a request form.
  • Hiring tracker - A hiring tracker, ready to use: Candidates and Roles as two linked tables. Candidates have photo, role, stage, recruiter, rating, interview date and resume; each role lists its candidates. Views for the pipeline as a Kanban, all candidates, a gallery of faces, interviews and an application form.