elysia-rack 0.2.0 → 0.3.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 -15
- 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-b8sq8rbd.js +19 -0
- package/dist/index-rg87f3v1.js +174 -0
- package/dist/panel-app.js +27 -11
- package/dist/panel.css +2 -1
- package/dist/rack/dashboard.d.ts +3 -0
- package/dist/rack/index.js +1 -0
- package/dist/rack/types/metadata.d.ts +2 -1
- package/dist/react/donate.d.ts +10 -0
- package/dist/react/index.js +6 -2
- package/dist/react/page/Dashboard.d.ts +2 -0
- package/dist/react/page/Panel.d.ts +2 -0
- package/dist/react/page/components/Icon.d.ts +10 -0
- package/dist/react/page/components/Masthead.d.ts +5 -4
- package/dist/react/types.d.ts +5 -0
- package/package.json +12 -3
- package/dist/Dashboard-2j561p7c.js +0 -225
- package/dist/Dashboard-5mt9hnbw.js +0 -252
- package/dist/Dashboard-s8nfyf7s.js +0 -39
- package/dist/Dashboard-xpstr7fr.js +0 -172
- package/dist/Panel-3yrk3krj.js +0 -914
- package/dist/Panel-b7jhgvnm.js +0 -916
- package/dist/Panel-pkvqf5v8.js +0 -912
- package/dist/index-tgk4jt4d.js +0 -63
- package/dist/index-zc7rgvmm.js +0 -13
package/README.md
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
# elysia-rack
|
|
2
2
|
|
|
3
|
+
[](https://ko-fi.com/notoofly_manu) [](https://www.npmjs.com/package/elysia-rack)
|
|
4
|
+
|
|
3
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+.
|
|
4
6
|
|
|
7
|
+
Full usage guide: [docs/USAGE.md](docs/USAGE.md).
|
|
8
|
+
|
|
9
|
+
## Documentation
|
|
10
|
+
|
|
11
|
+
End-user documentation covers installation, configuration, daily usage, and troubleshooting: [docs/index.md](docs/index.md).
|
|
12
|
+
|
|
5
13
|
## Install
|
|
6
14
|
|
|
7
15
|
```bash
|
|
@@ -83,7 +91,7 @@ work through plain `GET` navigation without JavaScript).
|
|
|
83
91
|
|
|
84
92
|
The panel ships create / edit / delete UI driven by the same API:
|
|
85
93
|
|
|
86
|
-
-
|
|
94
|
+
- `New record` opens a create dialog (fields derived from row columns,
|
|
87
95
|
primary key optional). Submit sends `POST /` with an auto-generated
|
|
88
96
|
`Idempotency-Key`. Tables without known columns fall back to a raw
|
|
89
97
|
JSON body field.
|
|
@@ -145,7 +153,7 @@ Conventions:
|
|
|
145
153
|
- String route ids are coerced to numbers for integer columns
|
|
146
154
|
(e.g. `serial`); anything else passes through. Malformed ids resolve
|
|
147
155
|
to `404`, missing rows to `404`.
|
|
148
|
-
- `searchable` fields use SQL `
|
|
156
|
+
- `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.
|
|
149
157
|
- `settings.softDelete: true` writes a timestamp to `deletedAt`
|
|
150
158
|
(override with `settings.deletedAtField`) instead of deleting, and
|
|
151
159
|
reads skip soft-deleted rows.
|
|
@@ -153,7 +161,7 @@ Conventions:
|
|
|
153
161
|
|
|
154
162
|
## Dashboard
|
|
155
163
|
|
|
156
|
-
`dashboard()`
|
|
164
|
+
`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.
|
|
157
165
|
|
|
158
166
|
Recommended setup:
|
|
159
167
|
|
|
@@ -163,7 +171,7 @@ import { reactPlugin } from "elysia-rack/react";
|
|
|
163
171
|
|
|
164
172
|
const app = new Elysia()
|
|
165
173
|
.use(reactPlugin()) // pages are optional — defaults to dashboard + panel
|
|
166
|
-
.use(dashboard({ title: "My Panel", path: "/" })) // →
|
|
174
|
+
.use(dashboard({ title: "My Panel", path: "/" })) // → Dashboard at /
|
|
167
175
|
.use(rack("/catalog/products", { model, metadata: { id: "products", group: "Catalog", order: 1 } }))
|
|
168
176
|
.use(rack("/catalog/variants", { model, metadata: { id: "variants", group: "Catalog", order: 2, parent: "products" } }));
|
|
169
177
|
// → Panel at /catalog/products now shows sidebar with Catalog > Products > Variants
|
|
@@ -180,6 +188,9 @@ rack("/catalog/variants", { metadata: { id: "variants", parent: "products", grou
|
|
|
180
188
|
|
|
181
189
|
Helpers: `listRacks()`, `getRack(id)`, `getRackTree()`, `flatRackTree()`, `buildRackTree(racks)`, `clearRacks()` from `elysia-rack`.
|
|
182
190
|
|
|
191
|
+
### Donate — disable on website
|
|
192
|
+
Donate link (Ko-fi) tampil di Dashboard & Panel footer. Disable via `dashboard({ donate: false })` atau global `reactPlugin({ donate: false })` (default `true`).
|
|
193
|
+
|
|
183
194
|
You can still render the dashboard manually:
|
|
184
195
|
|
|
185
196
|
```ts
|
|
@@ -222,14 +233,14 @@ rack("/catalog/products", {
|
|
|
222
233
|
pluralLabel: "Products", // shown in sidebar
|
|
223
234
|
group: "Catalog", // sidebar group
|
|
224
235
|
order: 1, // smaller = higher
|
|
225
|
-
icon: "
|
|
236
|
+
icon: "package", // Lucide icon name (kebab-case or PascalCase)
|
|
226
237
|
parent: "catalog-root", // nest under another id
|
|
227
238
|
hidden: false
|
|
228
239
|
}
|
|
229
240
|
});
|
|
230
241
|
```
|
|
231
242
|
|
|
232
|
-
**Decide what users can do.** Turn operations on/off. If you disable `create`, the
|
|
243
|
+
**Decide what users can do.** Turn operations on/off. If you disable `create`, the “New record” button disappears automatically.
|
|
233
244
|
|
|
234
245
|
```ts
|
|
235
246
|
rack("/catalog/products", {
|
|
@@ -288,9 +299,9 @@ rack("/catalog/products", {
|
|
|
288
299
|
|
|
289
300
|
That’s the whole `rack()` surface. Start with `model`, then add `metadata` → `query` → `validation/authorization` as your app grows.
|
|
290
301
|
|
|
291
|
-
### 2) `dashboard()
|
|
302
|
+
### 2) `dashboard()`: one line, navigation overview
|
|
292
303
|
|
|
293
|
-
`dashboard()` mounts `GET /`
|
|
304
|
+
`dashboard()` mounts `GET /` and renders a sidebar plus a table of every resource registered with `rack()`.
|
|
294
305
|
|
|
295
306
|
```ts
|
|
296
307
|
import { dashboard, rack } from "elysia-rack";
|
|
@@ -298,7 +309,7 @@ import { reactPlugin } from "elysia-rack/react";
|
|
|
298
309
|
|
|
299
310
|
const app = new Elysia()
|
|
300
311
|
.use(reactPlugin()) // 1) UI engine — pages optional
|
|
301
|
-
.use(dashboard({ title: "My Shop", path: "/" })) // 2)
|
|
312
|
+
.use(dashboard({ title: "My Shop", path: "/" })) // 2) Dashboard at /
|
|
302
313
|
.use(rack("/catalog/products", { model, metadata: { id: "products", group: "Catalog", order: 1 } }))
|
|
303
314
|
.use(rack("/catalog/variants", { model, metadata: { id: "variants", parent: "products", group: "Catalog", order: 2 } }));
|
|
304
315
|
// visit /catalog/products → panel shows sidebar with Catalog > Products > Variants
|
|
@@ -399,13 +410,13 @@ Fallback is `dist/panel.css`.
|
|
|
399
410
|
|
|
400
411
|
Static assets are served under `/__rack/*`:
|
|
401
412
|
|
|
402
|
-
- `dist/panel.css` — panel stylesheet (
|
|
413
|
+
- `dist/panel.css` — panel stylesheet (built with Tailwind CSS)
|
|
403
414
|
- `dist/panel-theme.js` — dark/light toggle
|
|
404
415
|
- `dist/panel-app.js` — live query + actions (imports `components/href.ts`, `components/panelForm.ts`)
|
|
405
416
|
|
|
406
417
|
Pages reference `/__rack/panel.css`, `/__rack/panel-theme.js` and
|
|
407
|
-
`/__rack/panel-app.js` via `reactPlugin`.
|
|
408
|
-
|
|
418
|
+
`/__rack/panel-app.js` via `reactPlugin`. Run `bun run build` to produce
|
|
419
|
+
`dist/` (`build:css` compiles the stylesheet, `build:js` bundles the clients).
|
|
409
420
|
|
|
410
421
|
## Panel theme
|
|
411
422
|
|
|
@@ -415,9 +426,10 @@ static and included in the published package.
|
|
|
415
426
|
chart, selection, focus, overlay, navigation, table, form, sidebar),
|
|
416
427
|
shadcn/Tailwind compatible, mapped via `@theme inline`.
|
|
417
428
|
- `theme/dark.css` — `.dark` overrides.
|
|
418
|
-
- `theme/
|
|
419
|
-
|
|
420
|
-
|
|
429
|
+
- `theme/rack.css` — default theme (black, white, dark blue, gold;
|
|
430
|
+
Poppins for UI, Chocolate Classical Sans for display headings).
|
|
431
|
+
Remove its import for the neutral look.
|
|
432
|
+
- `theme/components.css` — base styles, dialog reset, reduced motion.
|
|
421
433
|
|
|
422
434
|
Dark mode flips automatically: stored choice (`localStorage`), else
|
|
423
435
|
`prefers-color-scheme`. The `◐ Theme` button in the masthead toggles it.
|
|
@@ -442,6 +454,16 @@ bun run typecheck # tsc --noEmit
|
|
|
442
454
|
bun run build # dist/ (js via bun build + .d.ts via tsc)
|
|
443
455
|
```
|
|
444
456
|
|
|
457
|
+
## Donate
|
|
458
|
+
|
|
459
|
+
Support pengembangan elysia-rack — traktir kopi via Ko-fi:
|
|
460
|
+
|
|
461
|
+
[](https://ko-fi.com/notoofly_manu)
|
|
462
|
+
|
|
463
|
+
> https://ko-fi.com/notoofly_manu
|
|
464
|
+
|
|
465
|
+
Terima kasih sudah mendukung open-source!
|
|
466
|
+
|
|
445
467
|
## License
|
|
446
468
|
|
|
447
469
|
MIT © notoofly
|
|
@@ -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
|
+
};
|