elysia-rack 0.1.1
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/LICENSE +21 -0
- package/README.md +241 -0
- package/dist/Dashboard-xpstr7fr.js +172 -0
- package/dist/Panel-fbp3wrsw.js +738 -0
- package/dist/index-37x76zdn.js +4 -0
- package/dist/index-zc7rgvmm.js +13 -0
- package/dist/panel-app.js +1 -0
- package/dist/panel-theme.js +1 -0
- package/dist/panel.css +1 -0
- package/dist/rack/adapter.d.ts +5 -0
- package/dist/rack/adapters/drizzle.d.ts +2 -0
- package/dist/rack/adapters/index.d.ts +59 -0
- package/dist/rack/adapters/prisma.d.ts +2 -0
- package/dist/rack/authorization.d.ts +23 -0
- package/dist/rack/idempotency.d.ts +9 -0
- package/dist/rack/index.d.ts +40 -0
- package/dist/rack/index.js +1362 -0
- package/dist/rack/models/drizzle.d.ts +4 -0
- package/dist/rack/models/index.d.ts +24 -0
- package/dist/rack/models/prisma.d.ts +4 -0
- package/dist/rack/openapi.d.ts +6 -0
- package/dist/rack/page.d.ts +33 -0
- package/dist/rack/query.d.ts +22 -0
- package/dist/rack/registry.d.ts +20 -0
- package/dist/rack/types/authorization.d.ts +16 -0
- package/dist/rack/types/idempotency.d.ts +46 -0
- package/dist/rack/types/index.d.ts +89 -0
- package/dist/rack/types/metadata.d.ts +36 -0
- package/dist/rack/types/openapi.d.ts +22 -0
- package/dist/rack/types/operations.d.ts +14 -0
- package/dist/rack/types/page.d.ts +41 -0
- package/dist/rack/types/query.d.ts +30 -0
- package/dist/rack/types/validation.d.ts +19 -0
- package/dist/react/index.d.ts +64 -0
- package/dist/react/index.js +126 -0
- package/dist/react/page/Dashboard.d.ts +7 -0
- package/dist/react/page/Panel.d.ts +36 -0
- package/dist/react/page/client/panel.d.ts +1 -0
- package/dist/react/page/client/theme.d.ts +1 -0
- package/dist/react/page/components/Breadcrumb.d.ts +7 -0
- package/dist/react/page/components/DataTable.d.ts +10 -0
- package/dist/react/page/components/Masthead.d.ts +7 -0
- package/dist/react/page/components/Pagination.d.ts +10 -0
- package/dist/react/page/components/Sidebar.d.ts +16 -0
- package/dist/react/page/components/Toolbar.d.ts +7 -0
- package/dist/react/page/components/href.d.ts +10 -0
- package/dist/react/page/components/panelForm.d.ts +9 -0
- package/dist/react/render.d.ts +2 -0
- package/dist/react/types.d.ts +16 -0
- package/package.json +89 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 notoofly
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# elysia-rack
|
|
2
|
+
|
|
3
|
+
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
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add elysia-rack elysia react react-dom
|
|
9
|
+
# or
|
|
10
|
+
npm i elysia-rack elysia react react-dom
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`elysia`, `react`, and `react-dom` are peer dependencies.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { Elysia } from "elysia";
|
|
19
|
+
import { rack } from "elysia-rack";
|
|
20
|
+
|
|
21
|
+
const app = new Elysia().use(
|
|
22
|
+
rack("/catalog/products", {
|
|
23
|
+
model: { drizzle: { db, table: products } },
|
|
24
|
+
|
|
25
|
+
validation: {
|
|
26
|
+
create: CreateProductSchema,
|
|
27
|
+
replace: ReplaceProductSchema,
|
|
28
|
+
update: UpdateProductSchema,
|
|
29
|
+
params: ProductParamsSchema,
|
|
30
|
+
query: ProductQuerySchema,
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
authorization: {
|
|
34
|
+
list: "products.view",
|
|
35
|
+
create: "products.create",
|
|
36
|
+
delete: async ({ request }) => canDelete(request),
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
query: {
|
|
40
|
+
searchable: ["name", "sku"],
|
|
41
|
+
filterable: ["status", "categoryId"],
|
|
42
|
+
sortable: ["name", "price", "createdAt"],
|
|
43
|
+
defaultSort: { field: "createdAt", direction: "desc" },
|
|
44
|
+
pagination: { default: 20, max: 100 },
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
operations: { create: true, delete: false },
|
|
48
|
+
|
|
49
|
+
metadata: {
|
|
50
|
+
id: "products",
|
|
51
|
+
label: "Product",
|
|
52
|
+
pluralLabel: "Products",
|
|
53
|
+
group: "Catalog",
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
openapi: { tags: ["Products"] },
|
|
57
|
+
settings: { primaryKey: "id", returning: true },
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
app.listen(3000);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Generated routes (all gated by `operations`, default on):
|
|
65
|
+
|
|
66
|
+
| Operation | Method | Path |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| display panel | `GET` | `/` |
|
|
69
|
+
| list data | `QUERY` | `/data` |
|
|
70
|
+
| detail data | `QUERY` | `/data/:id` |
|
|
71
|
+
| create | `POST` | `/` |
|
|
72
|
+
| replace | `PUT` | `/:id` |
|
|
73
|
+
| update | `PATCH` | `/:id` |
|
|
74
|
+
| delete | `DELETE` | `/:id` |
|
|
75
|
+
|
|
76
|
+
`GET /` renders the React panel via `page("/panel")` (customize with the
|
|
77
|
+
`page` option, disable with `page: { enabled: false }`). All data reads go
|
|
78
|
+
through the `QUERY` method under `/data`. The panel UI calls the same
|
|
79
|
+
endpoint live via fetch (progressive enhancement — links and forms still
|
|
80
|
+
work through plain `GET` navigation without JavaScript).
|
|
81
|
+
|
|
82
|
+
## Panel actions
|
|
83
|
+
|
|
84
|
+
The panel ships create / edit / delete UI driven by the same API:
|
|
85
|
+
|
|
86
|
+
- `+ New` opens a create dialog (fields derived from row columns,
|
|
87
|
+
primary key optional). Submit sends `POST /` with an auto-generated
|
|
88
|
+
`Idempotency-Key`. Tables without known columns fall back to a raw
|
|
89
|
+
JSON body field.
|
|
90
|
+
- Row `Edit` loads the row via `QUERY /data/:id` into a dialog and saves
|
|
91
|
+
with `PATCH /:id`. Empty inputs are skipped (partial update).
|
|
92
|
+
- Row `Delete` confirms natively, then `DELETE /:id`.
|
|
93
|
+
- Checkbox selection enables the bulk bar: `Delete selected` loops
|
|
94
|
+
`DELETE /:id`, and the field applier loops `PATCH /:id` with the
|
|
95
|
+
chosen field/value. `Clear` resets the selection.
|
|
96
|
+
|
|
97
|
+
Action UI follows `operations`: disable `create`/`update`/`delete` and
|
|
98
|
+
the corresponding buttons, dialogs and bulk controls disappear.
|
|
99
|
+
|
|
100
|
+
## Idempotency (POST)
|
|
101
|
+
|
|
102
|
+
`POST /` requires an idempotency key by default. Send a client-generated
|
|
103
|
+
key per create attempt:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
curl -X POST localhost:3000/catalog/products \
|
|
107
|
+
-H 'Content-Type: application/json' \
|
|
108
|
+
-H 'Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000' \
|
|
109
|
+
-d '{"name":"Keyboard"}'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Replays with the same key return the stored `201` response with an
|
|
113
|
+
`Idempotent-Replayed: true` header instead of re-executing. Missing keys
|
|
114
|
+
are rejected with `400`. Tune via the `idempotency` option:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
rack("/catalog/products", {
|
|
118
|
+
model: { drizzle: { db, table: products } },
|
|
119
|
+
idempotency: {
|
|
120
|
+
required: false, // allow keyless POST (default true)
|
|
121
|
+
header: "X-Idempotency-Key",
|
|
122
|
+
ttl: 3600, // replay window in seconds (default 86400)
|
|
123
|
+
store: myRedisStore, // shared store for multi-instance deploys
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## ORM adapters
|
|
129
|
+
|
|
130
|
+
`model` selects the adapter. Drizzle needs `drizzle-orm` installed
|
|
131
|
+
(optional peer); Prisma needs no extra dependency — pass any client
|
|
132
|
+
delegate (e.g. `prisma.product`):
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
// Drizzle (Postgres, SQLite, ...)
|
|
136
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
137
|
+
model: { drizzle: { db: drizzle(client), table: products } },
|
|
138
|
+
|
|
139
|
+
// Prisma
|
|
140
|
+
model: { prisma: { client: prisma, model: prisma.product } },
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Conventions:
|
|
144
|
+
|
|
145
|
+
- String route ids are coerced to numbers for integer columns
|
|
146
|
+
(e.g. `serial`); anything else passes through. Malformed ids resolve
|
|
147
|
+
to `404`, missing rows to `404`.
|
|
148
|
+
- `searchable` fields use SQL `LIKE` (case-sensitivity follows the DB).
|
|
149
|
+
- `settings.softDelete: true` writes a timestamp to `deletedAt`
|
|
150
|
+
(override with `settings.deletedAtField`) instead of deleting, and
|
|
151
|
+
reads skip soft-deleted rows.
|
|
152
|
+
- `settings.returning: false` omits `data` from mutation responses.
|
|
153
|
+
|
|
154
|
+
## Dashboard (sidebar, breadcrumb, iframe)
|
|
155
|
+
|
|
156
|
+
Every `rack()` call registers its path, metadata and operations in a
|
|
157
|
+
registry (`listRacks()` / `clearRacks()` from `elysia-rack`). The default
|
|
158
|
+
`/dashboard` page builds a sidebar (grouped by `metadata.group`, ordered
|
|
159
|
+
by `metadata.order`, skipping `hidden`), a breadcrumb
|
|
160
|
+
(Dashboard / Group / Label), and shows the selected resource's `GET`
|
|
161
|
+
panel inside an `<iframe>`:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { page, pages, reactPlugin } from "elysia-rack/react";
|
|
165
|
+
|
|
166
|
+
const app = new Elysia()
|
|
167
|
+
.use(reactPlugin({ pages }))
|
|
168
|
+
.use(rack("/catalog/products", { model, metadata: { ... } }))
|
|
169
|
+
.get("/", ({ query }) =>
|
|
170
|
+
page("/dashboard", {
|
|
171
|
+
name: "My Shop",
|
|
172
|
+
resource: typeof query.resource === "string" ? query.resource : undefined,
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Sidebar links navigate with `?resource=<id>` (full reload, no JavaScript
|
|
178
|
+
needed). Without a selection the first registered resource is shown.
|
|
179
|
+
|
|
180
|
+
React panel rendering via the `./react` subpath:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import { page, pages, reactPlugin } from "elysia-rack/react";
|
|
184
|
+
|
|
185
|
+
const app = new Elysia()
|
|
186
|
+
.use(reactPlugin({ pages }))
|
|
187
|
+
.get("/", () => page("/dashboard", { name: "Ada" }));
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Panel assets (Vite)
|
|
191
|
+
|
|
192
|
+
CSS, Tailwind and browser JavaScript go through Vite (`vite.config.ts`,
|
|
193
|
+
`bun run build:assets`):
|
|
194
|
+
|
|
195
|
+
- `src/react/page/panel.css` — stylesheet entry (Tailwind v4) → `dist/panel.css`
|
|
196
|
+
- `src/react/page/client/theme.ts` — dark/light toggle → `dist/panel-theme.js`
|
|
197
|
+
- `src/react/page/client/panel.ts` — live query + actions → `dist/panel-app.js`
|
|
198
|
+
(imports shared `components/href.ts` and `components/panelForm.ts`)
|
|
199
|
+
|
|
200
|
+
Pages reference `/__rack/panel.css`, `/__rack/panel-theme.js` and
|
|
201
|
+
`/__rack/panel-app.js`, served by `reactPlugin` from `dist/` (run the
|
|
202
|
+
build before `dev`; CI and `prepublishOnly` already chain it).
|
|
203
|
+
|
|
204
|
+
## Panel theme
|
|
205
|
+
|
|
206
|
+
`src/react/page/panel.css` is the stylesheet entry:
|
|
207
|
+
|
|
208
|
+
- `theme/tokens.css` — full semantic tokens (surface, text, status,
|
|
209
|
+
chart, selection, focus, overlay, navigation, table, form, sidebar),
|
|
210
|
+
shadcn/Tailwind compatible, mapped via `@theme inline`.
|
|
211
|
+
- `theme/dark.css` — `.dark` overrides.
|
|
212
|
+
- `theme/koran.css` — default newspaper theme (warm paper, serif
|
|
213
|
+
headlines). Remove its import for the neutral look.
|
|
214
|
+
- `theme/components.css` — newspaper component classes + base styles.
|
|
215
|
+
|
|
216
|
+
Dark mode flips automatically: stored choice (`localStorage`), else
|
|
217
|
+
`prefers-color-scheme`. The `◐ Theme` button in the masthead toggles it.
|
|
218
|
+
Components must only use semantic utilities (`bg-background`,
|
|
219
|
+
`text-foreground`, `bg-table-row-hover`, …) so themes swap without
|
|
220
|
+
touching React code.
|
|
221
|
+
|
|
222
|
+
## Release (maintainer)
|
|
223
|
+
|
|
224
|
+
1. One-time setup: npmjs.com → package → Settings → Trusted Publisher → GitHub Actions (`notoofly/elysia-rack`, workflow `publish.yml`).
|
|
225
|
+
2. Bump `version` in `package.json`.
|
|
226
|
+
3. Create a GitHub Release with tag `v<version>` (e.g. `v0.1.0`).
|
|
227
|
+
4. The `publish` workflow runs automatically: install, typecheck, asset build, test, build, tag-vs-version check, `npm publish --provenance`.
|
|
228
|
+
|
|
229
|
+
## Scripts
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
bun install # install dependencies
|
|
233
|
+
bun run dev # run src/playground.ts
|
|
234
|
+
bun test # unit tests (bun:test)
|
|
235
|
+
bun run typecheck # tsc --noEmit
|
|
236
|
+
bun run build # dist/ (js via bun build + .d.ts via tsc)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT © notoofly
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listRacks
|
|
3
|
+
} from "./index-zc7rgvmm.js";
|
|
4
|
+
import"./index-37x76zdn.js";
|
|
5
|
+
|
|
6
|
+
// src/react/page/components/Breadcrumb.tsx
|
|
7
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
8
|
+
function Breadcrumb({ trail }) {
|
|
9
|
+
return /* @__PURE__ */ jsxDEV("nav", {
|
|
10
|
+
"aria-label": "Breadcrumb",
|
|
11
|
+
className: "pb-3 text-sm",
|
|
12
|
+
children: /* @__PURE__ */ jsxDEV("ol", {
|
|
13
|
+
className: "flex flex-wrap items-center gap-1.5",
|
|
14
|
+
children: trail.map((item, i) => {
|
|
15
|
+
const last = i === trail.length - 1;
|
|
16
|
+
return /* @__PURE__ */ jsxDEV("li", {
|
|
17
|
+
className: "flex items-center gap-1.5",
|
|
18
|
+
children: [
|
|
19
|
+
i > 0 ? /* @__PURE__ */ jsxDEV("span", {
|
|
20
|
+
"aria-hidden": "true",
|
|
21
|
+
className: "text-text-muted",
|
|
22
|
+
children: "/"
|
|
23
|
+
}, undefined, false, undefined, this) : null,
|
|
24
|
+
item.href && !last ? /* @__PURE__ */ jsxDEV("a", {
|
|
25
|
+
href: item.href,
|
|
26
|
+
className: "text-text-muted hover:text-foreground",
|
|
27
|
+
children: item.label
|
|
28
|
+
}, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV("span", {
|
|
29
|
+
"aria-current": last ? "page" : undefined,
|
|
30
|
+
className: last ? "text-foreground font-semibold" : "text-text-muted",
|
|
31
|
+
children: item.label
|
|
32
|
+
}, undefined, false, undefined, this)
|
|
33
|
+
]
|
|
34
|
+
}, `${item.label}-${i}`, true, undefined, this);
|
|
35
|
+
})
|
|
36
|
+
}, undefined, false, undefined, this)
|
|
37
|
+
}, undefined, false, undefined, this);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/react/page/components/Sidebar.tsx
|
|
41
|
+
import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
|
|
42
|
+
function Sidebar({ title, groups }) {
|
|
43
|
+
return /* @__PURE__ */ jsxDEV2("aside", {
|
|
44
|
+
className: "bg-sidebar text-sidebar-foreground border-sidebar-border min-h-0 w-64 shrink-0 overflow-y-auto rounded-lg border",
|
|
45
|
+
children: [
|
|
46
|
+
/* @__PURE__ */ jsxDEV2("div", {
|
|
47
|
+
className: "border-sidebar-border border-b px-4 py-3",
|
|
48
|
+
children: /* @__PURE__ */ jsxDEV2("p", {
|
|
49
|
+
className: "font-koran text-xl font-black tracking-tight",
|
|
50
|
+
children: title
|
|
51
|
+
}, undefined, false, undefined, this)
|
|
52
|
+
}, undefined, false, undefined, this),
|
|
53
|
+
/* @__PURE__ */ jsxDEV2("nav", {
|
|
54
|
+
className: "flex flex-col gap-4 p-3",
|
|
55
|
+
children: groups.map((group) => /* @__PURE__ */ jsxDEV2("section", {
|
|
56
|
+
children: /* @__PURE__ */ jsxDEV2("details", {
|
|
57
|
+
className: "group",
|
|
58
|
+
open: group.items.some((item) => item.active) || undefined,
|
|
59
|
+
children: [
|
|
60
|
+
/* @__PURE__ */ jsxDEV2("summary", {
|
|
61
|
+
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",
|
|
62
|
+
children: [
|
|
63
|
+
/* @__PURE__ */ jsxDEV2("span", {
|
|
64
|
+
children: group.name
|
|
65
|
+
}, undefined, false, undefined, this),
|
|
66
|
+
/* @__PURE__ */ jsxDEV2("span", {
|
|
67
|
+
"aria-hidden": "true",
|
|
68
|
+
className: "text-text-muted text-xs transition group-open:rotate-180",
|
|
69
|
+
children: "▾"
|
|
70
|
+
}, undefined, false, undefined, this)
|
|
71
|
+
]
|
|
72
|
+
}, undefined, true, undefined, this),
|
|
73
|
+
/* @__PURE__ */ jsxDEV2("ul", {
|
|
74
|
+
className: "border-sidebar-border mt-1 ml-2 flex flex-col gap-0.5 border-l py-1 pl-2",
|
|
75
|
+
children: group.items.map((item) => /* @__PURE__ */ jsxDEV2("li", {
|
|
76
|
+
children: /* @__PURE__ */ jsxDEV2("a", {
|
|
77
|
+
href: item.href,
|
|
78
|
+
"aria-current": item.active ? "page" : undefined,
|
|
79
|
+
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",
|
|
80
|
+
children: [
|
|
81
|
+
item.icon ? /* @__PURE__ */ jsxDEV2("span", {
|
|
82
|
+
"aria-hidden": "true",
|
|
83
|
+
children: item.icon
|
|
84
|
+
}, undefined, false, undefined, this) : null,
|
|
85
|
+
/* @__PURE__ */ jsxDEV2("span", {
|
|
86
|
+
children: item.label
|
|
87
|
+
}, undefined, false, undefined, this)
|
|
88
|
+
]
|
|
89
|
+
}, undefined, true, undefined, this)
|
|
90
|
+
}, item.id, false, undefined, this))
|
|
91
|
+
}, undefined, false, undefined, this)
|
|
92
|
+
]
|
|
93
|
+
}, undefined, true, undefined, this)
|
|
94
|
+
}, group.name, false, undefined, this))
|
|
95
|
+
}, undefined, false, undefined, this)
|
|
96
|
+
]
|
|
97
|
+
}, undefined, true, undefined, this);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/react/page/Dashboard.tsx
|
|
101
|
+
import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
|
|
102
|
+
function itemLabel(r) {
|
|
103
|
+
return r.metadata.pluralLabel ?? r.metadata.label ?? r.metadata.id;
|
|
104
|
+
}
|
|
105
|
+
function buildGroups(racks, selectedId) {
|
|
106
|
+
const byGroup = new Map;
|
|
107
|
+
for (const r of racks) {
|
|
108
|
+
if (r.metadata.hidden)
|
|
109
|
+
continue;
|
|
110
|
+
const name = r.metadata.group ?? "General";
|
|
111
|
+
let group = byGroup.get(name);
|
|
112
|
+
if (!group) {
|
|
113
|
+
group = { name, items: [] };
|
|
114
|
+
byGroup.set(name, group);
|
|
115
|
+
}
|
|
116
|
+
group.items.push({
|
|
117
|
+
id: r.metadata.id,
|
|
118
|
+
label: itemLabel(r),
|
|
119
|
+
icon: r.metadata.icon,
|
|
120
|
+
href: `?resource=${encodeURIComponent(r.metadata.id)}`,
|
|
121
|
+
active: r.metadata.id === selectedId
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const groups = [...byGroup.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
125
|
+
for (const group of groups) {
|
|
126
|
+
const orderOf = (id) => racks.find((r) => r.metadata.id === id)?.metadata.order ?? 0;
|
|
127
|
+
group.items.sort((a, b) => orderOf(a.id) - orderOf(b.id) || a.label.localeCompare(b.label));
|
|
128
|
+
}
|
|
129
|
+
return groups;
|
|
130
|
+
}
|
|
131
|
+
async function Dashboard(props) {
|
|
132
|
+
const racks = (props.racks ?? listRacks()).filter((r) => !r.metadata.hidden);
|
|
133
|
+
const selected = racks.find((r) => r.metadata.id === props.resource) ?? racks[0];
|
|
134
|
+
const groups = buildGroups(racks, selected?.metadata.id);
|
|
135
|
+
const trail = [
|
|
136
|
+
{ label: "Dashboard", href: "?" },
|
|
137
|
+
...selected?.metadata.group ? [{ label: selected.metadata.group }] : [],
|
|
138
|
+
...selected ? [{ label: itemLabel(selected) }] : []
|
|
139
|
+
];
|
|
140
|
+
return /* @__PURE__ */ jsxDEV3("div", {
|
|
141
|
+
className: "koran-paper font-koran-body text-foreground flex flex-col min-h-screen",
|
|
142
|
+
children: /* @__PURE__ */ jsxDEV3("div", {
|
|
143
|
+
className: "flex min-h-0 flex-1 gap-6 p-4 sm:p-6 h-full",
|
|
144
|
+
children: [
|
|
145
|
+
/* @__PURE__ */ jsxDEV3(Sidebar, {
|
|
146
|
+
title: props.name ?? "Panel",
|
|
147
|
+
groups
|
|
148
|
+
}, undefined, false, undefined, this),
|
|
149
|
+
/* @__PURE__ */ jsxDEV3("main", {
|
|
150
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
151
|
+
children: [
|
|
152
|
+
/* @__PURE__ */ jsxDEV3(Breadcrumb, {
|
|
153
|
+
trail
|
|
154
|
+
}, undefined, false, undefined, this),
|
|
155
|
+
selected ? /* @__PURE__ */ jsxDEV3("iframe", {
|
|
156
|
+
src: selected.path,
|
|
157
|
+
title: itemLabel(selected),
|
|
158
|
+
sandbox: "allow-scripts allow-same-origin allow-forms",
|
|
159
|
+
className: "bg-card min-h-0 w-full flex-1 rounded-lg"
|
|
160
|
+
}, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3("p", {
|
|
161
|
+
className: "text-text-muted border-border rounded-lg border px-4 py-10 text-center",
|
|
162
|
+
children: "No resources registered. Mount a resource with rack() first."
|
|
163
|
+
}, undefined, false, undefined, this)
|
|
164
|
+
]
|
|
165
|
+
}, undefined, true, undefined, this)
|
|
166
|
+
]
|
|
167
|
+
}, undefined, true, undefined, this)
|
|
168
|
+
}, undefined, false, undefined, this);
|
|
169
|
+
}
|
|
170
|
+
export {
|
|
171
|
+
Dashboard as default
|
|
172
|
+
};
|