@hachej/boring-data-explorer 0.1.41 → 0.1.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -217
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,271 +1,127 @@
|
|
|
1
1
|
# @hachej/boring-data-explorer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Searchable, faceted data tables for the workbench. This is the headless
|
|
4
|
+
primitive that `@hachej/boring-data-catalog` and other explorer-style plugins
|
|
5
|
+
build on. It is **not** a workspace plugin itself — it ships a React component,
|
|
6
|
+
a hook, and an adapter contract.
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
## What it does
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
- Renders a `<DataExplorer>` table: search box, faceted filter popover, optional
|
|
11
|
+
group/tree mode, row activation, and drag-out payloads.
|
|
12
|
+
- Manages query / facet / paging / selection state via `useExplorerState`.
|
|
13
|
+
- Defines the `ExplorerDataSource` adapter contract: you implement
|
|
14
|
+
`search(args)` (and optionally `fetchFacets(args)`) against any backend.
|
|
8
15
|
|
|
9
|
-
|
|
16
|
+
## Usage
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
> **Note:** This plugin is workspace-private (`"private": true`) — install from source within the monorepo.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## TL;DR
|
|
20
|
-
|
|
21
|
-
**The Problem**: You have data — customers, invoices, logs, metrics — and you want users to search, filter, and explore it inside an agent app. But building faceted tables with virtualization, paging, and selection from scratch is tedious.
|
|
22
|
-
|
|
23
|
-
**The Solution**: `@hachej/boring-data-explorer` provides a controlled `<DataExplorer>` component + `useExplorerState` hook + an `ExplorerDataSource` adapter contract. You implement `search(args)` (and optionally `fetchFacets(args)`) against any backend. The component handles the rest.
|
|
24
|
-
|
|
25
|
-
### Why Use @hachej/boring-data-explorer?
|
|
26
|
-
|
|
27
|
-
| Feature | What It Does |
|
|
28
|
-
|---------|--------------|
|
|
29
|
-
| **`<DataExplorer>` component** | Search box, faceted filters, virtualized rows, row selection, drag-and-drop payload |
|
|
30
|
-
| **`useExplorerState` hook** | Manages query, facets, paging, and selection state |
|
|
31
|
-
| **Adapter contract** | `search(args)` + optional `fetchFacets(args)` — implement once, plug any backend (SQL, REST, in-memory, gRPC) |
|
|
32
|
-
| **Agent-driven** | Rows expose a `DragPayload`; the agent can open any row via surface resolver |
|
|
33
|
-
| **Headless + styled** | Use the hook alone for custom UI, or mount the full component out of the box |
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## Quick Example
|
|
18
|
+
`<DataExplorer>` is self-contained — it calls `useExplorerState` internally, so
|
|
19
|
+
you pass the **adapter** (not a state object):
|
|
38
20
|
|
|
39
21
|
```tsx
|
|
40
|
-
import { DataExplorer
|
|
41
|
-
import type { ExplorerDataSource
|
|
22
|
+
import { DataExplorer } from "@hachej/boring-data-explorer/front"
|
|
23
|
+
import type { ExplorerDataSource } from "@hachej/boring-data-explorer/shared"
|
|
42
24
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
async search({ query, filters, limit, offset, signal }): Promise<SearchResult> {
|
|
25
|
+
const customers: ExplorerDataSource = {
|
|
26
|
+
async search({ query, filters, limit, offset, signal }) {
|
|
46
27
|
const res = await fetch(`/api/customers?q=${query}&limit=${limit}&offset=${offset}`, { signal })
|
|
47
28
|
const { items, total } = await res.json()
|
|
48
29
|
return { items, total, hasMore: offset + items.length < total }
|
|
49
30
|
},
|
|
50
31
|
}
|
|
51
32
|
|
|
52
|
-
// 2. Mount the explorer
|
|
53
33
|
export function CustomersPane() {
|
|
54
|
-
|
|
55
|
-
return <DataExplorer state={state} />
|
|
34
|
+
return <DataExplorer adapter={customers} pageSize={50} />
|
|
56
35
|
}
|
|
57
36
|
```
|
|
58
37
|
|
|
59
|
-
|
|
38
|
+
Use `useExplorerState({ adapter, facets, groupBy, pageSize })` directly only
|
|
39
|
+
when you need to drive a fully custom UI.
|
|
40
|
+
|
|
41
|
+
### `<DataExplorer>` props
|
|
60
42
|
|
|
61
|
-
|
|
43
|
+
`adapter` (required), `facets`, `groupBy`, `onActivate(row)`,
|
|
44
|
+
`getDragPayload(row)`, `emptyState`, `searchPlaceholder`, `toolbarTitle`,
|
|
45
|
+
`toolbarIcon`, `searchable`, `query` + `onQueryChange` (controlled search),
|
|
46
|
+
`pageSize`, `debounceMs`, `className`. Facets render only when the adapter
|
|
47
|
+
implements `fetchFacets`. `groupBy` enables tree mode (the key must match a
|
|
48
|
+
facet key); an active query/filter forces flat mode.
|
|
62
49
|
|
|
63
|
-
## Adapter
|
|
50
|
+
## Adapter contract
|
|
64
51
|
|
|
65
52
|
```ts
|
|
66
|
-
|
|
67
|
-
export type ExplorerItem = {
|
|
53
|
+
type ExplorerItem = {
|
|
68
54
|
id: string
|
|
69
55
|
title: string
|
|
70
|
-
subtitle?: string
|
|
71
|
-
group?: string
|
|
72
|
-
leading?: Badge
|
|
73
|
-
trailing?: Badge[]
|
|
74
|
-
meta?: string
|
|
56
|
+
subtitle?: string // muted second line
|
|
57
|
+
group?: string // group key — must match a facet value
|
|
58
|
+
leading?: Badge // leading mono chip
|
|
59
|
+
trailing?: Badge[] // trailing status chips
|
|
60
|
+
meta?: string // right-aligned plain text (e.g. "1.2M")
|
|
75
61
|
}
|
|
62
|
+
type Badge = { code: string; tooltip?: string } // code = 1–4 char chip
|
|
76
63
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
64
|
+
type FacetConfig = {
|
|
65
|
+
key: string
|
|
66
|
+
label: string
|
|
67
|
+
order?: string[] // explicit display order
|
|
68
|
+
formatValue?: (value: string) => string
|
|
80
69
|
}
|
|
81
70
|
|
|
82
|
-
|
|
83
|
-
export type SearchArgs = {
|
|
71
|
+
type SearchArgs = {
|
|
84
72
|
query: string
|
|
85
|
-
filters: Record<string, string[]>
|
|
86
|
-
group?: { key: string; value: string }
|
|
73
|
+
filters: Record<string, string[]>
|
|
74
|
+
group?: { key: string; value: string } // set when paging inside a group
|
|
87
75
|
limit: number
|
|
88
76
|
offset: number
|
|
89
77
|
signal?: AbortSignal
|
|
90
78
|
}
|
|
79
|
+
type SearchResult = { items: ExplorerItem[]; total: number; hasMore: boolean }
|
|
91
80
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
items: ExplorerItem[]
|
|
95
|
-
total: number // total count for the current scope (query + filters + optional group)
|
|
96
|
-
hasMore: boolean // whether there are more pages
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Facets — optional. When omitted, the explorer renders flat (no facet popover)
|
|
100
|
-
export type Facets = Record<string, FacetValue[]>
|
|
101
|
-
export type FacetValue = { value: string; count: number }
|
|
81
|
+
type FacetsArgs = { filters: Record<string, string[]>; signal?: AbortSignal }
|
|
82
|
+
type Facets = Record<string, { value: string; count: number }[]>
|
|
102
83
|
|
|
103
|
-
|
|
104
|
-
filters: Record<string, string[]>
|
|
105
|
-
signal?: AbortSignal
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// The adapter contract
|
|
109
|
-
export interface ExplorerDataSource {
|
|
84
|
+
interface ExplorerDataSource {
|
|
110
85
|
search(args: SearchArgs): Promise<SearchResult>
|
|
111
86
|
fetchFacets?(args: FacetsArgs): Promise<Facets> // optional
|
|
112
87
|
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Implement against **any backend**. The component doesn't care whether data comes from SQL, REST, Elasticsearch, or a JSON file.
|
|
116
|
-
|
|
117
|
-
### Static Data Adapter
|
|
118
88
|
|
|
119
|
-
|
|
120
|
-
import type { ExplorerDataSource, ExplorerItem } from "@hachej/boring-data-explorer/shared"
|
|
121
|
-
|
|
122
|
-
const entries: ExplorerItem[] = [
|
|
123
|
-
{ id: "customers", title: "Customers", subtitle: "Customer records", leading: { code: "tbl" } },
|
|
124
|
-
{ id: "invoices", title: "Invoices", subtitle: "Invoice records", leading: { code: "tbl" }, group: "finance" },
|
|
125
|
-
]
|
|
126
|
-
|
|
127
|
-
const adapter: ExplorerDataSource = {
|
|
128
|
-
async search({ query, limit, offset }) {
|
|
129
|
-
const normalized = query.trim().toLowerCase()
|
|
130
|
-
const matched = normalized
|
|
131
|
-
? entries.filter((entry) => `${entry.title} ${entry.subtitle ?? ""}`.toLowerCase().includes(normalized))
|
|
132
|
-
: entries
|
|
133
|
-
const items = matched.slice(offset, offset + limit)
|
|
134
|
-
return { items, total: matched.length, hasMore: offset + items.length < matched.length }
|
|
135
|
-
},
|
|
136
|
-
}
|
|
89
|
+
type DragPayload = { mimeType: string; value: string }
|
|
137
90
|
```
|
|
138
91
|
|
|
139
|
-
|
|
92
|
+
The adapter is backend-agnostic — SQL, REST, in-memory, anything that can return
|
|
93
|
+
`ExplorerItem[]`. A static in-memory adapter just filters and slices an array
|
|
94
|
+
inside `search()`.
|
|
140
95
|
|
|
141
|
-
##
|
|
96
|
+
## Package surfaces
|
|
142
97
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
98
|
+
| Import | Exports |
|
|
99
|
+
|--------|---------|
|
|
100
|
+
| `@hachej/boring-data-explorer` / `/front` | `DataExplorer`, `useExplorerState`, all types |
|
|
101
|
+
| `@hachej/boring-data-explorer/shared` | contract types only (no React) |
|
|
102
|
+
| `@hachej/boring-data-explorer/testing` | `createMockSeriesAdapter`, `createMockTablesAdapter` |
|
|
147
103
|
|
|
148
|
-
|
|
104
|
+
## Notes
|
|
149
105
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
| `@hachej/boring-data-explorer/shared` | Any | `ExplorerDataSource`, `ExplorerItem`, `Facets`, `SearchArgs`, `DragPayload`, `Badge` — no React deps |
|
|
157
|
-
| `@hachej/boring-data-explorer/testing` | Browser | Test utilities and mock data sources |
|
|
158
|
-
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
## Architecture
|
|
162
|
-
|
|
163
|
-
```
|
|
164
|
-
┌─────────────────────────────┐
|
|
165
|
-
│ <DataExplorer> │
|
|
166
|
-
│ ┌───┬─────┬─────────────┐ │
|
|
167
|
-
│ │ 🔍 │ Facets │ Results │ │
|
|
168
|
-
│ └───┴─────┴─────────────┘ │
|
|
169
|
-
│ ┌───┐ ┌───────────────┐ │
|
|
170
|
-
│ │ ← │ │ 1..25 of 430 │ │
|
|
171
|
-
│ └───┘ └───────────────┘ │
|
|
172
|
-
└──────────────┬─────────────┘
|
|
173
|
-
│ source.search(args)
|
|
174
|
-
│ source.fetchFacets?(args)
|
|
175
|
-
┌──────────────▼─────────────┐
|
|
176
|
-
│ ExplorerDataSource │
|
|
177
|
-
│ (your adapter impl) │
|
|
178
|
-
│ │
|
|
179
|
-
│ search({ query, filters, │
|
|
180
|
-
│ limit, offset }) │
|
|
181
|
-
│ │
|
|
182
|
-
│ fetchFacets?({ filters }) │
|
|
183
|
-
└──────────────┬─────────────┘
|
|
184
|
-
│
|
|
185
|
-
┌──────────────▼─────────────┐
|
|
186
|
-
│ Any Backend │
|
|
187
|
-
│ PostgreSQL · REST API │
|
|
188
|
-
│ Elasticsearch · In-memory│
|
|
189
|
-
│ gRPC · whatever │
|
|
190
|
-
└────────────────────────────┘
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
---
|
|
194
|
-
|
|
195
|
-
## How @hachej/boring-data-explorer Compares
|
|
196
|
-
|
|
197
|
-
| Feature | @hachej/boring-data-explorer | AG Grid | TanStack Table | Build your own |
|
|
198
|
-
|---------|------------------------------|---------|----------------|----------------|
|
|
199
|
-
| Built-in search + facets | ✅ One hook | ⚠️ Filter only | ❌ DIY | ❌ |
|
|
200
|
-
| Backend adapter contract | ✅ `search` + `fetchFacets` | ❌ In-memory | ⚠️ Sorting/filtering | ❌ |
|
|
201
|
-
| Drag-and-drop payload | ✅ `DragPayload` type | ⚠️ Add-on | ❌ | ❌ |
|
|
202
|
-
| Agent integration | ✅ Surface resolver | ❌ | ❌ | ❌ |
|
|
203
|
-
| Virtualized rows | ✅ | ✅ | ⚠️ Manual | ❌ |
|
|
204
|
-
| Complexity | ✅ ~10 lines to mount | ❌ Heavy API | ⚠️ Complex API | ❌ Weeks |
|
|
205
|
-
|
|
206
|
-
**When to use @hachej/boring-data-explorer:**
|
|
207
|
-
- You want a fast, searchable table with facet filters in a boring-ui workbench
|
|
208
|
-
- You're building a data catalog plugin for an agent app
|
|
209
|
-
- You need drag-and-drop from table rows to other panels
|
|
210
|
-
|
|
211
|
-
**When it might not fit:**
|
|
212
|
-
- You need enterprise-grade data grids (use AG Grid or TanStack Table)
|
|
213
|
-
- You need editable cells, pivot tables, or group-by aggregations
|
|
214
|
-
- You're building outside of a boring-ui workspace (you'd need the layout shell)
|
|
215
|
-
|
|
216
|
-
---
|
|
217
|
-
|
|
218
|
-
## Troubleshooting
|
|
219
|
-
|
|
220
|
-
| Error | Cause | Fix |
|
|
221
|
-
|-------|-------|-----|
|
|
222
|
-
| No rows showing | `search()` returned empty items | Check your backend query — log the response |
|
|
223
|
-
| Facets empty | `fetchFacets()` not implemented | Add the optional `fetchFacets` method to your adapter |
|
|
224
|
-
| Infinite loading loop | `useExplorerState` deps changing every render | Memoize your `source` object or move it outside the component |
|
|
225
|
-
| Drag not working | Row has no `DragPayload` | Pass `getDragPayload` prop to `<DataExplorer>` |
|
|
226
|
-
| Wrong source selected | Adapter returns the wrong item ids or payload | Check your adapter's `search()` mapping |
|
|
227
|
-
|
|
228
|
-
---
|
|
229
|
-
|
|
230
|
-
## Limitations
|
|
231
|
-
|
|
232
|
-
- **Workspace-private** — `"private": true` in package.json. Not published to npm. Install from source within the monorepo.
|
|
233
|
-
- **Not an enterprise data grid** — No cell editing, pivot tables, or group-by. It's a search-and-filter primitive, not AG Grid.
|
|
234
|
-
- **No built-in data fetching** — You implement `search()` and optionally `fetchFacets()`. The adapter doesn't cache or debounce.
|
|
235
|
-
- **Requires boring-ui workspace chrome** — The component assumes it's mounted inside a workspace panel layout. Standalone use is possible but you lose the drag-to-panel behavior.
|
|
236
|
-
- **No column customization UI** — Column shape is derived from `ExplorerItem` (title / subtitle / leading / trailing / meta) — no arbitrary column projection.
|
|
237
|
-
|
|
238
|
-
---
|
|
239
|
-
|
|
240
|
-
## FAQ
|
|
241
|
-
|
|
242
|
-
**Q: What's the difference between `data-explorer` and `data-catalog`?**
|
|
243
|
-
A: `data-explorer` is the primitive — a single table you mount wherever. `data-catalog` is a higher-level plugin: it puts a sidebar tab listing data sources, and clicking a source opens the explorer table.
|
|
244
|
-
|
|
245
|
-
**Q: Can I use this without the boring-ui workspace?**
|
|
246
|
-
A: Yes. `<DataExplorer>` and `useExplorerState` are standalone React exports. You'll just lose the drag-to-panel integration since there's no workbench to receive the payload.
|
|
247
|
-
|
|
248
|
-
**Q: How do I customize the table columns?**
|
|
249
|
-
A: Columns are derived from `ExplorerItem` fields: `title` (primary), `subtitle` (secondary), `leading` (mono chip), `trailing` (status badges), `meta` (right-aligned text). There is no per-column projection API.
|
|
250
|
-
|
|
251
|
-
**Q: Does it support server-side pagination?**
|
|
252
|
-
A: Yes — `search()` receives `limit` and `offset`. Your backend can return a slice. `total` and `hasMore` drive the pager.
|
|
253
|
-
|
|
254
|
-
**Q: Can I use this for non-tabular data?**
|
|
255
|
-
A: Yes — map any data shape (GraphQL results, CSV rows, API responses) to `ExplorerItem[]`. The adapter is backend-agnostic.
|
|
256
|
-
|
|
257
|
-
---
|
|
106
|
+
- Not an enterprise grid: no cell editing, pivots, group-by aggregation, or
|
|
107
|
+
per-column projection. Columns are derived from `ExplorerItem` fields.
|
|
108
|
+
- No built-in caching/debounce of fetches beyond the explorer's own search
|
|
109
|
+
debounce — cache at the adapter layer if needed.
|
|
110
|
+
- Drag-to-panel only works inside a boring-ui workspace (which receives the
|
|
111
|
+
payload); standalone use still renders the table.
|
|
258
112
|
|
|
259
113
|
## Used by
|
|
260
114
|
|
|
261
|
-
-
|
|
262
|
-
|
|
115
|
+
- `@hachej/boring-data-catalog` — wraps this into a configurable catalog tab +
|
|
116
|
+
visualization panel + agent tool.
|
|
263
117
|
|
|
264
|
-
|
|
118
|
+
## Validation
|
|
265
119
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
120
|
+
```bash
|
|
121
|
+
pnpm --filter @hachej/boring-data-explorer typecheck
|
|
122
|
+
pnpm --filter @hachej/boring-data-explorer test
|
|
123
|
+
pnpm --filter @hachej/boring-data-explorer build
|
|
124
|
+
```
|
|
269
125
|
|
|
270
126
|
## License
|
|
271
127
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-data-explorer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.43",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"clsx": "^2.1.1",
|
|
42
42
|
"lucide-react": "^1.8.0",
|
|
43
43
|
"tailwind-merge": "^3.5.0",
|
|
44
|
-
"@hachej/boring-ui-kit": "0.1.
|
|
44
|
+
"@hachej/boring-ui-kit": "0.1.43"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@testing-library/jest-dom": "^6.9.1",
|