@parallelworks/ui 0.55.0 → 0.57.0
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 +60 -24
- package/dist/access/AccessDrawer.d.ts +30 -19
- package/dist/access/AccessDrawer.js +92 -67
- package/dist/access/AccessList.d.ts +17 -16
- package/dist/access/AccessList.js +218 -114
- package/dist/access/GrantView.d.ts +9 -8
- package/dist/access/GrantView.js +34 -36
- package/dist/access/PrincipalPicker.d.ts +38 -0
- package/dist/access/{GroupPicker.js → PrincipalPicker.js} +173 -80
- package/dist/access/index.d.ts +1 -1
- package/dist/access/model.d.ts +67 -24
- package/dist/access/model.js +111 -47
- package/dist/access/strings.d.ts +63 -39
- package/dist/access/strings.js +114 -84
- package/dist/access/types.d.ts +32 -14
- package/dist/ai/adapter/mock/mockAdapter.js +1 -1
- package/dist/ai/core/useSSEStream.d.ts +1 -1
- package/dist/components/Checkbox.js +1 -1
- package/dist/components/Combobox.js +25 -20
- package/dist/components/DatePicker.js +58 -53
- package/dist/components/Drawer.js +3 -1
- package/dist/components/HoverCard.js +15 -10
- package/dist/components/Menu.js +3 -0
- package/dist/components/Modal.js +32 -27
- package/dist/components/Popover.js +30 -25
- package/dist/components/Radio.js +1 -1
- package/dist/components/Select.js +47 -42
- package/dist/components/Slider.js +3 -3
- package/dist/components/Switch.js +1 -1
- package/dist/components/ThemeEditor.js +1 -1
- package/dist/components/Tooltip.js +16 -11
- package/dist/components/fieldBox.js +3 -3
- package/dist/components/fullscreen.d.ts +4 -0
- package/dist/components/fullscreen.js +18 -0
- package/dist/graph/GraphEditorDialogs.d.ts +1 -1
- package/dist/icons.d.ts +2 -0
- package/dist/icons.js +22 -2
- package/dist/styles/theme.css +2 -1
- package/dist/styles.css +1 -1
- package/dist/theme/derive.d.ts +3 -1
- package/dist/theme/derive.js +17 -11
- package/package.json +1 -1
- package/dist/access/GroupPicker.d.ts +0 -35
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ so an application ships only what it imports:
|
|
|
14
14
|
| `./editor` | Monaco editor (`monaco-editor` is an optional peer), loaded on first mount with the languages it needs; YAML validation, completion and hover run in a bundled worker |
|
|
15
15
|
| `./logviewer` | log viewer with ANSI color and search |
|
|
16
16
|
| `./file-explorer` | object storage explorer over a host-supplied provider |
|
|
17
|
-
| `./access` | `AccessDrawer`: who has access to a resource, and granting roles to groups |
|
|
17
|
+
| `./access` | `AccessDrawer`: who has access to a resource, and granting roles to people and groups |
|
|
18
18
|
| `./ai`, `./ai/openai`, `./ai/mock` | chat interface and adapters |
|
|
19
19
|
| `./theme.css`, `./base.css`, `./fonts.css`, `./styles.css`, `./logviewer.css`, `./ai.css` | stylesheets |
|
|
20
20
|
|
|
@@ -272,9 +272,26 @@ until it reaches its target against every surface it sits on.
|
|
|
272
272
|
| Secondary text, links, status text, syntax and terminal colors | 4.5 / 4.5 / 7 | 60 / 70 / 85 |
|
|
273
273
|
| Placeholders | 3 / 3 / 4.5 | 45 / 50 / 65 |
|
|
274
274
|
| Focus ring, status icons, dots, borders and bars | 3 / 3 / 4.5 | |
|
|
275
|
-
| Control
|
|
275
|
+
| Control edges | 3 / 3 / 4.5 | |
|
|
276
|
+
| Text field edges | 1.3 / 1.5 / 3 | |
|
|
276
277
|
| Text on the accent, white text on a solid status fill | 4.5 | |
|
|
277
278
|
|
|
279
|
+
Lines come in three tokens. `--theme-border` is decorative: card outlines,
|
|
280
|
+
dividers and table lines, which stay quiet. `--theme-border-strong` is a
|
|
281
|
+
control's edge: the box of a checkbox, the ring of a radio, and the off track of
|
|
282
|
+
a switch or slider. It is all that shows where the control is, so it is solved
|
|
283
|
+
to WCAG 1.4.11's 3:1 on the page, the panels and the input fill. A checked
|
|
284
|
+
control's edge, an on switch's track and a slider's filled part take
|
|
285
|
+
`--theme-focus`: the accent as picked when it reaches 3:1, and otherwise the
|
|
286
|
+
accent solved to secondary text's contrast, so on always reads heavier than the
|
|
287
|
+
off track. `--theme-border-field` is a text field's resting edge: inputs,
|
|
288
|
+
selects, textareas and pickers. A field is also found by its label, its size and
|
|
289
|
+
the text inside it, and a form of 3:1 boxes reads heavy, so its edge sits
|
|
290
|
+
between the other two and reaches 3:1 at contrast 100. Pointing at a field
|
|
291
|
+
raises its edge to `--theme-border-strong`. A host that sets
|
|
292
|
+
`--theme-border-strong` itself sets it to a color that keeps 3:1 against its
|
|
293
|
+
surfaces.
|
|
294
|
+
|
|
278
295
|
Status colors (success, error, warning, info, favorite) and terminal colors keep
|
|
279
296
|
fixed hues, so red means the same thing in every theme.
|
|
280
297
|
|
|
@@ -477,11 +494,13 @@ into a tone. Pass an `id` to replace a toast rather than stack another.
|
|
|
477
494
|
## Access
|
|
478
495
|
|
|
479
496
|
`AccessDrawer` shows who holds which role on one resource, and grants and takes
|
|
480
|
-
them away. A role is a named set of permissions; groups are granted
|
|
481
|
-
permissions one by one. Two roles may overlap: the drawer works the
|
|
482
|
-
from each role's `permissions` and says so as a hint ("Sudo covers
|
|
483
|
-
and before a role is taken away it lists exactly what
|
|
484
|
-
permissions
|
|
497
|
+
them away. A role is a named set of permissions; people and groups are granted
|
|
498
|
+
roles, never permissions one by one. Two roles may overlap: the drawer works the
|
|
499
|
+
overlap out from each role's `permissions` and says so as a hint ("Sudo covers
|
|
500
|
+
this too"), and before a role is taken away it lists exactly what is lost, the
|
|
501
|
+
permissions nothing they keep still gives, and what they keep: their other
|
|
502
|
+
roles, a person's groups' roles, everyone's, and roles granted above the
|
|
503
|
+
resource.
|
|
485
504
|
|
|
486
505
|
```tsx
|
|
487
506
|
import { AccessDrawer, type AccessChange } from '@parallelworks/ui/access'
|
|
@@ -492,11 +511,17 @@ import { AccessDrawer, type AccessChange } from '@parallelworks/ui/access'
|
|
|
492
511
|
resourceName={cluster.name}
|
|
493
512
|
roles={[{ id: 'login', name: 'Login', description: 'Sign in.', permissions: ['cluster:login'] }]}
|
|
494
513
|
permissions={[{ id: 'cluster:login', label: 'Sign in' }]}
|
|
495
|
-
|
|
496
|
-
|
|
514
|
+
principals={[
|
|
515
|
+
{ id: 'org', name: 'Everyone', kind: 'everyone', memberCount: 214 },
|
|
516
|
+
{ id: 'u-ana', name: 'Ana Ruiz', kind: 'user', detail: 'ana@example.com', groupIds: ['g-data'] },
|
|
517
|
+
{ id: 'g-data', name: 'data-eng', kind: 'group', memberCount: 24 },
|
|
518
|
+
]}
|
|
519
|
+
grants={grants} // { principalId, roleId }[]
|
|
520
|
+
owner="u-ana"
|
|
521
|
+
inherited={[{ principalId: 'g-admins', roleId: 'org-admin', from: 'the organization' }]}
|
|
497
522
|
onChange={(change: AccessChange) => api.patch(change)} // { grant, revoke }
|
|
498
523
|
describeGroup={(id, signal) => api.group(id, { signal })}
|
|
499
|
-
|
|
524
|
+
searchPrincipals={(query, signal) => api.principals({ query, signal })}
|
|
500
525
|
readOnly={!canManage}
|
|
501
526
|
readOnlyReason="Only people with the Admin role can change it."
|
|
502
527
|
/>
|
|
@@ -507,18 +532,29 @@ import { AccessDrawer, type AccessChange } from '@parallelworks/ui/access'
|
|
|
507
532
|
and when it rejects the drawer says that nothing changed and offers Retry. A
|
|
508
533
|
saved change offers Undo in a toast. Pass the new grants back through
|
|
509
534
|
`grants`; the drawer never keeps its own copy.
|
|
510
|
-
- `
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
host as the reader
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
- `
|
|
519
|
-
|
|
520
|
-
-
|
|
521
|
-
|
|
535
|
+
- `principals` holds every person and group that holds a role here or owns it,
|
|
536
|
+
the groups those people are in, and everyone if the resource can be granted
|
|
537
|
+
to everyone (`kind: 'everyone'`, at most one). Without `searchPrincipals` it
|
|
538
|
+
is also the list to grant to; with it, the picker asks the host as the reader
|
|
539
|
+
types, and lists people and groups apart.
|
|
540
|
+
- A person's `groupIds` name the groups they are in. Only groups holding a role
|
|
541
|
+
here matter, and with them, taking a role from the person says what they keep
|
|
542
|
+
through those groups.
|
|
543
|
+
- `owner` names the principal who owns the resource. It has every permission,
|
|
544
|
+
shows first, and can't be removed; granting it a role adds nothing.
|
|
545
|
+
- `inherited` holds roles granted above the resource, such as to the whole
|
|
546
|
+
organization. They show locked, "through" where they were granted, and count
|
|
547
|
+
toward what someone keeps. A role held only that way takes
|
|
548
|
+
`grantable: false`, so it isn't offered to grant here.
|
|
549
|
+
- `describeGroup` fills the pane beside the picker for the highlighted group:
|
|
550
|
+
where it comes from, its first members and when it last changed. It is asked
|
|
551
|
+
once per group while the drawer is mounted, and aborted when the highlight
|
|
552
|
+
moves on. A person's pane comes from the principal itself.
|
|
553
|
+
- `privileged` roles are labeled in the role picker. Granting anything to
|
|
554
|
+
everyone says how many people it reaches and asks to confirm, with a
|
|
555
|
+
destructive button when a privileged role is among them.
|
|
556
|
+
- A development build warns about duplicate ids, and grants, inherited grants
|
|
557
|
+
or an owner that name unknown roles or principals.
|
|
522
558
|
|
|
523
559
|
## Host chrome
|
|
524
560
|
|
|
@@ -587,8 +623,8 @@ meaning across the apps.
|
|
|
587
623
|
`DisableUserIcon` turns off their account. `ChangesRequestedIcon` asks for
|
|
588
624
|
changes to something sent, while `FlaggedIcon` marks an item for attention.
|
|
589
625
|
`ProjectIcon` is a project, while `FolderIcon` is a folder of files.
|
|
590
|
-
`LibraryIcon` is a collection of write-ups to browse,
|
|
591
|
-
|
|
626
|
+
`LibraryIcon` is a collection of write-ups to browse, `ArticleIcon` one
|
|
627
|
+
write-up to read, such as a case study, and `NotebookIcon` one notebook.
|
|
592
628
|
`PendingIcon` is a requirement not met yet and `NotRunningIcon` a run not
|
|
593
629
|
started; they share the empty circle, so keep them out of the same list.
|
|
594
630
|
- **Adding an icon:** pick the [Lucide glyph](https://lucide.dev/icons) and add
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import { type AccessModel } from './model.js';
|
|
3
|
+
import type { SearchPrincipals } from './PrincipalPicker.js';
|
|
3
4
|
import { type AccessStrings } from './strings.js';
|
|
4
|
-
import type { AccessChange, AccessGrant,
|
|
5
|
-
export type AccessArrangement = 'role' | '
|
|
5
|
+
import type { AccessChange, AccessGrant, AccessGroupDetails, AccessInheritedGrant, AccessPermission, AccessPrincipal, AccessRole } from './types.js';
|
|
6
|
+
export type AccessArrangement = 'role' | 'principal';
|
|
6
7
|
export interface AccessDrawerProps {
|
|
7
8
|
open: boolean;
|
|
8
9
|
onClose: () => void;
|
|
9
10
|
/** What access is to, as the reader knows it: "gpu-cluster-east". */
|
|
10
11
|
resourceName: string;
|
|
11
|
-
/** Every role that can be granted here, in the order to list them
|
|
12
|
+
/** Every role that can be granted here, in the order to list them, and any held here only
|
|
13
|
+
* through somewhere else (`grantable: false`). */
|
|
12
14
|
roles: readonly AccessRole[];
|
|
13
|
-
/** Every group that holds a role here
|
|
14
|
-
* everyone
|
|
15
|
-
|
|
15
|
+
/** Every person and group that holds a role here or owns it, the groups those people are in,
|
|
16
|
+
* and everyone when the resource can be granted to everyone. Without `searchPrincipals`,
|
|
17
|
+
* also everyone who can be granted one. */
|
|
18
|
+
principals: readonly AccessPrincipal[];
|
|
16
19
|
/** Who holds which role. The drawer shows these and never changes them itself. */
|
|
17
20
|
grants: readonly AccessGrant[];
|
|
18
|
-
/**
|
|
21
|
+
/** The principal who owns it, among `principals`: it has every permission, always, and can't
|
|
22
|
+
* be removed. */
|
|
23
|
+
owner?: string | undefined;
|
|
24
|
+
/** Roles held through something the resource is part of, such as its organization: they
|
|
25
|
+
* show, locked, and count toward what someone keeps. */
|
|
26
|
+
inherited?: readonly AccessInheritedGrant[] | undefined;
|
|
27
|
+
/** Names the permissions roles list, for what someone loses when they lose a role. */
|
|
19
28
|
permissions?: readonly AccessPermission[] | undefined;
|
|
20
29
|
/** Saves a change. Return a promise that settles once it is saved: until then its rows show
|
|
21
30
|
* that they are saving, and when it rejects the drawer says that nothing changed and offers
|
|
@@ -27,26 +36,28 @@ export interface AccessDrawerProps {
|
|
|
27
36
|
readOnly?: boolean | undefined;
|
|
28
37
|
/** Who can change it, said in read-only mode: "Only people with the Admin role can change it." */
|
|
29
38
|
readOnlyReason?: ReactNode;
|
|
30
|
-
/** More about a group, asked for when it is highlighted in the
|
|
39
|
+
/** More about a group, asked for when it is highlighted in the picker and kept while the
|
|
31
40
|
* drawer is mounted. A highlight that moves on aborts `signal`. */
|
|
32
41
|
describeGroup?: ((groupId: string, signal: AbortSignal) => Promise<AccessGroupDetails>) | undefined;
|
|
33
|
-
/** Finds groups to grant to, for an organization with too many to pass in
|
|
34
|
-
* query aborts `signal`. Without it, the picker filters `
|
|
35
|
-
|
|
42
|
+
/** Finds people and groups to grant to, for an organization with too many to pass in
|
|
43
|
+
* `principals`. A newer query aborts `signal`. Without it, the picker filters `principals`. */
|
|
44
|
+
searchPrincipals?: SearchPrincipals | undefined;
|
|
36
45
|
/** How many groups the organization has, for the picker's hint and heading. */
|
|
37
46
|
groupCount?: number | undefined;
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
/**
|
|
47
|
+
/** How many people the organization has, for the picker's hint and heading. */
|
|
48
|
+
peopleCount?: number | undefined;
|
|
49
|
+
/** People and groups the reader granted to lately, listed first in the picker. */
|
|
50
|
+
recentPrincipalIds?: readonly string[] | undefined;
|
|
51
|
+
/** How the list starts out: role by role, or person and group one by one. */
|
|
41
52
|
defaultArrangement?: AccessArrangement | undefined;
|
|
42
53
|
/** As `Drawer`'s. */
|
|
43
54
|
width?: number | string | undefined;
|
|
44
55
|
}
|
|
45
|
-
/** Who has access to one resource, and the place to change it: roles granted to
|
|
46
|
-
* Overlapping roles are allowed and only redundant, which the drawer points out; taking
|
|
47
|
-
* role away first says exactly what
|
|
48
|
-
* Undo in a toast. */
|
|
49
|
-
export declare function AccessDrawer({ open, onClose, resourceName, roles,
|
|
56
|
+
/** Who has access to one resource, and the place to change it: roles granted to people and
|
|
57
|
+
* groups. Overlapping roles are allowed and only redundant, which the drawer points out; taking
|
|
58
|
+
* a role away first says exactly what is lost. The owner and roles granted above the resource
|
|
59
|
+
* show, locked. Each change is saved as it is made, with Undo in a toast. */
|
|
60
|
+
export declare function AccessDrawer({ open, onClose, resourceName, roles, principals, grants, owner, inherited, permissions, onChange, loading, readOnly, readOnlyReason, describeGroup, searchPrincipals, groupCount, peopleCount, recentPrincipalIds, defaultArrangement, width, }: AccessDrawerProps): import("react").JSX.Element;
|
|
50
61
|
type Match = (query: string, ...texts: (string | undefined)[]) => boolean;
|
|
51
62
|
export declare function filterAccess(model: AccessModel, arrangement: AccessArrangement, query: string, match: Match, t: AccessStrings): {
|
|
52
63
|
shown: Map<string, readonly string[]>;
|
|
@@ -8,8 +8,8 @@ import { Drawer } from "../components/Drawer.js";
|
|
|
8
8
|
import { LoadingRegion, Skeleton } from "../components/Skeleton.js";
|
|
9
9
|
import { SearchInput } from "../components/SearchInput.js";
|
|
10
10
|
import { SegmentedControl } from "../components/SegmentedControl.js";
|
|
11
|
-
import { buildModel, invert, key, warnAboutMisuse } from "./model.js";
|
|
12
|
-
import {
|
|
11
|
+
import { buildModel, invert, key, principalsWithAccess, warnAboutMisuse } from "./model.js";
|
|
12
|
+
import { ByPrincipal, ByRole, OWNER_SECTION, principalLabel, removeKey } from "./AccessList.js";
|
|
13
13
|
import { holdFocus, useFocusAfterRender } from "./focus.js";
|
|
14
14
|
import { useGrantView } from "./GrantView.js";
|
|
15
15
|
import { useAccessStrings } from "./strings.js";
|
|
@@ -17,29 +17,47 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
17
17
|
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
18
18
|
//#region src/access/AccessDrawer.tsx
|
|
19
19
|
var EMPTY_SELECTION = {
|
|
20
|
-
|
|
20
|
+
principalIds: [],
|
|
21
21
|
roleIds: []
|
|
22
22
|
};
|
|
23
|
-
/** Who has access to one resource, and the place to change it: roles granted to
|
|
24
|
-
* Overlapping roles are allowed and only redundant, which the drawer points out; taking
|
|
25
|
-
* role away first says exactly what
|
|
26
|
-
* Undo in a toast. */
|
|
27
|
-
function AccessDrawer({ open, onClose, resourceName, roles,
|
|
23
|
+
/** Who has access to one resource, and the place to change it: roles granted to people and
|
|
24
|
+
* groups. Overlapping roles are allowed and only redundant, which the drawer points out; taking
|
|
25
|
+
* a role away first says exactly what is lost. The owner and roles granted above the resource
|
|
26
|
+
* show, locked. Each change is saved as it is made, with Undo in a toast. */
|
|
27
|
+
function AccessDrawer({ open, onClose, resourceName, roles, principals, grants, owner, inherited, permissions, onChange, loading = false, readOnly = false, readOnlyReason, describeGroup, searchPrincipals, groupCount, peopleCount, recentPrincipalIds, defaultArrangement = "role", width = 560 }) {
|
|
28
28
|
const t = useAccessStrings();
|
|
29
29
|
const uid = useId();
|
|
30
|
-
const model = useMemo(() => buildModel(
|
|
30
|
+
const model = useMemo(() => buildModel({
|
|
31
31
|
roles,
|
|
32
|
-
|
|
32
|
+
principals,
|
|
33
33
|
grants,
|
|
34
|
-
permissions
|
|
34
|
+
permissions,
|
|
35
|
+
inherited,
|
|
36
|
+
owner
|
|
37
|
+
}), [
|
|
38
|
+
roles,
|
|
39
|
+
principals,
|
|
40
|
+
grants,
|
|
41
|
+
permissions,
|
|
42
|
+
inherited,
|
|
43
|
+
owner
|
|
35
44
|
]);
|
|
36
45
|
useEffect(() => {
|
|
37
|
-
if (!loading) warnAboutMisuse(
|
|
46
|
+
if (!loading) warnAboutMisuse({
|
|
47
|
+
roles,
|
|
48
|
+
principals,
|
|
49
|
+
grants,
|
|
50
|
+
permissions,
|
|
51
|
+
inherited,
|
|
52
|
+
owner
|
|
53
|
+
});
|
|
38
54
|
}, [
|
|
39
55
|
roles,
|
|
40
|
-
|
|
56
|
+
principals,
|
|
41
57
|
grants,
|
|
42
58
|
permissions,
|
|
59
|
+
inherited,
|
|
60
|
+
owner,
|
|
43
61
|
loading
|
|
44
62
|
]);
|
|
45
63
|
const [view, setView] = useState("list");
|
|
@@ -48,7 +66,7 @@ function AccessDrawer({ open, onClose, resourceName, roles, groups, grants, perm
|
|
|
48
66
|
const [query, setQuery] = useState("");
|
|
49
67
|
const [expanded, setExpanded] = useState({
|
|
50
68
|
role: /* @__PURE__ */ new Set(),
|
|
51
|
-
|
|
69
|
+
principal: /* @__PURE__ */ new Set()
|
|
52
70
|
});
|
|
53
71
|
const [removal, setRemoval] = useState();
|
|
54
72
|
const [saving, setSaving] = useState();
|
|
@@ -71,9 +89,9 @@ function AccessDrawer({ open, onClose, resourceName, roles, groups, grants, perm
|
|
|
71
89
|
const names = (changed) => {
|
|
72
90
|
const all = [...changed.grant, ...changed.revoke];
|
|
73
91
|
return {
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
return
|
|
92
|
+
principalNames: [...new Set(all.map((grant) => grant.principalId))].map((id) => {
|
|
93
|
+
const principal = seen.get(id) ?? model.principalById.get(id);
|
|
94
|
+
return principal?.kind === "everyone" ? t.everyone : principal?.name ?? id;
|
|
77
95
|
}),
|
|
78
96
|
roleNames: [...new Set(all.map((grant) => grant.roleId))].map((id) => model.roleById.get(id)?.name ?? id)
|
|
79
97
|
};
|
|
@@ -84,7 +102,7 @@ function AccessDrawer({ open, onClose, resourceName, roles, groups, grants, perm
|
|
|
84
102
|
/** Saves a change and reports how it went; resolves true once saved. */
|
|
85
103
|
const save = async (next, where) => {
|
|
86
104
|
if (!onChange || next.grant.length === 0 && next.revoke.length === 0) return false;
|
|
87
|
-
const {
|
|
105
|
+
const { principalNames, roleNames } = names(next);
|
|
88
106
|
setSaving(next);
|
|
89
107
|
setFailure(void 0);
|
|
90
108
|
try {
|
|
@@ -92,7 +110,7 @@ function AccessDrawer({ open, onClose, resourceName, roles, groups, grants, perm
|
|
|
92
110
|
} catch (error) {
|
|
93
111
|
setFailure({
|
|
94
112
|
change: next,
|
|
95
|
-
title: next.grant.length > 0 ? t.grantFailed(
|
|
113
|
+
title: next.grant.length > 0 ? t.grantFailed(principalNames, roleNames) : t.removeFailed(principalNames[0] ?? "", roleNames),
|
|
96
114
|
message: messageOf(error),
|
|
97
115
|
...where
|
|
98
116
|
});
|
|
@@ -100,7 +118,7 @@ function AccessDrawer({ open, onClose, resourceName, roles, groups, grants, perm
|
|
|
100
118
|
} finally {
|
|
101
119
|
setSaving(void 0);
|
|
102
120
|
}
|
|
103
|
-
toast.success(next.grant.length > 0 ? t.granted(
|
|
121
|
+
toast.success(next.grant.length > 0 ? t.granted(principalNames, roleNames) : t.removed(principalNames, roleNames), { action: {
|
|
104
122
|
label: t.common.undo,
|
|
105
123
|
onClick: () => undo(next)
|
|
106
124
|
} });
|
|
@@ -111,9 +129,9 @@ function AccessDrawer({ open, onClose, resourceName, roles, groups, grants, perm
|
|
|
111
129
|
setFailure((current) => current?.inGrant ? void 0 : current);
|
|
112
130
|
focusLater(() => visible(body.current?.querySelectorAll("[data-grant-access]")));
|
|
113
131
|
};
|
|
114
|
-
const toGrant = (
|
|
132
|
+
const toGrant = (principalIds = []) => {
|
|
115
133
|
setSelection({
|
|
116
|
-
|
|
134
|
+
principalIds,
|
|
117
135
|
roleIds: []
|
|
118
136
|
});
|
|
119
137
|
setRemoval(void 0);
|
|
@@ -123,11 +141,11 @@ function AccessDrawer({ open, onClose, resourceName, roles, groups, grants, perm
|
|
|
123
141
|
const grant = async (next) => {
|
|
124
142
|
if (await save(next, {
|
|
125
143
|
inGrant: true,
|
|
126
|
-
|
|
144
|
+
principalId: next.grant[0]?.principalId ?? ""
|
|
127
145
|
})) {
|
|
128
146
|
setExpanded((current) => ({
|
|
129
147
|
role: /* @__PURE__ */ new Set([...current.role, ...next.grant.map((g) => g.roleId)]),
|
|
130
|
-
|
|
148
|
+
principal: /* @__PURE__ */ new Set([...current.principal, ...next.grant.map((g) => g.principalId)])
|
|
131
149
|
}));
|
|
132
150
|
toList();
|
|
133
151
|
}
|
|
@@ -136,22 +154,22 @@ function AccessDrawer({ open, onClose, resourceName, roles, groups, grants, perm
|
|
|
136
154
|
const next = {
|
|
137
155
|
grant: [],
|
|
138
156
|
revoke: pending.roleIds.map((roleId) => ({
|
|
139
|
-
|
|
157
|
+
principalId: pending.principalId,
|
|
140
158
|
roleId
|
|
141
159
|
}))
|
|
142
160
|
};
|
|
143
161
|
const anchor = arrangement === "role" && !pending.all ? pending.roleIds[0] : void 0;
|
|
144
162
|
await save(next, {
|
|
145
163
|
inGrant: false,
|
|
146
|
-
|
|
164
|
+
principalId: pending.principalId,
|
|
147
165
|
roleId: anchor
|
|
148
166
|
});
|
|
149
167
|
setRemoval(void 0);
|
|
150
|
-
focusLater(() => document.getElementById(sectionId(anchor ?? pending.
|
|
168
|
+
focusLater(() => document.getElementById(sectionId(anchor ?? pending.principalId)) ?? body.current?.querySelector("input[type=\"search\"]"));
|
|
151
169
|
};
|
|
152
170
|
const retry = (failed) => failed.inGrant ? grant(failed.change) : save(failed.change, {
|
|
153
171
|
inGrant: false,
|
|
154
|
-
|
|
172
|
+
principalId: failed.principalId,
|
|
155
173
|
roleId: failed.roleId
|
|
156
174
|
});
|
|
157
175
|
const failureNode = failure && /* @__PURE__ */ jsx(Callout, {
|
|
@@ -179,9 +197,10 @@ function AccessDrawer({ open, onClose, resourceName, roles, groups, grants, perm
|
|
|
179
197
|
onSelectionChange: setSelection,
|
|
180
198
|
resourceName,
|
|
181
199
|
describeGroup,
|
|
182
|
-
|
|
200
|
+
searchPrincipals,
|
|
183
201
|
groupCount,
|
|
184
|
-
|
|
202
|
+
peopleCount,
|
|
203
|
+
recentPrincipalIds,
|
|
185
204
|
cache,
|
|
186
205
|
seen,
|
|
187
206
|
saving: saving !== void 0,
|
|
@@ -193,8 +212,9 @@ function AccessDrawer({ open, onClose, resourceName, roles, groups, grants, perm
|
|
|
193
212
|
t
|
|
194
213
|
});
|
|
195
214
|
const savingKeys = useMemo(() => new Set(saving?.revoke.map(key) ?? []), [saving]);
|
|
196
|
-
const
|
|
197
|
-
const
|
|
215
|
+
const withAccess = principalsWithAccess(model);
|
|
216
|
+
const empty = !loading && withAccess.length === 0;
|
|
217
|
+
const View = arrangement === "role" ? ByRole : ByPrincipal;
|
|
198
218
|
const grantButton = (className) => /* @__PURE__ */ jsx(Button, {
|
|
199
219
|
variant: "primary",
|
|
200
220
|
icon: /* @__PURE__ */ jsx(AddIcon, {}),
|
|
@@ -233,11 +253,12 @@ function AccessDrawer({ open, onClose, resourceName, roles, groups, grants, perm
|
|
|
233
253
|
value: "role",
|
|
234
254
|
label: t.byRole
|
|
235
255
|
}, {
|
|
236
|
-
value: "
|
|
237
|
-
label: t.
|
|
256
|
+
value: "principal",
|
|
257
|
+
label: t.byPrincipal
|
|
238
258
|
}]
|
|
239
259
|
})
|
|
240
260
|
}),
|
|
261
|
+
editable && grantButton("ms-auto hidden @min-[30rem]/access:inline-flex"),
|
|
241
262
|
/* @__PURE__ */ jsx(SearchInput, {
|
|
242
263
|
label: t.filter,
|
|
243
264
|
value: query,
|
|
@@ -245,9 +266,8 @@ function AccessDrawer({ open, onClose, resourceName, roles, groups, grants, perm
|
|
|
245
266
|
setQuery(next);
|
|
246
267
|
setExpanded(expandedFor(model, next.trim(), match, t));
|
|
247
268
|
},
|
|
248
|
-
className: "min-w-0
|
|
249
|
-
})
|
|
250
|
-
editable && grantButton("hidden @min-[30rem]/access:inline-flex")
|
|
269
|
+
className: "min-w-0 basis-full"
|
|
270
|
+
})
|
|
251
271
|
]
|
|
252
272
|
}),
|
|
253
273
|
failure && !failure.inGrant && !filtered.order.includes(anchorOf(failure, arrangement)) && /* @__PURE__ */ jsx("div", {
|
|
@@ -307,10 +327,10 @@ function AccessDrawer({ open, onClose, resourceName, roles, groups, grants, perm
|
|
|
307
327
|
if (asked) focusLater(() => body.current?.querySelector(`[data-remove="${CSS.escape(removeKey(asked))}"]`));
|
|
308
328
|
},
|
|
309
329
|
onRemove: remove,
|
|
310
|
-
onGrantMore: (
|
|
330
|
+
onGrantMore: (principalId) => toGrant([principalId]),
|
|
311
331
|
failure: failure && !failure.inGrant ? {
|
|
312
332
|
node: failureNode,
|
|
313
|
-
|
|
333
|
+
principalId: failure.principalId,
|
|
314
334
|
roleId: failure.roleId
|
|
315
335
|
} : void 0
|
|
316
336
|
}
|
|
@@ -329,7 +349,7 @@ function AccessDrawer({ open, onClose, resourceName, roles, groups, grants, perm
|
|
|
329
349
|
width,
|
|
330
350
|
padded: false,
|
|
331
351
|
title: granting ? t.grantAccess : t.title,
|
|
332
|
-
description: granting ? resourceName : loading ? resourceName : t.summary(resourceName,
|
|
352
|
+
description: granting ? resourceName : loading ? resourceName : t.summary(resourceName, withAccess.filter((principal) => principal.kind === "user").length, withAccess.filter((principal) => principal.kind === "group").length, withAccess.some((principal) => principal.kind === "everyone")),
|
|
333
353
|
onBack: granting ? toList : void 0,
|
|
334
354
|
backLabel: t.back,
|
|
335
355
|
footer: granting ? grantView.footer : void 0,
|
|
@@ -349,18 +369,21 @@ function AccessDrawer({ open, onClose, resourceName, roles, groups, grants, perm
|
|
|
349
369
|
});
|
|
350
370
|
}
|
|
351
371
|
var messageOf = (error) => error instanceof Error && error.message ? error.message : void 0;
|
|
352
|
-
var anchorOf = (failure, arrangement) => arrangement === "role" ? failure.roleId ?? "" : failure.
|
|
353
|
-
/** The
|
|
354
|
-
|
|
372
|
+
var anchorOf = (failure, arrangement) => arrangement === "role" ? failure.roleId ?? "" : failure.principalId;
|
|
373
|
+
/** The one of `elements` that is laid out, such as the one of two buttons a width shows, or the
|
|
374
|
+
* first where nothing is (as in tests). */
|
|
355
375
|
function visible(elements) {
|
|
356
376
|
const all = [...elements ?? []];
|
|
357
377
|
return all.find((element) => element.getClientRects().length > 0) ?? all[0];
|
|
358
378
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
t
|
|
362
|
-
|
|
363
|
-
|
|
379
|
+
/** What a principal can be found by. */
|
|
380
|
+
var principalTexts = (model, principal, t) => [
|
|
381
|
+
principalLabel(principal, t),
|
|
382
|
+
principal.name,
|
|
383
|
+
principal.detail,
|
|
384
|
+
...principal.kind === "everyone" ? [t.everyone] : [],
|
|
385
|
+
...model.owner?.id === principal.id ? [t.owner] : []
|
|
386
|
+
];
|
|
364
387
|
/** The sections in view and the rows under each. A section named by the query shows all its
|
|
365
388
|
* rows; otherwise only the rows the query names, and no section when none are. */
|
|
366
389
|
function useFiltered(model, arrangement, query, match, t) {
|
|
@@ -375,27 +398,29 @@ function useFiltered(model, arrangement, query, match, t) {
|
|
|
375
398
|
function filterAccess(model, arrangement, query, match, t) {
|
|
376
399
|
const shown = /* @__PURE__ */ new Map();
|
|
377
400
|
const order = [];
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
if (!query ||
|
|
382
|
-
order.push(
|
|
383
|
-
shown.set(
|
|
401
|
+
const found = (principal) => match(query, ...principalTexts(model, principal, t));
|
|
402
|
+
if (arrangement === "role") {
|
|
403
|
+
const owner = model.owner;
|
|
404
|
+
if (owner && (!query || match(query, t.owner) || found(owner))) {
|
|
405
|
+
order.push(OWNER_SECTION);
|
|
406
|
+
shown.set(OWNER_SECTION, [owner.id]);
|
|
384
407
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
for (const groupId of holders) {
|
|
390
|
-
const group = model.groupById.get(groupId);
|
|
391
|
-
const held = model.held.get(groupId) ?? [];
|
|
392
|
-
if (!group) continue;
|
|
393
|
-
const rows = !query || match(query, ...groupNames(group, t)) ? held : held.filter((role) => match(query, role.name, role.description));
|
|
408
|
+
for (const role of model.roles) {
|
|
409
|
+
const holders = [...new Map([...model.holders.get(role.id) ?? [], ...(model.inheritedHolders.get(role.id) ?? []).map((holder) => holder.principal)].map((principal) => [principal.id, principal])).values()];
|
|
410
|
+
if (role.grantable === false && holders.length === 0) continue;
|
|
411
|
+
const rows = !query || match(query, role.name, role.description) ? holders : holders.filter(found);
|
|
394
412
|
if (!query || rows.length > 0) {
|
|
395
|
-
order.push(
|
|
396
|
-
shown.set(
|
|
413
|
+
order.push(role.id);
|
|
414
|
+
shown.set(role.id, rows.map((principal) => principal.id));
|
|
397
415
|
}
|
|
398
416
|
}
|
|
417
|
+
} else for (const principal of principalsWithAccess(model)) {
|
|
418
|
+
const roles = [.../* @__PURE__ */ new Set([...model.held.get(principal.id) ?? [], ...(model.inherited.get(principal.id) ?? []).map((holding) => holding.role)])];
|
|
419
|
+
const rows = !query || found(principal) ? roles : roles.filter((role) => match(query, role.name, role.description));
|
|
420
|
+
if (!query || rows.length > 0 || model.owner?.id === principal.id && found(principal)) {
|
|
421
|
+
order.push(principal.id);
|
|
422
|
+
shown.set(principal.id, rows.map((role) => role.id));
|
|
423
|
+
}
|
|
399
424
|
}
|
|
400
425
|
return {
|
|
401
426
|
shown,
|
|
@@ -406,7 +431,7 @@ function filterAccess(model, arrangement, query, match, t) {
|
|
|
406
431
|
function expandedFor(model, query, match, t) {
|
|
407
432
|
if (!query) return {
|
|
408
433
|
role: /* @__PURE__ */ new Set(),
|
|
409
|
-
|
|
434
|
+
principal: /* @__PURE__ */ new Set()
|
|
410
435
|
};
|
|
411
436
|
const open = (arrangement) => {
|
|
412
437
|
const { shown } = filterAccess(model, arrangement, query, match, t);
|
|
@@ -414,7 +439,7 @@ function expandedFor(model, query, match, t) {
|
|
|
414
439
|
};
|
|
415
440
|
return {
|
|
416
441
|
role: open("role"),
|
|
417
|
-
|
|
442
|
+
principal: open("principal")
|
|
418
443
|
};
|
|
419
444
|
}
|
|
420
445
|
function RowsSkeleton() {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import { type AccessModel } from './model.js';
|
|
3
3
|
import type { AccessStrings } from './strings.js';
|
|
4
|
-
|
|
4
|
+
import type { AccessPrincipal } from './types.js';
|
|
5
|
+
/** The id the owner's section has among the roles' sections. */
|
|
6
|
+
export declare const OWNER_SECTION = "__owner";
|
|
7
|
+
/** A removal waiting on the reader: one role, or every role a principal holds here. */
|
|
5
8
|
export interface PendingRemoval {
|
|
6
|
-
|
|
9
|
+
principalId: string;
|
|
7
10
|
roleIds: string[];
|
|
8
|
-
/** Every role
|
|
11
|
+
/** Every role it holds, from its "Remove all access". */
|
|
9
12
|
all: boolean;
|
|
10
13
|
}
|
|
11
14
|
export interface ListActions {
|
|
@@ -16,18 +19,16 @@ export interface ListActions {
|
|
|
16
19
|
onAskRemove: (removal: PendingRemoval) => void;
|
|
17
20
|
onCancelRemove: () => void;
|
|
18
21
|
onRemove: (removal: PendingRemoval) => void;
|
|
19
|
-
onGrantMore: (
|
|
20
|
-
/** The failed save's callout, and where it belongs: under the role or
|
|
22
|
+
onGrantMore: (principalId: string) => void;
|
|
23
|
+
/** The failed save's callout, and where it belongs: under the role or principal it was about. */
|
|
21
24
|
failure: {
|
|
22
25
|
node: ReactNode;
|
|
23
26
|
roleId?: string | undefined;
|
|
24
|
-
|
|
27
|
+
principalId: string;
|
|
25
28
|
} | undefined;
|
|
26
29
|
}
|
|
27
|
-
export declare
|
|
28
|
-
|
|
29
|
-
}): import("react").JSX.Element;
|
|
30
|
-
/** Asks before a group loses roles, saying exactly what it loses and what it keeps. */
|
|
30
|
+
export declare const principalLabel: (principal: AccessPrincipal, t: AccessStrings) => string;
|
|
31
|
+
/** Asks before a principal loses roles, saying exactly what it loses and what it keeps. */
|
|
31
32
|
export declare function RemoveConfirm({ model, removal, saving, onCancel, onConfirm, t, }: {
|
|
32
33
|
model: AccessModel;
|
|
33
34
|
removal: PendingRemoval;
|
|
@@ -40,9 +41,9 @@ export declare function RemoveConfirm({ model, removal, saving, onCancel, onConf
|
|
|
40
41
|
export declare const removeKey: (removal: PendingRemoval) => string;
|
|
41
42
|
interface ViewProps {
|
|
42
43
|
model: AccessModel;
|
|
43
|
-
/** What the filter left: each role's
|
|
44
|
+
/** What the filter left: each role's principals, or each principal's roles. */
|
|
44
45
|
shown: ReadonlyMap<string, readonly string[]>;
|
|
45
|
-
/** The
|
|
46
|
+
/** The sections in view, in order. */
|
|
46
47
|
order: readonly string[];
|
|
47
48
|
expanded: ReadonlySet<string>;
|
|
48
49
|
onExpandedChange: (id: string, open: boolean) => void;
|
|
@@ -51,8 +52,8 @@ interface ViewProps {
|
|
|
51
52
|
t: AccessStrings;
|
|
52
53
|
sectionId: (id: string) => string;
|
|
53
54
|
}
|
|
54
|
-
/** Who has each role, role by role. */
|
|
55
|
-
export declare function ByRole({ model, shown, order, expanded, onExpandedChange, filtering, actions, t, sectionId, }: ViewProps): (import("react").JSX.Element | null)[];
|
|
56
|
-
/** Which roles each group has,
|
|
57
|
-
export declare function
|
|
55
|
+
/** Who has each role, role by role, after the owner. */
|
|
56
|
+
export declare function ByRole({ model, shown, order, expanded, onExpandedChange, filtering, actions, t, sectionId, }: ViewProps): (import("react").JSX.Element | null | undefined)[];
|
|
57
|
+
/** Which roles each person and group has, one by one. */
|
|
58
|
+
export declare function ByPrincipal({ model, shown, order, expanded, onExpandedChange, actions, t, sectionId, }: ViewProps): (import("react").JSX.Element | null)[];
|
|
58
59
|
export {};
|