rastack 0.0.44 → 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 +7 -0
- package/admin/AdminApp.tsx +311 -158
- package/admin/adapters.ts +99 -0
- package/admin/httpEngine.ts +56 -0
- package/admin/index.tsx +91 -39
- 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 +149 -14
- package/dist/admin.js +395 -17
- package/dist/dev/harness.d.ts +17 -0
- package/dist/dev/harness.js +39 -1
- package/dist/rastack-admin.d.ts +14 -0
- package/dist/rastack-admin.js +128 -0
- package/dist/rastack.d.ts +1 -1
- package/dist/rastack.js +7 -23
- package/dist/wasm/rastack_wasm.js +1 -1
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/src/dev/harness.ts +44 -0
- package/src/rastack-admin.ts +112 -0
- package/src/rastack.ts +8 -28
- package/test/dev.spec.ts +96 -0
- package/wasm/rastack_wasm.js +1 -1
- package/wasm/rastack_wasm_bg.wasm +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
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
|
+
|
|
5
12
|
### [0.0.44](https://github.com/theserverkid/reactapistack/compare/v0.0.43...v0.0.44) (2026-07-12)
|
|
6
13
|
|
|
7
14
|
|
package/admin/AdminApp.tsx
CHANGED
|
@@ -1,31 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
7
12
|
*/
|
|
8
|
-
|
|
9
13
|
import React, { useEffect, useMemo, useState } from "react";
|
|
10
|
-
import { useRastackClient } from "../provider";
|
|
11
14
|
import {
|
|
15
|
+
AutoForm,
|
|
12
16
|
Button,
|
|
13
17
|
Card,
|
|
14
18
|
DataTable,
|
|
15
|
-
AutoForm,
|
|
16
19
|
Spinner,
|
|
17
20
|
type DataTableColumnInput,
|
|
18
21
|
} from "../components";
|
|
22
|
+
import type { RastackEngine } from "../provider/types";
|
|
19
23
|
import {
|
|
20
24
|
AdminResource,
|
|
21
25
|
adminResources,
|
|
22
26
|
changelistHeaders,
|
|
23
|
-
detailPath,
|
|
24
27
|
findResource,
|
|
25
|
-
parseManifest,
|
|
26
28
|
} from "./adapters";
|
|
27
|
-
import {
|
|
28
|
-
|
|
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";
|
|
29
47
|
|
|
30
48
|
// -- hash routing (dependency-free) -----------------------------------------
|
|
31
49
|
|
|
@@ -60,78 +78,121 @@ const go = (hash: string) => {
|
|
|
60
78
|
location.hash = hash;
|
|
61
79
|
};
|
|
62
80
|
|
|
63
|
-
// --
|
|
81
|
+
// -- chrome -----------------------------------------------------------------
|
|
64
82
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
83
|
+
interface Crumb {
|
|
84
|
+
label: string;
|
|
85
|
+
hash?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function Topbar({ crumbs }: { crumbs: Crumb[] }): React.JSX.Element {
|
|
69
89
|
return (
|
|
70
|
-
<
|
|
71
|
-
<nav
|
|
72
|
-
{
|
|
73
|
-
<
|
|
74
|
-
{i > 0 ? <
|
|
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}
|
|
75
95
|
{c.hash ? (
|
|
76
|
-
<a href={c.hash}
|
|
77
|
-
{c.label}
|
|
78
|
-
</a>
|
|
96
|
+
<a href={c.hash}>{c.label}</a>
|
|
79
97
|
) : (
|
|
80
|
-
<span>{c.label}</span>
|
|
98
|
+
<span className="ras-crumb-current">{c.label}</span>
|
|
81
99
|
)}
|
|
82
|
-
</
|
|
100
|
+
</React.Fragment>
|
|
83
101
|
))}
|
|
84
102
|
</nav>
|
|
85
|
-
|
|
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}
|
|
86
121
|
</div>
|
|
87
122
|
);
|
|
88
123
|
}
|
|
89
124
|
|
|
90
|
-
// --
|
|
125
|
+
// -- index / overview -------------------------------------------------------
|
|
91
126
|
|
|
92
127
|
function IndexScreen(props: { resources: AdminResource[] }): React.JSX.Element {
|
|
93
128
|
const byApp = useMemo(() => {
|
|
94
129
|
const map = new Map<string, AdminResource[]>();
|
|
95
130
|
for (const r of adminResources(props.resources)) {
|
|
96
|
-
|
|
131
|
+
const list = map.get(r.app) ?? [];
|
|
132
|
+
list.push(r);
|
|
133
|
+
map.set(r.app, list);
|
|
97
134
|
}
|
|
98
135
|
return [...map.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
99
136
|
}, [props.resources]);
|
|
100
137
|
|
|
101
138
|
return (
|
|
102
|
-
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
</>
|
|
122
178
|
);
|
|
123
179
|
}
|
|
124
180
|
|
|
125
|
-
|
|
181
|
+
// -- changelist -------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
/** Changelist columns: mono id link, FK chips, boolean dots — everything else default. */
|
|
126
184
|
function columnsFor(resource: AdminResource): DataTableColumnInput[] {
|
|
127
185
|
return changelistHeaders(resource).map((h) => {
|
|
128
186
|
if (h.key === "id") {
|
|
129
187
|
return {
|
|
130
188
|
key: "id",
|
|
131
|
-
header: "
|
|
132
|
-
render: (row: Record<string, unknown
|
|
133
|
-
<a
|
|
134
|
-
|
|
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)}
|
|
135
196
|
</a>
|
|
136
197
|
),
|
|
137
198
|
};
|
|
@@ -144,14 +205,36 @@ function columnsFor(resource: AdminResource): DataTableColumnInput[] {
|
|
|
144
205
|
header: h.name,
|
|
145
206
|
render: (_row: Record<string, unknown>, value: unknown) =>
|
|
146
207
|
value == null || value === "" ? (
|
|
147
|
-
""
|
|
208
|
+
<span className="ras-muted">—</span>
|
|
148
209
|
) : (
|
|
149
|
-
<a
|
|
210
|
+
<a
|
|
211
|
+
className="ras-fk"
|
|
212
|
+
href={`#/${app}/${model}/${String(value)}`}
|
|
213
|
+
onClick={(e) => e.stopPropagation()}
|
|
214
|
+
>
|
|
215
|
+
<IconLink size={12} />
|
|
150
216
|
{model} #{String(value)}
|
|
151
217
|
</a>
|
|
152
218
|
),
|
|
153
219
|
};
|
|
154
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
|
+
}
|
|
155
238
|
return { key: h.key, header: h.name };
|
|
156
239
|
});
|
|
157
240
|
}
|
|
@@ -161,39 +244,80 @@ function ListScreen(props: {
|
|
|
161
244
|
resource: AdminResource;
|
|
162
245
|
}): React.JSX.Element {
|
|
163
246
|
const { engine, resource } = props;
|
|
164
|
-
const
|
|
247
|
+
const table = useResourceTable(engine, resource);
|
|
248
|
+
const { fields: facets } = useFilterModel(engine, resource);
|
|
165
249
|
const columns = useMemo(() => columnsFor(resource), [resource.key]);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
<div
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
>
|
|
181
|
-
<h1 style={{ fontSize: 20, margin: 0 }}>{resource.label}</h1>
|
|
182
|
-
<Button onClick={() => go(`#/${resource.app}/${resource.model}/add`)}>
|
|
183
|
-
Add {resource.label}
|
|
184
|
-
</Button>
|
|
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>
|
|
185
264
|
</div>
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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 }]}
|
|
192
275
|
/>
|
|
193
|
-
|
|
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
|
+
</>
|
|
194
316
|
);
|
|
195
317
|
}
|
|
196
318
|
|
|
319
|
+
// -- add / change -----------------------------------------------------------
|
|
320
|
+
|
|
197
321
|
function FormScreen(props: {
|
|
198
322
|
engine: RastackEngine;
|
|
199
323
|
resource: AdminResource;
|
|
@@ -208,101 +332,130 @@ function FormScreen(props: {
|
|
|
208
332
|
const del = useDeleteRecord(engine, resource);
|
|
209
333
|
const listHash = `#/${resource.app}/${resource.model}/`;
|
|
210
334
|
|
|
211
|
-
// On successful save, return to the changelist.
|
|
212
335
|
useEffect(() => {
|
|
213
336
|
if (done) go(listHash);
|
|
214
337
|
}, [done]);
|
|
215
338
|
|
|
216
339
|
const onDelete = async () => {
|
|
217
340
|
if (!id) return;
|
|
218
|
-
if (typeof confirm === "function" && !confirm(
|
|
341
|
+
if (typeof confirm === "function" && !confirm(`Delete this ${resource.label.toLowerCase()}?`))
|
|
342
|
+
return;
|
|
219
343
|
if (await del(id)) go(listHash);
|
|
220
344
|
};
|
|
221
345
|
|
|
222
346
|
return (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
{
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
{
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
+
</>
|
|
253
400
|
);
|
|
254
401
|
}
|
|
255
402
|
|
|
256
403
|
// -- root -------------------------------------------------------------------
|
|
257
404
|
|
|
258
405
|
export function AdminApp(props: {
|
|
259
|
-
|
|
406
|
+
engine: RastackEngine;
|
|
407
|
+
resources: AdminResource[];
|
|
260
408
|
}): React.JSX.Element {
|
|
261
|
-
const client = useRastackClient();
|
|
262
409
|
const route = useRoute();
|
|
263
|
-
const
|
|
410
|
+
const { engine, resources } = props;
|
|
264
411
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
<Shell crumbs={[{ label: "Admin" }]}>
|
|
268
|
-
<p style={{ color: "#c93c37" }}>
|
|
269
|
-
The WASM engine failed to start. Check the console.
|
|
270
|
-
</p>
|
|
271
|
-
</Shell>
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
if (!client.engine || client.status !== "ready") {
|
|
275
|
-
return (
|
|
276
|
-
<Shell crumbs={[{ label: "Admin" }]}>
|
|
277
|
-
<div style={{ display: "flex", gap: 10, alignItems: "center" }}>
|
|
278
|
-
<Spinner />
|
|
279
|
-
<span>
|
|
280
|
-
{client.status === "seeding"
|
|
281
|
-
? "Seeding the local warehouse…"
|
|
282
|
-
: "Starting the WASM engine…"}
|
|
283
|
-
</span>
|
|
284
|
-
</div>
|
|
285
|
-
</Shell>
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (route.name === "index") return <IndexScreen resources={resources} />;
|
|
412
|
+
const activeKey =
|
|
413
|
+
route.name === "index" ? undefined : `${route.app}.${route.model}`;
|
|
290
414
|
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
+
}
|
|
300
450
|
}
|
|
301
|
-
|
|
302
|
-
return <ListScreen engine={client.engine} resource={resource} />;
|
|
303
|
-
if (route.name === "add")
|
|
304
|
-
return <FormScreen engine={client.engine} resource={resource} />;
|
|
451
|
+
|
|
305
452
|
return (
|
|
306
|
-
|
|
453
|
+
<>
|
|
454
|
+
<AdminStyles />
|
|
455
|
+
<div className="ras-root">
|
|
456
|
+
<Sidebar resources={resources} activeKey={activeKey} />
|
|
457
|
+
<main className="ras-main">{screen}</main>
|
|
458
|
+
</div>
|
|
459
|
+
</>
|
|
307
460
|
);
|
|
308
461
|
}
|