hazo_admin 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/CHANGE_LOG.md +11 -0
- package/README.md +86 -0
- package/SETUP_CHECKLIST.md +36 -0
- package/dist/api/env_migrate_handler.d.ts +19 -0
- package/dist/api/env_migrate_handler.d.ts.map +1 -0
- package/dist/api/env_migrate_handler.js +44 -0
- package/dist/api/index.d.ts +46 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +257 -0
- package/dist/components/admin_app.d.ts +10 -0
- package/dist/components/admin_app.d.ts.map +1 -0
- package/dist/components/admin_app.js +32 -0
- package/dist/components/admin_layout.d.ts +15 -0
- package/dist/components/admin_layout.d.ts.map +1 -0
- package/dist/components/admin_layout.js +37 -0
- package/dist/components/admin_nav.d.ts +31 -0
- package/dist/components/admin_nav.d.ts.map +1 -0
- package/dist/components/admin_nav.js +73 -0
- package/dist/components/env_migration_panel.d.ts +6 -0
- package/dist/components/env_migration_panel.d.ts.map +1 -0
- package/dist/components/env_migration_panel.js +218 -0
- package/dist/components/health_panel.d.ts +5 -0
- package/dist/components/health_panel.d.ts.map +1 -0
- package/dist/components/health_panel.js +23 -0
- package/dist/components/jobs_panel.d.ts +6 -0
- package/dist/components/jobs_panel.d.ts.map +1 -0
- package/dist/components/jobs_panel.js +21 -0
- package/dist/components/logs_panel.d.ts +6 -0
- package/dist/components/logs_panel.d.ts.map +1 -0
- package/dist/components/logs_panel.js +17 -0
- package/dist/components/users_panel.d.ts +7 -0
- package/dist/components/users_panel.d.ts.map +1 -0
- package/dist/components/users_panel.js +21 -0
- package/dist/index.client.d.ts +8 -0
- package/dist/index.client.d.ts.map +1 -0
- package/dist/index.client.js +5 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.ui.d.ts +17 -0
- package/dist/index.ui.d.ts.map +1 -0
- package/dist/index.ui.js +9 -0
- package/dist/lib/admin_gate.server.d.ts +37 -0
- package/dist/lib/admin_gate.server.d.ts.map +1 -0
- package/dist/lib/admin_gate.server.js +58 -0
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +1 -0
- package/dist/preset/index.d.ts +3 -0
- package/dist/preset/index.d.ts.map +1 -0
- package/dist/preset/index.js +1 -0
- package/dist/preset/page_factory.d.ts +12 -0
- package/dist/preset/page_factory.d.ts.map +1 -0
- package/dist/preset/page_factory.js +57 -0
- package/package.json +139 -0
package/CHANGE_LOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# hazo_admin Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-06-09
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Package scaffold (Phase 0): standard hazo_* package structure
|
|
7
|
+
- `AdminGate`: `adminGate()` decision fn + `withAdminGate()` route HOF
|
|
8
|
+
- `HAZO_ADMIN_PERMISSIONS` constants (`admin_system`, `env.migrate`, `env.mask.manage`)
|
|
9
|
+
- `createAdminPresetRoutes` stub (empty, placeholder for future preset routes)
|
|
10
|
+
- test-app with sidebar, Tailwind, `/autotest` route
|
|
11
|
+
- DB-integration jest suite covering all gate branches
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# hazo_admin
|
|
2
|
+
|
|
3
|
+
Standard site-admin package for hazo apps — auth-gated admin shell + panel kit + drop-in `/admin` preset.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install hazo_admin
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer deps (required): `hazo_core`, `hazo_ui`, `react`, `react-dom`, `next`, `lucide-react`
|
|
12
|
+
Peer deps (optional, each feature-gated): `hazo_auth`, `hazo_connect`, `hazo_logs`, and other hazo_* packages
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
See [SETUP_CHECKLIST.md](./SETUP_CHECKLIST.md) for step-by-step setup.
|
|
17
|
+
|
|
18
|
+
## Tailwind v4 Setup (Required for consuming apps using Tailwind v4)
|
|
19
|
+
|
|
20
|
+
If your consuming app uses Tailwind CSS v4, you **must** configure `@source` to scan this package's Tailwind classes. Add the following to your app's `globals.css` (or main CSS file) AFTER the tailwindcss import:
|
|
21
|
+
|
|
22
|
+
```css
|
|
23
|
+
@import "tailwindcss";
|
|
24
|
+
|
|
25
|
+
/* Required: Enable Tailwind to scan hazo_admin's Tailwind classes */
|
|
26
|
+
@source "../node_modules/hazo_admin/dist";
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Without this directive, Tailwind v4's JIT compiler will not generate CSS for utility classes used in `hazo_admin` (colors, spacing, hover states, etc.), resulting in unstyled or broken layouts.
|
|
30
|
+
|
|
31
|
+
**Note:** If you are using Tailwind v3, you do not need to add `@source`.
|
|
32
|
+
|
|
33
|
+
## AdminGate
|
|
34
|
+
|
|
35
|
+
The `AdminGate` is the auth/permission/scope gate that protects all admin routes.
|
|
36
|
+
|
|
37
|
+
### `adminGate(request, opts)`
|
|
38
|
+
|
|
39
|
+
Non-throwing decision function. Returns an `AdminGateResult`:
|
|
40
|
+
- `{ allowed: true, user, permissions, scope_ok? }` — access granted
|
|
41
|
+
- `{ allowed: false, status: 403, reason: 'unauthenticated' | 'forbidden' | 'scope_denied', missing_permissions? }` — access denied
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { adminGate } from 'hazo_admin';
|
|
45
|
+
|
|
46
|
+
const result = await adminGate(request, {
|
|
47
|
+
required_permissions: ['admin_system'],
|
|
48
|
+
scope_id: params.orgId, // optional — for HRBAC scope isolation
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### `withAdminGate(handler, opts)`
|
|
53
|
+
|
|
54
|
+
Route HOF. On denial returns a 403 JSON response; on allow calls the handler.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { withAdminGate } from 'hazo_admin';
|
|
58
|
+
|
|
59
|
+
export const GET = withAdminGate(
|
|
60
|
+
async (request, gate) => {
|
|
61
|
+
// gate.user and gate.permissions are available here
|
|
62
|
+
return Response.json({ ok: true });
|
|
63
|
+
},
|
|
64
|
+
{ required_permissions: ['admin_system'] }
|
|
65
|
+
);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Permission Constants
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { HAZO_ADMIN_PERMISSIONS } from 'hazo_admin/client';
|
|
72
|
+
|
|
73
|
+
// HAZO_ADMIN_PERMISSIONS.ADMIN_SYSTEM = 'admin_system'
|
|
74
|
+
// HAZO_ADMIN_PERMISSIONS.ENV_MIGRATE = 'env.migrate'
|
|
75
|
+
// HAZO_ADMIN_PERMISSIONS.ENV_MASK_MANAGE = 'env.mask.manage'
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**These must exist as rows in the consuming app's `hazo_permissions` table.** The consuming app is responsible for seeding them.
|
|
79
|
+
|
|
80
|
+
## Entries
|
|
81
|
+
|
|
82
|
+
| Import path | Contents | Server-safe |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `hazo_admin` | `adminGate`, `withAdminGate`, `HAZO_ADMIN_PERMISSIONS`, `AdminGateResult` | Server only |
|
|
85
|
+
| `hazo_admin/client` | `HAZO_ADMIN_PERMISSIONS`, types | Yes |
|
|
86
|
+
| `hazo_admin/api` | `createAdminPresetRoutes` stub | Server only |
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# hazo_admin Setup Checklist
|
|
2
|
+
|
|
3
|
+
## 1. Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install hazo_admin hazo_auth hazo_core hazo_ui
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## 2. Seed permission rows
|
|
10
|
+
|
|
11
|
+
In your app's database migration, seed the permission strings that `hazo_admin` depends on:
|
|
12
|
+
|
|
13
|
+
```sql
|
|
14
|
+
INSERT INTO hazo_permissions (id, name) VALUES
|
|
15
|
+
(gen_random_uuid(), 'admin_system'),
|
|
16
|
+
(gen_random_uuid(), 'env.migrate'),
|
|
17
|
+
(gen_random_uuid(), 'env.mask.manage')
|
|
18
|
+
ON CONFLICT (name) DO NOTHING;
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 3. Protect your admin routes
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// app/admin/api/route.ts
|
|
25
|
+
import { withAdminGate } from 'hazo_admin';
|
|
26
|
+
import { HAZO_ADMIN_PERMISSIONS } from 'hazo_admin/client';
|
|
27
|
+
|
|
28
|
+
export const GET = withAdminGate(
|
|
29
|
+
async (req, gate) => Response.json({ user: gate.user.email_address }),
|
|
30
|
+
{ required_permissions: [HAZO_ADMIN_PERMISSIONS.ADMIN_SYSTEM] }
|
|
31
|
+
);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 4. Configure (optional)
|
|
35
|
+
|
|
36
|
+
Copy `config/hazo_admin_config.ini.sample` to `hazo_admin_config.ini` and adjust as needed.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
export declare const ENV_MIGRATE_JOB_TYPE: "env.migrate";
|
|
3
|
+
/**
|
|
4
|
+
* Job handler for env.migrate jobs. Calls hazo_env's runMigration with the
|
|
5
|
+
* job payload plus jobId + progressDir so progress is written to the filesystem.
|
|
6
|
+
*/
|
|
7
|
+
export declare function envMigrateJobHandler(job: {
|
|
8
|
+
id: string;
|
|
9
|
+
payload: unknown;
|
|
10
|
+
}, ctx: {
|
|
11
|
+
log: (...args: any[]) => void;
|
|
12
|
+
}): Promise<unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Register the env.migrate job type with a hazo_jobs worker.
|
|
15
|
+
*/
|
|
16
|
+
export declare function registerEnvJobs(worker: {
|
|
17
|
+
register: (type: string, handler: (job: any, ctx: any) => Promise<unknown>) => void;
|
|
18
|
+
}): void;
|
|
19
|
+
//# sourceMappingURL=env_migrate_handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env_migrate_handler.d.ts","sourceRoot":"","sources":["../../src/api/env_migrate_handler.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAGrB,eAAO,MAAM,oBAAoB,EAAG,aAAsB,CAAC;AAE3D;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,EACrC,GAAG,EAAE;IAAE,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;CAAE,GACrC,OAAO,CAAC,OAAO,CAAC,CAkClB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE;IACtC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;CACrF,GAAG,IAAI,CAEP"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export const ENV_MIGRATE_JOB_TYPE = 'env.migrate';
|
|
4
|
+
/**
|
|
5
|
+
* Job handler for env.migrate jobs. Calls hazo_env's runMigration with the
|
|
6
|
+
* job payload plus jobId + progressDir so progress is written to the filesystem.
|
|
7
|
+
*/
|
|
8
|
+
export async function envMigrateJobHandler(job, ctx) {
|
|
9
|
+
let runMigration;
|
|
10
|
+
let resolveFilesConfig;
|
|
11
|
+
try {
|
|
12
|
+
const hazoEnv = await import('hazo_env');
|
|
13
|
+
runMigration = hazoEnv.runMigration;
|
|
14
|
+
resolveFilesConfig = hazoEnv.resolveFilesConfig;
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
ctx.log('[env_migrate_handler] hazo_env is not installed:', err);
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
const progressDir = path.join(resolveFilesConfig().local.basePath, 'migrations', 'progress');
|
|
21
|
+
const raw = job.payload;
|
|
22
|
+
const payload = typeof raw === 'string' ? JSON.parse(raw)
|
|
23
|
+
: (raw && typeof raw === 'object' ? raw : {});
|
|
24
|
+
const { includeDb, includeFiles, ...rest } = payload;
|
|
25
|
+
try {
|
|
26
|
+
const result = await runMigration({
|
|
27
|
+
...rest,
|
|
28
|
+
include: { db: includeDb !== false, files: includeFiles !== false },
|
|
29
|
+
jobId: job.id,
|
|
30
|
+
progressDir,
|
|
31
|
+
});
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
ctx.log(`[env_migrate_handler] job ${job.id} failed:`, err);
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Register the env.migrate job type with a hazo_jobs worker.
|
|
41
|
+
*/
|
|
42
|
+
export function registerEnvJobs(worker) {
|
|
43
|
+
worker.register(ENV_MIGRATE_JOB_TYPE, envMigrateJobHandler);
|
|
44
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import type { AdminManifest } from '../components/admin_nav.js';
|
|
3
|
+
export type AdminPresetRoutesConfig = {
|
|
4
|
+
/**
|
|
5
|
+
* Returns the DB adapter for hazo_jobs. Must expose a `raw(sql, values)` method
|
|
6
|
+
* (the hazo_jobs adapter contract) — e.g. hazo_connect's `pg` adapter or a
|
|
7
|
+
* better-sqlite3 wrapper. See hazo_jobs README "Quick start".
|
|
8
|
+
*/
|
|
9
|
+
getHazoConnect: () => Promise<any> | any;
|
|
10
|
+
logDirectory?: string;
|
|
11
|
+
jobs?: {
|
|
12
|
+
supervisorUrl?: string;
|
|
13
|
+
supervisorToken?: string;
|
|
14
|
+
/** SQL dialect for the jobs adapter. Defaults to 'pg'; set 'sqlite' for a SQLite DB. */
|
|
15
|
+
dialect?: 'pg' | 'sqlite';
|
|
16
|
+
};
|
|
17
|
+
basePath?: string;
|
|
18
|
+
env?: {
|
|
19
|
+
/** Override default progress dir. Defaults to <dataRoot>/migrations/progress */
|
|
20
|
+
progressDir?: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export declare function createAdminPresetRoutes(manifest: AdminManifest, cfg: AdminPresetRoutesConfig): {
|
|
24
|
+
GET: (request: Request, ctx: {
|
|
25
|
+
params: Promise<{
|
|
26
|
+
path: string[];
|
|
27
|
+
}>;
|
|
28
|
+
}) => Promise<Response>;
|
|
29
|
+
POST: (request: Request, ctx: {
|
|
30
|
+
params: Promise<{
|
|
31
|
+
path: string[];
|
|
32
|
+
}>;
|
|
33
|
+
}) => Promise<Response>;
|
|
34
|
+
PUT: (request: Request, ctx: {
|
|
35
|
+
params: Promise<{
|
|
36
|
+
path: string[];
|
|
37
|
+
}>;
|
|
38
|
+
}) => Promise<Response>;
|
|
39
|
+
DELETE: (request: Request, ctx: {
|
|
40
|
+
params: Promise<{
|
|
41
|
+
path: string[];
|
|
42
|
+
}>;
|
|
43
|
+
}) => Promise<Response>;
|
|
44
|
+
};
|
|
45
|
+
export { envMigrateJobHandler, ENV_MIGRATE_JOB_TYPE, registerEnvJobs, } from './env_migrate_handler.js';
|
|
46
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAIrB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEhE,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;;OAIG;IACH,cAAc,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE;QACL,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,wFAAwF;QACxF,OAAO,CAAC,EAAE,IAAI,GAAG,QAAQ,CAAC;KAC3B,CAAC;IACF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE;QACJ,gFAAgF;QAChF,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;CACH,CAAC;AAuBF,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,EAAE,uBAAuB;mBA+MlE,OAAO,OAAO;QAAE,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC,CAAA;KAAE,KAAG,OAAO,CAAC,QAAQ,CAAC;oBAAzE,OAAO,OAAO;QAAE,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC,CAAA;KAAE,KAAG,OAAO,CAAC,QAAQ,CAAC;mBAAzE,OAAO,OAAO;QAAE,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC,CAAA;KAAE,KAAG,OAAO,CAAC,QAAQ,CAAC;sBAAzE,OAAO,OAAO;QAAE,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC,CAAA;KAAE,KAAG,OAAO,CAAC,QAAQ,CAAC;EAsCnG;AAGD,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,GAChB,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { withAdminGate } from '../lib/admin_gate.server.js';
|
|
5
|
+
function notFound() {
|
|
6
|
+
return new Response(JSON.stringify({ error: { reason: 'not_found' } }), {
|
|
7
|
+
status: 404,
|
|
8
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function notInstalled(pkg) {
|
|
12
|
+
return new Response(JSON.stringify({ error: { reason: `${pkg} not installed` } }), {
|
|
13
|
+
status: 503,
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function jsonToResponse(r) {
|
|
18
|
+
return new Response(JSON.stringify(r.body), {
|
|
19
|
+
status: r.status,
|
|
20
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function createAdminPresetRoutes(manifest, cfg) {
|
|
24
|
+
const jobsSection = manifest.sections.find((s) => s.kind === 'jobs');
|
|
25
|
+
const jobsPermission = jobsSection?.permission ?? 'admin_system';
|
|
26
|
+
const logsSection = manifest.sections.find((s) => s.kind === 'logs');
|
|
27
|
+
const logsPermission = logsSection?.permission ?? 'admin_system';
|
|
28
|
+
const envSection = manifest.sections.find((s) => s.kind === 'env_migration');
|
|
29
|
+
const envPermission = envSection?.permission ?? 'env.migrate';
|
|
30
|
+
async function handleJobs(request, method, segments) {
|
|
31
|
+
const jobsMod = await import('hazo_jobs/server').catch(() => null);
|
|
32
|
+
if (!jobsMod)
|
|
33
|
+
return notInstalled('hazo_jobs');
|
|
34
|
+
const connect = await cfg.getHazoConnect();
|
|
35
|
+
const jobsClient = jobsMod.createJobsClient({
|
|
36
|
+
connect: { adapter: connect },
|
|
37
|
+
dialect: cfg.jobs?.dialect ?? 'pg',
|
|
38
|
+
});
|
|
39
|
+
const h = jobsMod.createRouteHandlers(jobsClient, {
|
|
40
|
+
supervisorUrl: cfg.jobs?.supervisorUrl,
|
|
41
|
+
supervisorToken: cfg.jobs?.supervisorToken,
|
|
42
|
+
});
|
|
43
|
+
const searchParams = new URL(request.url).searchParams;
|
|
44
|
+
const s = segments;
|
|
45
|
+
if (method === 'GET' && s.length === 0)
|
|
46
|
+
return jsonToResponse(await h.list(searchParams));
|
|
47
|
+
if (method === 'GET' && s[0] === 'types')
|
|
48
|
+
return jsonToResponse(await h.getJobTypes());
|
|
49
|
+
if (method === 'GET' && s[0] === 'pool' && s.length === 1)
|
|
50
|
+
return jsonToResponse(await h.getPool());
|
|
51
|
+
if (method === 'GET' && s[0] === 'pool' && s[1] === 'health')
|
|
52
|
+
return jsonToResponse(await h.getPoolHealth());
|
|
53
|
+
if (method === 'POST' && s[0] === 'pool' && s[1] === 'size') {
|
|
54
|
+
const body = await request.json();
|
|
55
|
+
return jsonToResponse(await h.setPoolSize(Number(body.size)));
|
|
56
|
+
}
|
|
57
|
+
if (method === 'POST' && s[0] === 'bulk-retry') {
|
|
58
|
+
return jsonToResponse(await h.bulkRetry(await request.json()));
|
|
59
|
+
}
|
|
60
|
+
if (method === 'GET' && s[0] === 'schedules' && s.length === 1)
|
|
61
|
+
return jsonToResponse(await h.listSchedules());
|
|
62
|
+
if (method === 'POST' && s[0] === 'schedules' && s.length === 1) {
|
|
63
|
+
return jsonToResponse(await h.createSchedule(await request.json()));
|
|
64
|
+
}
|
|
65
|
+
if (method === 'GET' && s[0] === 'schedules' && s[1] && s.length === 2)
|
|
66
|
+
return jsonToResponse(await h.getSchedule(s[1]));
|
|
67
|
+
if (method === 'PUT' && s[0] === 'schedules' && s[1] && s.length === 2) {
|
|
68
|
+
return jsonToResponse(await h.updateSchedule(s[1], await request.json()));
|
|
69
|
+
}
|
|
70
|
+
if (method === 'DELETE' && s[0] === 'schedules' && s[1] && s.length === 2)
|
|
71
|
+
return jsonToResponse(await h.deleteSchedule(s[1]));
|
|
72
|
+
if (method === 'POST' && s[0] === 'schedules' && s[1] && s[2] === 'fire')
|
|
73
|
+
return jsonToResponse(await h.fireSchedule(s[1]));
|
|
74
|
+
// Job-level routes (s[0] is a job id, not a keyword)
|
|
75
|
+
if (method === 'GET' && s[0] && s.length === 1 && !['types', 'pool', 'schedules', 'bulk-retry'].includes(s[0])) {
|
|
76
|
+
return jsonToResponse(await h.get(s[0]));
|
|
77
|
+
}
|
|
78
|
+
if (method === 'GET' && s[0] && s[1] === 'logs')
|
|
79
|
+
return jsonToResponse(await h.getJobLogs(s[0], searchParams));
|
|
80
|
+
if (method === 'POST' && s[0] && s[1] === 'cancel')
|
|
81
|
+
return jsonToResponse(await h.cancel(s[0]));
|
|
82
|
+
if (method === 'POST' && s[0] && s[1] === 'retry')
|
|
83
|
+
return jsonToResponse(await h.retry(s[0]));
|
|
84
|
+
if (method === 'POST' && s[0] && s[1] === 'trigger')
|
|
85
|
+
return jsonToResponse(await h.trigger(s[0]));
|
|
86
|
+
if (method === 'DELETE' && s[0] && s.length === 1 && !['schedules'].includes(s[0])) {
|
|
87
|
+
return jsonToResponse(await h.delete(s[0]));
|
|
88
|
+
}
|
|
89
|
+
return notFound();
|
|
90
|
+
}
|
|
91
|
+
async function handleLogs(request, method) {
|
|
92
|
+
const logsMod = await import('hazo_logs/ui/server').catch(() => null);
|
|
93
|
+
if (!logsMod)
|
|
94
|
+
return notInstalled('hazo_logs');
|
|
95
|
+
const logsHandler = logsMod.createLogApiHandler({ logDirectory: cfg.logDirectory });
|
|
96
|
+
if (method === 'GET')
|
|
97
|
+
return logsHandler.GET(request);
|
|
98
|
+
return logsHandler.POST(request);
|
|
99
|
+
}
|
|
100
|
+
async function handleEnv(request, method, segments) {
|
|
101
|
+
const hazoEnv = await import('hazo_env').catch(() => null);
|
|
102
|
+
if (!hazoEnv)
|
|
103
|
+
return notInstalled('hazo_env');
|
|
104
|
+
const s = segments;
|
|
105
|
+
// Resolve progress dir
|
|
106
|
+
let progressDir;
|
|
107
|
+
if (cfg.env?.progressDir) {
|
|
108
|
+
progressDir = cfg.env.progressDir;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const filesConfig = hazoEnv.resolveFilesConfig();
|
|
112
|
+
progressDir = path.join(filesConfig.local.basePath, 'migrations', 'progress');
|
|
113
|
+
}
|
|
114
|
+
// GET /env/list
|
|
115
|
+
if (method === 'GET' && s[0] === 'list' && s.length === 1) {
|
|
116
|
+
const envs = hazoEnv.listEnvs();
|
|
117
|
+
return new Response(JSON.stringify(envs), {
|
|
118
|
+
status: 200,
|
|
119
|
+
headers: { 'Content-Type': 'application/json' },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// GET /env/health
|
|
123
|
+
if (method === 'GET' && s[0] === 'health' && s.length === 1) {
|
|
124
|
+
const report = await hazoEnv.doctor();
|
|
125
|
+
return new Response(JSON.stringify(report), {
|
|
126
|
+
status: 200,
|
|
127
|
+
headers: { 'Content-Type': 'application/json' },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// POST /env/migrate — submit migration job
|
|
131
|
+
if (method === 'POST' && s[0] === 'migrate' && s.length === 1) {
|
|
132
|
+
const jobsMod = await import('hazo_jobs/server').catch(() => null);
|
|
133
|
+
if (!jobsMod)
|
|
134
|
+
return notInstalled('hazo_jobs');
|
|
135
|
+
const body = await request.json();
|
|
136
|
+
const connect = await cfg.getHazoConnect();
|
|
137
|
+
const jobsClient = jobsMod.createJobsClient({
|
|
138
|
+
connect: { adapter: connect },
|
|
139
|
+
dialect: cfg.jobs?.dialect ?? 'pg',
|
|
140
|
+
});
|
|
141
|
+
const { jobId } = await jobsClient.submit({
|
|
142
|
+
type: 'env.migrate',
|
|
143
|
+
description: `env-migrate:${body.from}→${body.to}${body.dryRun ? ' (dry-run)' : ''}`,
|
|
144
|
+
payload: body,
|
|
145
|
+
});
|
|
146
|
+
return new Response(JSON.stringify({ jobId }), {
|
|
147
|
+
status: 200,
|
|
148
|
+
headers: { 'Content-Type': 'application/json' },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// GET /env/migrate/:jobId
|
|
152
|
+
if (method === 'GET' && s[0] === 'migrate' && s[1] && s.length === 2) {
|
|
153
|
+
const jobsMod = await import('hazo_jobs/server').catch(() => null);
|
|
154
|
+
if (!jobsMod)
|
|
155
|
+
return notInstalled('hazo_jobs');
|
|
156
|
+
const connect = await cfg.getHazoConnect();
|
|
157
|
+
const jobsClient = jobsMod.createJobsClient({
|
|
158
|
+
connect: { adapter: connect },
|
|
159
|
+
dialect: cfg.jobs?.dialect ?? 'pg',
|
|
160
|
+
});
|
|
161
|
+
const job = await jobsClient.get(s[1]);
|
|
162
|
+
return new Response(JSON.stringify(job), {
|
|
163
|
+
status: 200,
|
|
164
|
+
headers: { 'Content-Type': 'application/json' },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// GET /env/migrate/:jobId/progress
|
|
168
|
+
if (method === 'GET' && s[0] === 'migrate' && s[1] && s[2] === 'progress' && s.length === 3) {
|
|
169
|
+
const progress = hazoEnv.readMigrationProgress(progressDir, s[1]);
|
|
170
|
+
return new Response(JSON.stringify(progress), {
|
|
171
|
+
status: 200,
|
|
172
|
+
headers: { 'Content-Type': 'application/json' },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
// GET /env/snapshots?to=<env>
|
|
176
|
+
if (method === 'GET' && s[0] === 'snapshots' && s.length === 1) {
|
|
177
|
+
const url = new URL(request.url);
|
|
178
|
+
const toEnv = url.searchParams.get('to');
|
|
179
|
+
if (!toEnv) {
|
|
180
|
+
return new Response(JSON.stringify({ error: { reason: 'missing ?to= param' } }), {
|
|
181
|
+
status: 400,
|
|
182
|
+
headers: { 'Content-Type': 'application/json' },
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
let snapshots = [];
|
|
186
|
+
try {
|
|
187
|
+
const dbConfig = hazoEnv.resolveConnectConfig({ env: toEnv, allowOtherEnv: true });
|
|
188
|
+
if (dbConfig.type === 'sqlite' && dbConfig.sqlite?.database_path) {
|
|
189
|
+
const snapshotsDir = path.join(path.dirname(dbConfig.sqlite.database_path), 'snapshots');
|
|
190
|
+
if (fs.existsSync(snapshotsDir)) {
|
|
191
|
+
const files = fs.readdirSync(snapshotsDir);
|
|
192
|
+
snapshots = files.map((f) => {
|
|
193
|
+
const absPath = path.join(snapshotsDir, f);
|
|
194
|
+
return {
|
|
195
|
+
snapshotId: absPath,
|
|
196
|
+
fileName: f,
|
|
197
|
+
createdAt: fs.statSync(absPath).mtime.toISOString(),
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// If config resolution fails, return empty array
|
|
205
|
+
snapshots = [];
|
|
206
|
+
}
|
|
207
|
+
return new Response(JSON.stringify(snapshots), {
|
|
208
|
+
status: 200,
|
|
209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// POST /env/snapshots/restore
|
|
213
|
+
if (method === 'POST' && s[0] === 'snapshots' && s[1] === 'restore' && s.length === 2) {
|
|
214
|
+
const body = await request.json();
|
|
215
|
+
if (!body.to || !body.snapshotId) {
|
|
216
|
+
return new Response(JSON.stringify({ error: { reason: 'body must include to and snapshotId' } }), {
|
|
217
|
+
status: 400,
|
|
218
|
+
headers: { 'Content-Type': 'application/json' },
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
const toDbConfig = hazoEnv.resolveConnectConfig({ env: body.to, allowOtherEnv: true });
|
|
222
|
+
hazoEnv.restoreSnapshot(toDbConfig, body.snapshotId);
|
|
223
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
224
|
+
status: 200,
|
|
225
|
+
headers: { 'Content-Type': 'application/json' },
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return notFound();
|
|
229
|
+
}
|
|
230
|
+
function makeHandler(method) {
|
|
231
|
+
return async (request, ctx) => {
|
|
232
|
+
const { path: routePath } = await ctx.params;
|
|
233
|
+
const [group, ...rest] = routePath;
|
|
234
|
+
if (group === 'jobs') {
|
|
235
|
+
const gated = withAdminGate((_req, _gate) => handleJobs(_req, method, rest), { required_permissions: [jobsPermission] });
|
|
236
|
+
return gated(request);
|
|
237
|
+
}
|
|
238
|
+
if (group === 'logs') {
|
|
239
|
+
const gated = withAdminGate((_req, _gate) => handleLogs(_req, method), { required_permissions: [logsPermission] });
|
|
240
|
+
return gated(request);
|
|
241
|
+
}
|
|
242
|
+
if (group === 'env') {
|
|
243
|
+
const gated = withAdminGate((_req, _gate) => handleEnv(_req, method, rest), { required_permissions: [envPermission] });
|
|
244
|
+
return gated(request);
|
|
245
|
+
}
|
|
246
|
+
return notFound();
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
GET: makeHandler('GET'),
|
|
251
|
+
POST: makeHandler('POST'),
|
|
252
|
+
PUT: makeHandler('PUT'),
|
|
253
|
+
DELETE: makeHandler('DELETE'),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// Re-export env job handler utilities
|
|
257
|
+
export { envMigrateJobHandler, ENV_MIGRATE_JOB_TYPE, registerEnvJobs, } from './env_migrate_handler.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type AdminLayoutProps } from './admin_layout.js';
|
|
2
|
+
import type { AdminManifest } from './admin_nav.js';
|
|
3
|
+
export interface AdminAppProps {
|
|
4
|
+
manifest: AdminManifest;
|
|
5
|
+
user: AdminLayoutProps['user'];
|
|
6
|
+
permissions: string[];
|
|
7
|
+
correlationId?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function AdminApp({ manifest, user, permissions, correlationId }: AdminAppProps): import("react").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=admin_app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin_app.d.ts","sourceRoot":"","sources":["../../src/components/admin_app.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAOvE,OAAO,KAAK,EAAE,aAAa,EAAgB,MAAM,gBAAgB,CAAC;AAElE,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC/B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAYD,wBAAgB,QAAQ,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,aAAa,EAAE,EAAE,aAAa,+BAkBrF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { usePathname } from 'next/navigation';
|
|
4
|
+
import { AdminLayout } from './admin_layout.js';
|
|
5
|
+
import { UsersPanel } from './users_panel.js';
|
|
6
|
+
import { JobsPanel } from './jobs_panel.js';
|
|
7
|
+
import { LogsPanel } from './logs_panel.js';
|
|
8
|
+
import { EnvMigrationPanel } from './env_migration_panel.js';
|
|
9
|
+
import { HealthPanel } from './health_panel.js';
|
|
10
|
+
import { resolveNav } from './admin_nav.js';
|
|
11
|
+
function renderPanel(item) {
|
|
12
|
+
if (item.kind === 'users')
|
|
13
|
+
return _jsx(UsersPanel, { apiBasePath: item.basePath });
|
|
14
|
+
if (item.kind === 'jobs')
|
|
15
|
+
return _jsx(JobsPanel, { basePath: item.basePath });
|
|
16
|
+
if (item.kind === 'logs')
|
|
17
|
+
return _jsx(LogsPanel, { apiBasePath: item.basePath });
|
|
18
|
+
if (item.kind === 'env_migration')
|
|
19
|
+
return _jsx(EnvMigrationPanel, { basePath: item.basePath });
|
|
20
|
+
if (item.kind === 'health')
|
|
21
|
+
return _jsx(HealthPanel, { basePath: item.basePath });
|
|
22
|
+
if (item.kind === 'custom' && item.component)
|
|
23
|
+
return _jsx(_Fragment, { children: item.component });
|
|
24
|
+
return _jsx("div", { className: "p-6 text-sm text-gray-500", children: "Panel not found." });
|
|
25
|
+
}
|
|
26
|
+
export function AdminApp({ manifest, user, permissions, correlationId }) {
|
|
27
|
+
const pathname = usePathname();
|
|
28
|
+
const navItems = resolveNav(manifest, permissions);
|
|
29
|
+
// Find the active item by best-match path prefix
|
|
30
|
+
const active = navItems.find((item) => item.path === '/' ? pathname === item.path : pathname?.startsWith(item.path)) ?? navItems[0] ?? null;
|
|
31
|
+
return (_jsx(AdminLayout, { manifest: manifest, user: user, permissions: permissions, correlationId: correlationId, children: active ? renderPanel(active) : (_jsx("div", { className: "p-6 text-sm text-gray-500", children: navItems.length === 0 ? 'No admin sections configured.' : 'Select a section from the sidebar.' })) }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AdminManifest } from './admin_nav.js';
|
|
3
|
+
export interface AdminLayoutProps {
|
|
4
|
+
manifest: AdminManifest;
|
|
5
|
+
user: {
|
|
6
|
+
id: string;
|
|
7
|
+
email_address: string;
|
|
8
|
+
name?: string | null;
|
|
9
|
+
} | null;
|
|
10
|
+
permissions: string[];
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
correlationId?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function AdminLayout({ manifest, user, permissions, children, correlationId }: AdminLayoutProps): React.JSX.Element;
|
|
15
|
+
//# sourceMappingURL=admin_layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin_layout.d.ts","sourceRoot":"","sources":["../../src/components/admin_layout.tsx"],"names":[],"mappings":"AACA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAKxC,OAAO,KAAK,EAAE,aAAa,EAAgB,MAAM,gBAAgB,CAAC;AAGlE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IACzE,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAYD,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,aAAa,EAAE,EAAE,gBAAgB,qBAwErG"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { usePathname } from 'next/navigation';
|
|
6
|
+
import { ChevronLeft, ChevronRight, Users, Briefcase, FileText, LayoutDashboard, ArrowLeftRight, Activity } from 'lucide-react';
|
|
7
|
+
import { HazoContextProvider, cn } from 'hazo_ui';
|
|
8
|
+
import { resolveNav } from './admin_nav.js';
|
|
9
|
+
// Default icon per kind
|
|
10
|
+
function defaultIcon(kind) {
|
|
11
|
+
if (kind === 'users')
|
|
12
|
+
return _jsx(Users, { size: 16 });
|
|
13
|
+
if (kind === 'jobs')
|
|
14
|
+
return _jsx(Briefcase, { size: 16 });
|
|
15
|
+
if (kind === 'logs')
|
|
16
|
+
return _jsx(FileText, { size: 16 });
|
|
17
|
+
if (kind === 'env_migration')
|
|
18
|
+
return _jsx(ArrowLeftRight, { size: 16 });
|
|
19
|
+
if (kind === 'health')
|
|
20
|
+
return _jsx(Activity, { size: 16 });
|
|
21
|
+
return _jsx(LayoutDashboard, { size: 16 });
|
|
22
|
+
}
|
|
23
|
+
export function AdminLayout({ manifest, user, permissions, children, correlationId }) {
|
|
24
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
25
|
+
const pathname = usePathname();
|
|
26
|
+
const navItems = resolveNav(manifest, permissions);
|
|
27
|
+
function isActive(item) {
|
|
28
|
+
return item.path === '/' ? pathname === item.path : pathname?.startsWith(item.path);
|
|
29
|
+
}
|
|
30
|
+
return (_jsx(HazoContextProvider, { correlationId: correlationId, userId: user?.id, children: _jsxs("div", { className: "flex min-h-screen", children: [_jsxs("aside", { className: cn('flex flex-col border-r border-gray-200 bg-white transition-all duration-200', collapsed ? 'w-14' : 'w-56'), children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-200 px-3 py-3", children: [!collapsed && (_jsx("span", { className: "truncate text-sm font-semibold text-gray-800", children: manifest.title ?? 'Admin' })), _jsx("button", { onClick: () => setCollapsed((c) => !c), className: "ml-auto flex h-6 w-6 items-center justify-center rounded text-gray-400 hover:bg-gray-100 hover:text-gray-600", "aria-label": collapsed ? 'Expand sidebar' : 'Collapse sidebar', children: collapsed ? _jsx(ChevronRight, { size: 14 }) : _jsx(ChevronLeft, { size: 14 }) })] }), _jsx("nav", { className: "flex flex-col gap-1 p-2 flex-1", children: navItems.map((item) => {
|
|
31
|
+
const active = isActive(item);
|
|
32
|
+
const icon = item.icon ?? defaultIcon(item.kind);
|
|
33
|
+
return (_jsxs(Link, { href: item.path, className: cn('flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors', collapsed ? 'justify-center' : '', active
|
|
34
|
+
? 'bg-blue-50 text-blue-700 font-medium'
|
|
35
|
+
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'), title: collapsed ? item.label : undefined, children: [_jsx("span", { className: "shrink-0", children: icon }), !collapsed && _jsx("span", { className: "truncate", children: item.label })] }, item.path));
|
|
36
|
+
}) }), !collapsed && user && (_jsx("div", { className: "border-t border-gray-200 px-3 py-2 text-xs text-gray-400 truncate", children: user.name ?? user.email_address }))] }), _jsx("main", { className: "flex-1 overflow-auto", children: children })] }) }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
export type AdminNavSection = {
|
|
3
|
+
kind: 'users' | 'jobs' | 'logs' | 'env_migration' | 'health';
|
|
4
|
+
label?: string;
|
|
5
|
+
icon?: React.ReactNode;
|
|
6
|
+
permission?: string;
|
|
7
|
+
basePath?: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'custom';
|
|
10
|
+
label: string;
|
|
11
|
+
icon?: React.ReactNode;
|
|
12
|
+
permission?: string;
|
|
13
|
+
path: string;
|
|
14
|
+
component: React.ReactNode;
|
|
15
|
+
};
|
|
16
|
+
export type AdminManifest = {
|
|
17
|
+
sections: AdminNavSection[];
|
|
18
|
+
title?: string;
|
|
19
|
+
};
|
|
20
|
+
export type AdminNavItem = {
|
|
21
|
+
kind: 'users' | 'jobs' | 'logs' | 'env_migration' | 'health' | 'custom';
|
|
22
|
+
label: string;
|
|
23
|
+
path: string;
|
|
24
|
+
basePath: string;
|
|
25
|
+
icon?: React.ReactNode;
|
|
26
|
+
permission: string;
|
|
27
|
+
component?: React.ReactNode;
|
|
28
|
+
};
|
|
29
|
+
export declare function defaultPermissionForKind(kind: string): string;
|
|
30
|
+
export declare function resolveNav(manifest: AdminManifest, permissions: string[]): AdminNavItem[];
|
|
31
|
+
//# sourceMappingURL=admin_nav.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin_nav.d.ts","sourceRoot":"","sources":["../../src/components/admin_nav.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,MAAM,MAAM,eAAe,GACvB;IACE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,eAAe,GAAG,QAAQ,CAAC;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B,CAAC;AAEN,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAGF,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,eAAe,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC7B,CAAC;AAEF,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,YAAY,EAAE,CAkEzF"}
|