@hachej/boring-data-explorer 0.1.41 → 0.1.42

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.
Files changed (2) hide show
  1. package/README.md +73 -217
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,271 +1,127 @@
1
1
  # @hachej/boring-data-explorer
2
2
 
3
- <div align="center">
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
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+ ## What it does
6
9
 
7
- </div>
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
- Searchable, faceted data tables for the workbench. The headless primitive that `data-catalog` and other explorer-style plugins build on top of.
16
+ ## Usage
10
17
 
11
- ```bash
12
- git clone https://github.com/hachej/boring-ui.git && cd boring-ui && pnpm install
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, useExplorerState } from "@hachej/boring-data-explorer/front"
41
- import type { ExplorerDataSource, SearchArgs, SearchResult } from "@hachej/boring-data-explorer/shared"
22
+ import { DataExplorer } from "@hachej/boring-data-explorer/front"
23
+ import type { ExplorerDataSource } from "@hachej/boring-data-explorer/shared"
42
24
 
43
- // 1. Implement the adapter against your backend
44
- const customersSource: ExplorerDataSource = {
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
- const state = useExplorerState({ source: customersSource, pageSize: 50 })
55
- return <DataExplorer state={state} />
34
+ return <DataExplorer adapter={customers} pageSize={50} />
56
35
  }
57
36
  ```
58
37
 
59
- The result is a sortable, filterable table — users can search, filter by facets, select rows, and drag rows into other panels.
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 Contract
50
+ ## Adapter contract
64
51
 
65
52
  ```ts
66
- // Explorer item shape rendered by the DataExplorer
67
- export type ExplorerItem = {
53
+ type ExplorerItem = {
68
54
  id: string
69
55
  title: string
70
- subtitle?: string // muted second line (truncates with title)
71
- group?: string // group key — matches a facet value for grouping
72
- leading?: Badge // leading mono chip (e.g. type code)
73
- trailing?: Badge[] // trailing chips for status flags
74
- meta?: string // right-aligned plain text (e.g. "1.2M")
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
- export type Badge = {
78
- code: string // 1-4 char mono code rendered as a chip
79
- tooltip?: string
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
- // Search arguments from the explorer to your adapter
83
- export type SearchArgs = {
71
+ type SearchArgs = {
84
72
  query: string
85
- filters: Record<string, string[]> // active facet filters
86
- group?: { key: string; value: string } // scope to single group (paging inside a group)
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
- // Search result your adapter returns
93
- export type SearchResult = {
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
- export type FacetsArgs = {
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
- ```tsx
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
- ## Installation
96
+ ## Package surfaces
142
97
 
143
- ```bash
144
- # From source (workspace-only — not published to npm)
145
- cd boring-ui/plugins/data-explorer && pnpm install && pnpm build
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
- ## Package Surfaces
151
-
152
- | Import | Environment | What You Get |
153
- |--------|-------------|--------------|
154
- | `@hachej/boring-data-explorer` | Browser | `<DataExplorer>`, `useExplorerState`, all types re-exported |
155
- | `@hachej/boring-data-explorer/front` | Browser | Same as top-level (explicit subpath) |
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
- - **[`@hachej/boring-data-catalog`](../data-catalog/README.md)** — configurable catalog tab listing multiple data sources
262
- - Any plugin that needs a faceted, searchable table inside a workbench panel
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
- *About Contributions:* Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via `gh` and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.
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.41",
3
+ "version": "0.1.42",
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.41"
44
+ "@hachej/boring-ui-kit": "0.1.42"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@testing-library/jest-dom": "^6.9.1",