@parallelworks/ui 0.55.0 → 0.56.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.
Files changed (40) hide show
  1. package/README.md +52 -22
  2. package/dist/access/AccessDrawer.d.ts +30 -19
  3. package/dist/access/AccessDrawer.js +92 -67
  4. package/dist/access/AccessList.d.ts +17 -16
  5. package/dist/access/AccessList.js +218 -114
  6. package/dist/access/GrantView.d.ts +9 -8
  7. package/dist/access/GrantView.js +34 -36
  8. package/dist/access/PrincipalPicker.d.ts +38 -0
  9. package/dist/access/{GroupPicker.js → PrincipalPicker.js} +173 -80
  10. package/dist/access/index.d.ts +1 -1
  11. package/dist/access/model.d.ts +67 -24
  12. package/dist/access/model.js +111 -47
  13. package/dist/access/strings.d.ts +63 -39
  14. package/dist/access/strings.js +114 -84
  15. package/dist/access/types.d.ts +32 -14
  16. package/dist/ai/adapter/mock/mockAdapter.js +1 -1
  17. package/dist/ai/core/useSSEStream.d.ts +1 -1
  18. package/dist/components/Checkbox.js +1 -1
  19. package/dist/components/Combobox.js +25 -20
  20. package/dist/components/DatePicker.js +58 -53
  21. package/dist/components/Drawer.js +3 -1
  22. package/dist/components/HoverCard.js +15 -10
  23. package/dist/components/Menu.js +3 -0
  24. package/dist/components/Modal.js +32 -27
  25. package/dist/components/Popover.js +30 -25
  26. package/dist/components/Radio.js +1 -1
  27. package/dist/components/Select.js +47 -42
  28. package/dist/components/Slider.js +3 -3
  29. package/dist/components/Switch.js +1 -1
  30. package/dist/components/Tooltip.js +16 -11
  31. package/dist/components/fieldBox.js +1 -1
  32. package/dist/components/fullscreen.d.ts +4 -0
  33. package/dist/components/fullscreen.js +18 -0
  34. package/dist/graph/GraphEditorDialogs.d.ts +1 -1
  35. package/dist/styles/theme.css +1 -1
  36. package/dist/styles.css +1 -1
  37. package/dist/theme/derive.d.ts +2 -0
  38. package/dist/theme/derive.js +12 -13
  39. package/package.json +1 -1
  40. 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,20 @@ 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 borders | 1.6 / 2.2 / 3 | |
275
+ | Control edges | 3 / 3 / 4.5 | |
276
276
  | Text on the accent, white text on a solid status fill | 4.5 | |
277
277
 
278
+ Lines come in two tokens. `--theme-border` is decorative: card outlines,
279
+ dividers and table lines, which stay quiet. `--theme-border-strong` is a
280
+ control's edge: the box of an input, select or checkbox, the ring of a radio,
281
+ and the off track of a switch or slider. It is often all that shows where the
282
+ control is, so it is solved to WCAG 1.4.11's 3:1 on the page, the panels and the
283
+ input fill. A checked control's edge, an on switch's track and a slider's
284
+ filled part take `--theme-focus`: the accent as picked when it reaches 3:1, and
285
+ otherwise the accent solved to secondary text's contrast, so on always reads
286
+ heavier than the off track. A host that sets `--theme-border-strong` itself
287
+ sets it to a color that keeps 3:1 against its surfaces.
288
+
278
289
  Status colors (success, error, warning, info, favorite) and terminal colors keep
279
290
  fixed hues, so red means the same thing in every theme.
280
291
 
@@ -477,11 +488,13 @@ into a tone. Pass an `id` to replace a toast rather than stack another.
477
488
  ## Access
478
489
 
479
490
  `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 roles, never
481
- permissions one by one. Two roles may overlap: the drawer works the overlap out
482
- from each role's `permissions` and says so as a hint ("Sudo covers this too"),
483
- and before a role is taken away it lists exactly what the group loses, the
484
- permissions no role it keeps still gives, and what it keeps.
491
+ them away. A role is a named set of permissions; people and groups are granted
492
+ roles, never permissions one by one. Two roles may overlap: the drawer works the
493
+ overlap out from each role's `permissions` and says so as a hint ("Sudo covers
494
+ this too"), and before a role is taken away it lists exactly what is lost, the
495
+ permissions nothing they keep still gives, and what they keep: their other
496
+ roles, a person's groups' roles, everyone's, and roles granted above the
497
+ resource.
485
498
 
486
499
  ```tsx
487
500
  import { AccessDrawer, type AccessChange } from '@parallelworks/ui/access'
@@ -492,11 +505,17 @@ import { AccessDrawer, type AccessChange } from '@parallelworks/ui/access'
492
505
  resourceName={cluster.name}
493
506
  roles={[{ id: 'login', name: 'Login', description: 'Sign in.', permissions: ['cluster:login'] }]}
494
507
  permissions={[{ id: 'cluster:login', label: 'Sign in' }]}
