@hachej/boring-data-catalog 0.1.13
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 +298 -0
- package/dist/front/index.d.ts +105 -0
- package/dist/front/index.js +294 -0
- package/dist/server/index.d.ts +29 -0
- package/dist/server/index.js +127 -0
- package/dist/shared/index.d.ts +5 -0
- package/dist/shared/index.js +9 -0
- package/package.json +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# @hachej/boring-data-catalog
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
A configurable data catalog plugin for the workbench — left sidebar tab with a searchable, faceted list and a visualization panel to explore rows. Built on [`@hachej/boring-data-explorer`](../data-explorer/README.md).
|
|
10
|
+
|
|
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 a data source (customers, invoices, time series, whatever) and you want users to browse it from a sidebar tab, click rows to open detail visualizations, and let the agent open specific rows programmatically. But wiring up a catalog tab + explorer panel + surface resolver + agent tool is repetitive.
|
|
22
|
+
|
|
23
|
+
**The Solution**: `@hachej/boring-data-catalog` gives you a one-call plugin factory: pass in an `ExplorerDataSource` adapter and configure labels, facets, and behavior. It contributes a left tab, a visualization panel, a catalog, and a surface resolver — all wired up.
|
|
24
|
+
|
|
25
|
+
### Why Use @hachej/boring-data-catalog?
|
|
26
|
+
|
|
27
|
+
| Feature | What It Does |
|
|
28
|
+
|---------|--------------|
|
|
29
|
+
| **Left sidebar catalog tab** | Persistent sidebar listing rows from your adapter, with search and facet filters |
|
|
30
|
+
| **Visualization panel** | Click a row → opens a detail panel that also shows an explorer table |
|
|
31
|
+
| **Surface resolver** | Agent can open a specific row via `openSurface` with `DATA_CATALOG_ROW_SURFACE_KIND` |
|
|
32
|
+
| **Agent tool** | Server plugin ships a `search_catalog` tool the agent uses to find rows before opening them |
|
|
33
|
+
| **Pre-wired with data-explorer** | Search + facets + drag-out behavior comes for free |
|
|
34
|
+
| **Customizable** | Swap the visualization component, customize facets/groupBy/onSelect, or pick which contributions to include |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick Example
|
|
39
|
+
|
|
40
|
+
**Frontend (workbench):**
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { createDataCatalogPlugin } from "@hachej/boring-data-catalog/front"
|
|
44
|
+
import type { ExplorerDataSource } from "@hachej/boring-data-explorer/shared"
|
|
45
|
+
|
|
46
|
+
// Your data adapter
|
|
47
|
+
const adapter: ExplorerDataSource = {
|
|
48
|
+
async search({ query, filters, limit, offset }) {
|
|
49
|
+
// fetch from your backend
|
|
50
|
+
return { items: [...], total, hasMore: ... }
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// One plugin = left tab + visualization panel + catalog + surface resolver
|
|
55
|
+
const catalogPlugin = createDataCatalogPlugin({
|
|
56
|
+
id: "customers",
|
|
57
|
+
label: "Customers",
|
|
58
|
+
adapter,
|
|
59
|
+
facets: [
|
|
60
|
+
{ key: "industry", label: "Industry", formatValue: (v) => v },
|
|
61
|
+
{ key: "region", label: "Region", order: ["US", "EU", "APAC"], formatValue: (v) => v },
|
|
62
|
+
],
|
|
63
|
+
groupBy: "industry",
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Add `catalogPlugin` to your `WorkspaceProvider` plugins array.
|
|
68
|
+
|
|
69
|
+
**Server (agent runtime):**
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { createDataCatalogServerPlugin } from "@hachej/boring-data-catalog/server"
|
|
73
|
+
import { ExplorerDataSource } from "@hachej/boring-data-explorer/shared"
|
|
74
|
+
|
|
75
|
+
const catalogServerPlugin = createDataCatalogServerPlugin({
|
|
76
|
+
label: "Customers",
|
|
77
|
+
adapter, // same ExplorerDataSource as front
|
|
78
|
+
defaultLimit: 20,
|
|
79
|
+
maxLimit: 50,
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Add `catalogServerPlugin` to `createAgentApp` `plugins` array.
|
|
84
|
+
|
|
85
|
+
Now agents can search the catalog via the tool and open specific rows via the UI bridge `openSurface` command:
|
|
86
|
+
```
|
|
87
|
+
openSurface({ kind: DATA_CATALOG_ROW_SURFACE_KIND, target: row.id, meta: { catalogId, row } })
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## What It Contributes
|
|
93
|
+
|
|
94
|
+
One call to `createDataCatalogPlugin()` contributes four front registrations:
|
|
95
|
+
|
|
96
|
+
| Contribution | What | Toggle |
|
|
97
|
+
|--------|------|--------|
|
|
98
|
+
| **Left tab** | Sidebar tab with searchable, faceted table | `includeLeftTab` (default: true) |
|
|
99
|
+
| **Visualization panel** | Center panel for exploring a selected row's context | `includeVisualizationPanel` (default: true) |
|
|
100
|
+
| **Catalog** | Command-palette-searchable catalog entry | `includeCatalog` (default: true) |
|
|
101
|
+
| **Surface resolver** | Maps `openSurface` calls to panel open | `includeSurfaceResolver` (default: true if visualization panel is on) |
|
|
102
|
+
|
|
103
|
+
You can disable any of these with the `include*` flags. For example, a plugin that only contributes a left tab:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
createDataCatalogPlugin({
|
|
107
|
+
id: "metrics",
|
|
108
|
+
label: "Metrics",
|
|
109
|
+
adapter,
|
|
110
|
+
includeLeftTab: true,
|
|
111
|
+
includeVisualizationPanel: false,
|
|
112
|
+
includeCatalog: false,
|
|
113
|
+
includeSurfaceResolver: false,
|
|
114
|
+
})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Configuration
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
interface CreateDataCatalogPluginOptions {
|
|
123
|
+
pluginId?: string
|
|
124
|
+
id: string
|
|
125
|
+
label: string
|
|
126
|
+
adapter: ExplorerDataSource // required — your data source
|
|
127
|
+
facets?: FacetConfig[] // facet filter definitions
|
|
128
|
+
groupBy?: string // group key for the left tab rows
|
|
129
|
+
getDragPayload?: (row) => DragPayload | null
|
|
130
|
+
onSelect?: (row, context) => void // custom row click handler
|
|
131
|
+
emptyState?: ReactNode
|
|
132
|
+
searchPlaceholder?: string
|
|
133
|
+
pageSize?: number
|
|
134
|
+
debounceMs?: number
|
|
135
|
+
leftTabId?: string
|
|
136
|
+
leftTabTitle?: string
|
|
137
|
+
leftTabIcon?: IconType
|
|
138
|
+
catalogId?: string
|
|
139
|
+
catalogLabel?: string
|
|
140
|
+
visualizationPanelId?: string
|
|
141
|
+
visualizationTitle?: string
|
|
142
|
+
visualizationIcon?: IconType
|
|
143
|
+
visualizationComponent?: ComponentType<PaneProps<DataCatalogVisualizationParams>>
|
|
144
|
+
includeLeftTab?: boolean // default true
|
|
145
|
+
includeCatalog?: boolean // default true
|
|
146
|
+
includeVisualizationPanel?: boolean // default true
|
|
147
|
+
includeSurfaceResolver?: boolean // default true (if visualization panel on)
|
|
148
|
+
source?: "builtin" | "app" // panel attribution
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Architecture
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
┌───────────────────────────────────────┐
|
|
158
|
+
│ Workspace Left Sidebar │
|
|
159
|
+
│ ┌───────────────────────────────┐ │
|
|
160
|
+
│ │ 📊 Customers (left tab) │ │
|
|
161
|
+
│ │ ┌───────────────────────────┐ │ │
|
|
162
|
+
│ │ │ [Search: "acme..."] │ │ │
|
|
163
|
+
│ │ │ [Industry: Tech ▼] │ │ │
|
|
164
|
+
│ │ │ ───────────────────────── │ │ │
|
|
165
|
+
│ │ │ US: Acme Corp [T] ✓ │ │ │
|
|
166
|
+
│ │ │ EU: Acme GmbH [T] │ │ │
|
|
167
|
+
│ │ │ APAC: Acme KK [L] │ │ │
|
|
168
|
+
│ │ └─────────────────────────── │ │ │
|
|
169
|
+
│ └───────────────────────────────┘ │
|
|
170
|
+
└───────────────┬───────────────────────┘
|
|
171
|
+
│ click row → open panel
|
|
172
|
+
▼
|
|
173
|
+
┌───────────────────────────────────────┐
|
|
174
|
+
│ Center Panel (visualization) │
|
|
175
|
+
│ ┌───────────────────────────────┐ │
|
|
176
|
+
│ │ Acme Corp │ │
|
|
177
|
+
│ │ US: Acme Corp [T] │ │
|
|
178
|
+
│ │ ┌───────────────────────────┐│ │
|
|
179
|
+
│ │ │ [Search...] ││ │
|
|
180
|
+
│ │ │ [filtered by row context] ││ │
|
|
181
|
+
│ │ │ ...explorer table... ││ │
|
|
182
|
+
│ │ └───────────────────────────┘│ │
|
|
183
|
+
│ └───────────────────────────────┘ │
|
|
184
|
+
└───────────────────────────────────────┘
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The agent can open any row programmatically:
|
|
188
|
+
```
|
|
189
|
+
POST /api/v1/ui/commands
|
|
190
|
+
{ kind: "openSurface", params: {
|
|
191
|
+
surfaceKind: "data-catalog-row",
|
|
192
|
+
target: <row.id>,
|
|
193
|
+
meta: { catalogId: "customers", row: { title: "Acme Corp", ... } }
|
|
194
|
+
}}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Installation
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# From source (workspace-only — not published to npm)
|
|
203
|
+
cd boring-ui/plugins/data-catalog && pnpm install && pnpm build
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## How @hachej/boring-data-catalog Compares
|
|
209
|
+
|
|
210
|
+
| Feature | @hachej/boring-data-catalog | Custom sidebar + table | Embedded BI tool |
|
|
211
|
+
|---------|------------------------------|-----------------------|------------------|
|
|
212
|
+
| Catalog tab + panel | ✅ One plugin call | ❌ DIY each piece | ⚠️ Configuration-heavy |
|
|
213
|
+
| Agent tool + bridge | ✅ search + openSurface | ❌ DIY | ❌ |
|
|
214
|
+
| Wiring effort | ✅ ~10 lines | ❌ Hours | ❌ Days |
|
|
215
|
+
| Data source flexibility | ✅ Any backend via ExplorerDataSource | ⚠️ Custom per source | ⚠️ Vendor-defined |
|
|
216
|
+
| Workbench integration | ✅ Drag-to-panel, exec_ui | ⚠️ Manual | ❌ None |
|
|
217
|
+
| Customizable outputs | ✅ Toggle left-tab / panel / catalog / resolver | ❌ All or nothing | ❌ |
|
|
218
|
+
|
|
219
|
+
**When to use @hachej/boring-data-catalog:**
|
|
220
|
+
- You want a sidebar catalog tab that lets users browse your data
|
|
221
|
+
- You want the agent to search and open specific rows in panels
|
|
222
|
+
- You want a left-tab + visualization panel combo with minimal code
|
|
223
|
+
|
|
224
|
+
**When it might not fit:**
|
|
225
|
+
- You only need a standalone table (use `@hachej/boring-data-explorer` directly)
|
|
226
|
+
- You want a full BI dashboard with charting (embed a dedicated BI tool)
|
|
227
|
+
- You need real-time data streaming (not supported in v1)
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Package Surfaces
|
|
232
|
+
|
|
233
|
+
| Import | Environment | What You Get |
|
|
234
|
+
|--------|-------------|--------------|
|
|
235
|
+
| `@hachej/boring-data-catalog/front` | Browser | `createDataCatalogPlugin()`, types, hooks, surface resolver |
|
|
236
|
+
| `@hachej/boring-data-catalog/server` | Node | `createDataCatalogServerPlugin()` — agent tool + skill prompt |
|
|
237
|
+
| `@hachej/boring-data-catalog/shared` | Any | `DATA_CATALOG_PLUGIN_ID`, `DATA_CATALOG_ROW_SURFACE_KIND`, constants |
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Dependencies
|
|
242
|
+
|
|
243
|
+
| Package | Required | Why |
|
|
244
|
+
|---------|----------|-----|
|
|
245
|
+
| `@hachej/boring-data-explorer` | ✅ Yes | Core table component and `ExplorerDataSource` adapter |
|
|
246
|
+
| `@hachej/boring-workspace` | ✅ peerDependency | Plugin system and panel registry |
|
|
247
|
+
| `lucide-react` | ✅ Yes | Catalog icons (Database, BarChart3) |
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Troubleshooting
|
|
252
|
+
|
|
253
|
+
| Error | Cause | Fix |
|
|
254
|
+
|-------|-------|-----|
|
|
255
|
+
| Catalog tab doesn't appear | Front plugin not in workspace | Add `createDataCatalogPlugin()` to `WorkspaceProvider` plugins |
|
|
256
|
+
| Clicking a row does nothing | `adapter.search()` is broken or returns nothing | Test your adapter independently |
|
|
257
|
+
| Agent tool not found | Server plugin not registered | Add `createDataCatalogServerPlugin()` to agent app `plugins` |
|
|
258
|
+
| Agent can't open rows | Surface resolver disabled | Set `includeSurfaceResolver: true` (on by default) |
|
|
259
|
+
| Icons not rendering | Invalid lucide icon name | Check `leftTabIcon` / `visualizationIcon` against [lucide.dev](https://lucide.dev/icons/) |
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Limitations
|
|
264
|
+
|
|
265
|
+
- **Workspace-private** — `"private": true` in package.json. Not published to npm. Install from source within the monorepo.
|
|
266
|
+
- **Single data source per plugin** — Each call to `createDataCatalogPlugin()` wires to one `ExplorerDataSource`. For multiple catalogs, instantiate the plugin multiple times with different adapters and labels.
|
|
267
|
+
- **No charting or visualization** — The visualization panel shows another explorer table, not charts. For visualizations, provide a custom `visualizationComponent`.
|
|
268
|
+
- **No server-side caching** — Each search triggers a fresh `search()` call. Cache at your adapter level if needed.
|
|
269
|
+
- **Real-time data** — Not supported in v1. Search uses debounce and pagination but no live streaming.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## FAQ
|
|
274
|
+
|
|
275
|
+
**Q: How do I use multiple data sources?**
|
|
276
|
+
A: Call `createDataCatalogPlugin()` once per data source, each with a different `id`, `label`, and `adapter`. For a static source list, implement a small `ExplorerDataSource` whose `search()` filters and slices an in-memory `ExplorerItem[]`.
|
|
277
|
+
|
|
278
|
+
**Q: How does the agent open a specific row?**
|
|
279
|
+
A: The catalog registers a surface resolver. The agent uses the UI bridge `openSurface` command: `{ kind: DATA_CATALOG_ROW_SURFACE_KIND, target: row.id, meta: { catalogId, row } }`.
|
|
280
|
+
|
|
281
|
+
**Q: Can I customize the visualization panel?**
|
|
282
|
+
A: Yes. Pass a custom `visualizationComponent` to `createDataCatalogPlugin()`. It receives `PaneProps<DataCatalogVisualizationParams>` with the selected row and context.
|
|
283
|
+
|
|
284
|
+
**Q: What if my data source is a REST API, not a database?**
|
|
285
|
+
A: The `ExplorerDataSource` adapter is backend-agnostic. Implement `search()` (and optionally `fetchFacets()`) to hit your REST endpoint.
|
|
286
|
+
|
|
287
|
+
**Q: Can I disable outputs I don't need?**
|
|
288
|
+
A: Yes. Set `includeLeftTab`, `includeVisualizationPanel`, `includeCatalog`, or `includeSurfaceResolver` to `false`. A left-tab-only plugin is a perfectly valid output.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
*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.
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## License
|
|
297
|
+
|
|
298
|
+
MIT
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { BoringFrontSurfaceResolverRegistration, BoringFrontFactoryWithId } from '@hachej/boring-workspace/plugin';
|
|
2
|
+
import { ReactNode, ComponentType } from 'react';
|
|
3
|
+
import { ExplorerDataSource, FacetConfig, ExplorerItem, DragPayload } from '@hachej/boring-data-explorer/shared';
|
|
4
|
+
import { LeftTabParams, WorkspaceBridge, PanelConfig, PaneProps } from '@hachej/boring-workspace';
|
|
5
|
+
export { DATA_CATALOG_DEFAULT_TOOL_NAME, DATA_CATALOG_PLUGIN_ID, DATA_CATALOG_ROW_SURFACE_KIND } from '../shared/index.js';
|
|
6
|
+
export { DataExplorerProps } from '@hachej/boring-data-explorer/front';
|
|
7
|
+
|
|
8
|
+
interface DataCatalogVisualizationParams {
|
|
9
|
+
row?: ExplorerItem;
|
|
10
|
+
query?: string;
|
|
11
|
+
}
|
|
12
|
+
interface OpenDataCatalogVisualizationOptions {
|
|
13
|
+
catalogId: string;
|
|
14
|
+
surfaceKind?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
params?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
interface DataCatalogSelectContext {
|
|
19
|
+
params?: LeftTabParams;
|
|
20
|
+
bridge?: WorkspaceBridge;
|
|
21
|
+
}
|
|
22
|
+
interface CreateDataCatalogContributionsOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Base contribution id. Defaults catalog id to this value and left tab /
|
|
25
|
+
* visualization panel ids to derived names.
|
|
26
|
+
*/
|
|
27
|
+
id?: string;
|
|
28
|
+
label?: string;
|
|
29
|
+
adapter: ExplorerDataSource;
|
|
30
|
+
facets?: FacetConfig[];
|
|
31
|
+
groupBy?: string;
|
|
32
|
+
getDragPayload?: (row: ExplorerItem) => DragPayload | null | undefined;
|
|
33
|
+
onSelect?: (row: ExplorerItem, context: DataCatalogSelectContext) => void;
|
|
34
|
+
emptyState?: ReactNode;
|
|
35
|
+
searchPlaceholder?: string;
|
|
36
|
+
pageSize?: number;
|
|
37
|
+
debounceMs?: number;
|
|
38
|
+
leftTabId?: string;
|
|
39
|
+
leftTabTitle?: string;
|
|
40
|
+
leftTabIcon?: PanelConfig["icon"];
|
|
41
|
+
includeLeftTab?: boolean;
|
|
42
|
+
catalogId?: string;
|
|
43
|
+
catalogLabel?: string;
|
|
44
|
+
includeCatalog?: boolean;
|
|
45
|
+
visualizationPanelId?: string;
|
|
46
|
+
visualizationTitle?: string;
|
|
47
|
+
visualizationIcon?: PanelConfig["icon"];
|
|
48
|
+
visualizationComponent?: ComponentType<PaneProps<DataCatalogVisualizationParams>>;
|
|
49
|
+
includeVisualizationPanel?: boolean;
|
|
50
|
+
surfaceKind?: string;
|
|
51
|
+
surfaceResolverId?: string;
|
|
52
|
+
includeSurfaceResolver?: boolean;
|
|
53
|
+
source?: PanelConfig["source"];
|
|
54
|
+
}
|
|
55
|
+
interface CreateDataCatalogPluginOptions extends CreateDataCatalogContributionsOptions {
|
|
56
|
+
pluginId?: string;
|
|
57
|
+
}
|
|
58
|
+
interface DataCatalogResolvedQuery {
|
|
59
|
+
query?: string;
|
|
60
|
+
controlled: boolean;
|
|
61
|
+
}
|
|
62
|
+
interface DataCatalogVisualizationState extends DataCatalogResolvedQuery {
|
|
63
|
+
row?: ExplorerItem;
|
|
64
|
+
title: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
declare function dataCatalogPanelInstanceId(rowId: string, prefix?: string): string;
|
|
68
|
+
declare function openDataCatalogVisualization(row: ExplorerItem, options: OpenDataCatalogVisualizationOptions): void;
|
|
69
|
+
declare function createDataCatalogOpenHandler(options: OpenDataCatalogVisualizationOptions): (row: ExplorerItem) => void;
|
|
70
|
+
|
|
71
|
+
interface CreateDataCatalogSurfaceResolverOptions {
|
|
72
|
+
id: string;
|
|
73
|
+
catalogId: string;
|
|
74
|
+
visualizationPanelId: string;
|
|
75
|
+
visualizationTitle: string;
|
|
76
|
+
panelIdPrefix?: string;
|
|
77
|
+
surfaceKind?: string;
|
|
78
|
+
surfaceResolverId?: string;
|
|
79
|
+
source?: string;
|
|
80
|
+
}
|
|
81
|
+
declare function createDataCatalogSurfaceResolver(options: CreateDataCatalogSurfaceResolverOptions): BoringFrontSurfaceResolverRegistration;
|
|
82
|
+
|
|
83
|
+
declare function readDataCatalogRow(value: unknown): ExplorerItem | undefined;
|
|
84
|
+
declare function resolveDataCatalogQuery(params: LeftTabParams | DataCatalogVisualizationParams | undefined): string | undefined;
|
|
85
|
+
declare function resolveDataCatalogControlledQuery(params: LeftTabParams | DataCatalogVisualizationParams | undefined): DataCatalogResolvedQuery;
|
|
86
|
+
declare function resolveDataCatalogVisualizationState(params: DataCatalogVisualizationParams | undefined, fallbackTitle: string): DataCatalogVisualizationState;
|
|
87
|
+
declare function useDataCatalogQuery(params: LeftTabParams | DataCatalogVisualizationParams | undefined): DataCatalogResolvedQuery;
|
|
88
|
+
declare function useDataCatalogVisualizationState(params: DataCatalogVisualizationParams | undefined, fallbackTitle: string): DataCatalogVisualizationState;
|
|
89
|
+
declare function useDataCatalogOpenVisualization(options: OpenDataCatalogVisualizationOptions): (row: ExplorerItem) => void;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Builds a `BoringFrontFactoryWithId` for the data-catalog plugin.
|
|
93
|
+
* The factory captures `options` in a closure and registers the
|
|
94
|
+
* configured left tab, visualization panel, catalog entry, and
|
|
95
|
+
* surface resolver imperatively when the workspace calls it.
|
|
96
|
+
*
|
|
97
|
+
* Each contribution is opt-out via the `include*` flags so host apps
|
|
98
|
+
* can compose a subset (e.g. catalog-only without a visualization
|
|
99
|
+
* panel). The returned factory carries its `pluginId` + `pluginLabel`
|
|
100
|
+
* so it can be passed directly to `WorkspaceProvider.plugins` (the
|
|
101
|
+
* shell captures via the front factory API).
|
|
102
|
+
*/
|
|
103
|
+
declare function createDataCatalogPlugin(options: CreateDataCatalogPluginOptions): BoringFrontFactoryWithId;
|
|
104
|
+
|
|
105
|
+
export { type CreateDataCatalogPluginOptions, type CreateDataCatalogSurfaceResolverOptions, type DataCatalogResolvedQuery, type DataCatalogSelectContext, type DataCatalogVisualizationParams, type DataCatalogVisualizationState, type OpenDataCatalogVisualizationOptions, createDataCatalogOpenHandler, createDataCatalogPlugin, createDataCatalogSurfaceResolver, dataCatalogPanelInstanceId, openDataCatalogVisualization, readDataCatalogRow, resolveDataCatalogControlledQuery, resolveDataCatalogQuery, resolveDataCatalogVisualizationState, useDataCatalogOpenVisualization, useDataCatalogQuery, useDataCatalogVisualizationState };
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/front/index.tsx
|
|
4
|
+
import { BarChart3, Database } from "lucide-react";
|
|
5
|
+
import { PanelChrome } from "@hachej/boring-workspace";
|
|
6
|
+
import {
|
|
7
|
+
definePlugin
|
|
8
|
+
} from "@hachej/boring-workspace/plugin";
|
|
9
|
+
import { DataExplorer } from "@hachej/boring-data-explorer/front";
|
|
10
|
+
|
|
11
|
+
// src/shared/constants.ts
|
|
12
|
+
var DATA_CATALOG_PLUGIN_ID = "data-catalog";
|
|
13
|
+
var DATA_CATALOG_DEFAULT_TOOL_NAME = "query_data_catalog";
|
|
14
|
+
var DATA_CATALOG_ROW_SURFACE_KIND = "data-catalog.open-row";
|
|
15
|
+
|
|
16
|
+
// src/front/openVisualization.ts
|
|
17
|
+
import { postUiCommand } from "@hachej/boring-workspace";
|
|
18
|
+
function stableHash(input) {
|
|
19
|
+
let hash = 2166136261;
|
|
20
|
+
for (let i = 0; i < input.length; i++) {
|
|
21
|
+
hash ^= input.charCodeAt(i);
|
|
22
|
+
hash = Math.imul(hash, 16777619);
|
|
23
|
+
}
|
|
24
|
+
return (hash >>> 0).toString(36);
|
|
25
|
+
}
|
|
26
|
+
function slugForPanelId(input) {
|
|
27
|
+
const slug = input.trim().replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
|
|
28
|
+
return slug || "row";
|
|
29
|
+
}
|
|
30
|
+
function dataCatalogPanelInstanceId(rowId, prefix = "data") {
|
|
31
|
+
const hash = stableHash(rowId);
|
|
32
|
+
const slug = slugForPanelId(rowId);
|
|
33
|
+
const safePrefix = slugForPanelId(prefix);
|
|
34
|
+
const suffix = `${slug}-${hash}`;
|
|
35
|
+
const prefixBudget = Math.max(0, 64 - suffix.length - 1);
|
|
36
|
+
const trimmedPrefix = safePrefix.slice(0, prefixBudget);
|
|
37
|
+
return trimmedPrefix ? `${trimmedPrefix}-${suffix}` : suffix.slice(0, 64);
|
|
38
|
+
}
|
|
39
|
+
function openDataCatalogVisualization(row, options) {
|
|
40
|
+
const meta = {
|
|
41
|
+
...options.params ?? {},
|
|
42
|
+
// Keep routing keys authoritative even when callers pass extra params.
|
|
43
|
+
row,
|
|
44
|
+
catalogId: options.catalogId,
|
|
45
|
+
...options.title ? { title: options.title } : {}
|
|
46
|
+
};
|
|
47
|
+
postUiCommand({
|
|
48
|
+
kind: "openSurface",
|
|
49
|
+
params: {
|
|
50
|
+
kind: options.surfaceKind ?? DATA_CATALOG_ROW_SURFACE_KIND,
|
|
51
|
+
target: row.id,
|
|
52
|
+
meta
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function createDataCatalogOpenHandler(options) {
|
|
57
|
+
return (row) => openDataCatalogVisualization(row, options);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/front/surfaceResolver.ts
|
|
61
|
+
function isExplorerItem(value) {
|
|
62
|
+
if (!value || typeof value !== "object") return false;
|
|
63
|
+
const row = value;
|
|
64
|
+
return typeof row.id === "string" && typeof row.title === "string";
|
|
65
|
+
}
|
|
66
|
+
function stringMeta(meta, key) {
|
|
67
|
+
const value = meta[key];
|
|
68
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
69
|
+
}
|
|
70
|
+
function createDataCatalogSurfaceResolver(options) {
|
|
71
|
+
const kind = options.surfaceKind ?? DATA_CATALOG_ROW_SURFACE_KIND;
|
|
72
|
+
const panelIdPrefix = options.panelIdPrefix ?? options.id;
|
|
73
|
+
return {
|
|
74
|
+
id: options.surfaceResolverId ?? `${options.id}-row`,
|
|
75
|
+
kind,
|
|
76
|
+
source: options.source,
|
|
77
|
+
resolve(request) {
|
|
78
|
+
if (request.kind !== kind) return void 0;
|
|
79
|
+
const meta = request.meta ?? {};
|
|
80
|
+
const catalogId = stringMeta(meta, "catalogId");
|
|
81
|
+
if (catalogId && catalogId !== options.catalogId) return void 0;
|
|
82
|
+
const row = isExplorerItem(meta.row) ? meta.row : void 0;
|
|
83
|
+
const {
|
|
84
|
+
catalogId: _catalogId,
|
|
85
|
+
row: _row,
|
|
86
|
+
title: _title,
|
|
87
|
+
...extraParams
|
|
88
|
+
} = meta;
|
|
89
|
+
return {
|
|
90
|
+
id: dataCatalogPanelInstanceId(request.target, panelIdPrefix),
|
|
91
|
+
component: options.visualizationPanelId,
|
|
92
|
+
title: stringMeta(meta, "title") ?? row?.title ?? options.visualizationTitle,
|
|
93
|
+
params: {
|
|
94
|
+
...extraParams,
|
|
95
|
+
...row ? { row } : { query: request.target }
|
|
96
|
+
},
|
|
97
|
+
score: 0
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/front/hooks.ts
|
|
104
|
+
import { useCallback, useMemo } from "react";
|
|
105
|
+
function readDataCatalogRow(value) {
|
|
106
|
+
if (!value || typeof value !== "object") return void 0;
|
|
107
|
+
const row = value;
|
|
108
|
+
if (typeof row.id !== "string" || typeof row.title !== "string") return void 0;
|
|
109
|
+
return row;
|
|
110
|
+
}
|
|
111
|
+
function resolveDataCatalogQuery(params) {
|
|
112
|
+
if (!params) return void 0;
|
|
113
|
+
if ("searchQuery" in params && typeof params.searchQuery === "string") {
|
|
114
|
+
return params.searchQuery;
|
|
115
|
+
}
|
|
116
|
+
if ("query" in params && typeof params.query === "string") return params.query;
|
|
117
|
+
return void 0;
|
|
118
|
+
}
|
|
119
|
+
function resolveDataCatalogControlledQuery(params) {
|
|
120
|
+
const query = resolveDataCatalogQuery(params);
|
|
121
|
+
return { query, controlled: query !== void 0 };
|
|
122
|
+
}
|
|
123
|
+
function resolveDataCatalogVisualizationState(params, fallbackTitle) {
|
|
124
|
+
const row = readDataCatalogRow(params?.row);
|
|
125
|
+
const query = resolveDataCatalogQuery(params) ?? row?.id;
|
|
126
|
+
return {
|
|
127
|
+
row,
|
|
128
|
+
query,
|
|
129
|
+
controlled: query !== void 0,
|
|
130
|
+
title: row?.title ?? fallbackTitle
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function useDataCatalogQuery(params) {
|
|
134
|
+
return useMemo(() => resolveDataCatalogControlledQuery(params), [params]);
|
|
135
|
+
}
|
|
136
|
+
function useDataCatalogVisualizationState(params, fallbackTitle) {
|
|
137
|
+
return useMemo(
|
|
138
|
+
() => resolveDataCatalogVisualizationState(params, fallbackTitle),
|
|
139
|
+
[fallbackTitle, params]
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
function useDataCatalogOpenVisualization(options) {
|
|
143
|
+
const { catalogId, surfaceKind, title, params } = options;
|
|
144
|
+
return useCallback(
|
|
145
|
+
(row) => openDataCatalogVisualization(row, { catalogId, surfaceKind, title, params }),
|
|
146
|
+
[catalogId, surfaceKind, title, params]
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/front/index.tsx
|
|
151
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
152
|
+
function createDataCatalogPlugin(options) {
|
|
153
|
+
const id = options.id ?? DATA_CATALOG_PLUGIN_ID;
|
|
154
|
+
const label = options.label ?? "Data";
|
|
155
|
+
const catalogId = options.catalogId ?? id;
|
|
156
|
+
const catalogLabel = options.catalogLabel ?? label;
|
|
157
|
+
const leftTabId = options.leftTabId ?? `${id}-tab`;
|
|
158
|
+
const leftTabTitle = options.leftTabTitle ?? label;
|
|
159
|
+
const visualizationPanelId = options.visualizationPanelId ?? `${id}-visualization`;
|
|
160
|
+
const visualizationTitle = options.visualizationTitle ?? `${label} View`;
|
|
161
|
+
const surfaceKind = options.surfaceKind ?? DATA_CATALOG_ROW_SURFACE_KIND;
|
|
162
|
+
const source = options.source ?? "app";
|
|
163
|
+
const includeVisualizationPanel = options.includeVisualizationPanel ?? true;
|
|
164
|
+
const includeLeftTab = options.includeLeftTab ?? true;
|
|
165
|
+
const includeCatalog = options.includeCatalog ?? true;
|
|
166
|
+
const includeSurfaceResolver = options.includeSurfaceResolver ?? (includeVisualizationPanel && !options.onSelect);
|
|
167
|
+
const emptyState = options.emptyState ?? "No data found";
|
|
168
|
+
const searchPlaceholder = options.searchPlaceholder ?? `Search ${label.toLowerCase()}...`;
|
|
169
|
+
const onSelect = options.onSelect ?? (includeVisualizationPanel ? createDataCatalogOpenHandler({ catalogId, surfaceKind }) : () => {
|
|
170
|
+
});
|
|
171
|
+
function DataCatalogLeftTab({ params, className }) {
|
|
172
|
+
const { query, controlled } = useDataCatalogQuery(params);
|
|
173
|
+
const bridge = params?.bridge;
|
|
174
|
+
const handleSelect = (row) => onSelect(row, { params, bridge });
|
|
175
|
+
return /* @__PURE__ */ jsx(
|
|
176
|
+
DataExplorer,
|
|
177
|
+
{
|
|
178
|
+
adapter: options.adapter,
|
|
179
|
+
facets: options.facets,
|
|
180
|
+
groupBy: options.groupBy,
|
|
181
|
+
onActivate: handleSelect,
|
|
182
|
+
getDragPayload: options.getDragPayload,
|
|
183
|
+
emptyState,
|
|
184
|
+
searchPlaceholder,
|
|
185
|
+
query: controlled ? query : void 0,
|
|
186
|
+
searchable: !controlled,
|
|
187
|
+
pageSize: options.pageSize,
|
|
188
|
+
debounceMs: options.debounceMs,
|
|
189
|
+
className: className ?? "h-full"
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
function DefaultVisualizationPanel({
|
|
194
|
+
params,
|
|
195
|
+
api: panelApi,
|
|
196
|
+
className
|
|
197
|
+
}) {
|
|
198
|
+
const { row, query, controlled, title } = useDataCatalogVisualizationState(
|
|
199
|
+
params,
|
|
200
|
+
visualizationTitle
|
|
201
|
+
);
|
|
202
|
+
const handleSelect = (nextRow) => onSelect(nextRow, { params });
|
|
203
|
+
return /* @__PURE__ */ jsxs(
|
|
204
|
+
PanelChrome,
|
|
205
|
+
{
|
|
206
|
+
title,
|
|
207
|
+
icon: options.visualizationIcon ?? BarChart3,
|
|
208
|
+
panelApi,
|
|
209
|
+
className,
|
|
210
|
+
children: [
|
|
211
|
+
row ? /* @__PURE__ */ jsxs("div", { className: "border-b border-border/60 px-3 py-2", children: [
|
|
212
|
+
/* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium text-foreground", children: row.title }),
|
|
213
|
+
/* @__PURE__ */ jsx("div", { className: "truncate text-xs text-muted-foreground", children: row.subtitle ?? row.id })
|
|
214
|
+
] }) : null,
|
|
215
|
+
/* @__PURE__ */ jsx(
|
|
216
|
+
DataExplorer,
|
|
217
|
+
{
|
|
218
|
+
adapter: options.adapter,
|
|
219
|
+
facets: options.facets,
|
|
220
|
+
groupBy: options.groupBy,
|
|
221
|
+
onActivate: handleSelect,
|
|
222
|
+
getDragPayload: options.getDragPayload,
|
|
223
|
+
emptyState,
|
|
224
|
+
searchPlaceholder,
|
|
225
|
+
query: controlled ? query : void 0,
|
|
226
|
+
searchable: !controlled,
|
|
227
|
+
pageSize: options.pageSize,
|
|
228
|
+
debounceMs: options.debounceMs,
|
|
229
|
+
className: "h-full"
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
]
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const leftTab = includeLeftTab ? {
|
|
237
|
+
id: leftTabId,
|
|
238
|
+
title: leftTabTitle,
|
|
239
|
+
icon: options.leftTabIcon ?? Database,
|
|
240
|
+
component: DataCatalogLeftTab,
|
|
241
|
+
source,
|
|
242
|
+
chromeless: true,
|
|
243
|
+
panelId: leftTabId
|
|
244
|
+
} : void 0;
|
|
245
|
+
const visualizationPanel = includeVisualizationPanel ? {
|
|
246
|
+
id: visualizationPanelId,
|
|
247
|
+
label: visualizationTitle,
|
|
248
|
+
icon: options.visualizationIcon ?? BarChart3,
|
|
249
|
+
component: options.visualizationComponent ?? DefaultVisualizationPanel,
|
|
250
|
+
placement: "center",
|
|
251
|
+
source
|
|
252
|
+
} : void 0;
|
|
253
|
+
const catalog = includeCatalog ? {
|
|
254
|
+
id: catalogId,
|
|
255
|
+
label: catalogLabel,
|
|
256
|
+
adapter: options.adapter,
|
|
257
|
+
onSelect: (row) => onSelect(row, {})
|
|
258
|
+
} : void 0;
|
|
259
|
+
const resolver = includeSurfaceResolver ? createDataCatalogSurfaceResolver({
|
|
260
|
+
id,
|
|
261
|
+
catalogId,
|
|
262
|
+
visualizationPanelId,
|
|
263
|
+
visualizationTitle,
|
|
264
|
+
panelIdPrefix: id,
|
|
265
|
+
surfaceKind,
|
|
266
|
+
surfaceResolverId: options.surfaceResolverId,
|
|
267
|
+
source
|
|
268
|
+
}) : void 0;
|
|
269
|
+
return definePlugin({
|
|
270
|
+
id,
|
|
271
|
+
label,
|
|
272
|
+
leftTabs: leftTab ? [leftTab] : [],
|
|
273
|
+
panels: visualizationPanel ? [visualizationPanel] : [],
|
|
274
|
+
catalogs: catalog ? [catalog] : [],
|
|
275
|
+
surfaceResolvers: resolver ? [resolver] : []
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
export {
|
|
279
|
+
DATA_CATALOG_DEFAULT_TOOL_NAME,
|
|
280
|
+
DATA_CATALOG_PLUGIN_ID,
|
|
281
|
+
DATA_CATALOG_ROW_SURFACE_KIND,
|
|
282
|
+
createDataCatalogOpenHandler,
|
|
283
|
+
createDataCatalogPlugin,
|
|
284
|
+
createDataCatalogSurfaceResolver,
|
|
285
|
+
dataCatalogPanelInstanceId,
|
|
286
|
+
openDataCatalogVisualization,
|
|
287
|
+
readDataCatalogRow,
|
|
288
|
+
resolveDataCatalogControlledQuery,
|
|
289
|
+
resolveDataCatalogQuery,
|
|
290
|
+
resolveDataCatalogVisualizationState,
|
|
291
|
+
useDataCatalogOpenVisualization,
|
|
292
|
+
useDataCatalogQuery,
|
|
293
|
+
useDataCatalogVisualizationState
|
|
294
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ExplorerDataSource, SearchResult } from '@hachej/boring-data-explorer/shared';
|
|
2
|
+
import { WorkspaceServerPlugin } from '@hachej/boring-workspace/server';
|
|
3
|
+
import { AgentTool } from '@hachej/boring-workspace';
|
|
4
|
+
|
|
5
|
+
interface DataCatalogAgentToolOptions {
|
|
6
|
+
name?: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
adapter: ExplorerDataSource;
|
|
9
|
+
defaultLimit?: number;
|
|
10
|
+
maxLimit?: number;
|
|
11
|
+
}
|
|
12
|
+
interface DataCatalogSkillOptions {
|
|
13
|
+
label?: string;
|
|
14
|
+
toolName?: string;
|
|
15
|
+
surfaceKind?: string;
|
|
16
|
+
guidance?: string;
|
|
17
|
+
}
|
|
18
|
+
interface DataCatalogServerPluginOptions extends DataCatalogAgentToolOptions, DataCatalogSkillOptions {
|
|
19
|
+
id?: string;
|
|
20
|
+
}
|
|
21
|
+
declare function formatDataCatalogSearchResult(query: string, result: SearchResult): string;
|
|
22
|
+
declare function createDataCatalogAgentTool(options: DataCatalogAgentToolOptions): AgentTool;
|
|
23
|
+
declare function createDataCatalogSkillPrompt(options?: DataCatalogSkillOptions): string;
|
|
24
|
+
declare function createDataCatalogServerPlugin(options: DataCatalogServerPluginOptions): WorkspaceServerPlugin & {
|
|
25
|
+
agentTools: AgentTool[];
|
|
26
|
+
systemPrompt: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export { type DataCatalogAgentToolOptions, type DataCatalogServerPluginOptions, type DataCatalogSkillOptions, createDataCatalogAgentTool, createDataCatalogServerPlugin, createDataCatalogSkillPrompt, formatDataCatalogSearchResult };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// src/server/index.ts
|
|
2
|
+
import {
|
|
3
|
+
defineServerPlugin
|
|
4
|
+
} from "@hachej/boring-workspace/server";
|
|
5
|
+
|
|
6
|
+
// src/shared/constants.ts
|
|
7
|
+
var DATA_CATALOG_PLUGIN_ID = "data-catalog";
|
|
8
|
+
var DATA_CATALOG_DEFAULT_TOOL_NAME = "query_data_catalog";
|
|
9
|
+
var DATA_CATALOG_ROW_SURFACE_KIND = "data-catalog.open-row";
|
|
10
|
+
|
|
11
|
+
// src/server/index.ts
|
|
12
|
+
function textResult(text, details) {
|
|
13
|
+
return { content: [{ type: "text", text }], details };
|
|
14
|
+
}
|
|
15
|
+
function errorResult(text) {
|
|
16
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
17
|
+
}
|
|
18
|
+
function clampLimit(value, fallback, max) {
|
|
19
|
+
const numeric = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
20
|
+
if (!Number.isFinite(numeric)) return fallback;
|
|
21
|
+
return Math.max(1, Math.min(max, Math.floor(numeric)));
|
|
22
|
+
}
|
|
23
|
+
function normalizeLimitOptions(options) {
|
|
24
|
+
const rawMax = options.maxLimit ?? 50;
|
|
25
|
+
const maxLimit = typeof rawMax === "number" && Number.isFinite(rawMax) ? Math.max(1, Math.floor(rawMax)) : 50;
|
|
26
|
+
const rawDefault = options.defaultLimit ?? 20;
|
|
27
|
+
const defaultLimit = typeof rawDefault === "number" && Number.isFinite(rawDefault) ? Math.max(1, Math.min(maxLimit, Math.floor(rawDefault))) : Math.min(20, maxLimit);
|
|
28
|
+
return { defaultLimit, maxLimit };
|
|
29
|
+
}
|
|
30
|
+
function formatBadge(row) {
|
|
31
|
+
const parts = [
|
|
32
|
+
row.leading?.code,
|
|
33
|
+
...(row.trailing ?? []).map((badge) => badge.code),
|
|
34
|
+
row.meta
|
|
35
|
+
].filter(Boolean);
|
|
36
|
+
return parts.length > 0 ? ` [${parts.join(", ")}]` : "";
|
|
37
|
+
}
|
|
38
|
+
function formatDataCatalogSearchResult(query, result) {
|
|
39
|
+
if (result.items.length === 0) {
|
|
40
|
+
return `No ${query ? `results for "${query}"` : "catalog results"}.`;
|
|
41
|
+
}
|
|
42
|
+
const lines = result.items.map((row) => {
|
|
43
|
+
const subtitle = row.subtitle ? ` \u2014 ${row.subtitle}` : "";
|
|
44
|
+
return `${row.id}: ${row.title}${subtitle}${formatBadge(row)}`;
|
|
45
|
+
});
|
|
46
|
+
const total = Number.isFinite(result.total) ? result.total : result.items.length;
|
|
47
|
+
return `Found ${total} results (showing ${result.items.length}):
|
|
48
|
+
|
|
49
|
+
${lines.join("\n")}`;
|
|
50
|
+
}
|
|
51
|
+
function createDataCatalogAgentTool(options) {
|
|
52
|
+
const name = options.name ?? DATA_CATALOG_DEFAULT_TOOL_NAME;
|
|
53
|
+
const label = options.label ?? "data catalog";
|
|
54
|
+
const { defaultLimit, maxLimit } = normalizeLimitOptions(options);
|
|
55
|
+
return {
|
|
56
|
+
name,
|
|
57
|
+
description: `Search the ${label}. Use this before opening data visualizations or asking for a specific dataset.`,
|
|
58
|
+
parameters: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
query: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Search keywords for datasets, series, tables, or metrics."
|
|
64
|
+
},
|
|
65
|
+
limit: {
|
|
66
|
+
type: "number",
|
|
67
|
+
description: `Maximum number of results. Default ${defaultLimit}, max ${maxLimit}.`,
|
|
68
|
+
minimum: 1,
|
|
69
|
+
maximum: maxLimit
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
required: ["query"],
|
|
73
|
+
additionalProperties: false
|
|
74
|
+
},
|
|
75
|
+
async execute(params, ctx) {
|
|
76
|
+
const query = String(params.query ?? "").trim();
|
|
77
|
+
if (!query) return errorResult("query is required");
|
|
78
|
+
const limit = clampLimit(params.limit, defaultLimit, maxLimit);
|
|
79
|
+
try {
|
|
80
|
+
const result = await options.adapter.search({
|
|
81
|
+
query,
|
|
82
|
+
filters: {},
|
|
83
|
+
limit,
|
|
84
|
+
offset: 0,
|
|
85
|
+
signal: ctx.abortSignal
|
|
86
|
+
});
|
|
87
|
+
return textResult(formatDataCatalogSearchResult(query, result), result);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function createDataCatalogSkillPrompt(options = {}) {
|
|
95
|
+
const label = options.label ?? "data catalog";
|
|
96
|
+
const toolName = options.toolName ?? DATA_CATALOG_DEFAULT_TOOL_NAME;
|
|
97
|
+
const surfaceKind = options.surfaceKind ?? DATA_CATALOG_ROW_SURFACE_KIND;
|
|
98
|
+
const guidance = options.guidance?.trim();
|
|
99
|
+
return [
|
|
100
|
+
"## Data Catalog Plugin",
|
|
101
|
+
"",
|
|
102
|
+
`Use \`${toolName}\` to search the ${label} before referencing datasets, series, tables, or metrics.`,
|
|
103
|
+
`When you need to show a catalog row to the user, use the workspace UI bridge \`openSurface\` command with \`{ kind: '${surfaceKind}', target: row.id, meta: { catalogId, row } }\` so the client plugin resolver chooses the panel.`,
|
|
104
|
+
guidance ? "" : void 0,
|
|
105
|
+
guidance || void 0
|
|
106
|
+
].filter((line) => line !== void 0).join("\n");
|
|
107
|
+
}
|
|
108
|
+
function createDataCatalogServerPlugin(options) {
|
|
109
|
+
const tool = createDataCatalogAgentTool(options);
|
|
110
|
+
return defineServerPlugin({
|
|
111
|
+
id: options.id ?? DATA_CATALOG_PLUGIN_ID,
|
|
112
|
+
label: options.label ?? "Data Catalog",
|
|
113
|
+
agentTools: [tool],
|
|
114
|
+
systemPrompt: createDataCatalogSkillPrompt({
|
|
115
|
+
label: options.label,
|
|
116
|
+
toolName: tool.name,
|
|
117
|
+
surfaceKind: options.surfaceKind,
|
|
118
|
+
guidance: options.guidance
|
|
119
|
+
})
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
export {
|
|
123
|
+
createDataCatalogAgentTool,
|
|
124
|
+
createDataCatalogServerPlugin,
|
|
125
|
+
createDataCatalogSkillPrompt,
|
|
126
|
+
formatDataCatalogSearchResult
|
|
127
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
declare const DATA_CATALOG_PLUGIN_ID = "data-catalog";
|
|
2
|
+
declare const DATA_CATALOG_DEFAULT_TOOL_NAME = "query_data_catalog";
|
|
3
|
+
declare const DATA_CATALOG_ROW_SURFACE_KIND = "data-catalog.open-row";
|
|
4
|
+
|
|
5
|
+
export { DATA_CATALOG_DEFAULT_TOOL_NAME, DATA_CATALOG_PLUGIN_ID, DATA_CATALOG_ROW_SURFACE_KIND };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// src/shared/constants.ts
|
|
2
|
+
var DATA_CATALOG_PLUGIN_ID = "data-catalog";
|
|
3
|
+
var DATA_CATALOG_DEFAULT_TOOL_NAME = "query_data_catalog";
|
|
4
|
+
var DATA_CATALOG_ROW_SURFACE_KIND = "data-catalog.open-row";
|
|
5
|
+
export {
|
|
6
|
+
DATA_CATALOG_DEFAULT_TOOL_NAME,
|
|
7
|
+
DATA_CATALOG_PLUGIN_ID,
|
|
8
|
+
DATA_CATALOG_ROW_SURFACE_KIND
|
|
9
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hachej/boring-data-catalog",
|
|
3
|
+
"version": "0.1.13",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"private": false,
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"description": "Plugin BUILDER for Boring workspace data catalogs. Consumers call `createDataCatalogPlugin(options)` to bind their own adapter/facets and get a configured BoringFrontFactoryWithId. NOT a direct `defaultPluginPackages` entry (no default export, options required) \u2014 wrap in an app-local plugin module that default-exports the built factory, then list that wrapper in `defaultPluginPackages`.",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/hachej/boring-ui"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/hachej/boring-ui",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/front/index.d.ts",
|
|
19
|
+
"import": "./dist/front/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./front": {
|
|
22
|
+
"types": "./dist/front/index.d.ts",
|
|
23
|
+
"import": "./dist/front/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./server": {
|
|
26
|
+
"types": "./dist/server/index.d.ts",
|
|
27
|
+
"import": "./dist/server/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./shared": {
|
|
30
|
+
"types": "./dist/shared/index.d.ts",
|
|
31
|
+
"import": "./dist/shared/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"sideEffects": false,
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "pnpm --filter @hachej/boring-data-explorer build && pnpm --filter @hachej/boring-workspace build && tsup",
|
|
38
|
+
"typecheck": "pnpm --filter @hachej/boring-data-explorer build && pnpm --filter @hachej/boring-workspace build && tsc --noEmit",
|
|
39
|
+
"test": "pnpm --filter @hachej/boring-data-explorer build && pnpm --filter @hachej/boring-workspace build && vitest run --passWithNoTests",
|
|
40
|
+
"lint": "pnpm run typecheck",
|
|
41
|
+
"clean": "rm -rf dist .tsbuildinfo"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@hachej/boring-workspace": "workspace:*",
|
|
45
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
46
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@hachej/boring-data-explorer": "workspace:*",
|
|
50
|
+
"lucide-react": "^1.8.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@hachej/boring-workspace": "workspace:*",
|
|
54
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
55
|
+
"@testing-library/react": "^16.3.2",
|
|
56
|
+
"@types/node": "^22.15.3",
|
|
57
|
+
"@types/react": "^19.0.0",
|
|
58
|
+
"@types/react-dom": "^19.0.0",
|
|
59
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
60
|
+
"jsdom": "^29.0.2",
|
|
61
|
+
"react": "^19.0.0",
|
|
62
|
+
"react-dom": "^19.0.0",
|
|
63
|
+
"tsup": "^8.0.0",
|
|
64
|
+
"typescript": "^5.4.0",
|
|
65
|
+
"vitest": "^2.0.0"
|
|
66
|
+
}
|
|
67
|
+
}
|