rastack 0.0.43 → 0.0.45
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/CHANGELOG.md +14 -0
- package/admin/AdminApp.tsx +461 -0
- package/admin/adapters.ts +346 -0
- package/admin/httpEngine.ts +56 -0
- package/admin/index.tsx +140 -0
- package/admin/ui/FilterBar.tsx +231 -0
- package/admin/ui/Popover.tsx +54 -0
- package/admin/ui/Sidebar.tsx +86 -0
- package/admin/ui/icons.tsx +146 -0
- package/admin/ui/styles.ts +398 -0
- package/admin/useAdmin.ts +434 -0
- package/dist/admin.js +459 -0
- package/dist/dev/harness.d.ts +70 -0
- package/dist/dev/harness.js +189 -0
- package/dist/rastack-admin.d.ts +14 -0
- package/dist/rastack-admin.js +128 -0
- package/dist/rastack-dev.d.ts +18 -0
- package/dist/rastack-dev.js +197 -0
- package/dist/rastack-wasm-build.d.ts +9 -4
- package/dist/rastack-wasm-build.js +44 -33
- package/dist/rastack.d.ts +2 -3
- package/dist/rastack.js +13 -32
- package/dist/wasm/package.json +4 -0
- package/dist/wasm/rastack_wasm.d.ts +100 -0
- package/dist/wasm/rastack_wasm.js +745 -0
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/dist/wasm/rastack_wasm_bg.wasm.d.ts +28 -0
- package/package.json +6 -2
- package/scripts/build-admin.js +48 -0
- package/scripts/copy-wasm.js +35 -0
- package/src/dev/harness.ts +174 -0
- package/src/rastack-admin.ts +112 -0
- package/src/rastack-dev.ts +197 -0
- package/src/rastack-wasm-build.ts +52 -41
- package/src/rastack.ts +14 -37
- package/test/dev.spec.ts +373 -0
- package/wasm/package.json +4 -0
- package/wasm/rastack_wasm.d.ts +100 -0
- package/wasm/rastack_wasm.js +745 -0
- package/wasm/rastack_wasm_bg.wasm +0 -0
- package/wasm/rastack_wasm_bg.wasm.d.ts +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [0.0.45](https://github.com/theserverkid/reactapistack/compare/v0.0.44...v0.0.45) (2026-07-13)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **admin:** rebuild `rastack admin` as the dogfooded React console with Linear-style filters ([8755fb5](https://github.com/theserverkid/reactapistack/commit/8755fb5e8ce239851e5cd08b15045621419046b9))
|
|
11
|
+
|
|
12
|
+
### [0.0.44](https://github.com/theserverkid/reactapistack/compare/v0.0.43...v0.0.44) (2026-07-12)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **dev:** replace `rastack serve`/`wasm` with `rastack dev` — full in-browser admin over WASM ([6f3f579](https://github.com/theserverkid/reactapistack/commit/6f3f579422624bd0dd3d834b6c0eff47eeef2214))
|
|
18
|
+
|
|
5
19
|
### [0.0.43](https://github.com/theserverkid/reactapistack/compare/v0.0.42...v0.0.43) (2026-07-12)
|
|
6
20
|
|
|
7
21
|
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The admin — a full Django-admin-style, manifest-driven console, built entirely
|
|
3
|
+
* from React API Stack's own pieces: the headless `rastack/components`
|
|
4
|
+
* (`DataTable`, `AutoForm`, `Button`, `Card`, `Badge`…), the `rastack/theme`
|
|
5
|
+
* tokens, and the framework's list/detail contract. It is app-generic (every
|
|
6
|
+
* screen is derived from the compiled manifest) and transport-agnostic: the
|
|
7
|
+
* `engine` prop is the WASM engine under `rastack dev` or an HTTP engine to the
|
|
8
|
+
* real `rastack-server` under `rastack admin` — the UI never learns which.
|
|
9
|
+
*
|
|
10
|
+
* Layout: models on the left (`<Sidebar>`), the changelist table in the main
|
|
11
|
+
* area, a Linear-style filter bar on top (`<FilterBar>`), and add/change forms.
|
|
12
|
+
*/
|
|
13
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
14
|
+
import {
|
|
15
|
+
AutoForm,
|
|
16
|
+
Button,
|
|
17
|
+
Card,
|
|
18
|
+
DataTable,
|
|
19
|
+
Spinner,
|
|
20
|
+
type DataTableColumnInput,
|
|
21
|
+
} from "../components";
|
|
22
|
+
import type { RastackEngine } from "../provider/types";
|
|
23
|
+
import {
|
|
24
|
+
AdminResource,
|
|
25
|
+
adminResources,
|
|
26
|
+
changelistHeaders,
|
|
27
|
+
findResource,
|
|
28
|
+
} from "./adapters";
|
|
29
|
+
import {
|
|
30
|
+
useDeleteRecord,
|
|
31
|
+
useFilterModel,
|
|
32
|
+
useResourceForm,
|
|
33
|
+
useResourceTable,
|
|
34
|
+
} from "./useAdmin";
|
|
35
|
+
import { Sidebar } from "./ui/Sidebar";
|
|
36
|
+
import { FilterBar } from "./ui/FilterBar";
|
|
37
|
+
import { AdminStyles } from "./ui/styles";
|
|
38
|
+
import {
|
|
39
|
+
IconArrowLeft,
|
|
40
|
+
IconChevronRight,
|
|
41
|
+
IconInbox,
|
|
42
|
+
IconLink,
|
|
43
|
+
IconPlus,
|
|
44
|
+
IconTable,
|
|
45
|
+
IconTrash,
|
|
46
|
+
} from "./ui/icons";
|
|
47
|
+
|
|
48
|
+
// -- hash routing (dependency-free) -----------------------------------------
|
|
49
|
+
|
|
50
|
+
type Route =
|
|
51
|
+
| { name: "index" }
|
|
52
|
+
| { name: "list"; app: string; model: string }
|
|
53
|
+
| { name: "add"; app: string; model: string }
|
|
54
|
+
| { name: "change"; app: string; model: string; id: string };
|
|
55
|
+
|
|
56
|
+
function parseRoute(hash: string): Route {
|
|
57
|
+
const parts = hash.replace(/^#\/?/, "").split("/").filter(Boolean);
|
|
58
|
+
if (parts.length < 2) return { name: "index" };
|
|
59
|
+
const [app, model, third] = parts;
|
|
60
|
+
if (!third) return { name: "list", app, model };
|
|
61
|
+
if (third === "add") return { name: "add", app, model };
|
|
62
|
+
return { name: "change", app, model, id: decodeURIComponent(third) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function useRoute(): Route {
|
|
66
|
+
const [hash, setHash] = useState(() =>
|
|
67
|
+
typeof location !== "undefined" ? location.hash : "",
|
|
68
|
+
);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const on = () => setHash(location.hash);
|
|
71
|
+
window.addEventListener("hashchange", on);
|
|
72
|
+
return () => window.removeEventListener("hashchange", on);
|
|
73
|
+
}, []);
|
|
74
|
+
return useMemo(() => parseRoute(hash), [hash]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const go = (hash: string) => {
|
|
78
|
+
location.hash = hash;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// -- chrome -----------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
interface Crumb {
|
|
84
|
+
label: string;
|
|
85
|
+
hash?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function Topbar({ crumbs }: { crumbs: Crumb[] }): React.JSX.Element {
|
|
89
|
+
return (
|
|
90
|
+
<header className="ras-topbar">
|
|
91
|
+
<nav className="ras-crumbs">
|
|
92
|
+
{crumbs.map((c, i) => (
|
|
93
|
+
<React.Fragment key={i}>
|
|
94
|
+
{i > 0 ? <IconChevronRight size={13} className="ras-crumb-sep" /> : null}
|
|
95
|
+
{c.hash ? (
|
|
96
|
+
<a href={c.hash}>{c.label}</a>
|
|
97
|
+
) : (
|
|
98
|
+
<span className="ras-crumb-current">{c.label}</span>
|
|
99
|
+
)}
|
|
100
|
+
</React.Fragment>
|
|
101
|
+
))}
|
|
102
|
+
</nav>
|
|
103
|
+
</header>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function PageHead(props: {
|
|
108
|
+
eyebrow?: string;
|
|
109
|
+
title: string;
|
|
110
|
+
subtitle?: React.ReactNode;
|
|
111
|
+
actions?: React.ReactNode;
|
|
112
|
+
}): React.JSX.Element {
|
|
113
|
+
return (
|
|
114
|
+
<div className="ras-pagehead">
|
|
115
|
+
<div>
|
|
116
|
+
{props.eyebrow ? <div className="ras-eyebrow">{props.eyebrow}</div> : null}
|
|
117
|
+
<h1 className="ras-title">{props.title}</h1>
|
|
118
|
+
{props.subtitle ? <div className="ras-subtitle">{props.subtitle}</div> : null}
|
|
119
|
+
</div>
|
|
120
|
+
{props.actions ? <div>{props.actions}</div> : null}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// -- index / overview -------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
function IndexScreen(props: { resources: AdminResource[] }): React.JSX.Element {
|
|
128
|
+
const byApp = useMemo(() => {
|
|
129
|
+
const map = new Map<string, AdminResource[]>();
|
|
130
|
+
for (const r of adminResources(props.resources)) {
|
|
131
|
+
const list = map.get(r.app) ?? [];
|
|
132
|
+
list.push(r);
|
|
133
|
+
map.set(r.app, list);
|
|
134
|
+
}
|
|
135
|
+
return [...map.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
136
|
+
}, [props.resources]);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<>
|
|
140
|
+
<Topbar crumbs={[{ label: "Overview" }]} />
|
|
141
|
+
<div className="ras-content">
|
|
142
|
+
<PageHead
|
|
143
|
+
eyebrow="React API Stack"
|
|
144
|
+
title="Administration"
|
|
145
|
+
subtitle="Browse and edit every model in the manifest — stored as Apache Iceberg tables on object storage."
|
|
146
|
+
/>
|
|
147
|
+
{byApp.map(([app, models]) => (
|
|
148
|
+
<div key={app} style={{ marginBottom: 28 }}>
|
|
149
|
+
<div className="ras-nav-label" style={{ padding: "0 2px 10px" }}>
|
|
150
|
+
{app}
|
|
151
|
+
</div>
|
|
152
|
+
<div className="ras-cardgrid">
|
|
153
|
+
{models
|
|
154
|
+
.slice()
|
|
155
|
+
.sort((a, b) => a.model.localeCompare(b.model))
|
|
156
|
+
.map((r) => (
|
|
157
|
+
<Card
|
|
158
|
+
key={r.key}
|
|
159
|
+
className="ras-modelcard"
|
|
160
|
+
onClick={() => go(`#/${r.app}/${r.model}/`)}
|
|
161
|
+
>
|
|
162
|
+
<div className="ras-mc-top">
|
|
163
|
+
<span className="ras-mc-icon">
|
|
164
|
+
<IconTable size={19} />
|
|
165
|
+
</span>
|
|
166
|
+
</div>
|
|
167
|
+
<div className="ras-mc-name">{r.label}</div>
|
|
168
|
+
<div className="ras-mc-go">
|
|
169
|
+
View records <IconChevronRight size={14} />
|
|
170
|
+
</div>
|
|
171
|
+
</Card>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
))}
|
|
176
|
+
</div>
|
|
177
|
+
</>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// -- changelist -------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
/** Changelist columns: mono id link, FK chips, boolean dots — everything else default. */
|
|
184
|
+
function columnsFor(resource: AdminResource): DataTableColumnInput[] {
|
|
185
|
+
return changelistHeaders(resource).map((h) => {
|
|
186
|
+
if (h.key === "id") {
|
|
187
|
+
return {
|
|
188
|
+
key: "id",
|
|
189
|
+
header: "ID",
|
|
190
|
+
render: (row: Record<string, unknown>) => (
|
|
191
|
+
<a
|
|
192
|
+
className="ras-cell-link ras-id"
|
|
193
|
+
href={`#/${resource.app}/${resource.model}/${String(row.id)}`}
|
|
194
|
+
>
|
|
195
|
+
{String(row.id)}
|
|
196
|
+
</a>
|
|
197
|
+
),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const field = resource.fields.find((f) => f.name === h.key);
|
|
201
|
+
if (field?.type === "fk" && field.relation) {
|
|
202
|
+
const { app, model } = field.relation;
|
|
203
|
+
return {
|
|
204
|
+
key: h.key,
|
|
205
|
+
header: h.name,
|
|
206
|
+
render: (_row: Record<string, unknown>, value: unknown) =>
|
|
207
|
+
value == null || value === "" ? (
|
|
208
|
+
<span className="ras-muted">—</span>
|
|
209
|
+
) : (
|
|
210
|
+
<a
|
|
211
|
+
className="ras-fk"
|
|
212
|
+
href={`#/${app}/${model}/${String(value)}`}
|
|
213
|
+
onClick={(e) => e.stopPropagation()}
|
|
214
|
+
>
|
|
215
|
+
<IconLink size={12} />
|
|
216
|
+
{model} #{String(value)}
|
|
217
|
+
</a>
|
|
218
|
+
),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (field?.type === "bool") {
|
|
222
|
+
return {
|
|
223
|
+
key: h.key,
|
|
224
|
+
header: h.name,
|
|
225
|
+
render: (_row: Record<string, unknown>, value: unknown) => (
|
|
226
|
+
<span className="ras-bool">
|
|
227
|
+
<span
|
|
228
|
+
className="ras-dot"
|
|
229
|
+
style={{
|
|
230
|
+
background: value ? "var(--color-success)" : "var(--color-text-muted)",
|
|
231
|
+
}}
|
|
232
|
+
/>
|
|
233
|
+
{value ? "True" : "False"}
|
|
234
|
+
</span>
|
|
235
|
+
),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return { key: h.key, header: h.name };
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function ListScreen(props: {
|
|
243
|
+
engine: RastackEngine;
|
|
244
|
+
resource: AdminResource;
|
|
245
|
+
}): React.JSX.Element {
|
|
246
|
+
const { engine, resource } = props;
|
|
247
|
+
const table = useResourceTable(engine, resource);
|
|
248
|
+
const { fields: facets } = useFilterModel(engine, resource);
|
|
249
|
+
const columns = useMemo(() => columnsFor(resource), [resource.key]);
|
|
250
|
+
const addHash = `#/${resource.app}/${resource.model}/add`;
|
|
251
|
+
|
|
252
|
+
const empty = (
|
|
253
|
+
<div className="ras-empty">
|
|
254
|
+
<span className="ras-empty-ico">
|
|
255
|
+
<IconInbox size={24} />
|
|
256
|
+
</span>
|
|
257
|
+
<div>
|
|
258
|
+
<h3>Nothing here yet</h3>
|
|
259
|
+
<div style={{ marginTop: 4 }}>
|
|
260
|
+
{Object.keys(table.filters).length || table.search
|
|
261
|
+
? "No records match the current filters."
|
|
262
|
+
: `Add the first ${resource.label.toLowerCase()} to get started.`}
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
<Button size="sm" onClick={() => go(addHash)}>
|
|
266
|
+
<IconPlus size={15} /> Add {resource.label.toLowerCase()}
|
|
267
|
+
</Button>
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<>
|
|
273
|
+
<Topbar
|
|
274
|
+
crumbs={[{ label: "Overview", hash: "#/" }, { label: resource.label }]}
|
|
275
|
+
/>
|
|
276
|
+
<div className="ras-content">
|
|
277
|
+
<PageHead
|
|
278
|
+
eyebrow={resource.app}
|
|
279
|
+
title={resource.label}
|
|
280
|
+
subtitle={
|
|
281
|
+
table.query.isLoading
|
|
282
|
+
? "Loading…"
|
|
283
|
+
: `${table.itemCount} ${table.itemCount === 1 ? "record" : "records"}`
|
|
284
|
+
}
|
|
285
|
+
actions={
|
|
286
|
+
<Button onClick={() => go(addHash)}>
|
|
287
|
+
<IconPlus size={16} /> Add {resource.label.toLowerCase()}
|
|
288
|
+
</Button>
|
|
289
|
+
}
|
|
290
|
+
/>
|
|
291
|
+
|
|
292
|
+
<FilterBar
|
|
293
|
+
facets={facets}
|
|
294
|
+
filters={table.filters}
|
|
295
|
+
setFilter={table.setFilter}
|
|
296
|
+
removeFilter={table.removeFilter}
|
|
297
|
+
clearFilters={table.clearFilters}
|
|
298
|
+
search={table.search}
|
|
299
|
+
setSearch={table.setSearch}
|
|
300
|
+
searchable={resource.search.length > 0}
|
|
301
|
+
/>
|
|
302
|
+
|
|
303
|
+
<Card flush className="ras-tablecard">
|
|
304
|
+
<DataTable
|
|
305
|
+
query={table.query}
|
|
306
|
+
columns={columns}
|
|
307
|
+
searchable={false}
|
|
308
|
+
emptyMessage={empty}
|
|
309
|
+
onRowClick={(row) =>
|
|
310
|
+
go(`#/${resource.app}/${resource.model}/${String(row.id)}`)
|
|
311
|
+
}
|
|
312
|
+
/>
|
|
313
|
+
</Card>
|
|
314
|
+
</div>
|
|
315
|
+
</>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// -- add / change -----------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
function FormScreen(props: {
|
|
322
|
+
engine: RastackEngine;
|
|
323
|
+
resource: AdminResource;
|
|
324
|
+
id?: string;
|
|
325
|
+
}): React.JSX.Element {
|
|
326
|
+
const { engine, resource, id } = props;
|
|
327
|
+
const { model, loading, loadError, submitError, done } = useResourceForm(
|
|
328
|
+
engine,
|
|
329
|
+
resource,
|
|
330
|
+
id,
|
|
331
|
+
);
|
|
332
|
+
const del = useDeleteRecord(engine, resource);
|
|
333
|
+
const listHash = `#/${resource.app}/${resource.model}/`;
|
|
334
|
+
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
if (done) go(listHash);
|
|
337
|
+
}, [done]);
|
|
338
|
+
|
|
339
|
+
const onDelete = async () => {
|
|
340
|
+
if (!id) return;
|
|
341
|
+
if (typeof confirm === "function" && !confirm(`Delete this ${resource.label.toLowerCase()}?`))
|
|
342
|
+
return;
|
|
343
|
+
if (await del(id)) go(listHash);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<>
|
|
348
|
+
<Topbar
|
|
349
|
+
crumbs={[
|
|
350
|
+
{ label: "Overview", hash: "#/" },
|
|
351
|
+
{ label: resource.label, hash: listHash },
|
|
352
|
+
{ label: id ? `Edit #${id}` : "Add" },
|
|
353
|
+
]}
|
|
354
|
+
/>
|
|
355
|
+
<div className="ras-content">
|
|
356
|
+
<PageHead
|
|
357
|
+
eyebrow={resource.label}
|
|
358
|
+
title={id ? `Edit ${resource.label.toLowerCase()}` : `Add ${resource.label.toLowerCase()}`}
|
|
359
|
+
subtitle={id ? `Record #${id}` : "Create a new record."}
|
|
360
|
+
/>
|
|
361
|
+
|
|
362
|
+
{loadError ? (
|
|
363
|
+
<Card style={{ padding: 20, borderColor: "var(--color-danger)" }}>
|
|
364
|
+
<span style={{ color: "var(--color-danger)" }}>{loadError}</span>
|
|
365
|
+
</Card>
|
|
366
|
+
) : loading ? (
|
|
367
|
+
<div className="ras-boot" style={{ minHeight: 160 }}>
|
|
368
|
+
<Spinner /> Loading…
|
|
369
|
+
</div>
|
|
370
|
+
) : (
|
|
371
|
+
<>
|
|
372
|
+
<Card className="ras-formgrid" style={{ padding: 22 }}>
|
|
373
|
+
<AutoForm
|
|
374
|
+
form={model}
|
|
375
|
+
onCancel={() => go(listHash)}
|
|
376
|
+
submitLabel={id ? "Save changes" : `Create ${resource.label.toLowerCase()}`}
|
|
377
|
+
/>
|
|
378
|
+
{submitError ? (
|
|
379
|
+
<div style={{ color: "var(--color-danger)", marginTop: 14, fontSize: 13 }}>
|
|
380
|
+
{submitError}
|
|
381
|
+
</div>
|
|
382
|
+
) : null}
|
|
383
|
+
</Card>
|
|
384
|
+
|
|
385
|
+
{id ? (
|
|
386
|
+
<div className="ras-formgrid ras-danger-zone">
|
|
387
|
+
<div>
|
|
388
|
+
<div className="ras-dz-title">Delete this {resource.label.toLowerCase()}</div>
|
|
389
|
+
<div className="ras-dz-sub">This permanently removes the record from the table.</div>
|
|
390
|
+
</div>
|
|
391
|
+
<Button variant="danger" onClick={onDelete}>
|
|
392
|
+
<IconTrash size={15} /> Delete
|
|
393
|
+
</Button>
|
|
394
|
+
</div>
|
|
395
|
+
) : null}
|
|
396
|
+
</>
|
|
397
|
+
)}
|
|
398
|
+
</div>
|
|
399
|
+
</>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// -- root -------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
export function AdminApp(props: {
|
|
406
|
+
engine: RastackEngine;
|
|
407
|
+
resources: AdminResource[];
|
|
408
|
+
}): React.JSX.Element {
|
|
409
|
+
const route = useRoute();
|
|
410
|
+
const { engine, resources } = props;
|
|
411
|
+
|
|
412
|
+
const activeKey =
|
|
413
|
+
route.name === "index" ? undefined : `${route.app}.${route.model}`;
|
|
414
|
+
|
|
415
|
+
let screen: React.ReactNode;
|
|
416
|
+
if (route.name === "index") {
|
|
417
|
+
screen = <IndexScreen resources={resources} />;
|
|
418
|
+
} else {
|
|
419
|
+
const resource = findResource(resources, route.app, route.model);
|
|
420
|
+
if (!resource) {
|
|
421
|
+
screen = (
|
|
422
|
+
<>
|
|
423
|
+
<Topbar crumbs={[{ label: "Overview", hash: "#/" }, { label: "Not found" }]} />
|
|
424
|
+
<div className="ras-content">
|
|
425
|
+
<div className="ras-empty">
|
|
426
|
+
<span className="ras-empty-ico">
|
|
427
|
+
<IconArrowLeft size={22} />
|
|
428
|
+
</span>
|
|
429
|
+
<h3>Unknown resource</h3>
|
|
430
|
+
<div>
|
|
431
|
+
<code>
|
|
432
|
+
{route.app}.{route.model}
|
|
433
|
+
</code>{" "}
|
|
434
|
+
is not in the manifest.
|
|
435
|
+
</div>
|
|
436
|
+
<Button size="sm" variant="outline" onClick={() => go("#/")}>
|
|
437
|
+
Back to overview
|
|
438
|
+
</Button>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
</>
|
|
442
|
+
);
|
|
443
|
+
} else if (route.name === "list") {
|
|
444
|
+
screen = <ListScreen engine={engine} resource={resource} />;
|
|
445
|
+
} else if (route.name === "add") {
|
|
446
|
+
screen = <FormScreen engine={engine} resource={resource} />;
|
|
447
|
+
} else {
|
|
448
|
+
screen = <FormScreen engine={engine} resource={resource} id={route.id} />;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return (
|
|
453
|
+
<>
|
|
454
|
+
<AdminStyles />
|
|
455
|
+
<div className="ras-root">
|
|
456
|
+
<Sidebar resources={resources} activeKey={activeKey} />
|
|
457
|
+
<main className="ras-main">{screen}</main>
|
|
458
|
+
</div>
|
|
459
|
+
</>
|
|
460
|
+
);
|
|
461
|
+
}
|