495
- groups={[{ id: 'org', name: 'Everyone', kind: 'everyone', memberCount: 214 }, ...granted]}
496
- grants={grants} // { groupId, roleId }[]
508
+ principals={[
509
+ { id: 'org', name: 'Everyone', kind: 'everyone', memberCount: 214 },
510
+ { id: 'u-ana', name: 'Ana Ruiz', kind: 'user', detail: 'ana@example.com', groupIds: ['g-data'] },
511
+ { id: 'g-data', name: 'data-eng', kind: 'group', memberCount: 24 },
512
+ ]}
513
+ grants={grants} // { principalId, roleId }[]
514
+ owner="u-ana"
515
+ inherited={[{ principalId: 'g-admins', roleId: 'org-admin', from: 'the organization' }]}
497
516
  onChange={(change: AccessChange) => api.patch(change)} // { grant, revoke }
498
517
  describeGroup={(id, signal) => api.group(id, { signal })}
499
- searchGroups={(query, signal) => api.groups({ query, signal })}
518
+ searchPrincipals={(query, signal) => api.principals({ query, signal })}
500
519
  readOnly={!canManage}
501
520
  readOnlyReason="Only people with the Admin role can change it."
502
521
  />
@@ -507,18 +526,29 @@ import { AccessDrawer, type AccessChange } from '@parallelworks/ui/access'
507
526
  and when it rejects the drawer says that nothing changed and offers Retry. A
508
527
  saved change offers Undo in a toast. Pass the new grants back through
509
528
  `grants`; the drawer never keeps its own copy.
510
- - `groups` holds every group that has a role here, and everyone if the resource
511
- can be granted to everyone (`kind: 'everyone'`, at most one). Without
512
- `searchGroups` it is also the list to grant to; with it, the picker asks the
513
- host as the reader types.
514
- - `describeGroup` fills the pane beside the group picker for the highlighted
515
- group: where it comes from, its first members and when it last changed. It is
516
- asked once per group while the drawer is mounted, and aborted when the
517
- highlight moves on.
518
- - `highImpact` roles carry a tag, and granting anything to everyone says how
519
- many people it reaches and asks to confirm.
520
- - A development build warns about duplicate ids and grants that name unknown
521
- roles or groups.
529
+ - `principals` holds every person and group that holds a role here or owns it,
530
+ the groups those people are in, and everyone if the resource can be granted
531
+ to everyone (`kind: 'everyone'`, at most one). Without `searchPrincipals` it
532
+ is also the list to grant to; with it, the picker asks the host as the reader
533
+ types, and lists people and groups apart.
534
+ - A person's `groupIds` name the groups they are in. Only groups holding a role
535
+ here matter, and with them, taking a role from the person says what they keep
536
+ through those groups.
537
+ - `owner` names the principal who owns the resource. It has every permission,
538
+ shows first, and can't be removed; granting it a role adds nothing.
539
+ - `inherited` holds roles granted above the resource, such as to the whole
540
+ organization. They show locked, "through" where they were granted, and count
541
+ toward what someone keeps. A role held only that way takes
542
+ `grantable: false`, so it isn't offered to grant here.
543
+ - `describeGroup` fills the pane beside the picker for the highlighted group:
544
+ where it comes from, its first members and when it last changed. It is asked
545
+ once per group while the drawer is mounted, and aborted when the highlight
546
+ moves on. A person's pane comes from the principal itself.
547
+ - `privileged` roles are labeled in the role picker. Granting anything to
548
+ everyone says how many people it reaches and asks to confirm, with a
549
+ destructive button when a privileged role is among them.
550
+ - A development build warns about duplicate ids, and grants, inherited grants
551
+ or an owner that name unknown roles or principals.
522
552
 
523
553
  ## Host chrome
524
554
 
@@ -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, AccessGroup, AccessGroupDetails, AccessPermission, AccessRole } from './types.js';
5
- export type AccessArrangement = 'role' | 'group';
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, and everyone when the resource can be granted to
14
- * everyone. Without `searchGroups`, also every group that can be granted one. */
15
- groups: readonly AccessGroup[];
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
- /** Names the permissions roles list, for what a group loses when it loses a role. */
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 group picker and kept while 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 `groups`. A newer
34
- * query aborts `signal`. Without it, the picker filters `groups`. */
35
- searchGroups?: ((query: string, signal: AbortSignal) => Promise<readonly AccessGroup[]>) | undefined;
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
- /** Groups the reader granted to lately, listed first in the picker. */
39
- recentGroupIds?: readonly string[] | undefined;
40
- /** How the list starts out: role by role, or group by group. */
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 groups.
46
- * Overlapping roles are allowed and only redundant, which the drawer points out; taking a
47
- * role away first says exactly what the group loses. Each change is saved as it is made, with
48
- * Undo in a toast. */
49
- export declare function AccessDrawer({ open, onClose, resourceName, roles, groups, grants, permissions, onChange, loading, readOnly, readOnlyReason, describeGroup, searchGroups, groupCount, recentGroupIds, defaultArrangement, width, }: AccessDrawerProps): import("react").JSX.Element;
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 { ByGroup, ByRole, removeKey } from "./AccessList.js";
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
- groupIds: [],
20
+ principalIds: [],
21
21
  roleIds: []
