Component
Data grid
<DomDataGrid>A dense spreadsheet-like grid for database records, with typed column filters, sorting, row selection, and a server-query friendly API. We talk about databases, rendering pipelines, virtual DOMs, and scalable app architecture. But really, they just want Excel.
Demo
Database-style account table
Column filters use typed controls: date columns open a date range, number columns use min/max, select columns use checklist filters, and applied filters are visible in both the header and toolbar.
Customer accounts
15 of 24 accounts
| A | B | C | D | E | F | G | H | I | J | K | L | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| # | Filter Account | Filter Status Values | Filter Plan Values | Filter Owner | Filter Seats | Filter ARR | Filter Health Values | Filter Region Values | Filter Renewal | Filter Active users | Filter Verified | Filter Updated | |
| 1 | Anchor Cloud | Active | Enterprise | Sofia Jensen | 264 | £310,000 | Strong | NA | 31 Jul 2026 | 240 | Yes | 11 Jun 2026 | |
| 2 | Orchid Health | Active | Enterprise | Laura Chen | 232 | £268,000 | Strong | NA | 22 Sept 2026 | 211 | Yes | 11 Jun 2026 | |
| 3 | Forge Robotics | Active | Enterprise | Priya Singh | 188 | £224,500 | Strong | APAC | 04 Sept 2026 | 169 | Yes | 10 Jun 2026 | |
| 4 | Mercury Media | Active | Enterprise | Caleb Stone | 176 | £201,300 | Strong | NA | 09 Jul 2026 | 151 | Yes | 09 Jun 2026 | |
| 5 | Northstar Analytics | Active | Enterprise | Amelia Hart | 142 | £184,000 | Strong | EMEA | 19 Aug 2026 | 118 | Yes | 10 Jun 2026 | |
| 6 | Signal Harbor | Active | Scale | Nora Ellis | 96 | £112,900 | Strong | EMEA | 02 Aug 2026 | 88 | Yes | 07 Jun 2026 | |
| 7 | Atlas Finance | Active | Scale | Ravi Patel | 84 | £96,500 | Watch | NA | 28 Jul 2026 | 62 | Yes | 08 Jun 2026 | |
| 8 | Sable Security | Active | Scale | Elena Rossi | 72 | £88,900 | Strong | EMEA | 29 Sept 2026 | 70 | Yes | 02 Jun 2026 | |
| 9 | Violet Manufacturing | Trial | Scale | Kai Foster | 58 | £65,400 | Watch | APAC | 24 Aug 2026 | 43 | No | 01 Jun 2026 | |
| 10 | Summit Grocery | Active | Growth | Maeve Walsh | 36 | £41,800 | Strong | NA | 16 Sept 2026 | 34 | Yes | 06 Jun 2026 | |
| 11 | Bluejay Travel | Trial | Growth | Tara Lewis | 22 | £24,800 | Watch | LATAM | 19 Jul 2026 | 17 | No | 04 Jun 2026 | |
| 12 | Brightwell Studio | Trial | Growth | Mina Okafor | 18 | £21,400 | Strong | EMEA | 14 Sept 2026 | 16 | No | 09 Jun 2026 | |
| 13 | Mosaic Civic | Active | Starter | George White | 16 | £15,800 | Strong | EMEA | 11 Sept 2026 | 15 | Yes | 10 Jun 2026 | |
| 14 | Kite Education | Trial | Starter | Owen Price | 14 | £11,800 | Strong | EMEA | 31 Aug 2026 | 13 | No | 06 Jun 2026 | |
| 15 | Pioneer Retail | Trial | Starter | Miles Brooks | 9 | £7,800 | Watch | NA | 12 Jul 2026 | 6 | No | 03 Jun 2026 |
Resource API shape
The grid can filter locally today, but the same query-change payload can be sent to a resource endpoint for SQL-backed search, where, order, and pagination later.
{
"search": "",
"filters": {
"status": {
"type": "select",
"values": [
"active",
"trial"
]
},
"renewalDate": {
"type": "date",
"start": "2026-07-01",
"end": "2026-09-30"
}
},
"sort": {
"key": "arr",
"direction": "desc"
}
}Large memory
Virtual rows for in-memory data
Use virtualRows when a large local array is already in memory. The grid keeps the full 10,000-row data set available for client-side search, filters, and sort, while the isolated virtual row renderer mounts only the visible rows plus the configured buffer.
Usage events
10000 of 10000 events
| A | B | C | D | E | F | G | H | |
|---|---|---|---|---|---|---|---|---|
| # | Filter Event | Filter Account | Filter Service Values | Filter Status Values | Filter Region Values | Filter Requests | Filter Spend | Filter Updated |
| 1 | 1 | Atlas Works 1 | Billing API | Healthy | UK | 2,500 | £150.00 | 01 Jan 2026 |
| 2 | 2 | Northstar Labs 1 | Delayed | EU | 2,597 | £187.00 | 02 Feb 2026 | |
| 3 | 3 | Pioneer Health 1 | Billing API | Queued | US | 2,694 | £224.00 | 03 Mar 2026 |
| 4 | 4 | Evergreen Finance 1 | Healthy | CA | 2,791 | £261.00 | 04 Apr 2026 | |
| 5 | 5 | Lunar Retail 1 | Billing API | Healthy | AU | 2,888 | £298.00 | 05 May 2026 |
| 6 | 6 | Cobalt Systems 1 | Failed | UK | 2,985 | £335.00 | 06 Jun 2026 | |
| 7 | 7 | Atlas Works 2 | Billing API | Healthy | EU | 3,082 | £372.00 | 07 Jan 2026 |
| 8 | 8 | Northstar Labs 2 | Delayed | US | 3,179 | £409.00 | 08 Feb 2026 | |
| 9 | 9 | Pioneer Health 2 | Billing API | Queued | CA | 3,276 | £446.00 | 09 Mar 2026 |
| 10 | 10 | Evergreen Finance 2 | Healthy | AU | 3,373 | £483.00 | 10 Apr 2026 | |
| 11 | 11 | Lunar Retail 2 | Billing API | Healthy | UK | 3,470 | £520.00 | 11 May 2026 |
| 12 | 12 | Cobalt Systems 2 | Failed | EU | 3,567 | £557.00 | 12 Jun 2026 | |
| 13 | 13 | Atlas Works 3 | Billing API | Healthy | US | 3,664 | £594.00 | 13 Jan 2026 |
| 14 | 14 | Northstar Labs 3 | Delayed | CA | 3,761 | £631.00 | 14 Feb 2026 | |
| 15 | 15 | Pioneer Health 3 | Billing API | Queued | AU | 3,858 | £668.00 | 15 Mar 2026 |
| 16 | 16 | Evergreen Finance 3 | Healthy | UK | 3,955 | £705.00 | 16 Apr 2026 | |
| 17 | 17 | Lunar Retail 3 | Billing API | Healthy | EU | 4,052 | £742.00 | 17 May 2026 |
| 18 | 18 | Cobalt Systems 3 | Failed | US | 4,149 | £779.00 | 18 Jun 2026 | |
| 19 | 19 | Atlas Works 4 | Billing API | Healthy | CA | 4,246 | £816.00 | 19 Jan 2026 |
| 20 | 20 | Northstar Labs 4 | Delayed | AU | 4,343 | £853.00 | 20 Feb 2026 | |
| 21 | 21 | Pioneer Health 4 | Billing API | Queued | UK | 4,440 | £890.00 | 21 Mar 2026 |
| 22 | 22 | Evergreen Finance 4 | Healthy | EU | 4,537 | £927.00 | 22 Apr 2026 | |
| 23 | 23 | Lunar Retail 4 | Billing API | Healthy | US | 4,634 | £964.00 | 23 May 2026 |
| 24 | 24 | Cobalt Systems 4 | Failed | CA | 4,731 | £1,001 | 24 Jun 2026 | |
| 25 | 25 | Atlas Works 5 | Billing API | Healthy | AU | 4,828 | £1,038 | 25 Jan 2026 |
| 26 | 26 | Northstar Labs 5 | Delayed | UK | 4,925 | £1,075 | 26 Feb 2026 | |
| 27 | 27 | Pioneer Health 5 | Billing API | Queued | EU | 5,022 | £1,112 | 27 Mar 2026 |
| 28 | 28 | Evergreen Finance 5 | Healthy | US | 5,119 | £1,149 | 28 Apr 2026 | |
| 29 | 29 | Lunar Retail 5 | Billing API | Healthy | CA | 5,216 | £1,186 | 01 May 2026 |
| 30 | 30 | Cobalt Systems 5 | Failed | AU | 5,313 | £1,223 | 02 Jun 2026 | |
| 31 | 31 | Atlas Works 6 | Billing API | Healthy | UK | 5,410 | £1,260 | 03 Jan 2026 |
| 32 | 32 | Northstar Labs 6 | Delayed | EU | 5,507 | £1,297 | 04 Feb 2026 | |
| 33 | 33 | Pioneer Health 6 | Billing API | Queued | US | 5,604 | £1,334 | 05 Mar 2026 |
| 34 | 34 | Evergreen Finance 6 | Healthy | CA | 5,701 | £1,371 | 06 Apr 2026 | |
| 35 | 35 | Lunar Retail 6 | Billing API | Healthy | AU | 5,798 | £1,408 | 07 May 2026 |
| 36 | 36 | Cobalt Systems 6 | Failed | UK | 5,895 | £1,445 | 08 Jun 2026 | |
| 37 | 37 | Atlas Works 7 | Billing API | Healthy | EU | 5,992 | £1,482 | 09 Jan 2026 |
| 38 | 38 | Northstar Labs 7 | Delayed | US | 6,089 | £1,519 | 10 Feb 2026 | |
| 39 | 39 | Pioneer Health 7 | Billing API | Queued | CA | 6,186 | £1,556 | 11 Mar 2026 |
| 40 | 40 | Evergreen Finance 7 | Healthy | AU | 6,283 | £1,593 | 12 Apr 2026 | |
| 41 | 41 | Lunar Retail 7 | Billing API | Healthy | UK | 6,380 | £1,630 | 13 May 2026 |
| 42 | 42 | Cobalt Systems 7 | Failed | EU | 6,477 | £1,667 | 14 Jun 2026 | |
| 43 | 43 | Atlas Works 8 | Billing API | Healthy | US | 6,574 | £1,704 | 15 Jan 2026 |
| 44 | 44 | Northstar Labs 8 | Delayed | CA | 6,671 | £1,741 | 16 Feb 2026 | |
| 45 | 45 | Pioneer Health 8 | Billing API | Queued | AU | 6,768 | £1,778 | 17 Mar 2026 |
| 46 | 46 | Evergreen Finance 8 | Healthy | UK | 6,865 | £1,815 | 18 Apr 2026 | |
| 47 | 47 | Lunar Retail 8 | Billing API | Healthy | EU | 6,962 | £1,852 | 19 May 2026 |
| 48 | 48 | Cobalt Systems 8 | Failed | US | 7,059 | £1,889 | 20 Jun 2026 | |
| 49 | 49 | Atlas Works 9 | Billing API | Healthy | CA | 7,156 | £1,926 | 21 Jan 2026 |
| 50 | 50 | Northstar Labs 9 | Delayed | AU | 7,253 | £1,963 | 22 Feb 2026 | |
| 51 | 51 | Pioneer Health 9 | Billing API | Queued | UK | 7,350 | £2,000 | 23 Mar 2026 |
Advanced sorting
Keyword table sort presets
Enable multiSort when header clicks should toggle ordered sort rules per column. Click Volume to add it, click Difficulty to add a second rule, and click a sorted header through asc, desc, and off.
Keyword opportunity sort
The selected preset sends ordered sort rules, so APIs can translate them to stable ORDER BY clauses.
Keyword research
10 of 10 keywords
| A | B | C | D | E | F | |
|---|---|---|---|---|---|---|
| # | Filter Keyword | Filter Intent Values | Filter Volume | Filter Difficulty | Filter Opportunity | Filter CPC |
| 1 | vue data grid | Commercial | 5,400 | 28 | 3,942 | $5.40 |
| 2 | javascript spreadsheet component | Comparison | 5,400 | 43 | 3,132 | $7.10 |
| 3 | editable table vue | Commercial | 2,900 | 21 | 2,320 | $4.80 |
| 4 | tailwind data table | Informational | 8,100 | 61 | 3,240 | $3.90 |
| 5 | server side data grid | Commercial | 3,600 | 35 | 2,376 | $8.20 |
| 6 | open source data grid vue | Comparison | 4,400 | 24 | 3,388 | $4.60 |
| 7 | keyword clustering tool | Transactional | 6,600 | 48 | 3,498 | $6.70 |
| 8 | saas dashboard table | Informational | 1,900 | 17 | 1,596 | $3.20 |
| 9 | best vue table library | Comparison | 5,400 | 31 | 3,780 | $5.90 |
| 10 | crm data grid | Commercial | 2,400 | 19 | 1,968 | $9.40 |
{
"search": "",
"sort": {
"key": "",
"direction": "",
"rules": []
}
}Remote data
Server pagination and forever scrolling
The same resource endpoint handles page-number navigation and cursor-based forever scrolling. The grid sends search, filters, and sort; pagination adds either page/pageSize or cursor/limit.
Remote customer resource
The grid asks for rows; the mock API applies search, filters, sort, and pagination.
Customers
0 of 0 customers from resource API
| A | B | C | D | E | F | G | ||
|---|---|---|---|---|---|---|---|---|
| # | Filter Customer | Filter Status Values | Filter Owner | Filter Region Values | Filter Seats | Filter ARR | Filter Updated | |
| No rows match the current filters. | ||||||||
Request
{
"resource": "customers",
"query": {
"search": "",
"filters": {},
"sort": {
"key": "updatedAt",
"direction": "desc"
}
},
"pagination": {
"strategy": "page",
"page": 1,
"pageSize": 8
}
}Response
{
"data": [],
"pageInfo": {
"strategy": "page",
"page": 1,
"pageSize": 8,
"pageCount": 1,
"total": 0,
"hasNextPage": false,
"hasPreviousPage": false,
"nextCursor": ""
},
"meta": {
"total": 0,
"returned": 0
}
}Live data
SQL query log with duration ordering
A live developer query-log surface that treats the grid as a server-backed resource: new query rows stream into the buffer, search and typed filters update the resource query, and duration_ms desc exposes the slowest statements first.
Buffered
140
P95 duration
901 ms
Slow queries
29
Errors
4
SQL log
80 of 140 queries from resource API
| # | Filter Database Values | Filter Service Values | Filter Verb Values | Filter Table Values | Filter Duration ms | Filter Lock wait | Filter Rows examined | Filter Status Values | Filter Plan Values | Filter Endpoint | Filter SQL statement | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 23:53:27 | Read replica | Billing sync | SELECT | audit_events | 1426 ms | 0 ms | 369,374 | Slow | Seq scan | stripe:select | select * from audit_events where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 2 | 23:59:34 | Analytics | Worker | SELECT | jobs | 1296 ms | 272 ms | 520,033 | Slow | Seq scan | job:jobs:sync | select * from jobs where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 3 | 23:55:53 | Read replica | Worker | SELECT | website_pages | 1214 ms | 656 ms | 716,665 | Slow | Seq scan | job:website_pages:sync | select * from website_pages where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 4 | 23:57:52 | Read replica | Reporting | UPDATE | sessions | 1211 ms | 0 ms | 72 | Timeout | Nested loop | /reports/sessions | update sessions set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 5 | 23:57:36 | Billing | Billing sync | SELECT | audit_events | 1170 ms | 0 ms | 376,671 | Slow | Seq scan | stripe:select | select * from audit_events where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 6 | 23:50:58 | Billing | Reporting | UPDATE | audit_events | 1140 ms | 0 ms | 74 | Slow | Nested loop | /reports/audit_events | update audit_events set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 7 | 00:00:22 | Billing | Worker | DELETE | organisations | 1070 ms | 0 ms | 968 | Slow | Index scan | job:organisations:sync | delete from organisations where organisation_id = ? and archived_at < now() - interval '90 days' |
| 8 | 23:59:47 | Billing | Reporting | SELECT | content_items | 901 ms | 0 ms | 15,790 | Slow | Seq scan | /reports/content_items | select * from content_items where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 9 | 23:58:02 | Primary writer | Webhook | UPDATE | users | 745 ms | 72 ms | 722 | Slow | Index scan | /webhooks/users | update users set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 10 | 23:57:53 | Billing | Webhook | SELECT | audit_events | 639 ms | 0 ms | 43,933 | Slow | Bitmap heap | /webhooks/audit_events | select * from audit_events where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 11 | 23:55:24 | Analytics | Worker | SELECT | audit_events | 546 ms | 0 ms | 713,223 | Slow | Seq scan | job:audit_events:sync | select * from audit_events where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 12 | 23:58:40 | Billing | Worker | INSERT | feature_flags | 528 ms | 168 ms | 54 | Slow | Nested loop | job:feature_flags:sync | insert into feature_flags (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 13 | 23:55:05 | Primary writer | App API | SELECT | audit_events | 401 ms | 0 ms | 533,966 | Slow | Seq scan | /api/audit_events | select * from audit_events where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 14 | 23:56:12 | Analytics | App API | DELETE | audit_events | 311 ms | 0 ms | 211 | Slow | Nested loop | /api/audit_events | delete from audit_events where organisation_id = ? and archived_at < now() - interval '90 days' |
| 15 | 23:55:18 | Primary writer | Worker | INSERT | feature_flags | 311 ms | 25 ms | 847 | Slow | Nested loop | job:feature_flags:sync | insert into feature_flags (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 16 | 23:59:37 | Billing | Worker | INSERT | website_pages | 300 ms | 0 ms | 695 | Slow | Nested loop | job:website_pages:sync | insert into website_pages (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 17 | 00:00:05 | Analytics | Webhook | UPDATE | audit_events | 299 ms | 23 ms | 762 | Slow | Index scan | /webhooks/audit_events | update audit_events set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 18 | 23:58:13 | Primary writer | Reporting | UPDATE | billing_events | 297 ms | 28 ms | 382 | Slow | Nested loop | /reports/billing_events | update billing_events set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 19 | 23:54:48 | Billing | Billing sync | INSERT | feature_flags | 295 ms | 0 ms | 237 | Slow | Index scan | stripe:insert | insert into feature_flags (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 20 | 23:54:56 | Primary writer | Reporting | INSERT | billing_events | 294 ms | 26 ms | 1,022 | Slow | Nested loop | /reports/billing_events | insert into billing_events (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 21 | 23:59:16 | Primary writer | Billing sync | UPDATE | billing_events | 286 ms | 0 ms | 738 | Slow | Index scan | stripe:update | update billing_events set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 22 | 00:00:18 | Primary writer | Webhook | UPDATE | audit_events | 284 ms | 0 ms | 193 | Slow | Index scan | /webhooks/audit_events | update audit_events set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 23 | 23:59:31 | Read replica | Webhook | UPDATE | users | 284 ms | 54 ms | 735 | Slow | Index scan | /webhooks/users | update users set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 24 | 23:55:45 | Primary writer | App API | UPDATE | website_pages | 279 ms | 117 ms | 366 | Slow | Index scan | /api/website_pages | update website_pages set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 25 | 23:57:24 | Primary writer | Reporting | INSERT | feature_flags | 274 ms | 93 ms | 73 | Slow | Index scan | /reports/feature_flags | insert into feature_flags (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 26 | 23:57:11 | Billing | App API | UPDATE | organisations | 271 ms | 68 ms | 1,077 | Slow | Index scan | /api/organisations | update organisations set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 27 | 23:53:44 | Billing | Reporting | INSERT | feature_flags | 257 ms | 0 ms | 1,141 | Slow | Index scan | /reports/feature_flags | insert into feature_flags (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 28 | 23:59:26 | Read replica | Webhook | INSERT | invoices | 252 ms | 137 ms | 507 | Slow | Index scan | /webhooks/invoices | insert into invoices (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 29 | 23:57:36 | Primary writer | App API | UPDATE | accounts | 251 ms | 90 ms | 225 | Slow | Nested loop | /api/accounts | update accounts set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 30 | 00:00:15 | Primary writer | App API | UPDATE | accounts | 246 ms | 0 ms | 222 | OK | Nested loop | /api/accounts | update accounts set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 31 | 23:58:20 | Primary writer | Worker | DELETE | billing_events | 243 ms | 0 ms | 254 | OK | Index scan | job:billing_events:sync | delete from billing_events where organisation_id = ? and archived_at < now() - interval '90 days' |
| 32 | 23:59:07 | Billing | Reporting | DELETE | users | 242 ms | 67 ms | 725 | OK | Index scan | /reports/users | delete from users where organisation_id = ? and archived_at < now() - interval '90 days' |
| 33 | 23:53:05 | Read replica | Webhook | UPDATE | website_pages | 232 ms | 0 ms | 206 | OK | Index scan | /webhooks/website_pages | update website_pages set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 34 | 23:59:03 | Analytics | Webhook | INSERT | organisations | 231 ms | 0 ms | 1,018 | OK | Index scan | /webhooks/organisations | insert into organisations (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 35 | 23:59:55 | Analytics | Webhook | UPDATE | audit_events | 220 ms | 109 ms | 339 | OK | Nested loop | /webhooks/audit_events | update audit_events set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 36 | 23:59:19 | Analytics | App API | INSERT | feature_flags | 219 ms | 0 ms | 122 | OK | Nested loop | /api/feature_flags | insert into feature_flags (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 37 | 23:57:46 | Primary writer | Webhook | UPDATE | sessions | 216 ms | 0 ms | 264 | OK | Index scan | /webhooks/sessions | update sessions set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 38 | 23:59:48 | Primary writer | Webhook | UPDATE | billing_events | 190 ms | 0 ms | 565 | OK | Index scan | /webhooks/billing_events | update billing_events set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 39 | 23:58:25 | Read replica | Webhook | UPDATE | content_items | 190 ms | 0 ms | 797 | OK | Index scan | /webhooks/content_items | update content_items set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 40 | 23:56:51 | Primary writer | App API | SELECT | accounts | 185 ms | 0 ms | 7,038 | OK | Index scan | /api/accounts | select id, organisation_id, status, updated_at from accounts where organisation_id = ? and id = ? limit 1 |
| 41 | 23:54:32 | Read replica | Worker | INSERT | users | 184 ms | 82 ms | 1,098 | OK | Index scan | job:users:sync | insert into users (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 42 | 00:00:07 | Analytics | Billing sync | INSERT | accounts | 183 ms | 0 ms | 751 | OK | Nested loop | stripe:insert | insert into accounts (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 43 | 00:00:04 | Analytics | Worker | SELECT | website_pages | 183 ms | 0 ms | 3,298 | OK | Index scan | job:website_pages:sync | select id, organisation_id, status, updated_at from website_pages where organisation_id = ? and id = ? limit 1 |
| 44 | 23:56:28 | Primary writer | Reporting | SELECT | jobs | 182 ms | 0 ms | 1,201 | OK | Index scan | /reports/jobs | select id, organisation_id, status, updated_at from jobs where organisation_id = ? and id = ? limit 1 |
| 45 | 23:58:10 | Read replica | Webhook | INSERT | accounts | 181 ms | 95 ms | 567 | OK | Nested loop | /webhooks/accounts | insert into accounts (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 46 | 23:55:39 | Read replica | Reporting | UPDATE | invoices | 175 ms | 0 ms | 120 | OK | Nested loop | /reports/invoices | update invoices set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 47 | 00:00:15 | Read replica | App API | SELECT | organisations | 174 ms | 0 ms | 7,247 | OK | Index scan | /api/organisations | select id, organisation_id, status, updated_at from organisations where organisation_id = ? and id = ? limit 1 |
| 48 | 23:52:55 | Billing | Webhook | SELECT | accounts | 168 ms | 0 ms | 4,366 | OK | Index scan | /webhooks/accounts | select id, organisation_id, status, updated_at from accounts where organisation_id = ? and id = ? limit 1 |
| 49 | 23:59:04 | Analytics | App API | SELECT | audit_events | 167 ms | 0 ms | 3,629 | OK | Index scan | /api/audit_events | select id, organisation_id, status, updated_at from audit_events where organisation_id = ? and id = ? limit 1 |
| 50 | 23:57:25 | Read replica | App API | SELECT | feature_flags | 166 ms | 0 ms | 397 | OK | Index scan | /api/feature_flags | select id, organisation_id, status, updated_at from feature_flags where organisation_id = ? and id = ? limit 1 |
| 51 | 23:55:02 | Read replica | Reporting | SELECT | billing_events | 162 ms | 0 ms | 773 | OK | Index scan | /reports/billing_events | select id, organisation_id, status, updated_at from billing_events where organisation_id = ? and id = ? limit 1 |
| 52 | 23:59:32 | Read replica | Webhook | SELECT | accounts | 158 ms | 0 ms | 623 | OK | Index scan | /webhooks/accounts | select id, organisation_id, status, updated_at from accounts where organisation_id = ? and id = ? limit 1 |
| 53 | 23:59:08 | Billing | App API | SELECT | feature_flags | 157 ms | 0 ms | 5,654 | OK | Index scan | /api/feature_flags | select id, organisation_id, status, updated_at from feature_flags where organisation_id = ? and id = ? limit 1 |
| 54 | 23:57:07 | Billing | Worker | SELECT | content_items | 157 ms | 0 ms | 1,634 | OK | Index scan | job:content_items:sync | select id, organisation_id, status, updated_at from content_items where organisation_id = ? and id = ? limit 1 |
| 55 | 23:59:55 | Analytics | Worker | SELECT | feature_flags | 156 ms | 0 ms | 6,912 | Error | Index scan | job:feature_flags:sync | select id, organisation_id, status, updated_at from feature_flags where organisation_id = ? and id = ? limit 1 |
| 56 | 23:59:09 | Primary writer | Billing sync | SELECT | feature_flags | 155 ms | 0 ms | 127 | OK | Index scan | stripe:select | select id, organisation_id, status, updated_at from feature_flags where organisation_id = ? and id = ? limit 1 |
| 57 | 23:58:37 | Analytics | App API | SELECT | billing_events | 155 ms | 0 ms | 8,298 | OK | Index scan | /api/billing_events | select id, organisation_id, status, updated_at from billing_events where organisation_id = ? and id = ? limit 1 |
| 58 | 23:54:14 | Billing | Reporting | SELECT | content_items | 154 ms | 0 ms | 609 | OK | Index scan | /reports/content_items | select id, organisation_id, status, updated_at from content_items where organisation_id = ? and id = ? limit 1 |
| 59 | 23:51:43 | Primary writer | Worker | SELECT | feature_flags | 154 ms | 0 ms | 2,281 | OK | Index scan | job:feature_flags:sync | select id, organisation_id, status, updated_at from feature_flags where organisation_id = ? and id = ? limit 1 |
| 60 | 23:58:03 | Billing | Reporting | SELECT | organisations | 153 ms | 0 ms | 8,471 | OK | Index scan | /reports/organisations | select id, organisation_id, status, updated_at from organisations where organisation_id = ? and id = ? limit 1 |
| 61 | 23:58:40 | Billing | Webhook | INSERT | feature_flags | 152 ms | 52 ms | 655 | OK | Nested loop | /webhooks/feature_flags | insert into feature_flags (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 62 | 23:52:20 | Read replica | App API | SELECT | billing_events | 152 ms | 0 ms | 6,811 | OK | Index scan | /api/billing_events | select id, organisation_id, status, updated_at from billing_events where organisation_id = ? and id = ? limit 1 |
| 63 | 23:59:56 | Billing | App API | SELECT | jobs | 151 ms | 0 ms | 3,310 | OK | Index scan | /api/jobs | select id, organisation_id, status, updated_at from jobs where organisation_id = ? and id = ? limit 1 |
| 64 | 23:55:22 | Analytics | Webhook | SELECT | feature_flags | 150 ms | 0 ms | 2,302 | OK | Index scan | /webhooks/feature_flags | select id, organisation_id, status, updated_at from feature_flags where organisation_id = ? and id = ? limit 1 |
| 65 | 23:56:35 | Read replica | Billing sync | SELECT | feature_flags | 147 ms | 0 ms | 3,082 | OK | Index scan | stripe:select | select id, organisation_id, status, updated_at from feature_flags where organisation_id = ? and id = ? limit 1 |
| 66 | 23:52:13 | Read replica | Reporting | INSERT | accounts | 146 ms | 8 ms | 309 | OK | Index scan | /reports/accounts | insert into accounts (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 67 | 23:59:31 | Primary writer | App API | SELECT | content_items | 142 ms | 0 ms | 5,958 | OK | Index scan | /api/content_items | select id, organisation_id, status, updated_at from content_items where organisation_id = ? and id = ? limit 1 |
| 68 | 23:58:00 | Analytics | Billing sync | SELECT | invoices | 141 ms | 0 ms | 3,117 | OK | Index scan | stripe:select | select id, organisation_id, status, updated_at from invoices where organisation_id = ? and id = ? limit 1 |
| 69 | 23:57:36 | Billing | Billing sync | UPDATE | sessions | 141 ms | 16 ms | 122 | OK | Index scan | stripe:update | update sessions set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 70 | 23:56:53 | Analytics | Reporting | SELECT | jobs | 138 ms | 0 ms | 9,245 | OK | Index scan | /reports/jobs | select id, organisation_id, status, updated_at from jobs where organisation_id = ? and id = ? limit 1 |
| 71 | 23:51:13 | Billing | Billing sync | SELECT | content_items | 136 ms | 0 ms | 2,634 | OK | Index scan | stripe:select | select id, organisation_id, status, updated_at from content_items where organisation_id = ? and id = ? limit 1 |
| 72 | 23:50:16 | Billing | Worker | SELECT | users | 136 ms | 0 ms | 4,448 | OK | Index scan | job:users:sync | select id, organisation_id, status, updated_at from users where organisation_id = ? and id = ? limit 1 |
| 73 | 23:54:11 | Read replica | App API | SELECT | billing_events | 135 ms | 0 ms | 3,479 | OK | Index scan | /api/billing_events | select id, organisation_id, status, updated_at from billing_events where organisation_id = ? and id = ? limit 1 |
| 74 | 23:59:32 | Billing | Reporting | SELECT | invoices | 128 ms | 0 ms | 5,888 | OK | Index scan | /reports/invoices | select id, organisation_id, status, updated_at from invoices where organisation_id = ? and id = ? limit 1 |
| 75 | 23:59:14 | Billing | App API | SELECT | sessions | 127 ms | 0 ms | 2,489 | OK | Index scan | /api/sessions | select id, organisation_id, status, updated_at from sessions where organisation_id = ? and id = ? limit 1 |
| 76 | 23:51:25 | Primary writer | Webhook | SELECT | accounts | 127 ms | 0 ms | 863 | OK | Index scan | /webhooks/accounts | select id, organisation_id, status, updated_at from accounts where organisation_id = ? and id = ? limit 1 |
| 77 | 23:55:20 | Read replica | Webhook | SELECT | website_pages | 126 ms | 0 ms | 6,152 | OK | Index scan | /webhooks/website_pages | select id, organisation_id, status, updated_at from website_pages where organisation_id = ? and id = ? limit 1 |
| 78 | 00:00:07 | Analytics | Billing sync | SELECT | content_items | 125 ms | 0 ms | 2,793 | OK | Index scan | stripe:select | select id, organisation_id, status, updated_at from content_items where organisation_id = ? and id = ? limit 1 |
| 79 | 00:00:02 | Read replica | Webhook | UPDATE | invoices | 125 ms | 0 ms | 737 | OK | Index scan | /webhooks/invoices | update invoices set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 80 | 23:59:12 | Read replica | App API | SELECT | users | 119 ms | 0 ms | 2,657 | OK | Index scan | /api/users | select id, organisation_id, status, updated_at from users where organisation_id = ? and id = ? limit 1 |
SQL generated from grid state
select id, started_at, database_name, service_name, operation, table_name, duration_ms, lock_wait_ms, rows_examined, status, plan, endpoint, statement from observability.query_log order by duration_ms DESC limit 80;
Request
{
"resource": "observability.query_log",
"query": {
"search": "",
"filters": {},
"sort": {
"key": "durationMs",
"direction": "desc",
"rules": [
{
"key": "durationMs",
"direction": "desc"
}
]
}
},
"pagination": {
"strategy": "live-window",
"limit": 80,
"buffered": 140
}
}Response window
{
"returned": 80,
"total": 140,
"buffered": 140,
"lastUpdatedAt": "00:00:22"
}Custom cells
Column components and row actions
Set column.component to a Vue component. The component receives row, column, value, formatted, rowIndex, and rowKey. Display-only columns such as actions still use a stable key, but do not need a matching row property.
Team members
6 of 6 users
Filter User | Filter Team | Filter Status Values | Filter Last active | |
|---|---|---|---|---|
| Product | Pending | 17 Jun 2026 | ||
| Engineering | Active | 16 Jun 2026 | ||
| Design | Active | 12 Jun 2026 | ||
| Support | Paused | 08 Jun 2026 | ||
| Operations | Pending | 06 Jun 2026 | ||
| Sales | Active | 04 Jun 2026 |
EditNameCell.vue
RowActionsCell.vue
Architecture
Resource query API
The component filters client-side by default so it works immediately with local rows. Set remote when the grid should render API results exactly as supplied and use query-change to request a SQL-backed resource. Keep pagination beside the grid query so the same endpoint can support numbered pages, previous/next controls, and cursor-fed scrolling.
Use virtualRows when rows are already held in memory but the DOM should stay small. It swaps the table body to a dedicated virtual row renderer, using virtualRowBuffer to keep extra rows mounted above and below the viewport.
For custom rendering, add component to a column definition. The column key is the stable column id; set field when the component should read a different row property, or omit it for display-only columns such as row actions.
Column definitions are the contract. A date column gets a date-range filter, numeric columns get min/max, boolean columns get a yes/no segmented control, and select columns can render known options from the resource schema. Set filterable: false on display-only or operational columns so they stay out of the filter UI and resource query.
For more complex ordering, pass sort.rules as an ordered array, or enable multiSort so header clicks build that array interactively. A keyword table can send searchVolume desc followed by difficulty asc, and a resource API can translate that directly to a multi-column ORDER BY.
Reference
Props
Control props
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
rows | array | Array<unknown> | [] | Rows to render. Each row should be a plain object from an API, store, or database result. |
columns | array | Array< | [] | Column definitions. Each type chooses its own sorting, formatting, and filter controls. |
rowKey | string | function | string | 'id' | Field name or function used to identify each row. |
filters | object | Record<string, unknown> | {} | Controlled filter map keyed by column key. |
sort | object | array | Array<unknown> | {"key":"","direction":""} | Controlled sort state as { key, direction }, or { rules } for multi-column and computed sorting. |
multiSort | boolean | boolean | false | When true, header clicks toggle each sortable column inside an ordered sort.rules array. |
search | string | string | '' | Global search text. |
selectedKeys | array | Array<unknown> | [] | Selected row keys. |
remote | boolean | boolean | false | When true, rows render as supplied and query-change can drive a server/API request. |
loading | boolean | boolean | false | Show a loading row while server data is being fetched. |
virtualRows | boolean | boolean | false | Render large in-memory row sets through the isolated virtual row renderer. |
virtualRowBuffer | number | number | 50 | Rows kept mounted above and below the visible viewport when virtualRows is true. |
virtualRowHeight | number | number | — | Fixed row height in pixels for virtual row calculations. Defaults to the grid density. |
selectable | boolean | boolean | true | Show row selection checkboxes. |
showRowNumbers | boolean | boolean | true | Show spreadsheet-style row numbers. |
showColumnLetters | boolean | boolean | true | Show spreadsheet-style column letters above headers. |
stickyHeader | boolean | boolean | true | Keep header rows visible while the grid scrolls. |
resizableColumns | boolean | boolean | true | Allow users to drag header dividers to resize columns. |
minColumnWidth | number | number | 40 | Minimum column width in pixels while resizing. The default leaves room for the filter icon. |
toolbar | boolean | boolean | true | Show the title, search box, filter chips, and row count toolbar. |
title | string | string | 'Data grid' | Toolbar title. |
resourceLabel | string | string | 'rows' | Human label for the resource being rendered. |
totalRows | number | number | — | Server-side total row count when remote is true. |
density | 'compact' | 'comfortable' | string | 'compact' | Row height density. |
height | string | number | string | '34rem' | Scrollable grid height as a CSS size or pixel number. |
emptyText | string | string | 'No rows match the current filters.' | Message shown when no rows are visible. |
Auto-generated from Data grid.props and inline _edit hints.
Events
| Name | Payload | Description |
|---|---|---|
| @update:filters | Record<string, DataGridFilter> | Fired when a column filter is applied or cleared. |
| @filter-change | ( | — |
| @update:sort | DataGridSort | Fired when column sorting changes. Complex sort values may include an ordered rules array. |
| @sort-change | ( | — |
| @update:search | string | Fired when the toolbar search changes. |
| @query-change | DataGridQuery | Fired with the normalized search, filters, and sort state. Use this to call a resource API. |
| @viewport-scroll | { scrollTop, scrollHeight, clientHeight, remaining, atEnd } | Fired when the scrollable row viewport scrolls. |
| @viewport-end | { scrollTop, scrollHeight, clientHeight, remaining, atEnd } | Fired when the scrollable row viewport reaches the bottom threshold. Useful for cursor pagination and infinite loading. |
| @cell-update | ({ row, column, rowIndex, rowKey, value, previousValue }) | Re-emitted when a column component emits update:value. |
| @cell-action | ({ row, column, rowIndex, rowKey, value, formatted, action, payload }) | Re-emitted when a column component emits action. |
| @column-resize | ({ column, key, width, previousWidth, startWidth }) | Fired while a header divider is dragged and a column width changes. |
| @column-resize-end | ({ column, key, width, previousWidth, startWidth }) | Fired when the user finishes resizing a column. |
| @render-window-change | ({ startIndex, endIndex, renderedRows, totalRows, firstVisibleIndex, lastVisibleIndex }) | Fired by the virtual row renderer when the mounted row window changes. |
| @update:selectedKeys | Array<string | number> | Fired when row selection changes. |
| @selection-change | ( | — |
| @row-click | ({ row, index }) | Fired when a data row is clicked. |
Names auto-detected from defineEmits and source emit() calls; payload and description from __doc.events when present.
Slots
| Name | Scope | Description |
|---|---|---|
| #cell | { row, column, value, formatted, rowIndex, rowKey } | Custom cell renderer. |
| #toolbar-actions | { query, clearFilters } | Extra controls rendered next to search and Clear filters. |
| #empty | — | Custom empty state. |