portal-mojo 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/docs/README.md +1 -0
- package/docs/admin-identity-directories-sources.json +130 -0
- package/docs/admin-identity-directories.md +60 -0
- package/docs/admin-identity-users.md +6 -1
- package/docs/admin-members.md +16 -0
- package/docs/admin-network-security.md +1 -1
- package/docs/menus-and-access.md +6 -2
- package/docs/user-menu.md +131 -0
- package/package.json +1 -1
- package/src/admin/core/index.ts +4 -4
- package/src/admin/domains/identity.ts +19 -1
- package/src/admin/identity/groups/GroupDetail.tsx +287 -0
- package/src/admin/identity/groups/GroupsPage.tsx +125 -0
- package/src/admin/identity/groups/group-permissions.ts +5 -0
- package/src/admin/identity/groups/group-sections/AuditSection.tsx +57 -0
- package/src/admin/identity/groups/group-sections/AuthConfigDialog.tsx +677 -0
- package/src/admin/identity/groups/group-sections/EventsSection.tsx +53 -0
- package/src/admin/identity/groups/group-sections/GeofenceSection.tsx +198 -0
- package/src/admin/identity/groups/group-sections/IdentitySection.tsx +211 -0
- package/src/admin/identity/groups/group-sections/MembersSection.tsx +25 -0
- package/src/admin/identity/groups/group-sections/OverviewSection.tsx +112 -0
- package/src/admin/identity/groups/group-sections/SubGroupsSection.tsx +102 -0
- package/src/admin/identity/groups/group-sections/auth-config.ts +285 -0
- package/src/admin/identity/groups/group-sections/geofence-data.ts +40 -0
- package/src/admin/identity/groups/group-sections/models.ts +110 -0
- package/src/admin/identity/groups/group-sections/shared.ts +21 -0
- package/src/admin/identity/groups/index.ts +6 -0
- package/src/admin/identity/groups/models.ts +70 -0
- package/src/admin/identity/index.ts +1 -0
- package/src/admin/identity/members/MemberDetail.tsx +32 -3
- package/src/admin/identity/members/models.ts +3 -0
- package/src/admin/identity/users/PersonalApiKeysPage.tsx +220 -0
- package/src/admin/identity/users/UserDetail.tsx +2 -2
- package/src/admin/identity/users/index.ts +2 -0
- package/src/admin/identity/users/sections/PermissionsSection.tsx +2 -2
- package/src/admin/registry.ts +3 -1
- package/src/ui/FileField.tsx +1 -10
- package/src/ui/SidebarNav.tsx +2 -2
- package/src/ui/UserMenu.tsx +401 -0
- package/src/ui/index.ts +1 -0
- package/src/ui/safe-url.ts +14 -0
- package/src/ui/shell.ts +1 -0
package/README.md
CHANGED
|
@@ -23,8 +23,8 @@ additional exports are required.
|
|
|
23
23
|
|---|---|
|
|
24
24
|
| `portal-mojo/client` | Typed django-mojo protocol layer: envelope unwrap at exactly one boundary (a failed save **rejects**), `start`/`size` paging, `'-field'` sort, Django lookups, the URL-synced `useTableParams` store (single source of truth for table state), TanStack Query hooks. Auth client: password / magic-link / passkey login, forgot/reset, cross-origin handoff (`?auth_code=` scrub-before-network), single-flight refresh + pre-request gate (synthetic-401 reject, `/api/token/refresh` recursion guard), `X-Mojo-UID` device header, `getAuthSnapshot`/`subscribeAuth` for React. Auth-challenged realtime transport: `RealtimeProvider`/`RealtimeClient`, refcounted topics, typed event projection, and a deterministic mock. Boot auth with `initAuth()`. The in-memory HTTP mock transport lives here too — it is the wire contract's executable spec and evolves in lockstep with the client (any seeded active user logs in with email + `"mojo"`). |
|
|
25
25
|
| `portal-mojo/client/runtime` | Stable, narrow client boundary for first-party application runtime code. It omits eager mock-test controls; the mock transport itself loads on the first mock request. |
|
|
26
|
-
| `portal-mojo/ui` | Mission-control UI: `ModelTable`, `FilterBar`/`FilterPills`, `SchemaForm`/`formModal`, `DetailView`, `RecordFeed`, `AttachmentQueue`/`UploadQueue`, `ImageEditor`/`imageEditorModal`, awaitable native-`<dialog>` `modal`, `toast`, `ThemeProvider`, `Guarded` (permission slot), `GroupSwitcher` (searchable tree selector), `RequiresGroup`, the sidebar engine (`registerMenus`/`setDefaultMenu` registry + `SidebarNav` — static or searchable accordion with a compact icon rail; global and group scopes are explicit), `Badge`/`MetricCard`/`Spark`, `fmt` formatter namespace. |
|
|
27
|
-
| `portal-mojo/ui/shell` | Stable, narrow boundary for application-shell primitives: theme, guards, menus/sidebar, right panel, modal/toast hosts, password helpers, and formatters. |
|
|
26
|
+
| `portal-mojo/ui` | Mission-control UI: `ModelTable`, `FilterBar`/`FilterPills`, `SchemaForm`/`formModal`, `DetailView`, `RecordFeed`, `AttachmentQueue`/`UploadQueue`, `ImageEditor`/`imageEditorModal`, awaitable native-`<dialog>` `modal`, `toast`, `ThemeProvider`, `UserMenu` (top-nav account menu), `Guarded` (permission slot), `GroupSwitcher` (searchable tree selector), `RequiresGroup`, the sidebar engine (`registerMenus`/`setDefaultMenu` registry + `SidebarNav` — static or searchable accordion with a compact icon rail; global and group scopes are explicit), `Badge`/`MetricCard`/`Spark`, `fmt` formatter namespace. |
|
|
27
|
+
| `portal-mojo/ui/shell` | Stable, narrow boundary for application-shell primitives: theme, guards, menus/sidebar, right panel, account menu (`UserMenu`), modal/toast hosts, password helpers, and formatters. |
|
|
28
28
|
| `portal-mojo/charts` | Dependency-free SVG charts: `SeriesChart` (line/bar/area, stacked bars, legend toggle, crosshair tooltip) and `MetricsChart` (granularity/range/type control bar for `/api/metrics/fetch`). |
|
|
29
29
|
| `portal-mojo/admin/core` | Side-effect-light Admin contracts and host: `AdminSection`, the discriminated `AdminRoute` XOR, `adminSectionRoutes`, `adminSectionsMenu`, and `AdminLazyPage`. |
|
|
30
30
|
| `portal-mojo/admin/registry` | Stable, narrow built-in `ADMIN_SECTIONS` roster for first-party hosts, without broad page barrels. It installs the optional Email→DNS adapter synchronously so communications routes work before DNS navigation. |
|
package/docs/README.md
CHANGED
|
@@ -51,6 +51,7 @@ everything: run the showcase (`npm run dev:showcase`) → **Develop → Componen
|
|
|
51
51
|
| [image-editor.md](image-editor.md) | Full-resolution crop/transform/filter composition, bounded history, PNG Blob results, modal helper, and ImageField pre-upload editing |
|
|
52
52
|
| [loading.md](loading.md) | `Spinner`, `busy()`/`busyWhile()` blocking overlay, `ViewLoader`, `InlineLoader`, `Busy` — the anti-flash delay rule and skeleton-vs-loader guidance |
|
|
53
53
|
| [popover.md](popover.md) | Anchored top-layer popover shell — placement, reposition, outside/Escape close, the dialog stacking story |
|
|
54
|
+
| [user-menu.md](user-menu.md) | Top-nav account menu — avatar trigger, identity header, theme control, app items, pending Sign out, signed-out matrix, keyboard map |
|
|
54
55
|
| [taginput.md](taginput.md) | Chip/tag entry — CSV wire shape, keyboard matrix, validation + inline errors |
|
|
55
56
|
| [combobox.md](combobox.md) | The house autocomplete — options with descriptions/meta, commit-only change pipeline, allowCustom, ARIA |
|
|
56
57
|
| [location-address.md](location-address.md) | Six-endpoint LocationClient, private provider sessions, stale guards, controlled AddressField, atomic form patching |
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
{
|
|
2
|
+
"package": "portal-mojo",
|
|
3
|
+
"package_version": "0.2.5",
|
|
4
|
+
"source_repository": "https://github.com/NativeMojo/portal-mojo",
|
|
5
|
+
"source_tag": "v0.2.5",
|
|
6
|
+
"source_commit": "93c7fdfeb3096d33010e72bd4483cf9ca6e3d6f5",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"license_file": "packages/portal-mojo/LICENSE",
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"source_path": "apps/portal/src/group-permissions.ts",
|
|
12
|
+
"source_sha256": "6237ad45328ecb9637a1a07d8954dd27cd019e22fa32942f5e7d05a76037fb5c",
|
|
13
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-permissions.ts",
|
|
14
|
+
"package_sha256": "b909f5a5cf029b8b910a70e6817fea8abd9ded6797af14a16170b2695c0c1699",
|
|
15
|
+
"adaptation": "Moves system-pinned group permission clauses into the canonical package identity surface."
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"source_path": "apps/portal/src/models.ts",
|
|
19
|
+
"source_sha256": "0b98a906a774e572f9109703fe22444c3a2e190ca5e02c3a93dfbd02cc3fcb84",
|
|
20
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/models.ts",
|
|
21
|
+
"package_sha256": "6ac12a64d00e409874bc1b13d7941cf54c38a715af5ebcfddd2b2e9257356759",
|
|
22
|
+
"adaptation": "Extracts the Group model/type and group kind options; shared User, Member and Log models remain owned by their package domains."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"source_path": "apps/portal/src/pages/ApiKeysPage.tsx",
|
|
26
|
+
"source_sha256": "3d338cf77c3110b72a9361565c1cced10a7fccc52c486e3dafda119d66c676b2",
|
|
27
|
+
"package_path": "packages/portal-mojo/src/admin/identity/users/PersonalApiKeysPage.tsx",
|
|
28
|
+
"package_sha256": "f377d8a97b5c0ccbce745c333fdd7e4a5fd5f5a89b868b058677b31e3efe64ff",
|
|
29
|
+
"adaptation": "Expanded row facts and actions are presented in a native DetailView modal that observes the current API key record."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"source_path": "apps/portal/src/pages/GroupDetail.tsx",
|
|
33
|
+
"source_sha256": "01a8a6032a59a337636ec28e5e2aef90717e5bc00656807135e02820a92a394b",
|
|
34
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/GroupDetail.tsx",
|
|
35
|
+
"package_sha256": "e26943218e3e8f5ac79ddc7f1b0bd40736bcab0bd6e9c694e01830086349a4e2",
|
|
36
|
+
"adaptation": "Raw metadata is visible in the explicit Metadata section without a collapsible control."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"source_path": "apps/portal/src/pages/GroupsPage.tsx",
|
|
40
|
+
"source_sha256": "82e4aa5e7ebf67832bbd3bcf7e15dcfdeea64383039cc426591fa15262e4d4c8",
|
|
41
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/GroupsPage.tsx",
|
|
42
|
+
"package_sha256": "e16f3d0630fdb1937cf667cd2abe8cd1aabc9bb2b731113112930e66bde2f1df",
|
|
43
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"source_path": "apps/portal/src/pages/group-sections/AuditSection.tsx",
|
|
47
|
+
"source_sha256": "49848f57e5050c0008aa83f7092af9abb8f55d262791a2591e525eaa6d741df4",
|
|
48
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/AuditSection.tsx",
|
|
49
|
+
"package_sha256": "07a40636549a0a1d8636a115ee330313b84c0c82500890c6464301fd39de1e06",
|
|
50
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"source_path": "apps/portal/src/pages/group-sections/AuthConfigDialog.tsx",
|
|
54
|
+
"source_sha256": "f42e18a802f206a2b3f30248c30ad12887598b170deb73b532232a6bf89699dd",
|
|
55
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/AuthConfigDialog.tsx",
|
|
56
|
+
"package_sha256": "e2280185bde7eab3ca7108f7d6da9e0e66c4f4d8323ec97f3fd7d49376932b65",
|
|
57
|
+
"adaptation": "Raw auth overrides remain visible in the explicit configuration dialog without collapsible content."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"source_path": "apps/portal/src/pages/group-sections/EventsSection.tsx",
|
|
61
|
+
"source_sha256": "b8cb12ae99d7c8d5931a090b7be8ea4e0373ff4657e786094e9ac862167d43ec",
|
|
62
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/EventsSection.tsx",
|
|
63
|
+
"package_sha256": "f0004605fbf7ceb2704adb42c0a3dd14dc5a468c445f0f94f2548fbc5f1bfeb4",
|
|
64
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"source_path": "apps/portal/src/pages/group-sections/GeofenceSection.tsx",
|
|
68
|
+
"source_sha256": "d0ce22f5ef922371d7a78c353e1a29e5d9ca4932e45814499e9c3cdeda7dc137",
|
|
69
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/GeofenceSection.tsx",
|
|
70
|
+
"package_sha256": "227850a8ef844867ea55d599997144b462ee058967224c361dd821713d1b677a",
|
|
71
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"source_path": "apps/portal/src/pages/group-sections/IdentitySection.tsx",
|
|
75
|
+
"source_sha256": "cda776a26f1e21b73820ed88ab7dcbafdaf2b146c4c3ee4bf6b24dcbaeecc9f4",
|
|
76
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/IdentitySection.tsx",
|
|
77
|
+
"package_sha256": "888ff5f7a70c80d8a868f13675b39af9a2a7076d6ea5fbc323493c0265ce9880",
|
|
78
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"source_path": "apps/portal/src/pages/group-sections/MembersSection.tsx",
|
|
82
|
+
"source_sha256": "dc2e99924b8ba856ce2ed48dffae082e98cac9ef78df3ddb1f05fe8a71f9725d",
|
|
83
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/MembersSection.tsx",
|
|
84
|
+
"package_sha256": "abccb58bcdb62d0d1de2611cad080ca7237e0dfa781b10c5fc1d7e251398f628",
|
|
85
|
+
"adaptation": "Passes the group navigation callback into UserDetail to keep cross-record navigation available."
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"source_path": "apps/portal/src/pages/group-sections/OverviewSection.tsx",
|
|
89
|
+
"source_sha256": "fb395569018b89e6efd45cea241ba665b73efd15549802a6f0d98d90116ce894",
|
|
90
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/OverviewSection.tsx",
|
|
91
|
+
"package_sha256": "08b460272af71ba6209fe388b71f37f79d94a9fecc21c0c2ee3abaf11f01372f",
|
|
92
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"source_path": "apps/portal/src/pages/group-sections/SubGroupsSection.tsx",
|
|
96
|
+
"source_sha256": "81074a2d74d80fda93e40379b87620807c31db7979736f90559e359bdafc0672",
|
|
97
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/SubGroupsSection.tsx",
|
|
98
|
+
"package_sha256": "61e412e709afb4384fdcbaa1d8fbb6df6ba414634ecc6aaa8201c66cf722af93",
|
|
99
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"source_path": "apps/portal/src/pages/group-sections/auth-config.ts",
|
|
103
|
+
"source_sha256": "37d75dff84bca2577bcd1804c77aa0ef1519e4d913dcfdf7914266045fdbabe6",
|
|
104
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/auth-config.ts",
|
|
105
|
+
"package_sha256": "37d75dff84bca2577bcd1804c77aa0ef1519e4d913dcfdf7914266045fdbabe6",
|
|
106
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"source_path": "apps/portal/src/pages/group-sections/geofence-data.ts",
|
|
110
|
+
"source_sha256": "169d5feb72fbe16a0463d591328ce49963e95f1bc0f5f7e5fa9ed271a5ace8e6",
|
|
111
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/geofence-data.ts",
|
|
112
|
+
"package_sha256": "169d5feb72fbe16a0463d591328ce49963e95f1bc0f5f7e5fa9ed271a5ace8e6",
|
|
113
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"source_path": "apps/portal/src/pages/group-sections/models.ts",
|
|
117
|
+
"source_sha256": "ec6a2fd700a0a91e8f1d9e49c364ffe5466db340b435004971e719da08b49371",
|
|
118
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/models.ts",
|
|
119
|
+
"package_sha256": "ec6a2fd700a0a91e8f1d9e49c364ffe5466db340b435004971e719da08b49371",
|
|
120
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"source_path": "apps/portal/src/pages/group-sections/shared.ts",
|
|
124
|
+
"source_sha256": "68aa819b14edcbbd8ae399d5c5a74360cc50ffd281b6d0d3be1c4f8ebc4e4f6c",
|
|
125
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/shared.ts",
|
|
126
|
+
"package_sha256": "68aa819b14edcbbd8ae399d5c5a74360cc50ffd281b6d0d3be1c4f8ebc4e4f6c",
|
|
127
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Admin Groups and Personal API Keys
|
|
2
|
+
|
|
3
|
+
The global Admin registry contributes Groups and Personal API Keys to the
|
|
4
|
+
Identity & Access navigation group. `adminSectionRoutes()` and
|
|
5
|
+
`adminSectionsMenu()` therefore mount the same pages in the standalone
|
|
6
|
+
reference portal and in an embedded System Admin boundary:
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
import {
|
|
10
|
+
GROUPS_ADMIN_SECTION,
|
|
11
|
+
GROUP_VIEW_PERMS,
|
|
12
|
+
GroupDetail,
|
|
13
|
+
GroupModel,
|
|
14
|
+
GroupsPage,
|
|
15
|
+
PERSONAL_API_KEYS_ADMIN_SECTION,
|
|
16
|
+
PersonalApiKeysPage,
|
|
17
|
+
} from 'portal-mojo/admin/identity';
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The registered routes are `/groups` and `/apikeys`; an embedded mount prefixes
|
|
21
|
+
both routes. Group directory and detail access use the system-pinned
|
|
22
|
+
`sys.groups | sys.view_groups` clause. Group mutations use
|
|
23
|
+
`sys.groups | sys.manage_groups`. Active-group membership cannot satisfy
|
|
24
|
+
these gates. The personal key section uses the system identity-view clause,
|
|
25
|
+
while its API requests remain scoped to the signed-in user's own keys.
|
|
26
|
+
|
|
27
|
+
## Groups
|
|
28
|
+
|
|
29
|
+
`GroupsPage` retains the native server-owned `ModelTable` search, filters,
|
|
30
|
+
presets, paging, sorting, column chooser, persistence and exports against
|
|
31
|
+
`GET /api/group`. Opening a row presents `GroupDetail` in `modal.detail`.
|
|
32
|
+
`GroupDetail` preserves the Overview, Identity, Members, Sub-Groups, API Keys,
|
|
33
|
+
Webhooks, Geofencing, Events, Audit and Metadata surfaces, with each section's
|
|
34
|
+
existing system permission gate and conditional queries. Parent, child, member
|
|
35
|
+
and user navigation opens native detail modals while keeping the directory
|
|
36
|
+
underneath.
|
|
37
|
+
|
|
38
|
+
The group model and section code are adapted from the reference portal at
|
|
39
|
+
`v0.2.5` commit `93c7fdfeb3096d33010e72bd4483cf9ca6e3d6f5`. The source hashes
|
|
40
|
+
and corresponding package hashes are recorded in
|
|
41
|
+
[`admin-identity-directories-sources.json`](admin-identity-directories-sources.json).
|
|
42
|
+
The moved implementation retains the package Apache-2.0 license. Two raw JSON
|
|
43
|
+
surfaces render their values directly in their explicit Metadata/Auth details;
|
|
44
|
+
they do not use collapsible content.
|
|
45
|
+
|
|
46
|
+
## Personal API Keys
|
|
47
|
+
|
|
48
|
+
`PersonalApiKeysPage` uses the existing `ApiKeyModel` at
|
|
49
|
+
`/api/account/api_keys` and the caller-only generation mutation at
|
|
50
|
+
`/api/auth/generate_api_key`. The generated token is shown once and scrubbed
|
|
51
|
+
before it can reach the query or mutation cache. Key facts and actions open in
|
|
52
|
+
a native detail modal. Disable/enable remains reversible with an undo toast;
|
|
53
|
+
revoke remains armed because it rotates the secret and cannot be undone. There
|
|
54
|
+
is no delete action. This page does not use the separate group credential
|
|
55
|
+
endpoint.
|
|
56
|
+
|
|
57
|
+
The Personal API Keys route was added to the package identity section registry
|
|
58
|
+
alongside Users, Members, Groups, Credentials and Sign-in. Compatibility page
|
|
59
|
+
paths in the reference app re-export the package-owned pages, and its menu and
|
|
60
|
+
route composition do not inject duplicate directory entries.
|
|
@@ -25,7 +25,12 @@ reason per batch; rows already in the target state are ineligible.
|
|
|
25
25
|
|
|
26
26
|
The detail preserves all 14 sections: Overview, Profile, Personal, Security,
|
|
27
27
|
OAuth, Groups, Sys Perms, conditional App Perms, API Keys, Devices, Logins,
|
|
28
|
-
Audit, Notifications, and Metadata.
|
|
28
|
+
Audit, Notifications, and Metadata. Permission sections are labeled **System
|
|
29
|
+
permissions** and **Product permissions** in the UI. Consuming apps install
|
|
30
|
+
their product tabs with `registerFormTabs(USER_APP_PERMS_TABSET, tabs)`; the
|
|
31
|
+
section stays absent when none are registered. User grants can cover multiple
|
|
32
|
+
groups; use a Member product permission for a single group.
|
|
33
|
+
Subsidiary count/list queries are enabled
|
|
29
34
|
only when their exact system permission is present. Logs use
|
|
30
35
|
`sys.view_logs | sys.manage_logs | sys.security`; incident events use
|
|
31
36
|
`sys.view_security | sys.security`. Permission revocation therefore removes
|
package/docs/admin-members.md
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
MEMBERS_ADMIN_SECTION,
|
|
13
13
|
openMemberAdmissionDialog,
|
|
14
14
|
registerMemberPermissions,
|
|
15
|
+
MEMBER_APP_PERMS_TABSET,
|
|
15
16
|
} from 'portal-mojo/admin';
|
|
16
17
|
```
|
|
17
18
|
|
|
@@ -103,6 +104,16 @@ and read-only rather than disappearing. Permission switches autosave through
|
|
|
103
104
|
`FormView`; a deployment's `MEMBER_PERMS_PROTECTION` remains authoritative,
|
|
104
105
|
and rejected changes revert with the server error shown.
|
|
105
106
|
|
|
107
|
+
For a larger product catalog, also register native `FormView` tabs under
|
|
108
|
+
`MEMBER_APP_PERMS_TABSET` with `registerFormTabs` from `portal-mojo/ui`.
|
|
109
|
+
Use dotted `permissions.<grant>` fields and retain the corresponding
|
|
110
|
+
`registerMemberPermissions` definitions. A separate **Product permissions**
|
|
111
|
+
detail section appears when this tabset is populated. Fields covered by those
|
|
112
|
+
tabs leave the ordinary **Permissions** section, so each grant has one editor.
|
|
113
|
+
Both forms save the same membership row with the same gates, partial-dict
|
|
114
|
+
merge, escalation confirmation and rejection handling. The tabset creates no
|
|
115
|
+
new grant semantics: the backend still decides what each key authorizes.
|
|
116
|
+
|
|
106
117
|
## Composition
|
|
107
118
|
|
|
108
119
|
`MEMBERS_ADMIN_SECTION` is root-relative and dual-mounts as `/members` in the
|
|
@@ -110,3 +121,8 @@ standalone Admin and `/system/members` when embedded. It never consumes active
|
|
|
110
121
|
group context. `GroupMembersPanel` is the reusable Group-detail composition;
|
|
111
122
|
pass optional `onNavigateUser` and `onNavigateGroup` callbacks to connect
|
|
112
123
|
sibling identity details without coupling the package to an app.
|
|
124
|
+
|
|
125
|
+
Member product tabs only render `permissions.<grant>` fields whose grant is
|
|
126
|
+
registered through `registerMemberPermissions()` and editable. System/derived
|
|
127
|
+
grants, unregistered names and unrelated fields (such as `group` or `user`)
|
|
128
|
+
are ignored; tabs with no valid fields do not appear.
|
|
@@ -163,7 +163,7 @@ flag is detected, `true` REQUIRES the flag (rare), absent/`null` means don't
|
|
|
163
163
|
care.
|
|
164
164
|
|
|
165
165
|
`geofence-data.ts` is the ONE definition of the lossy projection between that
|
|
166
|
-
DSL and the friendly form. `
|
|
166
|
+
DSL and the friendly form. `packages/portal-mojo/src/admin/identity/groups/group-sections/geofence-data.ts`
|
|
167
167
|
is a re-export shim over it, so the platform page and the group panel cannot
|
|
168
168
|
drift. `GeofenceRuleEditor` is the ONE editor both render.
|
|
169
169
|
|
package/docs/menus-and-access.md
CHANGED
|
@@ -26,8 +26,10 @@ setDefaultMenu('admin');
|
|
|
26
26
|
Resolution: route containment in registration order (group menus eligible
|
|
27
27
|
only under a kind-matched active group) → defaultMenu → first non-group
|
|
28
28
|
visible. Items support `permissions` (any-of) ∧ `requiresGroupKind`;
|
|
29
|
-
dividers with no visible children drop
|
|
30
|
-
|
|
29
|
+
dividers with no visible children drop. Only the destination row is selected;
|
|
30
|
+
an accordion parent is a neutral disclosure control and opens for its active
|
|
31
|
+
child. `exact: true` end-matches both the visual selection and `aria-current`.
|
|
32
|
+
`scope: 'admin' | 'group' | 'account' | 'global'` makes ownership
|
|
31
33
|
explicit; legacy `groupKind` menus still infer group scope. Static rendering
|
|
32
34
|
remains the default for compatibility. `presentation: 'accordion'` enables
|
|
33
35
|
searchable, single-open-section navigation. Give accordion parents stable
|
|
@@ -57,6 +59,8 @@ Admin section bundles contribute through `adminSectionsMenu`. Use
|
|
|
57
59
|
`{grouped: true, presentation: 'accordion'}` for the domain-grouped Admin
|
|
58
60
|
experience. Routes remain mount-relative; multiple embedded root sections
|
|
59
61
|
share one `/system` landing instead of emitting duplicate index routes.
|
|
62
|
+
Generated Admin destinations are exact matched, so an embedded mount’s root
|
|
63
|
+
Dashboard cannot remain selected on every nested page.
|
|
60
64
|
|
|
61
65
|
Import the route/menu machinery from `portal-mojo/admin/core`, then import only
|
|
62
66
|
the registry domains the shell installs. The stable domains are `identity`,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# UserMenu — the top-nav account menu
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import { UserMenu, type UserMenuItem } from 'portal-mojo/ui';
|
|
5
|
+
// also on the narrow shell boundary:
|
|
6
|
+
import { UserMenu, type UserMenuItem } from 'portal-mojo/ui/shell';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The shared identity menu for every django-mojo portal's top bar — replaces
|
|
10
|
+
hand-rolled "user chips" and the separate theme-cycle button. Ported from
|
|
11
|
+
web-mojo's TopNav user dropdown (`src/core/views/navigation/TopNav.js`:
|
|
12
|
+
avatar-or-icon trigger, end-aligned dropdown, divider rows) and
|
|
13
|
+
`PortalApp._injectThemeToggleItems` (theme choice sits just above the
|
|
14
|
+
trailing logout); the theme dialog becomes an inline Light/Dark/System
|
|
15
|
+
segmented control.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<header className="topnav">
|
|
19
|
+
<h2 className="topnav-title">Dashboard</h2>
|
|
20
|
+
<div className="topnav-right">
|
|
21
|
+
<UserMenu
|
|
22
|
+
authMode="inapp"
|
|
23
|
+
items={[
|
|
24
|
+
{ label: 'My profile', icon: 'bi-person', to: '/profile' },
|
|
25
|
+
{ label: 'Leave organization', icon: 'bi-box-arrow-left', danger: true, divider: true, onSelect: leave },
|
|
26
|
+
]}
|
|
27
|
+
onSignOut={async () => { await revokeServerSession(); logout(); }}
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
</header>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Live demo: showcase → Components → Overlays → **UserMenu**.
|
|
34
|
+
|
|
35
|
+
## Props
|
|
36
|
+
|
|
37
|
+
| Prop | Type | Notes |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `items?` | `UserMenuItem[]` | App entries, rendered between the theme control and Sign out. |
|
|
40
|
+
| `children?` | `ReactNode` | Rendered after `items`, above Sign out. Elements with `role="menuitem"` (and `tabIndex={-1}`, class `user-menu-item` for the look) join arrow-key navigation, and clicking one closes the menu. |
|
|
41
|
+
| `authMode?` | `'inapp' \| 'hosted'` | Signed-out affordance (see matrix). The app knows its mode — the toolkit never imports app config. Unknown values warn once and render the signed-out chip. |
|
|
42
|
+
| `onSignOut?` | `() => void \| Promise<void>` | Replaces the default `logout()`. Sign out shows a pending state ("Signing out…", spinner, `aria-busy`) until it settles; repeat clicks are ignored. A rejection toasts `Sign out failed: <message>` and leaves the menu open for a retry. |
|
|
43
|
+
| `size?` | `'sm' \| 'md'` | Trigger density: 24px vs 28px avatar. Default `md`; unknown values warn once and fall back. |
|
|
44
|
+
| `className?` | `string` | Appended to the trigger (or the signed-out element). |
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
interface UserMenuItem {
|
|
48
|
+
label: string;
|
|
49
|
+
icon?: string; // bootstrap-icons class, e.g. 'bi-person'
|
|
50
|
+
to?: string; // react-router <Link> destination
|
|
51
|
+
onSelect?: () => void; // runs after the menu closes (also for `to` items)
|
|
52
|
+
danger?: boolean; // danger tone
|
|
53
|
+
divider?: boolean; // separator line ABOVE this item
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`divider` is a flag on the following item, not a standalone row (web-mojo's
|
|
58
|
+
`{divider: true}` rows map to `divider: true` on the next entry).
|
|
59
|
+
|
|
60
|
+
## Anatomy
|
|
61
|
+
|
|
62
|
+
- **Trigger** — `<button>` with avatar (image, or `fmt.initials(name)` while
|
|
63
|
+
loading / without one / after an image error), display name, caret.
|
|
64
|
+
`aria-haspopup="menu"`, `aria-expanded`, `aria-controls` (while open), and
|
|
65
|
+
`aria-label="Account menu: <name>"` so the avatar-only phone trigger keeps
|
|
66
|
+
an accessible name. Under **720px** the name and caret hide.
|
|
67
|
+
- **Popover** (`placement="bottom-end"`, 280px, clamped to the viewport):
|
|
68
|
+
identity header (42px avatar, name, email, `is_superuser` → "System admin"
|
|
69
|
+
chip), then the `role="menu"` list: Theme (three `menuitemradio`s wired to
|
|
70
|
+
`useTheme().setPref`), items, children, Sign out (danger tone).
|
|
71
|
+
- Name = `me.display_name || me.email || token email || '…'`.
|
|
72
|
+
|
|
73
|
+
## Wire contract
|
|
74
|
+
|
|
75
|
+
- Identity: `useMe()` → `GET /api/user/me` (cache key `['me', uid]`, the row
|
|
76
|
+
is sanitized so `avatar` is `{id} | null`).
|
|
77
|
+
- Avatar: when `me.avatar?.id` is set, `mojoCall('/api/fileman/file/<id>')`
|
|
78
|
+
runs **only while mounted** (AbortController on unmount/id change; failures
|
|
79
|
+
silent). The image is `thumbnail ?? url`, each through `safePreviewUrl`
|
|
80
|
+
(`ui/safe-url.ts`, shared with FileField): same-origin `/paths` or
|
|
81
|
+
credential-free `http(s)` only.
|
|
82
|
+
- Sign out: `onSignOut()` or `logout()` (client-side; django-mojo has no
|
|
83
|
+
logout endpoint).
|
|
84
|
+
|
|
85
|
+
## Invariants
|
|
86
|
+
|
|
87
|
+
- The avatar URL is a **capability**: it lives in component state only and
|
|
88
|
+
is never written into any Query cache (the `me` cache holds `{id}` by
|
|
89
|
+
design). A previous identity's image can never render for the next
|
|
90
|
+
(state is keyed by avatar id).
|
|
91
|
+
- No avatar request while signed out or when `avatar` is null.
|
|
92
|
+
- Popover is controlled; outside-mousedown closes without moving focus.
|
|
93
|
+
- Tokens only — `apps/portal/src/theme/user-menu.css`; both themes from the
|
|
94
|
+
token swap. Reuses `.seg`/`.seg-btn`, `.chip-primary`, `.spin`,
|
|
95
|
+
`.btn-primary`/`.btn-compact`, `.chip-muted`.
|
|
96
|
+
|
|
97
|
+
## Signed-out matrix
|
|
98
|
+
|
|
99
|
+
`useAuthSnapshot().authenticated === false` renders no menu:
|
|
100
|
+
|
|
101
|
+
| `authMode` | Renders |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `'inapp'` | `<Link to="/auth/login">` Sign in button |
|
|
104
|
+
| `'hosted'` and `hostedAuthUrl()` resolves | Sign in button → `redirectToHostedAuth()` |
|
|
105
|
+
| `'hosted'` with no API origin, or omitted | muted "Signed out" chip |
|
|
106
|
+
|
|
107
|
+
## Keyboard
|
|
108
|
+
|
|
109
|
+
| Where | Key | Effect |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| Trigger | Enter / Space / click | Toggle; on open focus the first item |
|
|
112
|
+
| Trigger | ↓ / ↑ | Open (or enter the open menu) on the first / last item |
|
|
113
|
+
| Menu | ↓ / ↑ | Next / previous item (wraps; includes the theme radios) |
|
|
114
|
+
| Menu | Home / End | First / last item |
|
|
115
|
+
| Theme control | ← / → | Previous / next theme option (wraps) |
|
|
116
|
+
| Menu | Enter / Space | Activate (Space also activates `to` links) |
|
|
117
|
+
| Menu | Escape | Close, focus back to the trigger (first Escape only closes the menu inside a modal) |
|
|
118
|
+
| Menu | Tab / Shift+Tab | Close; focus continues from the trigger |
|
|
119
|
+
|
|
120
|
+
Selecting an item or a `role="menuitem"` child closes the menu and returns
|
|
121
|
+
focus to the trigger before `onSelect` runs (so dialogs opened from
|
|
122
|
+
`onSelect` restore focus to a live element). A successful Sign out closes the
|
|
123
|
+
menu. Theme picks apply immediately and keep the menu open.
|
|
124
|
+
|
|
125
|
+
## Pitfalls
|
|
126
|
+
|
|
127
|
+
- Needs a router context (items with `to`, and the in-app Sign in link) and
|
|
128
|
+
`ThemeProvider` for the theme control; `ToastHost` for the failure toast.
|
|
129
|
+
- Don't also render a theme toggle button in the same bar — the menu owns it.
|
|
130
|
+
- `onSignOut` that never settles leaves Sign out pending; resolve or reject.
|
|
131
|
+
- Apps must ship `theme/user-menu.css` (the package ships no CSS).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portal-mojo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "React portal toolkit for django-mojo — typed client + params store, mission-control UI, dependency-free SVG charts, admin section bundles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
package/src/admin/core/index.ts
CHANGED
|
@@ -207,8 +207,8 @@ export function adminSectionsMenu(sections: readonly AdminSection[], opts: {
|
|
|
207
207
|
const labeled = section.routes.filter((route) => route.label);
|
|
208
208
|
const destinations: MenuItem[] = labeled.length > 0 ? labeled.map((route) => ({
|
|
209
209
|
id: `admin:${section.id}:${route.path || 'index'}`, label: route.label!, keywords: [section.title],
|
|
210
|
-
route: absolutePath(base, route.path), permissionClauses: [section.permissions, ...(route.permissions ? [route.permissions] : [])],
|
|
211
|
-
})) : [{ id: `admin:${section.id}`, label: section.title, keywords: [section.title], route: absolutePath(base), permissionClauses: [section.permissions] }];
|
|
210
|
+
route: absolutePath(base, route.path), exact: true, permissionClauses: [section.permissions, ...(route.permissions ? [route.permissions] : [])],
|
|
211
|
+
})) : [{ id: `admin:${section.id}`, label: section.title, keywords: [section.title], route: absolutePath(base), exact: true, permissionClauses: [section.permissions] }];
|
|
212
212
|
if (section.navigationGroup === null) {
|
|
213
213
|
items.push(...destinations.map((destination) => ({ ...destination, icon: section.icon })));
|
|
214
214
|
continue;
|
|
@@ -227,8 +227,8 @@ export function adminSectionsMenu(sections: readonly AdminSection[], opts: {
|
|
|
227
227
|
const base = relativePath(mount, section.basePath ?? section.id);
|
|
228
228
|
items.push({
|
|
229
229
|
label: section.title, icon: section.icon, permissions: section.permissions,
|
|
230
|
-
...(base ? { route: absolutePath(base) } : {}),
|
|
231
|
-
children: labeled.map((route) => ({ label: route.label!, route: absolutePath(base, route.path), permissions: route.permissions })),
|
|
230
|
+
...(base ? { route: absolutePath(base), exact: true } : {}),
|
|
231
|
+
children: labeled.map((route) => ({ label: route.label!, route: absolutePath(base, route.path), exact: true, permissions: route.permissions })),
|
|
232
232
|
});
|
|
233
233
|
}
|
|
234
234
|
return { name: opts.name ?? 'admin', scope: 'admin', presentation: opts.presentation ?? 'static', items };
|
|
@@ -3,6 +3,7 @@ import { GLOBAL_CREDENTIAL_PERMS } from '../credentials/models';
|
|
|
3
3
|
import { MEMBER_READ_PERMISSIONS } from '../identity/members/models';
|
|
4
4
|
import { USER_VIEW_PERMISSIONS } from '../identity/users/models';
|
|
5
5
|
import { SIGNIN_ADMIN_PERMISSIONS } from '../signin/api';
|
|
6
|
+
import { GROUP_VIEW_PERMS } from '../identity/groups/group-permissions';
|
|
6
7
|
|
|
7
8
|
export const USERS_ADMIN_SECTION: AdminSection = {
|
|
8
9
|
id: 'users', basePath: '', title: 'Users', icon: 'bi-people', navigationGroup: 'identity-access', permissions: USER_VIEW_PERMISSIONS,
|
|
@@ -27,4 +28,21 @@ export const SIGNIN_ADMIN_SECTION: AdminSection = {
|
|
|
27
28
|
routes: [{ path: '', loadComponent: () => import('../signin/SigninPage').then(({ SigninPage }) => ({ default: SigninPage })), permissions: SIGNIN_ADMIN_PERMISSIONS }],
|
|
28
29
|
};
|
|
29
30
|
|
|
30
|
-
export const
|
|
31
|
+
export const GROUPS_ADMIN_SECTION: AdminSection = {
|
|
32
|
+
id: 'groups', basePath: '', title: 'Groups', icon: 'bi-diagram-3', navigationGroup: 'identity-access', permissions: GROUP_VIEW_PERMS,
|
|
33
|
+
routes: [{ path: 'groups', label: 'Groups', loadComponent: () => import('../identity/groups/GroupsPage').then(({ GroupsPage }) => ({ default: GroupsPage })), permissions: GROUP_VIEW_PERMS }],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const PERSONAL_API_KEYS_ADMIN_SECTION: AdminSection = {
|
|
37
|
+
id: 'personal-api-keys', basePath: '', title: 'Personal API Keys', icon: 'bi-key', navigationGroup: 'identity-access', permissions: USER_VIEW_PERMISSIONS,
|
|
38
|
+
routes: [{ path: 'apikeys', label: 'Personal API Keys', loadComponent: () => import('../identity/users/PersonalApiKeysPage').then(({ PersonalApiKeysPage }) => ({ default: PersonalApiKeysPage })) }],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const IDENTITY_ADMIN_SECTIONS = [
|
|
42
|
+
GROUPS_ADMIN_SECTION,
|
|
43
|
+
PERSONAL_API_KEYS_ADMIN_SECTION,
|
|
44
|
+
USERS_ADMIN_SECTION,
|
|
45
|
+
MEMBERS_ADMIN_SECTION,
|
|
46
|
+
CREDENTIALS_ADMIN_SECTION,
|
|
47
|
+
SIGNIN_ADMIN_SECTION,
|
|
48
|
+
] as const;
|