elysia-rack 0.2.2 → 0.4.0
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 +37 -20
- package/dist/Dashboard-4wv82gwx.js +212 -0
- package/dist/Dashboard-jkvfzyk2.js +210 -0
- package/dist/Dashboard-zhg2jtq5.js +211 -0
- package/dist/{Panel-fbp3wrsw.js → Panel-1qkzh8t7.js} +355 -311
- package/dist/Panel-7zpqz8gy.js +782 -0
- package/dist/Panel-9mmh3tbh.js +790 -0
- package/dist/Panel-ba0qzknj.js +789 -0
- package/dist/Panel-e6jyf4rc.js +789 -0
- package/dist/index-0dwhzk3j.js +145 -0
- package/dist/{index-6v0ns1xn.js → index-b8sq8rbd.js} +1 -1
- package/dist/index-rg87f3v1.js +174 -0
- package/dist/panel-app.js +27 -11
- package/dist/panel.css +2 -1
- package/dist/rack/index.js +5 -5
- package/dist/rack/types/metadata.d.ts +2 -1
- package/dist/react/donate.d.ts +1 -1
- package/dist/react/index.js +40 -15
- package/dist/react/page/Panel.d.ts +1 -0
- package/dist/react/page/components/Icon.d.ts +10 -0
- package/dist/react/page/components/Masthead.d.ts +5 -4
- package/dist/react/render.d.ts +1 -1
- package/dist/react/types.d.ts +18 -2
- package/package.json +8 -3
- package/dist/Dashboard-0da8dvbv.js +0 -225
- package/dist/Dashboard-2j561p7c.js +0 -225
- package/dist/Dashboard-5mt9hnbw.js +0 -252
- package/dist/Dashboard-7gmx8c9y.js +0 -248
- package/dist/Dashboard-s8nfyf7s.js +0 -39
- package/dist/Dashboard-xkh2g9f7.js +0 -248
- package/dist/Dashboard-xpstr7fr.js +0 -172
- package/dist/Panel-3yrk3krj.js +0 -914
- package/dist/Panel-b7jhgvnm.js +0 -916
- package/dist/Panel-n0kstkyw.js +0 -735
- package/dist/Panel-pkvqf5v8.js +0 -912
- package/dist/Panel-wjxjtbmr.js +0 -753
- package/dist/Panel-za4av616.js +0 -753
- package/dist/index-h3zbq796.js +0 -39
- package/dist/index-tgk4jt4d.js +0 -63
- package/dist/index-xsfqad54.js +0 -187
- package/dist/index-zc7rgvmm.js +0 -13
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
Model-driven CRUD rack for [Elysia](https://elysiajs.com): declare a resource once, get REST + `QUERY` routes, validation, authorization, query engine, OpenAPI metadata, and a React panel — running on [Bun](https://bun.com) or Node.js 20+.
|
|
6
6
|
|
|
7
|
+
## Documentation
|
|
8
|
+
|
|
9
|
+
End-user documentation covers installation, configuration, daily usage, and troubleshooting: [docs/index.md](docs/index.md).
|
|
10
|
+
|
|
7
11
|
## Install
|
|
8
12
|
|
|
9
13
|
```bash
|
|
@@ -85,7 +89,7 @@ work through plain `GET` navigation without JavaScript).
|
|
|
85
89
|
|
|
86
90
|
The panel ships create / edit / delete UI driven by the same API:
|
|
87
91
|
|
|
88
|
-
-
|
|
92
|
+
- `New record` opens a create dialog (fields derived from row columns,
|
|
89
93
|
primary key optional). Submit sends `POST /` with an auto-generated
|
|
90
94
|
`Idempotency-Key`. Tables without known columns fall back to a raw
|
|
91
95
|
JSON body field.
|
|
@@ -147,7 +151,7 @@ Conventions:
|
|
|
147
151
|
- String route ids are coerced to numbers for integer columns
|
|
148
152
|
(e.g. `serial`); anything else passes through. Malformed ids resolve
|
|
149
153
|
to `404`, missing rows to `404`.
|
|
150
|
-
- `searchable` fields use SQL `
|
|
154
|
+
- `searchable` fields use SQL `ILIKE` through the Drizzle adapter, so search requires a database that supports `ILIKE` (for example PostgreSQL). SQLite search fails with a SQL syntax error.
|
|
151
155
|
- `settings.softDelete: true` writes a timestamp to `deletedAt`
|
|
152
156
|
(override with `settings.deletedAtField`) instead of deleting, and
|
|
153
157
|
reads skip soft-deleted rows.
|
|
@@ -155,7 +159,7 @@ Conventions:
|
|
|
155
159
|
|
|
156
160
|
## Dashboard
|
|
157
161
|
|
|
158
|
-
`dashboard()`
|
|
162
|
+
`dashboard()` renders a navigation overview of every registered resource, with a sidebar and a resource table. Each `rack()` panel also shows a sidebar with the entire tree.
|
|
159
163
|
|
|
160
164
|
Recommended setup:
|
|
161
165
|
|
|
@@ -165,7 +169,7 @@ import { reactPlugin } from "elysia-rack/react";
|
|
|
165
169
|
|
|
166
170
|
const app = new Elysia()
|
|
167
171
|
.use(reactPlugin()) // pages are optional — defaults to dashboard + panel
|
|
168
|
-
.use(dashboard({ title: "My Panel", path: "/" })) // →
|
|
172
|
+
.use(dashboard({ title: "My Panel", path: "/" })) // → Dashboard at /
|
|
169
173
|
.use(rack("/catalog/products", { model, metadata: { id: "products", group: "Catalog", order: 1 } }))
|
|
170
174
|
.use(rack("/catalog/variants", { model, metadata: { id: "variants", group: "Catalog", order: 2, parent: "products" } }));
|
|
171
175
|
// → Panel at /catalog/products now shows sidebar with Catalog > Products > Variants
|
|
@@ -227,14 +231,14 @@ rack("/catalog/products", {
|
|
|
227
231
|
pluralLabel: "Products", // shown in sidebar
|
|
228
232
|
group: "Catalog", // sidebar group
|
|
229
233
|
order: 1, // smaller = higher
|
|
230
|
-
icon: "
|
|
234
|
+
icon: "package", // Lucide icon name (kebab-case or PascalCase)
|
|
231
235
|
parent: "catalog-root", // nest under another id
|
|
232
236
|
hidden: false
|
|
233
237
|
}
|
|
234
238
|
});
|
|
235
239
|
```
|
|
236
240
|
|
|
237
|
-
**Decide what users can do.** Turn operations on/off. If you disable `create`, the
|
|
241
|
+
**Decide what users can do.** Turn operations on/off. If you disable `create`, the “New record” button disappears automatically.
|
|
238
242
|
|
|
239
243
|
```ts
|
|
240
244
|
rack("/catalog/products", {
|
|
@@ -293,9 +297,9 @@ rack("/catalog/products", {
|
|
|
293
297
|
|
|
294
298
|
That’s the whole `rack()` surface. Start with `model`, then add `metadata` → `query` → `validation/authorization` as your app grows.
|
|
295
299
|
|
|
296
|
-
### 2) `dashboard()
|
|
300
|
+
### 2) `dashboard()`: one line, navigation overview
|
|
297
301
|
|
|
298
|
-
`dashboard()` mounts `GET /`
|
|
302
|
+
`dashboard()` mounts `GET /` and renders a sidebar plus a table of every resource registered with `rack()`.
|
|
299
303
|
|
|
300
304
|
```ts
|
|
301
305
|
import { dashboard, rack } from "elysia-rack";
|
|
@@ -303,7 +307,7 @@ import { reactPlugin } from "elysia-rack/react";
|
|
|
303
307
|
|
|
304
308
|
const app = new Elysia()
|
|
305
309
|
.use(reactPlugin()) // 1) UI engine — pages optional
|
|
306
|
-
.use(dashboard({ title: "My Shop", path: "/" })) // 2)
|
|
310
|
+
.use(dashboard({ title: "My Shop", path: "/" })) // 2) Dashboard at /
|
|
307
311
|
.use(rack("/catalog/products", { model, metadata: { id: "products", group: "Catalog", order: 1 } }))
|
|
308
312
|
.use(rack("/catalog/variants", { model, metadata: { id: "variants", parent: "products", group: "Catalog", order: 2 } }));
|
|
309
313
|
// visit /catalog/products → panel shows sidebar with Catalog > Products > Variants
|
|
@@ -329,17 +333,28 @@ Helpers to inspect the tree yourself: `listRacks()`, `getRack(id)`, `getRackTree
|
|
|
329
333
|
`reactPlugin` is the bridge between Elysia and React. `pages` is now optional and mergeable — defaults already include `"/dashboard"` and `"/panel"`, and any custom pages you pass are merged in. You can pass a single registry or an array.
|
|
330
334
|
|
|
331
335
|
```ts
|
|
332
|
-
import { pages, reactPlugin, page } from "elysia-rack/react";
|
|
336
|
+
import { lazyPage, pages, reactPlugin, page } from "elysia-rack/react";
|
|
333
337
|
|
|
334
338
|
// Option A: just use defaults
|
|
335
339
|
new Elysia().use(reactPlugin());
|
|
336
340
|
|
|
337
341
|
// Option B: extend defaults with your own page
|
|
338
|
-
const myPages = { "/hello": () => import("./Hello") };
|
|
342
|
+
const myPages = { "/hello": lazyPage(() => import("./Hello")) };
|
|
339
343
|
new Elysia().use(reactPlugin({ pages: myPages })); // merged with defaults
|
|
340
344
|
|
|
341
|
-
// Option C:
|
|
342
|
-
|
|
345
|
+
// Option C: pass a component directly (no import wrapper)
|
|
346
|
+
import Hello from "./Hello";
|
|
347
|
+
new Elysia().use(reactPlugin({ pages: { "/hello": Hello } }));
|
|
348
|
+
|
|
349
|
+
// Option D: auto-mount a view — no page() call needed
|
|
350
|
+
new Elysia().use(
|
|
351
|
+
reactPlugin({
|
|
352
|
+
views: [{ path: "/hello", component: Hello, props: { name: "Ada" } }],
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// Option E: merge multiple registries — array will be merged
|
|
357
|
+
const extra = { "/admin": lazyPage(() => import("./Admin")) };
|
|
343
358
|
new Elysia().use(reactPlugin({ pages: [pages, myPages, extra] }));
|
|
344
359
|
|
|
345
360
|
// With a style override
|
|
@@ -354,7 +369,8 @@ new Elysia().use(reactPlugin({
|
|
|
354
369
|
|
|
355
370
|
What each option does:
|
|
356
371
|
|
|
357
|
-
- `pages` *(optional, `PageRegistry | PageRegistry[]`)* — registry of
|
|
372
|
+
- `pages` *(optional, `PageRegistry | PageRegistry[]`)* — registry of page components or loaders wrapped with `lazyPage(() => import(...))`. If omitted, defaults are used. If you pass one or an array, they are merged with defaults so `"/dashboard"` + `"/panel"` stay available. Later entries win on key collision.
|
|
373
|
+
- `views` *(optional, `ReactView[]`)* — components auto-mounted at their `path` with fixed `props`. Equivalent to registering a page and returning `page(path, props)` yourself.
|
|
358
374
|
- `css` / `panelCss` — replace `dist/panel.css` served at `/__rack/panel.css`. If the string points to an existing file it’s used, otherwise it’s treated as raw CSS. Useful for quick theming without forking.
|
|
359
375
|
|
|
360
376
|
Under the hood `reactPlugin` also serves `/__rack/panel-theme.js` and `/__rack/panel-app.js` and turns any `page()` return value into streamed HTML using `src/react/app.html` (`{{title}}` + `<!--app-->`).
|
|
@@ -404,13 +420,13 @@ Fallback is `dist/panel.css`.
|
|
|
404
420
|
|
|
405
421
|
Static assets are served under `/__rack/*`:
|
|
406
422
|
|
|
407
|
-
- `dist/panel.css` — panel stylesheet (
|
|
423
|
+
- `dist/panel.css` — panel stylesheet (built with Tailwind CSS)
|
|
408
424
|
- `dist/panel-theme.js` — dark/light toggle
|
|
409
425
|
- `dist/panel-app.js` — live query + actions (imports `components/href.ts`, `components/panelForm.ts`)
|
|
410
426
|
|
|
411
427
|
Pages reference `/__rack/panel.css`, `/__rack/panel-theme.js` and
|
|
412
|
-
`/__rack/panel-app.js` via `reactPlugin`.
|
|
413
|
-
|
|
428
|
+
`/__rack/panel-app.js` via `reactPlugin`. Run `bun run build` to produce
|
|
429
|
+
`dist/` (`build:css` compiles the stylesheet, `build:js` bundles the clients).
|
|
414
430
|
|
|
415
431
|
## Panel theme
|
|
416
432
|
|
|
@@ -420,9 +436,10 @@ static and included in the published package.
|
|
|
420
436
|
chart, selection, focus, overlay, navigation, table, form, sidebar),
|
|
421
437
|
shadcn/Tailwind compatible, mapped via `@theme inline`.
|
|
422
438
|
- `theme/dark.css` — `.dark` overrides.
|
|
423
|
-
- `theme/
|
|
424
|
-
|
|
425
|
-
|
|
439
|
+
- `theme/rack.css` — default theme (black, white, dark blue, gold;
|
|
440
|
+
Poppins for UI, Chocolate Classical Sans for display headings).
|
|
441
|
+
Remove its import for the neutral look.
|
|
442
|
+
- `theme/components.css` — base styles, dialog reset, reduced motion.
|
|
426
443
|
|
|
427
444
|
Dark mode flips automatically: stored choice (`localStorage`), else
|
|
428
445
|
`prefers-color-scheme`. The `◐ Theme` button in the masthead toggles it.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Masthead,
|
|
3
|
+
RackIcon,
|
|
4
|
+
Sidebar
|
|
5
|
+
} from "./index-rg87f3v1.js";
|
|
6
|
+
import {
|
|
7
|
+
getRackTree
|
|
8
|
+
} from "./index-4wxvmd8a.js";
|
|
9
|
+
import {
|
|
10
|
+
resolveDonateProp
|
|
11
|
+
} from "./index-b8sq8rbd.js";
|
|
12
|
+
import"./index-37x76zdn.js";
|
|
13
|
+
|
|
14
|
+
// src/react/page/Dashboard.tsx
|
|
15
|
+
import { Fragment } from "react";
|
|
16
|
+
import { jsxDEV, Fragment as Fragment2 } from "react/jsx-dev-runtime";
|
|
17
|
+
function itemLabel(r) {
|
|
18
|
+
return r.metadata.pluralLabel ?? r.metadata.label ?? r.metadata.id;
|
|
19
|
+
}
|
|
20
|
+
function toSidebarItem(node, activeId) {
|
|
21
|
+
return {
|
|
22
|
+
id: node.metadata.id,
|
|
23
|
+
label: itemLabel(node),
|
|
24
|
+
icon: node.metadata.icon,
|
|
25
|
+
href: node.path,
|
|
26
|
+
active: node.metadata.id === activeId,
|
|
27
|
+
children: node.children.filter((c) => !c.metadata.hidden).map((c) => toSidebarItem(c, activeId))
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function buildGroupsFromTree(tree, activeId) {
|
|
31
|
+
const byGroup = new Map;
|
|
32
|
+
for (const root of tree) {
|
|
33
|
+
if (root.metadata.hidden)
|
|
34
|
+
continue;
|
|
35
|
+
const name = root.metadata.group ?? "General";
|
|
36
|
+
let group = byGroup.get(name);
|
|
37
|
+
if (!group) {
|
|
38
|
+
group = { name, items: [] };
|
|
39
|
+
byGroup.set(name, group);
|
|
40
|
+
}
|
|
41
|
+
group.items.push(toSidebarItem(root, activeId));
|
|
42
|
+
}
|
|
43
|
+
return [...byGroup.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
44
|
+
}
|
|
45
|
+
function countNodes(nodes) {
|
|
46
|
+
return nodes.reduce((sum, n) => sum + 1 + countNodes(n.children), 0);
|
|
47
|
+
}
|
|
48
|
+
function operationsLabel(node) {
|
|
49
|
+
const op = node.operations;
|
|
50
|
+
const write = [op.create, op.update, op.replace, op.delete];
|
|
51
|
+
if (!write.some(Boolean))
|
|
52
|
+
return "Read only";
|
|
53
|
+
if (write.every(Boolean))
|
|
54
|
+
return "Full";
|
|
55
|
+
const labels = [];
|
|
56
|
+
if (op.create)
|
|
57
|
+
labels.push("Create");
|
|
58
|
+
if (op.update)
|
|
59
|
+
labels.push("Update");
|
|
60
|
+
if (op.replace)
|
|
61
|
+
labels.push("Replace");
|
|
62
|
+
if (op.delete)
|
|
63
|
+
labels.push("Delete");
|
|
64
|
+
return labels.join(", ");
|
|
65
|
+
}
|
|
66
|
+
function plural(count, word) {
|
|
67
|
+
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
68
|
+
}
|
|
69
|
+
function ResourceRow({ node, depth = 0 }) {
|
|
70
|
+
return /* @__PURE__ */ jsxDEV(Fragment2, {
|
|
71
|
+
children: [
|
|
72
|
+
/* @__PURE__ */ jsxDEV("tr", {
|
|
73
|
+
className: "hover:bg-table-row-hover",
|
|
74
|
+
children: [
|
|
75
|
+
/* @__PURE__ */ jsxDEV("td", {
|
|
76
|
+
className: "border-table-border border-t px-3 py-2.5",
|
|
77
|
+
children: /* @__PURE__ */ jsxDEV("a", {
|
|
78
|
+
href: node.path,
|
|
79
|
+
className: "text-link hover:text-link-hover font-medium hover:underline",
|
|
80
|
+
style: depth > 0 ? { paddingLeft: depth * 16 } : undefined,
|
|
81
|
+
children: [
|
|
82
|
+
node.metadata.icon ? /* @__PURE__ */ jsxDEV(RackIcon, {
|
|
83
|
+
name: node.metadata.icon,
|
|
84
|
+
size: 16,
|
|
85
|
+
className: "mr-1.5 inline size-4 align-text-bottom"
|
|
86
|
+
}, undefined, false, undefined, this) : null,
|
|
87
|
+
node.metadata.label ?? node.metadata.id
|
|
88
|
+
]
|
|
89
|
+
}, undefined, true, undefined, this)
|
|
90
|
+
}, undefined, false, undefined, this),
|
|
91
|
+
/* @__PURE__ */ jsxDEV("td", {
|
|
92
|
+
className: "border-table-border text-text-muted border-t px-3 py-2.5 font-mono text-xs",
|
|
93
|
+
children: node.path
|
|
94
|
+
}, undefined, false, undefined, this),
|
|
95
|
+
/* @__PURE__ */ jsxDEV("td", {
|
|
96
|
+
className: "border-table-border text-text-muted border-t px-3 py-2.5 text-xs",
|
|
97
|
+
children: operationsLabel(node)
|
|
98
|
+
}, undefined, false, undefined, this)
|
|
99
|
+
]
|
|
100
|
+
}, undefined, true, undefined, this),
|
|
101
|
+
node.children.filter((c) => !c.metadata.hidden).map((child) => /* @__PURE__ */ jsxDEV(ResourceRow, {
|
|
102
|
+
node: child,
|
|
103
|
+
depth: depth + 1
|
|
104
|
+
}, child.metadata.id, false, undefined, this))
|
|
105
|
+
]
|
|
106
|
+
}, undefined, true, undefined, this);
|
|
107
|
+
}
|
|
108
|
+
async function Dashboard(props) {
|
|
109
|
+
const tree = getRackTree().filter((r) => !r.metadata.hidden);
|
|
110
|
+
const groups = buildGroupsFromTree(tree, props.resource);
|
|
111
|
+
const totalResources = countNodes(tree);
|
|
112
|
+
const totalGroups = groups.length;
|
|
113
|
+
const donate = resolveDonateProp(props.donate);
|
|
114
|
+
return /* @__PURE__ */ jsxDEV("div", {
|
|
115
|
+
className: "text-foreground flex min-h-screen flex-col lg:flex-row",
|
|
116
|
+
children: [
|
|
117
|
+
/* @__PURE__ */ jsxDEV(Sidebar, {
|
|
118
|
+
title: props.name ?? "Panel",
|
|
119
|
+
groups
|
|
120
|
+
}, undefined, false, undefined, this),
|
|
121
|
+
/* @__PURE__ */ jsxDEV("main", {
|
|
122
|
+
className: "min-w-0 flex-1",
|
|
123
|
+
children: /* @__PURE__ */ jsxDEV("div", {
|
|
124
|
+
className: "mx-auto w-full max-w-5xl px-4 py-4 sm:px-6",
|
|
125
|
+
children: [
|
|
126
|
+
/* @__PURE__ */ jsxDEV(Masthead, {
|
|
127
|
+
title: props.name ?? "Panel",
|
|
128
|
+
detail: `${plural(totalResources, "resource")} in ${plural(totalGroups, "group")}`
|
|
129
|
+
}, undefined, false, undefined, this),
|
|
130
|
+
/* @__PURE__ */ jsxDEV("div", {
|
|
131
|
+
className: "bg-table border-border overflow-x-auto rounded-lg border",
|
|
132
|
+
children: /* @__PURE__ */ jsxDEV("table", {
|
|
133
|
+
className: "w-full border-collapse text-left text-sm",
|
|
134
|
+
children: [
|
|
135
|
+
/* @__PURE__ */ jsxDEV("thead", {
|
|
136
|
+
children: /* @__PURE__ */ jsxDEV("tr", {
|
|
137
|
+
children: [
|
|
138
|
+
/* @__PURE__ */ jsxDEV("th", {
|
|
139
|
+
className: "border-border text-text-muted border-b px-3 py-2 text-xs font-medium",
|
|
140
|
+
children: "Resource"
|
|
141
|
+
}, undefined, false, undefined, this),
|
|
142
|
+
/* @__PURE__ */ jsxDEV("th", {
|
|
143
|
+
className: "border-border text-text-muted border-b px-3 py-2 text-xs font-medium",
|
|
144
|
+
children: "Path"
|
|
145
|
+
}, undefined, false, undefined, this),
|
|
146
|
+
/* @__PURE__ */ jsxDEV("th", {
|
|
147
|
+
className: "border-border text-text-muted border-b px-3 py-2 text-xs font-medium",
|
|
148
|
+
children: "Operations"
|
|
149
|
+
}, undefined, false, undefined, this)
|
|
150
|
+
]
|
|
151
|
+
}, undefined, true, undefined, this)
|
|
152
|
+
}, undefined, false, undefined, this),
|
|
153
|
+
/* @__PURE__ */ jsxDEV("tbody", {
|
|
154
|
+
children: groups.length === 0 ? /* @__PURE__ */ jsxDEV("tr", {
|
|
155
|
+
children: /* @__PURE__ */ jsxDEV("td", {
|
|
156
|
+
colSpan: 3,
|
|
157
|
+
className: "text-text-muted px-4 py-10 text-center",
|
|
158
|
+
children: "No resources registered. Call rack() before rendering this page."
|
|
159
|
+
}, undefined, false, undefined, this)
|
|
160
|
+
}, undefined, false, undefined, this) : groups.map((group) => {
|
|
161
|
+
const nodes = tree.filter((r) => (r.metadata.group ?? "General") === group.name);
|
|
162
|
+
return /* @__PURE__ */ jsxDEV(Fragment, {
|
|
163
|
+
children: [
|
|
164
|
+
/* @__PURE__ */ jsxDEV("tr", {
|
|
165
|
+
className: "bg-surface-muted",
|
|
166
|
+
children: /* @__PURE__ */ jsxDEV("th", {
|
|
167
|
+
colSpan: 3,
|
|
168
|
+
className: "border-border text-text-muted border-b px-3 py-1.5 text-left text-xs font-medium",
|
|
169
|
+
children: [
|
|
170
|
+
group.name,
|
|
171
|
+
" ",
|
|
172
|
+
/* @__PURE__ */ jsxDEV("span", {
|
|
173
|
+
className: "text-text-subtle font-normal",
|
|
174
|
+
children: plural(countNodes(nodes), "resource")
|
|
175
|
+
}, undefined, false, undefined, this)
|
|
176
|
+
]
|
|
177
|
+
}, undefined, true, undefined, this)
|
|
178
|
+
}, undefined, false, undefined, this),
|
|
179
|
+
nodes.map((node) => /* @__PURE__ */ jsxDEV(ResourceRow, {
|
|
180
|
+
node
|
|
181
|
+
}, node.metadata.id, false, undefined, this))
|
|
182
|
+
]
|
|
183
|
+
}, group.name, true, undefined, this);
|
|
184
|
+
})
|
|
185
|
+
}, undefined, false, undefined, this)
|
|
186
|
+
]
|
|
187
|
+
}, undefined, true, undefined, this)
|
|
188
|
+
}, undefined, false, undefined, this),
|
|
189
|
+
/* @__PURE__ */ jsxDEV("footer", {
|
|
190
|
+
className: "border-border text-text-muted mt-6 flex flex-wrap items-center justify-between gap-2 border-t pt-3 text-xs",
|
|
191
|
+
children: [
|
|
192
|
+
/* @__PURE__ */ jsxDEV("span", {
|
|
193
|
+
children: "elysia-rack"
|
|
194
|
+
}, undefined, false, undefined, this),
|
|
195
|
+
donate.enabled ? /* @__PURE__ */ jsxDEV("a", {
|
|
196
|
+
href: donate.url,
|
|
197
|
+
target: "_blank",
|
|
198
|
+
rel: "noopener noreferrer",
|
|
199
|
+
className: "hover:text-foreground underline-offset-4 hover:underline",
|
|
200
|
+
children: donate.label
|
|
201
|
+
}, undefined, false, undefined, this) : null
|
|
202
|
+
]
|
|
203
|
+
}, undefined, true, undefined, this)
|
|
204
|
+
]
|
|
205
|
+
}, undefined, true, undefined, this)
|
|
206
|
+
}, undefined, false, undefined, this)
|
|
207
|
+
]
|
|
208
|
+
}, undefined, true, undefined, this);
|
|
209
|
+
}
|
|
210
|
+
export {
|
|
211
|
+
Dashboard as default
|
|
212
|
+
};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Masthead,
|
|
3
|
+
Sidebar
|
|
4
|
+
} from "./index-0dwhzk3j.js";
|
|
5
|
+
import {
|
|
6
|
+
getRackTree
|
|
7
|
+
} from "./index-4wxvmd8a.js";
|
|
8
|
+
import {
|
|
9
|
+
resolveDonateProp
|
|
10
|
+
} from "./index-b8sq8rbd.js";
|
|
11
|
+
import"./index-37x76zdn.js";
|
|
12
|
+
|
|
13
|
+
// src/react/page/Dashboard.tsx
|
|
14
|
+
import { Fragment } from "react";
|
|
15
|
+
import { jsxDEV, Fragment as Fragment2 } from "react/jsx-dev-runtime";
|
|
16
|
+
function itemLabel(r) {
|
|
17
|
+
return r.metadata.pluralLabel ?? r.metadata.label ?? r.metadata.id;
|
|
18
|
+
}
|
|
19
|
+
function toSidebarItem(node, activeId) {
|
|
20
|
+
return {
|
|
21
|
+
id: node.metadata.id,
|
|
22
|
+
label: itemLabel(node),
|
|
23
|
+
icon: node.metadata.icon,
|
|
24
|
+
href: node.path,
|
|
25
|
+
active: node.metadata.id === activeId,
|
|
26
|
+
children: node.children.filter((c) => !c.metadata.hidden).map((c) => toSidebarItem(c, activeId))
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function buildGroupsFromTree(tree, activeId) {
|
|
30
|
+
const byGroup = new Map;
|
|
31
|
+
for (const root of tree) {
|
|
32
|
+
if (root.metadata.hidden)
|
|
33
|
+
continue;
|
|
34
|
+
const name = root.metadata.group ?? "General";
|
|
35
|
+
let group = byGroup.get(name);
|
|
36
|
+
if (!group) {
|
|
37
|
+
group = { name, items: [] };
|
|
38
|
+
byGroup.set(name, group);
|
|
39
|
+
}
|
|
40
|
+
group.items.push(toSidebarItem(root, activeId));
|
|
41
|
+
}
|
|
42
|
+
return [...byGroup.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
43
|
+
}
|
|
44
|
+
function countNodes(nodes) {
|
|
45
|
+
return nodes.reduce((sum, n) => sum + 1 + countNodes(n.children), 0);
|
|
46
|
+
}
|
|
47
|
+
function operationsLabel(node) {
|
|
48
|
+
const op = node.operations;
|
|
49
|
+
const write = [op.create, op.update, op.replace, op.delete];
|
|
50
|
+
if (!write.some(Boolean))
|
|
51
|
+
return "Read only";
|
|
52
|
+
if (write.every(Boolean))
|
|
53
|
+
return "Full";
|
|
54
|
+
const labels = [];
|
|
55
|
+
if (op.create)
|
|
56
|
+
labels.push("Create");
|
|
57
|
+
if (op.update)
|
|
58
|
+
labels.push("Update");
|
|
59
|
+
if (op.replace)
|
|
60
|
+
labels.push("Replace");
|
|
61
|
+
if (op.delete)
|
|
62
|
+
labels.push("Delete");
|
|
63
|
+
return labels.join(", ");
|
|
64
|
+
}
|
|
65
|
+
function plural(count, word) {
|
|
66
|
+
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
67
|
+
}
|
|
68
|
+
function ResourceRow({ node, depth = 0 }) {
|
|
69
|
+
return /* @__PURE__ */ jsxDEV(Fragment2, {
|
|
70
|
+
children: [
|
|
71
|
+
/* @__PURE__ */ jsxDEV("tr", {
|
|
72
|
+
className: "hover:bg-table-row-hover",
|
|
73
|
+
children: [
|
|
74
|
+
/* @__PURE__ */ jsxDEV("td", {
|
|
75
|
+
className: "border-table-border border-t px-3 py-2.5",
|
|
76
|
+
children: /* @__PURE__ */ jsxDEV("a", {
|
|
77
|
+
href: node.path,
|
|
78
|
+
className: "text-link hover:text-link-hover font-medium hover:underline",
|
|
79
|
+
style: depth > 0 ? { paddingLeft: depth * 16 } : undefined,
|
|
80
|
+
children: [
|
|
81
|
+
node.metadata.icon ? /* @__PURE__ */ jsxDEV("span", {
|
|
82
|
+
"aria-hidden": "true",
|
|
83
|
+
className: "mr-1.5",
|
|
84
|
+
children: node.metadata.icon
|
|
85
|
+
}, undefined, false, undefined, this) : null,
|
|
86
|
+
node.metadata.label ?? node.metadata.id
|
|
87
|
+
]
|
|
88
|
+
}, undefined, true, undefined, this)
|
|
89
|
+
}, undefined, false, undefined, this),
|
|
90
|
+
/* @__PURE__ */ jsxDEV("td", {
|
|
91
|
+
className: "border-table-border text-text-muted border-t px-3 py-2.5 font-mono text-xs",
|
|
92
|
+
children: node.path
|
|
93
|
+
}, undefined, false, undefined, this),
|
|
94
|
+
/* @__PURE__ */ jsxDEV("td", {
|
|
95
|
+
className: "border-table-border text-text-muted border-t px-3 py-2.5 text-xs",
|
|
96
|
+
children: operationsLabel(node)
|
|
97
|
+
}, undefined, false, undefined, this)
|
|
98
|
+
]
|
|
99
|
+
}, undefined, true, undefined, this),
|
|
100
|
+
node.children.filter((c) => !c.metadata.hidden).map((child) => /* @__PURE__ */ jsxDEV(ResourceRow, {
|
|
101
|
+
node: child,
|
|
102
|
+
depth: depth + 1
|
|
103
|
+
}, child.metadata.id, false, undefined, this))
|
|
104
|
+
]
|
|
105
|
+
}, undefined, true, undefined, this);
|
|
106
|
+
}
|
|
107
|
+
async function Dashboard(props) {
|
|
108
|
+
const tree = getRackTree().filter((r) => !r.metadata.hidden);
|
|
109
|
+
const groups = buildGroupsFromTree(tree, props.resource);
|
|
110
|
+
const totalResources = countNodes(tree);
|
|
111
|
+
const totalGroups = groups.length;
|
|
112
|
+
const donate = resolveDonateProp(props.donate);
|
|
113
|
+
return /* @__PURE__ */ jsxDEV("div", {
|
|
114
|
+
className: "text-foreground flex min-h-screen flex-col lg:flex-row",
|
|
115
|
+
children: [
|
|
116
|
+
/* @__PURE__ */ jsxDEV(Sidebar, {
|
|
117
|
+
title: props.name ?? "Panel",
|
|
118
|
+
groups
|
|
119
|
+
}, undefined, false, undefined, this),
|
|
120
|
+
/* @__PURE__ */ jsxDEV("main", {
|
|
121
|
+
className: "min-w-0 flex-1",
|
|
122
|
+
children: /* @__PURE__ */ jsxDEV("div", {
|
|
123
|
+
className: "mx-auto w-full max-w-5xl px-4 py-4 sm:px-6",
|
|
124
|
+
children: [
|
|
125
|
+
/* @__PURE__ */ jsxDEV(Masthead, {
|
|
126
|
+
title: props.name ?? "Panel",
|
|
127
|
+
detail: `${plural(totalResources, "resource")} in ${plural(totalGroups, "group")}`
|
|
128
|
+
}, undefined, false, undefined, this),
|
|
129
|
+
/* @__PURE__ */ jsxDEV("div", {
|
|
130
|
+
className: "bg-table border-border overflow-x-auto rounded-lg border",
|
|
131
|
+
children: /* @__PURE__ */ jsxDEV("table", {
|
|
132
|
+
className: "w-full border-collapse text-left text-sm",
|
|
133
|
+
children: [
|
|
134
|
+
/* @__PURE__ */ jsxDEV("thead", {
|
|
135
|
+
children: /* @__PURE__ */ jsxDEV("tr", {
|
|
136
|
+
children: [
|
|
137
|
+
/* @__PURE__ */ jsxDEV("th", {
|
|
138
|
+
className: "border-border text-text-muted border-b px-3 py-2 text-xs font-medium",
|
|
139
|
+
children: "Resource"
|
|
140
|
+
}, undefined, false, undefined, this),
|
|
141
|
+
/* @__PURE__ */ jsxDEV("th", {
|
|
142
|
+
className: "border-border text-text-muted border-b px-3 py-2 text-xs font-medium",
|
|
143
|
+
children: "Path"
|
|
144
|
+
}, undefined, false, undefined, this),
|
|
145
|
+
/* @__PURE__ */ jsxDEV("th", {
|
|
146
|
+
className: "border-border text-text-muted border-b px-3 py-2 text-xs font-medium",
|
|
147
|
+
children: "Operations"
|
|
148
|
+
}, undefined, false, undefined, this)
|
|
149
|
+
]
|
|
150
|
+
}, undefined, true, undefined, this)
|
|
151
|
+
}, undefined, false, undefined, this),
|
|
152
|
+
/* @__PURE__ */ jsxDEV("tbody", {
|
|
153
|
+
children: groups.length === 0 ? /* @__PURE__ */ jsxDEV("tr", {
|
|
154
|
+
children: /* @__PURE__ */ jsxDEV("td", {
|
|
155
|
+
colSpan: 3,
|
|
156
|
+
className: "text-text-muted px-4 py-10 text-center",
|
|
157
|
+
children: "No resources registered. Call rack() before rendering this page."
|
|
158
|
+
}, undefined, false, undefined, this)
|
|
159
|
+
}, undefined, false, undefined, this) : groups.map((group) => {
|
|
160
|
+
const nodes = tree.filter((r) => (r.metadata.group ?? "General") === group.name);
|
|
161
|
+
return /* @__PURE__ */ jsxDEV(Fragment, {
|
|
162
|
+
children: [
|
|
163
|
+
/* @__PURE__ */ jsxDEV("tr", {
|
|
164
|
+
className: "bg-surface-muted",
|
|
165
|
+
children: /* @__PURE__ */ jsxDEV("th", {
|
|
166
|
+
colSpan: 3,
|
|
167
|
+
className: "border-border text-text-muted border-b px-3 py-1.5 text-left text-xs font-medium",
|
|
168
|
+
children: [
|
|
169
|
+
group.name,
|
|
170
|
+
/* @__PURE__ */ jsxDEV("span", {
|
|
171
|
+
className: "text-text-subtle ml-2 font-normal",
|
|
172
|
+
children: plural(countNodes(nodes), "resource")
|
|
173
|
+
}, undefined, false, undefined, this)
|
|
174
|
+
]
|
|
175
|
+
}, undefined, true, undefined, this)
|
|
176
|
+
}, undefined, false, undefined, this),
|
|
177
|
+
nodes.map((node) => /* @__PURE__ */ jsxDEV(ResourceRow, {
|
|
178
|
+
node
|
|
179
|
+
}, node.metadata.id, false, undefined, this))
|
|
180
|
+
]
|
|
181
|
+
}, group.name, true, undefined, this);
|
|
182
|
+
})
|
|
183
|
+
}, undefined, false, undefined, this)
|
|
184
|
+
]
|
|
185
|
+
}, undefined, true, undefined, this)
|
|
186
|
+
}, undefined, false, undefined, this),
|
|
187
|
+
/* @__PURE__ */ jsxDEV("footer", {
|
|
188
|
+
className: "border-border text-text-muted mt-6 flex flex-wrap items-center justify-between gap-2 border-t pt-3 text-xs",
|
|
189
|
+
children: [
|
|
190
|
+
/* @__PURE__ */ jsxDEV("span", {
|
|
191
|
+
children: "elysia-rack"
|
|
192
|
+
}, undefined, false, undefined, this),
|
|
193
|
+
donate.enabled ? /* @__PURE__ */ jsxDEV("a", {
|
|
194
|
+
href: donate.url,
|
|
195
|
+
target: "_blank",
|
|
196
|
+
rel: "noopener noreferrer",
|
|
197
|
+
className: "hover:text-foreground underline-offset-4 hover:underline",
|
|
198
|
+
children: donate.label
|
|
199
|
+
}, undefined, false, undefined, this) : null
|
|
200
|
+
]
|
|
201
|
+
}, undefined, true, undefined, this)
|
|
202
|
+
]
|
|
203
|
+
}, undefined, true, undefined, this)
|
|
204
|
+
}, undefined, false, undefined, this)
|
|
205
|
+
]
|
|
206
|
+
}, undefined, true, undefined, this);
|
|
207
|
+
}
|
|
208
|
+
export {
|
|
209
|
+
Dashboard as default
|
|
210
|
+
};
|