22
22
  };
23
- /** Who has access to one resource, and the place to change it: roles granted to groups.
24
- * Overlapping roles are allowed and only redundant, which the drawer points out; taking a
25
- * role away first says exactly what the group loses. Each change is saved as it is made, with
26
- * Undo in a toast. */
27
- function AccessDrawer({ open, onClose, resourceName, roles, groups, grants, permissions, onChange, loading = false, readOnly = false, readOnlyReason, describeGroup, searchGroups, groupCount, recentGroupIds, defaultArrangement = "role", width = 560 }) {
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(roles, groups, grants, permissions), [
30
+ const model = useMemo(() => buildModel({
31
31
  roles,
32
- groups,
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(roles, groups, grants, permissions);
46
+ if (!loading) warnAboutMisuse({
47
+ roles,
48
+ principals,
49
+ grants,
50
+ permissions,
51
+ inherited,
52
+ owner
53
+ });
38
54
  }, [
39
55
  roles,
40
- groups,
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
- group: /* @__PURE__ */ new Set()
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
- groupNames: [...new Set(all.map((grant) => grant.groupId))].map((id) => {
75
- const group = seen.get(id) ?? model.groupById.get(id);
76
- return group?.kind === "everyone" ? t.everyone : group?.name ?? id;
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 { groupNames, roleNames } = names(next);
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(groupNames, roleNames) : t.removeFailed(groupNames[0] ?? "", roleNames),
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(groupNames, roleNames) : t.removed(groupNames, roleNames), { action: {
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 = (groupIds = []) => {
132
+ const toGrant = (principalIds = []) => {
115
133
  setSelection({
116
- groupIds,
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
- groupId: next.grant[0]?.groupId ?? ""
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
- group: /* @__PURE__ */ new Set([...current.group, ...next.grant.map((g) => g.groupId)])
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
- groupId: pending.groupId,
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
- groupId: pending.groupId,
164
+ principalId: pending.principalId,
147
165
  roleId: anchor
148
166
  });
149
167
  setRemoval(void 0);
150
- focusLater(() => document.getElementById(sectionId(anchor ?? pending.groupId)) ?? body.current?.querySelector("input[type=\"search\"]"));
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
- groupId: failed.groupId,
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
- searchGroups,
200
+ searchPrincipals,
183
201
  groupCount,
184
- recentGroupIds,
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 empty = !loading && model.held.size === 0;
197
- const View = arrangement === "role" ? ByRole : ByGroup;
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: "group",
237
- label: t.byGroup
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 flex-1 basis-full @min-[30rem]/access:basis-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: (groupId) => toGrant([groupId]),
330
+ onGrantMore: (principalId) => toGrant([principalId]),
311
331
  failure: failure && !failure.inGrant ? {
312
332
  node: failureNode,
313
- groupId: failure.groupId,
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, [...model.held.keys()].filter((id) => id !== model.everyone?.id).length, model.everyone !== void 0 && model.held.has(model.everyone.id)),
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.groupId;
353
- /** The first of several elements that is laid out, such as the one of two buttons a width shows. */
354
- /** The one of `elements` that is laid out, or the first where nothing is (as in tests). */
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
- var groupNames = (group, t) => group.kind === "everyone" ? [
360
- group.name,
361
- t.everyone,
362
- t.everyoneInOrganization
363
- ] : [group.name];
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
- if (arrangement === "role") for (const role of model.roles) {
379
- const holders = model.holders.get(role.id) ?? [];
380
- const rows = !query || match(query, role.name, role.description) ? holders : holders.filter((group) => match(query, ...groupNames(group, t)));
381
- if (!query || rows.length > 0) {
382
- order.push(role.id);
383
- shown.set(role.id, rows.map((group) => group.id));
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
- else {
387
- const everyone = model.everyone;
388
- const holders = [...model.held.keys()].sort((a, b) => a === everyone?.id ? -1 : b === everyone?.id ? 1 : 0);
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(groupId);
396
- shown.set(groupId, rows.map((role) => role.id));
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
- group: /* @__PURE__ */ new Set()
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
- group: open("group")
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
- /** A removal waiting on the reader: one role, or every role the group holds. */
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
- groupId: string;
9
+ principalId: string;
7
10
  roleIds: string[];
8
- /** Every role the group holds, from its "Remove all access". */
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: (groupId: string) => void;
20
- /** The failed save's callout, and where it belongs: under the role or group it was about. */
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
- groupId: string;
27
+ principalId: string;
25
28
  } | undefined;
26
29
  }
27
- export declare function HighImpact({ t }: {
28
- t: AccessStrings;
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 groups, or each group's roles. */
44
+ /** What the filter left: each role's principals, or each principal's roles. */
44
45
  shown: ReadonlyMap<string, readonly string[]>;
45
- /** The rows in view, in order. */
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, group by group. */
57
- export declare function ByGroup({ model, shown, order, expanded, onExpandedChange, actions, t, sectionId, }: ViewProps): (import("react").JSX.Element | null)[];
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 {};