elysia-rack 0.1.1 → 0.1.2
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 +74 -24
- package/dist/Dashboard-2j561p7c.js +225 -0
- package/dist/Dashboard-5mt9hnbw.js +252 -0
- package/dist/app.html +14 -0
- package/dist/index-4wxvmd8a.js +65 -0
- package/dist/index-tgk4jt4d.js +63 -0
- package/dist/panel.css +1 -1
- package/dist/rack/dashboard.d.ts +71 -0
- package/dist/rack/index.d.ts +2 -1
- package/dist/rack/index.js +27 -3
- package/dist/rack/registry.d.ts +16 -0
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +133 -13
- package/dist/react/page/components/Sidebar.d.ts +1 -0
- package/dist/react/types.d.ts +14 -0
- package/package.json +4 -9
package/README.md
CHANGED
|
@@ -151,14 +151,36 @@ Conventions:
|
|
|
151
151
|
reads skip soft-deleted rows.
|
|
152
152
|
- `settings.returning: false` omits `data` from mutation responses.
|
|
153
153
|
|
|
154
|
-
## Dashboard
|
|
154
|
+
## Dashboard
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
Recommended setup uses `react()`, `dashboard()`, and `rack()`. Each `rack()`
|
|
157
|
+
stores its metadata tree in memory (registry) and `dashboard()` loads it at
|
|
158
|
+
render time via `getRackTree()`.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { dashboard, rack } from "elysia-rack";
|
|
162
|
+
import { pages, reactPlugin } from "elysia-rack/react";
|
|
163
|
+
|
|
164
|
+
const app = new Elysia()
|
|
165
|
+
.use(reactPlugin({ pages }))
|
|
166
|
+
.use(dashboard({ title: "My Panel", path: "/" }))
|
|
167
|
+
.use(rack("/catalog/products", { model, metadata: { id: "products", group: "Catalog", order: 1 } }))
|
|
168
|
+
.use(rack("/catalog/variants", { model, metadata: { id: "variants", group: "Catalog", order: 2, parent: "products" } }));
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Tree metadata
|
|
172
|
+
|
|
173
|
+
`metadata.parent` builds a hierarchy. Children are rendered nested under their
|
|
174
|
+
parent in the sidebar and kept ordered by `order` then label.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
rack("/catalog/products", { metadata: { id: "products", group: "Catalog", order: 1 } });
|
|
178
|
+
rack("/catalog/variants", { metadata: { id: "variants", parent: "products", group: "Catalog", order: 2 } });
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Helpers: `listRacks()`, `getRack(id)`, `getRackTree()`, `flatRackTree()`, `buildRackTree(racks)`, `clearRacks()` from `elysia-rack`.
|
|
182
|
+
|
|
183
|
+
The dashboard page itself can also be used manually:
|
|
162
184
|
|
|
163
185
|
```ts
|
|
164
186
|
import { page, pages, reactPlugin } from "elysia-rack/react";
|
|
@@ -174,32 +196,60 @@ const app = new Elysia()
|
|
|
174
196
|
);
|
|
175
197
|
```
|
|
176
198
|
|
|
177
|
-
|
|
178
|
-
|
|
199
|
+
The sidebar groups by `metadata.group`, sorts by `order`, skips `hidden`,
|
|
200
|
+
and shows the selected resource's panel inside an `<iframe>`. Links use
|
|
201
|
+
`?resource=<id>` (no JavaScript required).
|
|
202
|
+
|
|
203
|
+
## HTML template
|
|
204
|
+
|
|
205
|
+
SSR uses `src/react/app.html` as the shell. It is read at runtime, cached,
|
|
206
|
+
and streamed with the React output.
|
|
207
|
+
|
|
208
|
+
```html
|
|
209
|
+
<!doctype html>
|
|
210
|
+
<html><head>
|
|
211
|
+
<title>{{title}}</title>
|
|
212
|
+
<link rel="stylesheet" href="/__rack/panel.css" />
|
|
213
|
+
<!--app-head-->
|
|
214
|
+
</head><body>
|
|
215
|
+
<!--app-->
|
|
216
|
+
</body></html>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- `{{title}}` is replaced (escaped) per page.
|
|
220
|
+
- `<!--app-->` (also `{{body}}`, `<!--app-html-->`) marks where the React stream is injected.
|
|
221
|
+
- Fallback shell is used if the file is missing. `dist/app.html` is a static copy of `src/react/app.html` included in the package.
|
|
179
222
|
|
|
180
|
-
|
|
223
|
+
Override the template by editing `src/react/app.html` (and `dist/app.html` for the published package).
|
|
224
|
+
|
|
225
|
+
## Panel CSS override
|
|
226
|
+
|
|
227
|
+
Override `panel.css` at `react()` config without forking:
|
|
181
228
|
|
|
182
229
|
```ts
|
|
183
|
-
import {
|
|
230
|
+
import { pages, reactPlugin } from "elysia-rack/react";
|
|
184
231
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
232
|
+
// file path (resolved from cwd/dist), raw string, or object
|
|
233
|
+
reactPlugin({ pages, css: "./my-panel.css" });
|
|
234
|
+
reactPlugin({ pages, css: { path: "./my-panel.css" } });
|
|
235
|
+
reactPlugin({ pages, css: { content: ":root{--color-primary:red}" } });
|
|
236
|
+
reactPlugin({ pages, panelCss: "body{...}" }); // alias
|
|
188
237
|
```
|
|
189
238
|
|
|
190
|
-
|
|
239
|
+
If a file path exists it is served; otherwise a raw CSS string is served.
|
|
240
|
+
Fallback is `dist/panel.css`.
|
|
241
|
+
|
|
242
|
+
## Panel assets
|
|
191
243
|
|
|
192
|
-
|
|
193
|
-
`bun run build:assets`):
|
|
244
|
+
Static assets are served under `/__rack/*`:
|
|
194
245
|
|
|
195
|
-
- `
|
|
196
|
-
- `
|
|
197
|
-
- `
|
|
198
|
-
(imports shared `components/href.ts` and `components/panelForm.ts`)
|
|
246
|
+
- `dist/panel.css` — panel stylesheet (prebuilt)
|
|
247
|
+
- `dist/panel-theme.js` — dark/light toggle
|
|
248
|
+
- `dist/panel-app.js` — live query + actions (imports `components/href.ts`, `components/panelForm.ts`)
|
|
199
249
|
|
|
200
250
|
Pages reference `/__rack/panel.css`, `/__rack/panel-theme.js` and
|
|
201
|
-
`/__rack/panel-app.js
|
|
202
|
-
|
|
251
|
+
`/__rack/panel-app.js` via `reactPlugin`. No build step required — assets are
|
|
252
|
+
static and included in the published package.
|
|
203
253
|
|
|
204
254
|
## Panel theme
|
|
205
255
|
|
|
@@ -224,7 +274,7 @@ touching React code.
|
|
|
224
274
|
1. One-time setup: npmjs.com → package → Settings → Trusted Publisher → GitHub Actions (`notoofly/elysia-rack`, workflow `publish.yml`).
|
|
225
275
|
2. Bump `version` in `package.json`.
|
|
226
276
|
3. Create a GitHub Release with tag `v<version>` (e.g. `v0.1.0`).
|
|
227
|
-
4. The `publish` workflow runs automatically: install, typecheck,
|
|
277
|
+
4. The `publish` workflow runs automatically: install, typecheck, test, build, tag-vs-version check, `npm publish --provenance`.
|
|
228
278
|
|
|
229
279
|
## Scripts
|
|
230
280
|
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildRackTree,
|
|
3
|
+
flatRackTree,
|
|
4
|
+
getRackTree
|
|
5
|
+
} from "./index-4wxvmd8a.js";
|
|
6
|
+
import"./index-37x76zdn.js";
|
|
7
|
+
|
|
8
|
+
// src/react/page/components/Breadcrumb.tsx
|
|
9
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
10
|
+
function Breadcrumb({ trail }) {
|
|
11
|
+
return /* @__PURE__ */ jsxDEV("nav", {
|
|
12
|
+
"aria-label": "Breadcrumb",
|
|
13
|
+
className: "pb-3 text-sm",
|
|
14
|
+
children: /* @__PURE__ */ jsxDEV("ol", {
|
|
15
|
+
className: "flex flex-wrap items-center gap-1.5",
|
|
16
|
+
children: trail.map((item, i) => {
|
|
17
|
+
const last = i === trail.length - 1;
|
|
18
|
+
return /* @__PURE__ */ jsxDEV("li", {
|
|
19
|
+
className: "flex items-center gap-1.5",
|
|
20
|
+
children: [
|
|
21
|
+
i > 0 ? /* @__PURE__ */ jsxDEV("span", {
|
|
22
|
+
"aria-hidden": "true",
|
|
23
|
+
className: "text-text-muted",
|
|
24
|
+
children: "/"
|
|
25
|
+
}, undefined, false, undefined, this) : null,
|
|
26
|
+
item.href && !last ? /* @__PURE__ */ jsxDEV("a", {
|
|
27
|
+
href: item.href,
|
|
28
|
+
className: "text-text-muted hover:text-foreground",
|
|
29
|
+
children: item.label
|
|
30
|
+
}, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV("span", {
|
|
31
|
+
"aria-current": last ? "page" : undefined,
|
|
32
|
+
className: last ? "text-foreground font-semibold" : "text-text-muted",
|
|
33
|
+
children: item.label
|
|
34
|
+
}, undefined, false, undefined, this)
|
|
35
|
+
]
|
|
36
|
+
}, `${item.label}-${i}`, true, undefined, this);
|
|
37
|
+
})
|
|
38
|
+
}, undefined, false, undefined, this)
|
|
39
|
+
}, undefined, false, undefined, this);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/react/page/components/Sidebar.tsx
|
|
43
|
+
import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
|
|
44
|
+
function SidebarItemView({ item, depth = 0 }) {
|
|
45
|
+
const hasChildren = item.children && item.children.length > 0;
|
|
46
|
+
return /* @__PURE__ */ jsxDEV2("li", {
|
|
47
|
+
children: [
|
|
48
|
+
/* @__PURE__ */ jsxDEV2("a", {
|
|
49
|
+
href: item.href,
|
|
50
|
+
"aria-current": item.active ? "page" : undefined,
|
|
51
|
+
className: item.active ? "bg-sidebar-primary text-sidebar-primary-foreground flex items-center gap-2 rounded-md px-2 py-1.5 text-sm font-semibold" : "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex items-center gap-2 rounded-md px-2 py-1.5 text-sm",
|
|
52
|
+
style: depth > 0 ? { paddingLeft: `${8 + depth * 12}px` } : undefined,
|
|
53
|
+
children: [
|
|
54
|
+
item.icon ? /* @__PURE__ */ jsxDEV2("span", {
|
|
55
|
+
"aria-hidden": "true",
|
|
56
|
+
children: item.icon
|
|
57
|
+
}, undefined, false, undefined, this) : null,
|
|
58
|
+
/* @__PURE__ */ jsxDEV2("span", {
|
|
59
|
+
children: item.label
|
|
60
|
+
}, undefined, false, undefined, this)
|
|
61
|
+
]
|
|
62
|
+
}, undefined, true, undefined, this),
|
|
63
|
+
hasChildren ? /* @__PURE__ */ jsxDEV2("ul", {
|
|
64
|
+
className: "border-sidebar-border mt-0.5 ml-2 flex flex-col gap-0.5 border-l py-1 pl-2",
|
|
65
|
+
children: item.children.map((child) => /* @__PURE__ */ jsxDEV2(SidebarItemView, {
|
|
66
|
+
item: child,
|
|
67
|
+
depth: depth + 1
|
|
68
|
+
}, child.id, false, undefined, this))
|
|
69
|
+
}, undefined, false, undefined, this) : null
|
|
70
|
+
]
|
|
71
|
+
}, undefined, true, undefined, this);
|
|
72
|
+
}
|
|
73
|
+
function hasActive(items) {
|
|
74
|
+
for (const it of items) {
|
|
75
|
+
if (it.active)
|
|
76
|
+
return true;
|
|
77
|
+
if (it.children && hasActive(it.children))
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
function Sidebar({ title, groups }) {
|
|
83
|
+
return /* @__PURE__ */ jsxDEV2("aside", {
|
|
84
|
+
className: "bg-sidebar text-sidebar-foreground border-sidebar-border min-h-0 w-64 shrink-0 overflow-y-auto rounded-lg border",
|
|
85
|
+
children: [
|
|
86
|
+
/* @__PURE__ */ jsxDEV2("div", {
|
|
87
|
+
className: "border-sidebar-border border-b px-4 py-3",
|
|
88
|
+
children: /* @__PURE__ */ jsxDEV2("p", {
|
|
89
|
+
className: "font-koran text-xl font-black tracking-tight",
|
|
90
|
+
children: title
|
|
91
|
+
}, undefined, false, undefined, this)
|
|
92
|
+
}, undefined, false, undefined, this),
|
|
93
|
+
/* @__PURE__ */ jsxDEV2("nav", {
|
|
94
|
+
className: "flex flex-col gap-4 p-3",
|
|
95
|
+
children: groups.map((group) => /* @__PURE__ */ jsxDEV2("section", {
|
|
96
|
+
children: /* @__PURE__ */ jsxDEV2("details", {
|
|
97
|
+
className: "group",
|
|
98
|
+
open: hasActive(group.items) || undefined,
|
|
99
|
+
children: [
|
|
100
|
+
/* @__PURE__ */ jsxDEV2("summary", {
|
|
101
|
+
className: "font-koran border-sidebar-border flex cursor-pointer list-none items-center justify-between border-b px-2 pb-1 text-base font-bold tracking-tight [&::-webkit-details-marker]:hidden",
|
|
102
|
+
children: [
|
|
103
|
+
/* @__PURE__ */ jsxDEV2("span", {
|
|
104
|
+
children: group.name
|
|
105
|
+
}, undefined, false, undefined, this),
|
|
106
|
+
/* @__PURE__ */ jsxDEV2("span", {
|
|
107
|
+
"aria-hidden": "true",
|
|
108
|
+
className: "text-text-muted text-xs transition group-open:rotate-180",
|
|
109
|
+
children: "▾"
|
|
110
|
+
}, undefined, false, undefined, this)
|
|
111
|
+
]
|
|
112
|
+
}, undefined, true, undefined, this),
|
|
113
|
+
/* @__PURE__ */ jsxDEV2("ul", {
|
|
114
|
+
className: "border-sidebar-border mt-1 ml-2 flex flex-col gap-0.5 border-l py-1 pl-2",
|
|
115
|
+
children: group.items.map((item) => /* @__PURE__ */ jsxDEV2(SidebarItemView, {
|
|
116
|
+
item
|
|
117
|
+
}, item.id, false, undefined, this))
|
|
118
|
+
}, undefined, false, undefined, this)
|
|
119
|
+
]
|
|
120
|
+
}, undefined, true, undefined, this)
|
|
121
|
+
}, group.name, false, undefined, this))
|
|
122
|
+
}, undefined, false, undefined, this)
|
|
123
|
+
]
|
|
124
|
+
}, undefined, true, undefined, this);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/react/page/Dashboard.tsx
|
|
128
|
+
import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
|
|
129
|
+
function itemLabel(r) {
|
|
130
|
+
return r.metadata.pluralLabel ?? r.metadata.label ?? r.metadata.id;
|
|
131
|
+
}
|
|
132
|
+
function buildGroups(racks, selectedId) {
|
|
133
|
+
const byGroup = new Map;
|
|
134
|
+
for (const r of racks) {
|
|
135
|
+
if (r.metadata.hidden)
|
|
136
|
+
continue;
|
|
137
|
+
const name = r.metadata.group ?? "General";
|
|
138
|
+
let group = byGroup.get(name);
|
|
139
|
+
if (!group) {
|
|
140
|
+
group = { name, items: [] };
|
|
141
|
+
byGroup.set(name, group);
|
|
142
|
+
}
|
|
143
|
+
group.items.push({
|
|
144
|
+
id: r.metadata.id,
|
|
145
|
+
label: itemLabel(r),
|
|
146
|
+
icon: r.metadata.icon,
|
|
147
|
+
href: `?resource=${encodeURIComponent(r.metadata.id)}`,
|
|
148
|
+
active: r.metadata.id === selectedId
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const groups = [...byGroup.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
152
|
+
for (const group of groups) {
|
|
153
|
+
const orderOf = (id) => racks.find((r) => r.metadata.id === id)?.metadata.order ?? 0;
|
|
154
|
+
group.items.sort((a, b) => orderOf(a.id) - orderOf(b.id) || a.label.localeCompare(b.label));
|
|
155
|
+
}
|
|
156
|
+
return groups;
|
|
157
|
+
}
|
|
158
|
+
function toSidebarItem(node, selectedId) {
|
|
159
|
+
return {
|
|
160
|
+
id: node.metadata.id,
|
|
161
|
+
label: itemLabel(node),
|
|
162
|
+
icon: node.metadata.icon,
|
|
163
|
+
href: `?resource=${encodeURIComponent(node.metadata.id)}`,
|
|
164
|
+
active: node.metadata.id === selectedId,
|
|
165
|
+
children: node.children.filter((c) => !c.metadata.hidden).map((c) => toSidebarItem(c, selectedId))
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function buildGroupsFromTree(tree, selectedId) {
|
|
169
|
+
const byGroup = new Map;
|
|
170
|
+
for (const root of tree) {
|
|
171
|
+
if (root.metadata.hidden)
|
|
172
|
+
continue;
|
|
173
|
+
const name = root.metadata.group ?? "General";
|
|
174
|
+
let group = byGroup.get(name);
|
|
175
|
+
if (!group) {
|
|
176
|
+
group = { name, items: [] };
|
|
177
|
+
byGroup.set(name, group);
|
|
178
|
+
}
|
|
179
|
+
group.items.push(toSidebarItem(root, selectedId));
|
|
180
|
+
}
|
|
181
|
+
return [...byGroup.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
182
|
+
}
|
|
183
|
+
async function Dashboard(props) {
|
|
184
|
+
const tree = props.racks !== undefined ? buildRackTree(props.racks.filter((r) => !r.metadata.hidden)) : getRackTree().filter((r) => !r.metadata.hidden);
|
|
185
|
+
const flat = props.racks !== undefined ? props.racks.filter((r) => !r.metadata.hidden) : flatRackTree(tree).filter((r) => !r.metadata.hidden);
|
|
186
|
+
const selected = flat.find((r) => r.metadata.id === props.resource) ?? flat[0];
|
|
187
|
+
const groups = tree.length ? buildGroupsFromTree(tree, selected?.metadata.id) : buildGroups(flat, selected?.metadata.id);
|
|
188
|
+
const trail = [
|
|
189
|
+
{ label: "Dashboard", href: "?" },
|
|
190
|
+
...selected?.metadata.group ? [{ label: selected.metadata.group }] : [],
|
|
191
|
+
...selected ? [{ label: itemLabel(selected) }] : []
|
|
192
|
+
];
|
|
193
|
+
return /* @__PURE__ */ jsxDEV3("div", {
|
|
194
|
+
className: "koran-paper font-koran-body text-foreground flex flex-col min-h-screen",
|
|
195
|
+
children: /* @__PURE__ */ jsxDEV3("div", {
|
|
196
|
+
className: "flex min-h-0 flex-1 gap-6 p-4 sm:p-6 h-full",
|
|
197
|
+
children: [
|
|
198
|
+
/* @__PURE__ */ jsxDEV3(Sidebar, {
|
|
199
|
+
title: props.name ?? "Panel",
|
|
200
|
+
groups
|
|
201
|
+
}, undefined, false, undefined, this),
|
|
202
|
+
/* @__PURE__ */ jsxDEV3("main", {
|
|
203
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
204
|
+
children: [
|
|
205
|
+
/* @__PURE__ */ jsxDEV3(Breadcrumb, {
|
|
206
|
+
trail
|
|
207
|
+
}, undefined, false, undefined, this),
|
|
208
|
+
selected ? /* @__PURE__ */ jsxDEV3("iframe", {
|
|
209
|
+
src: selected.path,
|
|
210
|
+
title: itemLabel(selected),
|
|
211
|
+
sandbox: "allow-scripts allow-same-origin allow-forms",
|
|
212
|
+
className: "bg-card min-h-0 w-full flex-1 rounded-lg"
|
|
213
|
+
}, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3("p", {
|
|
214
|
+
className: "text-text-muted border-border rounded-lg border px-4 py-10 text-center",
|
|
215
|
+
children: "No resources registered. Mount a resource with rack() first."
|
|
216
|
+
}, undefined, false, undefined, this)
|
|
217
|
+
]
|
|
218
|
+
}, undefined, true, undefined, this)
|
|
219
|
+
]
|
|
220
|
+
}, undefined, true, undefined, this)
|
|
221
|
+
}, undefined, false, undefined, this);
|
|
222
|
+
}
|
|
223
|
+
export {
|
|
224
|
+
Dashboard as default
|
|
225
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import {
|
|
2
|
+
flatRackTree,
|
|
3
|
+
getRackTree
|
|
4
|
+
} from "./index-tgk4jt4d.js";
|
|
5
|
+
import"./index-37x76zdn.js";
|
|
6
|
+
|
|
7
|
+
// src/react/page/components/Breadcrumb.tsx
|
|
8
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
9
|
+
function Breadcrumb({ trail }) {
|
|
10
|
+
return /* @__PURE__ */ jsxDEV("nav", {
|
|
11
|
+
"aria-label": "Breadcrumb",
|
|
12
|
+
className: "pb-3 text-sm",
|
|
13
|
+
children: /* @__PURE__ */ jsxDEV("ol", {
|
|
14
|
+
className: "flex flex-wrap items-center gap-1.5",
|
|
15
|
+
children: trail.map((item, i) => {
|
|
16
|
+
const last = i === trail.length - 1;
|
|
17
|
+
return /* @__PURE__ */ jsxDEV("li", {
|
|
18
|
+
className: "flex items-center gap-1.5",
|
|
19
|
+
children: [
|
|
20
|
+
i > 0 ? /* @__PURE__ */ jsxDEV("span", {
|
|
21
|
+
"aria-hidden": "true",
|
|
22
|
+
className: "text-text-muted",
|
|
23
|
+
children: "/"
|
|
24
|
+
}, undefined, false, undefined, this) : null,
|
|
25
|
+
item.href && !last ? /* @__PURE__ */ jsxDEV("a", {
|
|
26
|
+
href: item.href,
|
|
27
|
+
className: "text-text-muted hover:text-foreground",
|
|
28
|
+
children: item.label
|
|
29
|
+
}, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV("span", {
|
|
30
|
+
"aria-current": last ? "page" : undefined,
|
|
31
|
+
className: last ? "text-foreground font-semibold" : "text-text-muted",
|
|
32
|
+
children: item.label
|
|
33
|
+
}, undefined, false, undefined, this)
|
|
34
|
+
]
|
|
35
|
+
}, `${item.label}-${i}`, true, undefined, this);
|
|
36
|
+
})
|
|
37
|
+
}, undefined, false, undefined, this)
|
|
38
|
+
}, undefined, false, undefined, this);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/react/page/components/Sidebar.tsx
|
|
42
|
+
import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
|
|
43
|
+
function SidebarItemView({ item, depth = 0 }) {
|
|
44
|
+
const hasChildren = item.children && item.children.length > 0;
|
|
45
|
+
return /* @__PURE__ */ jsxDEV2("li", {
|
|
46
|
+
children: [
|
|
47
|
+
/* @__PURE__ */ jsxDEV2("a", {
|
|
48
|
+
href: item.href,
|
|
49
|
+
"aria-current": item.active ? "page" : undefined,
|
|
50
|
+
className: item.active ? "bg-sidebar-primary text-sidebar-primary-foreground flex items-center gap-2 rounded-md px-2 py-1.5 text-sm font-semibold" : "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex items-center gap-2 rounded-md px-2 py-1.5 text-sm",
|
|
51
|
+
style: depth > 0 ? { paddingLeft: `${8 + depth * 12}px` } : undefined,
|
|
52
|
+
children: [
|
|
53
|
+
item.icon ? /* @__PURE__ */ jsxDEV2("span", {
|
|
54
|
+
"aria-hidden": "true",
|
|
55
|
+
children: item.icon
|
|
56
|
+
}, undefined, false, undefined, this) : null,
|
|
57
|
+
/* @__PURE__ */ jsxDEV2("span", {
|
|
58
|
+
children: item.label
|
|
59
|
+
}, undefined, false, undefined, this)
|
|
60
|
+
]
|
|
61
|
+
}, undefined, true, undefined, this),
|
|
62
|
+
hasChildren ? /* @__PURE__ */ jsxDEV2("ul", {
|
|
63
|
+
className: "border-sidebar-border mt-0.5 ml-2 flex flex-col gap-0.5 border-l py-1 pl-2",
|
|
64
|
+
children: item.children.map((child) => /* @__PURE__ */ jsxDEV2(SidebarItemView, {
|
|
65
|
+
item: child,
|
|
66
|
+
depth: depth + 1
|
|
67
|
+
}, child.id, false, undefined, this))
|
|
68
|
+
}, undefined, false, undefined, this) : null
|
|
69
|
+
]
|
|
70
|
+
}, undefined, true, undefined, this);
|
|
71
|
+
}
|
|
72
|
+
function hasActive(items) {
|
|
73
|
+
for (const it of items) {
|
|
74
|
+
if (it.active)
|
|
75
|
+
return true;
|
|
76
|
+
if (it.children && hasActive(it.children))
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
function Sidebar({ title, groups }) {
|
|
82
|
+
return /* @__PURE__ */ jsxDEV2("aside", {
|
|
83
|
+
className: "bg-sidebar text-sidebar-foreground border-sidebar-border min-h-0 w-64 shrink-0 overflow-y-auto rounded-lg border",
|
|
84
|
+
children: [
|
|
85
|
+
/* @__PURE__ */ jsxDEV2("div", {
|
|
86
|
+
className: "border-sidebar-border border-b px-4 py-3",
|
|
87
|
+
children: /* @__PURE__ */ jsxDEV2("p", {
|
|
88
|
+
className: "font-koran text-xl font-black tracking-tight",
|
|
89
|
+
children: title
|
|
90
|
+
}, undefined, false, undefined, this)
|
|
91
|
+
}, undefined, false, undefined, this),
|
|
92
|
+
/* @__PURE__ */ jsxDEV2("nav", {
|
|
93
|
+
className: "flex flex-col gap-4 p-3",
|
|
94
|
+
children: groups.map((group) => /* @__PURE__ */ jsxDEV2("section", {
|
|
95
|
+
children: /* @__PURE__ */ jsxDEV2("details", {
|
|
96
|
+
className: "group",
|
|
97
|
+
open: hasActive(group.items) || undefined,
|
|
98
|
+
children: [
|
|
99
|
+
/* @__PURE__ */ jsxDEV2("summary", {
|
|
100
|
+
className: "font-koran border-sidebar-border flex cursor-pointer list-none items-center justify-between border-b px-2 pb-1 text-base font-bold tracking-tight [&::-webkit-details-marker]:hidden",
|
|
101
|
+
children: [
|
|
102
|
+
/* @__PURE__ */ jsxDEV2("span", {
|
|
103
|
+
children: group.name
|
|
104
|
+
}, undefined, false, undefined, this),
|
|
105
|
+
/* @__PURE__ */ jsxDEV2("span", {
|
|
106
|
+
"aria-hidden": "true",
|
|
107
|
+
className: "text-text-muted text-xs transition group-open:rotate-180",
|
|
108
|
+
children: "▾"
|
|
109
|
+
}, undefined, false, undefined, this)
|
|
110
|
+
]
|
|
111
|
+
}, undefined, true, undefined, this),
|
|
112
|
+
/* @__PURE__ */ jsxDEV2("ul", {
|
|
113
|
+
className: "border-sidebar-border mt-1 ml-2 flex flex-col gap-0.5 border-l py-1 pl-2",
|
|
114
|
+
children: group.items.map((item) => /* @__PURE__ */ jsxDEV2(SidebarItemView, {
|
|
115
|
+
item
|
|
116
|
+
}, item.id, false, undefined, this))
|
|
117
|
+
}, undefined, false, undefined, this)
|
|
118
|
+
]
|
|
119
|
+
}, undefined, true, undefined, this)
|
|
120
|
+
}, group.name, false, undefined, this))
|
|
121
|
+
}, undefined, false, undefined, this)
|
|
122
|
+
]
|
|
123
|
+
}, undefined, true, undefined, this);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/react/page/Dashboard.tsx
|
|
127
|
+
import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
|
|
128
|
+
function itemLabel(r) {
|
|
129
|
+
return r.metadata.pluralLabel ?? r.metadata.label ?? r.metadata.id;
|
|
130
|
+
}
|
|
131
|
+
function buildGroups(racks, selectedId) {
|
|
132
|
+
const byGroup = new Map;
|
|
133
|
+
for (const r of racks) {
|
|
134
|
+
if (r.metadata.hidden)
|
|
135
|
+
continue;
|
|
136
|
+
const name = r.metadata.group ?? "General";
|
|
137
|
+
let group = byGroup.get(name);
|
|
138
|
+
if (!group) {
|
|
139
|
+
group = { name, items: [] };
|
|
140
|
+
byGroup.set(name, group);
|
|
141
|
+
}
|
|
142
|
+
group.items.push({
|
|
143
|
+
id: r.metadata.id,
|
|
144
|
+
label: itemLabel(r),
|
|
145
|
+
icon: r.metadata.icon,
|
|
146
|
+
href: `?resource=${encodeURIComponent(r.metadata.id)}`,
|
|
147
|
+
active: r.metadata.id === selectedId
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const groups = [...byGroup.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
151
|
+
for (const group of groups) {
|
|
152
|
+
const orderOf = (id) => racks.find((r) => r.metadata.id === id)?.metadata.order ?? 0;
|
|
153
|
+
group.items.sort((a, b) => orderOf(a.id) - orderOf(b.id) || a.label.localeCompare(b.label));
|
|
154
|
+
}
|
|
155
|
+
return groups;
|
|
156
|
+
}
|
|
157
|
+
function toSidebarItem(node, selectedId) {
|
|
158
|
+
return {
|
|
159
|
+
id: node.metadata.id,
|
|
160
|
+
label: itemLabel(node),
|
|
161
|
+
icon: node.metadata.icon,
|
|
162
|
+
href: `?resource=${encodeURIComponent(node.metadata.id)}`,
|
|
163
|
+
active: node.metadata.id === selectedId,
|
|
164
|
+
children: node.children.filter((c) => !c.metadata.hidden).map((c) => toSidebarItem(c, selectedId))
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function buildGroupsFromTree(tree, selectedId) {
|
|
168
|
+
const byGroup = new Map;
|
|
169
|
+
for (const root of tree) {
|
|
170
|
+
if (root.metadata.hidden)
|
|
171
|
+
continue;
|
|
172
|
+
const name = root.metadata.group ?? "General";
|
|
173
|
+
let group = byGroup.get(name);
|
|
174
|
+
if (!group) {
|
|
175
|
+
group = { name, items: [] };
|
|
176
|
+
byGroup.set(name, group);
|
|
177
|
+
}
|
|
178
|
+
group.items.push(toSidebarItem(root, selectedId));
|
|
179
|
+
}
|
|
180
|
+
return [...byGroup.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
181
|
+
}
|
|
182
|
+
function treeFromRacks(racks) {
|
|
183
|
+
const nodes = new Map;
|
|
184
|
+
for (const r of racks)
|
|
185
|
+
nodes.set(r.metadata.id, { ...r, children: [] });
|
|
186
|
+
const roots = [];
|
|
187
|
+
for (const node of nodes.values()) {
|
|
188
|
+
const parentId = node.metadata.parent;
|
|
189
|
+
if (parentId && nodes.has(parentId) && parentId !== node.metadata.id) {
|
|
190
|
+
nodes.get(parentId).children.push(node);
|
|
191
|
+
} else {
|
|
192
|
+
roots.push(node);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const sort = (list) => {
|
|
196
|
+
list.sort((a, b) => {
|
|
197
|
+
const oa = a.metadata.order ?? 0;
|
|
198
|
+
const ob = b.metadata.order ?? 0;
|
|
199
|
+
if (oa !== ob)
|
|
200
|
+
return oa - ob;
|
|
201
|
+
return itemLabel(a).localeCompare(itemLabel(b));
|
|
202
|
+
});
|
|
203
|
+
for (const n of list)
|
|
204
|
+
if (n.children.length)
|
|
205
|
+
sort(n.children);
|
|
206
|
+
};
|
|
207
|
+
sort(roots);
|
|
208
|
+
return roots;
|
|
209
|
+
}
|
|
210
|
+
async function Dashboard(props) {
|
|
211
|
+
const tree = props.racks !== undefined ? treeFromRacks(props.racks.filter((r) => !r.metadata.hidden)) : getRackTree().filter((r) => !r.metadata.hidden);
|
|
212
|
+
const flat = props.racks !== undefined ? props.racks.filter((r) => !r.metadata.hidden) : flatRackTree(tree).filter((r) => !r.metadata.hidden);
|
|
213
|
+
const selected = flat.find((r) => r.metadata.id === props.resource) ?? flat[0];
|
|
214
|
+
const groups = tree.length ? buildGroupsFromTree(tree, selected?.metadata.id) : buildGroups(flat, selected?.metadata.id);
|
|
215
|
+
const trail = [
|
|
216
|
+
{ label: "Dashboard", href: "?" },
|
|
217
|
+
...selected?.metadata.group ? [{ label: selected.metadata.group }] : [],
|
|
218
|
+
...selected ? [{ label: itemLabel(selected) }] : []
|
|
219
|
+
];
|
|
220
|
+
return /* @__PURE__ */ jsxDEV3("div", {
|
|
221
|
+
className: "koran-paper font-koran-body text-foreground flex flex-col min-h-screen",
|
|
222
|
+
children: /* @__PURE__ */ jsxDEV3("div", {
|
|
223
|
+
className: "flex min-h-0 flex-1 gap-6 p-4 sm:p-6 h-full",
|
|
224
|
+
children: [
|
|
225
|
+
/* @__PURE__ */ jsxDEV3(Sidebar, {
|
|
226
|
+
title: props.name ?? "Panel",
|
|
227
|
+
groups
|
|
228
|
+
}, undefined, false, undefined, this),
|
|
229
|
+
/* @__PURE__ */ jsxDEV3("main", {
|
|
230
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
231
|
+
children: [
|
|
232
|
+
/* @__PURE__ */ jsxDEV3(Breadcrumb, {
|
|
233
|
+
trail
|
|
234
|
+
}, undefined, false, undefined, this),
|
|
235
|
+
selected ? /* @__PURE__ */ jsxDEV3("iframe", {
|
|
236
|
+
src: selected.path,
|
|
237
|
+
title: itemLabel(selected),
|
|
238
|
+
sandbox: "allow-scripts allow-same-origin allow-forms",
|
|
239
|
+
className: "bg-card min-h-0 w-full flex-1 rounded-lg"
|
|
240
|
+
}, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3("p", {
|
|
241
|
+
className: "text-text-muted border-border rounded-lg border px-4 py-10 text-center",
|
|
242
|
+
children: "No resources registered. Mount a resource with rack() first."
|
|
243
|
+
}, undefined, false, undefined, this)
|
|
244
|
+
]
|
|
245
|
+
}, undefined, true, undefined, this)
|
|
246
|
+
]
|
|
247
|
+
}, undefined, true, undefined, this)
|
|
248
|
+
}, undefined, false, undefined, this);
|
|
249
|
+
}
|
|
250
|
+
export {
|
|
251
|
+
Dashboard as default
|
|
252
|
+
};
|
package/dist/app.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>{{title}}</title>
|
|
7
|
+
<link rel="stylesheet" href="/__rack/panel.css" />
|
|
8
|
+
<script type="module" src="/__rack/panel-theme.js"></script>
|
|
9
|
+
<!--app-head-->
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<!--app-->
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|