Component
Vue Data Grid with Filtering
<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.
Quick start
Use DomDataGrid now
Choose the installation path that suits this stage
Use npm for the fastest working version. Copy the source when you want DomDataGrid to become part of your codebase: yours to edit, adapt, and maintain.
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
1093 ms
Slow queries
37
Errors
10
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 | 14:15:56 | Billing | App API | DELETE | accounts | 1611 ms | 0 ms | 518 | Slow | Nested loop | /api/accounts | delete from accounts where organisation_id = ? and archived_at < now() - interval '90 days' |
| 2 | 14:15:47 | Read replica | Billing sync | UPDATE | sessions | 1587 ms | 0 ms | 1,052 | Slow | Index scan | stripe:update | update sessions set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 3 | 14:14:56 | Read replica | Worker | UPDATE | organisations | 1431 ms | 0 ms | 839 | Slow | Index scan | job:organisations:sync | update organisations set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 4 | 14:11:45 | Primary writer | Billing sync | INSERT | invoices | 1183 ms | 0 ms | 826 | Slow | Nested loop | stripe:insert | insert into invoices (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 5 | 14:16:39 | Analytics | Reporting | SELECT | accounts | 1164 ms | 600 ms | 219,063 | Slow | Seq scan | /reports/accounts | select * from accounts where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 6 | 14:10:44 | Read replica | Worker | SELECT | organisations | 1134 ms | 105 ms | 208,570 | Slow | Seq scan | job:organisations:sync | select * from organisations where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 7 | 14:16:03 | Read replica | Reporting | SELECT | jobs | 1098 ms | 0 ms | 668,897 | Slow | Seq scan | /reports/jobs | select * from jobs where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 8 | 14:15:10 | Read replica | Reporting | SELECT | content_items | 1093 ms | 201 ms | 210,665 | 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 | 14:15:28 | Billing | Billing sync | SELECT | accounts | 1092 ms | 31 ms | 47,090 | Slow | Seq scan | stripe:select | select * from accounts where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 10 | 14:16:19 | Primary writer | Reporting | SELECT | organisations | 1013 ms | 0 ms | 451,919 | Slow | Seq scan | /reports/organisations | select * from organisations where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 11 | 14:10:12 | Read replica | Reporting | INSERT | audit_events | 986 ms | 173 ms | 27 | Error | Index scan | /reports/audit_events | insert into audit_events (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 12 | 14:08:14 | Read replica | Webhook | SELECT | organisations | 820 ms | 0 ms | 357,379 | Slow | Seq scan | /webhooks/organisations | select * from organisations where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 13 | 14:09:04 | Primary writer | Reporting | INSERT | accounts | 801 ms | 290 ms | 771 | Slow | Nested loop | /reports/accounts | insert into accounts (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 14 | 14:16:44 | Analytics | Webhook | SELECT | audit_events | 799 ms | 0 ms | 291,587 | Slow | Seq scan | /webhooks/audit_events | select * from audit_events where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 15 | 14:09:20 | Billing | App API | SELECT | content_items | 759 ms | 0 ms | 173,111 | Slow | Seq scan | /api/content_items | select * from content_items where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 16 | 14:15:36 | Analytics | Reporting | DELETE | organisations | 712 ms | 0 ms | 1,121 | Slow | Nested loop | /reports/organisations | delete from organisations where organisation_id = ? and archived_at < now() - interval '90 days' |
| 17 | 14:16:52 | Billing | Webhook | SELECT | sessions | 707 ms | 131 ms | 22,808 | Slow | Nested loop | /webhooks/sessions | select * from sessions where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 18 | 14:12:37 | Billing | Worker | SELECT | organisations | 707 ms | 269 ms | 327,434 | Slow | Seq scan | job:organisations:sync | select * from organisations where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 19 | 14:13:29 | Billing | Reporting | INSERT | organisations | 699 ms | 0 ms | 391 | Slow | Index scan | /reports/organisations | insert into organisations (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 20 | 14:16:44 | Billing | App API | SELECT | sessions | 518 ms | 0 ms | 230,239 | Slow | Seq scan | /api/sessions | select * from sessions where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 21 | 14:17:00 | Analytics | Reporting | INSERT | audit_events | 467 ms | 0 ms | 715 | Slow | Nested loop | /reports/audit_events | insert into audit_events (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 22 | 14:12:25 | Read replica | App API | INSERT | feature_flags | 448 ms | 98 ms | 933 | Slow | Index scan | /api/feature_flags | insert into feature_flags (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 23 | 14:10:22 | Analytics | Billing sync | SELECT | content_items | 415 ms | 0 ms | 376,649 | Slow | Seq scan | stripe:select | select * from content_items where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 24 | 14:16:57 | Billing | Reporting | SELECT | invoices | 414 ms | 0 ms | 233,617 | Slow | Seq scan | /reports/invoices | select * from invoices where organisation_id = ? and lower(search_text) like ? order by updated_at desc limit 100 |
| 25 | 14:16:54 | Read replica | Webhook | INSERT | organisations | 317 ms | 0 ms | 1,063 | Slow | Index scan | /webhooks/organisations | insert into organisations (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 26 | 14:17:15 | Read replica | App API | DELETE | accounts | 307 ms | 0 ms | 681 | Slow | Index scan | /api/accounts | delete from accounts where organisation_id = ? and archived_at < now() - interval '90 days' |
| 27 | 14:12:07 | Read replica | Webhook | INSERT | audit_events | 289 ms | 0 ms | 28 | Slow | Nested loop | /webhooks/audit_events | insert into audit_events (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 28 | 14:14:37 | Primary writer | Webhook | UPDATE | content_items | 281 ms | 0 ms | 233 | Slow | Index scan | /webhooks/content_items | update content_items set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 29 | 14:09:47 | Primary writer | Reporting | INSERT | website_pages | 275 ms | 0 ms | 128 | Slow | Index scan | /reports/website_pages | insert into website_pages (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 30 | 14:15:24 | Primary writer | App API | UPDATE | billing_events | 273 ms | 0 ms | 251 | Slow | Nested loop | /api/billing_events | update billing_events set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 31 | 14:13:30 | Primary writer | Webhook | UPDATE | accounts | 270 ms | 102 ms | 1,133 | Slow | Nested loop | /webhooks/accounts | update accounts set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 32 | 14:13:05 | Analytics | Worker | UPDATE | website_pages | 266 ms | 106 ms | 345 | Slow | Index scan | job:website_pages:sync | update website_pages set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 33 | 14:12:43 | Analytics | App API | DELETE | jobs | 266 ms | 0 ms | 876 | Slow | Index scan | /api/jobs | delete from jobs where organisation_id = ? and archived_at < now() - interval '90 days' |
| 34 | 14:11:55 | Analytics | Webhook | UPDATE | billing_events | 264 ms | 0 ms | 1,151 | Slow | Index scan | /webhooks/billing_events | update billing_events set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 35 | 14:16:55 | Read replica | Worker | DELETE | users | 256 ms | 0 ms | 756 | Slow | Nested loop | job:users:sync | delete from users where organisation_id = ? and archived_at < now() - interval '90 days' |
| 36 | 14:15:58 | Billing | Reporting | INSERT | invoices | 256 ms | 135 ms | 1,070 | Slow | Nested loop | /reports/invoices | insert into invoices (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 37 | 14:16:35 | Analytics | Worker | UPDATE | organisations | 255 ms | 0 ms | 252 | Slow | Index scan | job:organisations:sync | update organisations set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 38 | 14:15:57 | Billing | Webhook | INSERT | invoices | 248 ms | 28 ms | 434 | OK | Index scan | /webhooks/invoices | insert into invoices (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 39 | 14:14:58 | Analytics | Billing sync | UPDATE | organisations | 246 ms | 0 ms | 183 | OK | Nested loop | stripe:update | update organisations set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 40 | 14:15:08 | Primary writer | Billing sync | UPDATE | content_items | 245 ms | 0 ms | 687 | OK | Index scan | stripe:update | update content_items set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 41 | 14:14:47 | Read replica | Reporting | UPDATE | users | 244 ms | 0 ms | 130 | Error | Nested loop | /reports/users | update users set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 42 | 14:17:08 | Primary writer | Worker | INSERT | content_items | 234 ms | 0 ms | 107 | OK | Index scan | job:content_items:sync | insert into content_items (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 43 | 14:17:08 | Billing | Reporting | UPDATE | jobs | 233 ms | 0 ms | 783 | OK | Index scan | /reports/jobs | update jobs set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 44 | 14:15:39 | Read replica | Reporting | UPDATE | users | 222 ms | 0 ms | 97 | OK | Index scan | /reports/users | update users set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 45 | 14:07:42 | Billing | Worker | DELETE | jobs | 221 ms | 102 ms | 1,071 | OK | Index scan | job:jobs:sync | delete from jobs where organisation_id = ? and archived_at < now() - interval '90 days' |
| 46 | 14:14:30 | Read replica | Worker | INSERT | billing_events | 219 ms | 0 ms | 966 | OK | Index scan | job:billing_events:sync | insert into billing_events (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 47 | 14:16:05 | Primary writer | Reporting | INSERT | content_items | 207 ms | 0 ms | 588 | OK | Nested loop | /reports/content_items | insert into content_items (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 48 | 14:13:29 | Billing | Webhook | UPDATE | users | 206 ms | 0 ms | 195 | OK | Index scan | /webhooks/users | update users set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 49 | 14:11:47 | Analytics | Webhook | UPDATE | invoices | 204 ms | 0 ms | 389 | OK | Index scan | /webhooks/invoices | update invoices set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 50 | 14:15:28 | Read replica | Billing sync | SELECT | billing_events | 190 ms | 0 ms | 6,391 | OK | Index scan | stripe:select | select id, organisation_id, status, updated_at from billing_events where organisation_id = ? and id = ? limit 1 |
| 51 | 14:14:08 | Primary writer | Webhook | SELECT | jobs | 190 ms | 0 ms | 6,684 | OK | Index scan | /webhooks/jobs | select id, organisation_id, status, updated_at from jobs where organisation_id = ? and id = ? limit 1 |
| 52 | 14:16:50 | Read replica | Reporting | SELECT | accounts | 189 ms | 0 ms | 5,871 | OK | Index scan | /reports/accounts | select id, organisation_id, status, updated_at from accounts where organisation_id = ? and id = ? limit 1 |
| 53 | 14:07:30 | Read replica | Worker | INSERT | accounts | 189 ms | 0 ms | 544 | OK | Nested loop | job:accounts:sync | insert into accounts (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 54 | 14:16:07 | Analytics | Worker | INSERT | sessions | 184 ms | 0 ms | 300 | OK | Index scan | job:sessions:sync | insert into sessions (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 55 | 14:13:46 | Billing | App API | SELECT | accounts | 184 ms | 0 ms | 5,551 | Error | Index scan | /api/accounts | select id, organisation_id, status, updated_at from accounts where organisation_id = ? and id = ? limit 1 |
| 56 | 14:14:45 | Primary writer | Reporting | SELECT | organisations | 180 ms | 0 ms | 1,813 | OK | Index scan | /reports/organisations | select id, organisation_id, status, updated_at from organisations where organisation_id = ? and id = ? limit 1 |
| 57 | 14:15:50 | Billing | Worker | SELECT | feature_flags | 174 ms | 0 ms | 6,814 | OK | Index scan | job:feature_flags:sync | select id, organisation_id, status, updated_at from feature_flags where organisation_id = ? and id = ? limit 1 |
| 58 | 14:12:26 | Billing | Worker | INSERT | feature_flags | 174 ms | 40 ms | 35 | OK | Index scan | job:feature_flags:sync | insert into feature_flags (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 59 | 14:13:58 | Billing | App API | SELECT | sessions | 168 ms | 0 ms | 4,040 | Error | Index scan | /api/sessions | select id, organisation_id, status, updated_at from sessions where organisation_id = ? and id = ? limit 1 |
| 60 | 14:14:36 | Billing | App API | SELECT | sessions | 166 ms | 0 ms | 3,951 | OK | Index scan | /api/sessions | select id, organisation_id, status, updated_at from sessions where organisation_id = ? and id = ? limit 1 |
| 61 | 14:16:45 | Billing | Worker | SELECT | invoices | 164 ms | 0 ms | 26 | OK | Index scan | job:invoices:sync | select id, organisation_id, status, updated_at from invoices where organisation_id = ? and id = ? limit 1 |
| 62 | 14:14:17 | Primary writer | Billing sync | SELECT | invoices | 163 ms | 0 ms | 3,514 | OK | Index scan | stripe:select | select id, organisation_id, status, updated_at from invoices where organisation_id = ? and id = ? limit 1 |
| 63 | 14:17:17 | Billing | App API | SELECT | website_pages | 158 ms | 0 ms | 8,089 | OK | Index scan | /api/website_pages | select id, organisation_id, status, updated_at from website_pages where organisation_id = ? and id = ? limit 1 |
| 64 | 14:15:07 | Billing | Webhook | SELECT | invoices | 156 ms | 0 ms | 4,286 | OK | Index scan | /webhooks/invoices | select id, organisation_id, status, updated_at from invoices where organisation_id = ? and id = ? limit 1 |
| 65 | 14:16:26 | Read replica | Webhook | SELECT | invoices | 151 ms | 0 ms | 5,048 | OK | Index scan | /webhooks/invoices | select id, organisation_id, status, updated_at from invoices where organisation_id = ? and id = ? limit 1 |
| 66 | 14:08:44 | Primary writer | Reporting | SELECT | audit_events | 144 ms | 0 ms | 8,726 | OK | Index scan | /reports/audit_events | select id, organisation_id, status, updated_at from audit_events where organisation_id = ? and id = ? limit 1 |
| 67 | 14:16:44 | Billing | Billing sync | SELECT | billing_events | 143 ms | 0 ms | 8,523 | OK | Index scan | stripe:select | select id, organisation_id, status, updated_at from billing_events where organisation_id = ? and id = ? limit 1 |
| 68 | 14:14:41 | Read replica | Billing sync | SELECT | accounts | 143 ms | 0 ms | 7,571 | OK | Index scan | stripe:select | select id, organisation_id, status, updated_at from accounts where organisation_id = ? and id = ? limit 1 |
| 69 | 14:14:37 | Primary writer | Reporting | SELECT | sessions | 141 ms | 0 ms | 64 | OK | Index scan | /reports/sessions | select id, organisation_id, status, updated_at from sessions where organisation_id = ? and id = ? limit 1 |
| 70 | 14:13:09 | Analytics | Webhook | SELECT | invoices | 141 ms | 0 ms | 4,486 | OK | Index scan | /webhooks/invoices | select id, organisation_id, status, updated_at from invoices where organisation_id = ? and id = ? limit 1 |
| 71 | 14:15:15 | Billing | App API | SELECT | accounts | 138 ms | 0 ms | 9,095 | OK | Index scan | /api/accounts | select id, organisation_id, status, updated_at from accounts where organisation_id = ? and id = ? limit 1 |
| 72 | 14:16:04 | Read replica | Reporting | SELECT | content_items | 136 ms | 0 ms | 8,754 | OK | Index scan | /reports/content_items | select id, organisation_id, status, updated_at from content_items where organisation_id = ? and id = ? limit 1 |
| 73 | 14:15:13 | Read replica | App API | SELECT | billing_events | 136 ms | 0 ms | 6,543 | OK | Index scan | /api/billing_events | select id, organisation_id, status, updated_at from billing_events where organisation_id = ? and id = ? limit 1 |
| 74 | 14:12:43 | Primary writer | Webhook | DELETE | feature_flags | 134 ms | 0 ms | 419 | OK | Index scan | /webhooks/feature_flags | delete from feature_flags where organisation_id = ? and archived_at < now() - interval '90 days' |
| 75 | 14:14:11 | Read replica | Billing sync | INSERT | sessions | 132 ms | 0 ms | 949 | Error | Nested loop | stripe:insert | insert into sessions (organisation_id, payload, created_at) values (?, ?, now()) returning id |
| 76 | 14:14:52 | Primary writer | Worker | DELETE | users | 131 ms | 0 ms | 198 | OK | Nested loop | job:users:sync | delete from users where organisation_id = ? and archived_at < now() - interval '90 days' |
| 77 | 14:15:54 | Billing | App API | UPDATE | feature_flags | 129 ms | 0 ms | 475 | OK | Nested loop | /api/feature_flags | update feature_flags set updated_at = now(), metadata = jsonb_set(metadata, ?, ?) where organisation_id = ? and id = ? |
| 78 | 14:14:25 | Primary writer | App API | SELECT | organisations | 129 ms | 0 ms | 6,383 | OK | Index scan | /api/organisations | select id, organisation_id, status, updated_at from organisations where organisation_id = ? and id = ? limit 1 |
| 79 | 14:09:32 | Analytics | Worker | SELECT | sessions | 127 ms | 0 ms | 8,987 | OK | Index scan | job:sessions:sync | select id, organisation_id, status, updated_at from sessions where organisation_id = ? and id = ? limit 1 |
| 80 | 14:11:59 | Read replica | Billing sync | SELECT | feature_flags | 124 ms | 0 ms | 5,913 | OK | Index scan | stripe:select | select id, organisation_id, status, updated_at from feature_flags 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": "14:17:17"
}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. |