@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.
Files changed (43) hide show
  1. package/README.md +60 -24
  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/ThemeEditor.js +1 -1
  31. package/dist/components/Tooltip.js +16 -11
  32. package/dist/components/fieldBox.js +3 -3
  33. package/dist/components/fullscreen.d.ts +4 -0
  34. package/dist/components/fullscreen.js +18 -0
  35. package/dist/graph/GraphEditorDialogs.d.ts +1 -1
  36. package/dist/icons.d.ts +2 -0
  37. package/dist/icons.js +22 -2
  38. package/dist/styles/theme.css +2 -1
  39. package/dist/styles.css +1 -1
  40. package/dist/theme/derive.d.ts +3 -1
  41. package/dist/theme/derive.js +17 -11
  42. package/package.json +1 -1
  43. package/dist/access/GroupPicker.d.ts +0 -35
@@ -2,60 +2,58 @@ import { Button } from "../components/Button.js";
2
2
  import { Callout } from "../components/Callout.js";
3
3
  import { Combobox } from "../components/Combobox.js";
4
4
  import { StatusBadge } from "../components/StatusBadge.js";
5
- import { covers, grantPlan, overlapOf } from "./model.js";
6
- import { GroupPicker } from "./GroupPicker.js";
5
+ import { covers, grantPlan, isOwner } from "./model.js";
6
+ import { PrincipalPicker } from "./PrincipalPicker.js";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
8
  import { useId, useRef, useState } from "react";
9
9
  //#region src/access/GrantView.tsx
10
10
  /** The grant form's body and footer, which the drawer lays out in its own body and footer. */
11
- function useGrantView({ model, selection, onSelectionChange, resourceName, describeGroup, searchGroups, groupCount, recentGroupIds, cache, seen, saving, failure, onGrant, onCancel, focusLater, boundary, t }) {
11
+ function useGrantView({ model, selection, onSelectionChange, resourceName, describeGroup, searchPrincipals, groupCount, peopleCount, recentPrincipalIds, cache, seen, saving, failure, onGrant, onCancel, focusLater, boundary, t }) {
12
12
  const uid = useId();
13
13
  const [confirming, setConfirming] = useState(false);
14
14
  const keepButton = useRef(null);
15
15
  const grantId = `${uid}grant`;
16
- const { groupIds, roleIds } = selection;
16
+ const { principalIds, roleIds } = selection;
17
17
  const roles = roleIds.map((id) => model.roleById.get(id)).filter((role) => role !== void 0);
18
18
  const roleNames = roles.map((role) => role.name);
19
- const groupName = (id) => {
20
- const group = seen.get(id);
21
- return group?.kind === "everyone" ? t.everyone : group?.name ?? id;
19
+ const principalName = (id) => {
20
+ const principal = seen.get(id);
21
+ return principal?.kind === "everyone" ? t.everyone : principal?.name ?? id;
22
22
  };
23
- const plan = grantPlan(model, groupIds, roleIds);
23
+ const plan = grantPlan(model, principalIds, roleIds);
24
24
  const everyone = model.everyone;
25
- const toEveryone = everyone !== void 0 && groupIds.includes(everyone.id);
25
+ const toEveryone = everyone !== void 0 && principalIds.includes(everyone.id);
26
26
  const people = everyone?.memberCount;
27
- const roleItems = model.roles.map((role) => {
27
+ const everyoneHolds = (role) => everyone !== void 0 && [...model.held.get(everyone.id) ?? [], ...(model.inherited.get(everyone.id) ?? []).map((holding) => holding.role)].some((held) => held.id === role.id || covers(held, role));
28
+ const roleItems = model.grantable.map((role) => {
28
29
  const chosen = roleIds.filter((id) => id !== role.id).map((id) => model.roleById.get(id)).filter((other) => other !== void 0);
29
- const holders = groupIds.filter((id) => (model.held.get(id) ?? []).some((held) => held.id === role.id));
30
+ const holders = principalIds.filter((id) => isOwner(model, id) || (model.held.get(id) ?? []).some((held) => held.id === role.id));
30
31
  const coveredBy = chosen.filter((other) => covers(other, role)).map((r) => r.name);
31
- const everyoneHas = everyone !== void 0 && !groupIds.includes(everyone.id) && (overlapOf(model, everyone.id, role).coveredBy.length > 0 || (model.held.get(everyone.id) ?? []).some((held) => held.id === role.id));
32
+ const everyoneHas = everyone !== void 0 && !principalIds.includes(everyone.id) && everyoneHolds(role);
32
33
  const hints = [
33
- holders.length > 0 && t.alreadyHave(holders.map(groupName)),
34
+ holders.length > 0 && t.alreadyHave(holders.map(principalName)),
34
35
  coveredBy.length > 0 && t.alreadyCovered(coveredBy),
35
36
  everyoneHas && t.everyoneHasIt
36
37
  ].filter((hint) => typeof hint === "string");
37
38
  return {
38
39
  value: role.id,
39
40
  label: role.name,
40
- description: [role.description, ...hints].filter(Boolean).join(" "),
41
- badge: role.highImpact ? /* @__PURE__ */ jsx(StatusBadge, {
42
- variant: "warning",
43
- children: t.highImpact
44
- }) : null
41
+ description: [role.description, hints.join(" · ")].filter(Boolean).join(" "),
42
+ badge: role.privileged ? /* @__PURE__ */ jsx(StatusBadge, { children: t.privileged }) : null
45
43
  };
46
44
  });
47
- const getting = [...new Set(plan.grant.map((grant) => grant.groupId))].map(groupName);
48
- const status = groupIds.length === 0 && roleIds.length === 0 ? {
49
- text: t.pickGroupsAndRoles,
45
+ const getting = [...new Set(plan.grant.map((grant) => grant.principalId))].map(principalName);
46
+ const status = principalIds.length === 0 && roleIds.length === 0 ? {
47
+ text: t.pickPrincipalsAndRoles,
50
48
  ready: false
51
- } : groupIds.length === 0 ? {
52
- text: t.pickGroups,
49
+ } : principalIds.length === 0 ? {
50
+ text: t.pickPrincipals,
53
51
  ready: false
54
52
  } : roleIds.length === 0 ? {
55
53
  text: t.pickRoles,
56
54
  ready: false
57
55
  } : plan.grant.length === 0 ? {
58
- text: t.nothingToGrant(groupIds.map(groupName), roleNames),
56
+ text: t.nothingToGrant(principalIds.map(principalName), roleNames),
59
57
  ready: false
60
58
  } : {
61
59
  text: t.grantSummary(getting, [...new Set(plan.grant.map((grant) => grant.roleId))].map((id) => model.roleById.get(id)?.name ?? id)),
@@ -73,24 +71,24 @@ function useGrantView({ model, selection, onSelectionChange, resourceName, descr
73
71
  className: "flex flex-col gap-5 p-4",
74
72
  children: [
75
73
  /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
76
- htmlFor: `${uid}groups`,
74
+ htmlFor: `${uid}principals`,
77
75
  className: "mb-1.5 block text-sm font-medium",
78
- children: t.groupsLabel
79
- }), /* @__PURE__ */ jsx(GroupPicker, {
80
- id: `${uid}groups`,
81
- describedBy: `${uid}groupsHint`,
76
+ children: t.principalsLabel
77
+ }), /* @__PURE__ */ jsx(PrincipalPicker, {
78
+ id: `${uid}principals`,
79
+ describedBy: `${uid}principalsHint`,
82
80
  model,
83
- value: groupIds,
81
+ value: principalIds,
84
82
  onValueChange: (next) => onSelectionChange({
85
- groupIds: next,
83
+ principalIds: next,
86
84
  roleIds
87
85
  }),
88
86
  granting: roles,
89
- resourceName,
90
87
  describeGroup,
91
- searchGroups,
88
+ searchPrincipals,
92
89
  groupCount,
93
- recentGroupIds,
90
+ peopleCount,
91
+ recentPrincipalIds,
94
92
  cache,
95
93
  seen,
96
94
  boundary,
@@ -106,7 +104,7 @@ function useGrantView({ model, selection, onSelectionChange, resourceName, descr
106
104
  items: roleItems,
107
105
  value: roleIds,
108
106
  onValueChange: (next) => onSelectionChange({
109
- groupIds,
107
+ principalIds,
110
108
  roleIds: next
111
109
  }),
112
110
  placeholder: t.searchRoles
@@ -147,7 +145,7 @@ function useGrantView({ model, selection, onSelectionChange, resourceName, descr
147
145
  disabled: saving,
148
146
  children: t.common.cancel
149
147
  }), /* @__PURE__ */ jsx(Button, {
150
- variant: roles.some((role) => role.highImpact) ? "danger" : "primary",
148
+ variant: roles.some((role) => role.privileged) ? "danger" : "primary",
151
149
  loading: saving,
152
150
  onClick: grant,
153
151
  children: t.grantToEveryone
@@ -0,0 +1,38 @@
1
+ import { type AccessModel } from './model.js';
2
+ import type { AccessStrings } from './strings.js';
3
+ import type { AccessGroupDetails, AccessPrincipal, AccessRole } from './types.js';
4
+ export type DescribeGroup = (groupId: string, signal: AbortSignal) => Promise<AccessGroupDetails>;
5
+ export type SearchPrincipals = (query: string, signal: AbortSignal) => Promise<readonly AccessPrincipal[]>;
6
+ /** Details already loaded, kept for as long as the drawer is open. */
7
+ export type DetailsCache = Map<string, AccessGroupDetails>;
8
+ /** A person's face, or a group's or everyone's icon, beside their name. */
9
+ export declare function PrincipalTile({ principal, compact, className, }: {
10
+ principal: AccessPrincipal;
11
+ /** The smaller size, for a picker row. */
12
+ compact?: boolean;
13
+ className?: string;
14
+ }): import("react").JSX.Element;
15
+ interface PrincipalPickerProps {
16
+ model: AccessModel;
17
+ value: readonly string[];
18
+ onValueChange: (ids: string[]) => void;
19
+ /** The roles being granted, which the details weigh against what the highlighted holds. */
20
+ granting: readonly AccessRole[];
21
+ describeGroup?: DescribeGroup | undefined;
22
+ searchPrincipals?: SearchPrincipals | undefined;
23
+ groupCount?: number | undefined;
24
+ peopleCount?: number | undefined;
25
+ recentPrincipalIds?: readonly string[] | undefined;
26
+ cache: DetailsCache;
27
+ /** Everyone the picker has seen, by id, so a chosen one found by a search keeps its name. */
28
+ seen: Map<string, AccessPrincipal>;
29
+ id: string;
30
+ describedBy: string;
31
+ /** The drawer's body, above its footer, which the open list fills. */
32
+ boundary: Element | null;
33
+ t: AccessStrings;
34
+ }
35
+ /** The people and groups field: everyone pinned first, then recent picks, then people and
36
+ * groups; a pane beside the list says more about the highlighted one and what it holds here. */
37
+ export declare function PrincipalPicker({ model, value, onValueChange, granting, describeGroup, searchPrincipals, groupCount, peopleCount, recentPrincipalIds, cache, seen, id, describedBy, boundary, t, }: PrincipalPickerProps): import("react").JSX.Element;
38
+ export {};
@@ -7,10 +7,10 @@ import { Combobox } from "../components/Combobox.js";
7
7
  import { Skeleton } from "../components/Skeleton.js";
8
8
  import { useRelativeTime } from "../components/relativeTime.js";
9
9
  import { StatusBadge } from "../components/StatusBadge.js";
10
- import { covers } from "./model.js";
10
+ import { covers, isOwner } from "./model.js";
11
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
12
  import { useEffect, useMemo, useState } from "react";
13
- //#region src/access/GroupPicker.tsx
13
+ //#region src/access/PrincipalPicker.tsx
14
14
  /** Waits this long on a highlighted group before asking for its details, so arrowing past
15
15
  * groups doesn't ask for each one. */
16
16
  var DESCRIBE_DELAY = 120;
@@ -20,45 +20,70 @@ var SEARCH_DELAY = 200;
20
20
  var FACES = 5;
21
21
  /** Names in a group's details, before the rest are a count. */
22
22
  var NAMES = 3;
23
- function GroupTile({ everyone, className }) {
23
+ /** A person's face, or a group's or everyone's icon, beside their name. */
24
+ function PrincipalTile({ principal, compact = false, className }) {
25
+ if (principal.kind === "user") return /* @__PURE__ */ jsx("span", {
26
+ "aria-hidden": "true",
27
+ className: cx("flex shrink-0 items-center justify-center", compact ? "size-6" : "size-8", className),
28
+ children: /* @__PURE__ */ jsx(Avatar, {
29
+ src: principal.avatarUrl,
30
+ name: principal.name,
31
+ size: compact ? "sm" : "md"
32
+ })
33
+ });
34
+ const everyone = principal.kind === "everyone";
24
35
  const Icon = everyone ? OrganizationIcon : UsersIcon;
25
36
  return /* @__PURE__ */ jsx("span", {
26
37
  "aria-hidden": "true",
27
- className: cx("flex size-8 shrink-0 items-center justify-center rounded-md [&_svg]:size-4", everyone ? "bg-(--theme-info-muted) text-(--theme-info-text)" : "bg-(--theme-muted-panel-bg) text-(--theme-muted-text-color)", className),
38
+ className: cx("flex size-8 shrink-0 items-center justify-center rounded-md [&_svg]:size-4", compact && "size-6 [&_svg]:size-3.5", everyone ? "bg-(--theme-info-muted) text-(--theme-info-text)" : "bg-(--theme-muted-panel-bg) text-(--theme-muted-text-color)", className),
28
39
  children: /* @__PURE__ */ jsx(Icon, {})
29
40
  });
30
41
  }
31
- /** The groups field: everyone pinned first, then recent groups, then all of them; a pane beside
32
- * the list says who is in the highlighted group and what it holds here. */
33
- function GroupPicker({ model, value, onValueChange, granting, resourceName, describeGroup, searchGroups, groupCount, recentGroupIds = [], cache, seen, id, describedBy, boundary, t }) {
42
+ /** The people and groups field: everyone pinned first, then recent picks, then people and
43
+ * groups; a pane beside the list says more about the highlighted one and what it holds here. */
44
+ function PrincipalPicker({ model, value, onValueChange, granting, describeGroup, searchPrincipals, groupCount, peopleCount, recentPrincipalIds = [], cache, seen, id, describedBy, boundary, t }) {
34
45
  const [highlighted, setHighlighted] = useState(void 0);
35
- const details = useDetails(highlighted, describeGroup, cache);
36
- const search = useSearch(searchGroups);
46
+ const search = useSearch(searchPrincipals);
37
47
  const match = useQueryMatch();
38
- for (const group of [...model.groupById.values(), ...search.results ?? []]) seen.set(group.id, group);
48
+ for (const principal of [...model.principalById.values(), ...search.results ?? []]) seen.set(principal.id, principal);
49
+ const details = useDetails(highlighted !== void 0 && seen.get(highlighted)?.kind !== "user" ? highlighted : void 0, describeGroup, cache);
39
50
  const items = useMemo(() => {
40
- const roleNames = (groupId) => (model.held.get(groupId) ?? []).map((r) => r.name);
41
- const item = (group) => {
42
- const everyone = group.kind === "everyone";
43
- const held = roleNames(group.id);
44
- const size = group.memberCount === void 0 ? void 0 : everyone ? t.people(group.memberCount) : t.members(group.memberCount);
51
+ const roleNames = (principalId) => (model.held.get(principalId) ?? []).map((r) => r.name);
52
+ const item = (principal) => {
53
+ const held = roleNames(principal.id);
54
+ const icon = /* @__PURE__ */ jsx(PrincipalTile, {
55
+ principal,
56
+ compact: true
57
+ });
58
+ if (principal.kind === "user") {
59
+ const owner = isOwner(model, principal.id);
60
+ return {
61
+ value: principal.id,
62
+ label: principal.name,
63
+ description: [principal.detail, owner ? t.owner : held.length > 0 && t.hasRoles(held)].filter(Boolean).join(" · "),
64
+ accessibleDescription: t.personSummary(principal.detail, owner, held),
65
+ icon
66
+ };
67
+ }
68
+ const everyone = principal.kind === "everyone";
69
+ const size = principal.memberCount === void 0 ? void 0 : everyone ? t.people(principal.memberCount) : t.members(principal.memberCount);
45
70
  return {
46
- value: group.id,
47
- label: everyone ? t.everyone : group.name,
71
+ value: principal.id,
72
+ label: everyone ? t.everyone : principal.name,
48
73
  description: [size, held.length > 0 && t.hasRoles(held)].filter(Boolean).join(" · "),
49
- accessibleDescription: t.groupSummary(group.memberCount, everyone, cache.get(group.id)?.source, held),
50
- icon: /* @__PURE__ */ jsx(GroupTile, {
51
- everyone,
52
- className: "size-7"
53
- })
74
+ accessibleDescription: t.groupSummary(principal.memberCount, everyone, cache.get(principal.id)?.source, held),
75
+ icon
54
76
  };
55
77
  };
56
78
  const everyone = model.everyone;
57
- const recent = recentGroupIds.map((groupId) => seen.get(groupId)).filter((group) => group !== void 0 && group.kind !== "everyone");
58
- const listed = search.results ?? [...model.groupById.values()];
59
- const pinned = /* @__PURE__ */ new Set([everyone?.id, ...recent.map((group) => group.id)]);
60
- const rest = listed.filter((group) => group.kind !== "everyone" && !pinned.has(group.id));
79
+ const recent = recentPrincipalIds.map((principalId) => seen.get(principalId)).filter((principal) => principal !== void 0 && principal.kind !== "everyone");
80
+ const listed = search.results ?? [...model.principalById.values()];
61
81
  const searching = search.results !== void 0;
82
+ const pinned = new Set(searching ? [] : recent.map((principal) => principal.id));
83
+ const rest = listed.filter((principal) => principal.kind !== "everyone" && !pinned.has(principal.id));
84
+ const people = rest.filter((principal) => principal.kind === "user");
85
+ const groups = rest.filter((principal) => principal.kind === "group");
86
+ const count = (given, kind) => searching ? void 0 : (given ?? listed.filter((principal) => principal.kind === kind).length).toLocaleString();
62
87
  return [
63
88
  ...everyone && (!searching || match(search.query, everyone.name, t.everyone)) ? [{
64
89
  label: t.organization,
@@ -69,28 +94,35 @@ function GroupPicker({ model, value, onValueChange, granting, resourceName, desc
69
94
  items: recent.map(item)
70
95
  }] : [],
71
96
  {
72
- label: t.allGroups,
73
- description: (groupCount ?? listed.length).toLocaleString(),
74
- items: (searching ? listed.filter((g) => g.kind !== "everyone") : rest).map(item)
97
+ label: t.peopleSection,
98
+ description: count(peopleCount, "user"),
99
+ items: people.map(item)
100
+ },
101
+ {
102
+ label: t.groupsSection,
103
+ description: count(groupCount, "group"),
104
+ items: groups.map(item)
75
105
  }
76
- ].filter((group) => group.items.length > 0);
106
+ ].filter((section) => section.items.length > 0);
77
107
  }, [
78
108
  model,
79
- recentGroupIds,
109
+ recentPrincipalIds,
80
110
  search.results,
81
111
  search.query,
82
112
  match,
83
113
  groupCount,
114
+ peopleCount,
84
115
  seen,
85
116
  cache,
86
117
  t,
87
118
  details.details
88
119
  ]);
89
120
  const everyoneChosen = model.everyone !== void 0 && value.includes(model.everyone.id);
90
- const nameOf = (groupId) => {
91
- const group = seen.get(groupId);
92
- return group?.kind === "everyone" ? t.everyoneInOrganization : group?.name ?? groupId;
121
+ const nameOf = (principalId) => {
122
+ const principal = seen.get(principalId);
123
+ return principal?.kind === "everyone" ? t.everyoneInOrganization : principal?.name ?? principalId;
93
124
  };
125
+ const highlightedPrincipal = highlighted === void 0 ? void 0 : seen.get(highlighted);
94
126
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Combobox, {
95
127
  multiple: true,
96
128
  id,
@@ -99,27 +131,34 @@ function GroupPicker({ model, value, onValueChange, granting, resourceName, desc
99
131
  value,
100
132
  onValueChange,
101
133
  itemToStringLabel: nameOf,
102
- placeholder: t.addGroup,
103
- onQueryChange: searchGroups ? search.setQuery : void 0,
134
+ placeholder: t.addPrincipal,
135
+ onQueryChange: searchPrincipals ? search.setQuery : void 0,
104
136
  loading: search.loading,
105
137
  onItemHighlighted: setHighlighted,
106
138
  collisionBoundary: boundary,
107
- details: (groupId) => /* @__PURE__ */ jsx(GroupDetailsPane, {
108
- group: groupId === void 0 ? void 0 : seen.get(groupId),
109
- state: details,
110
- model,
111
- granting,
112
- resourceName,
113
- t
114
- }),
115
- detailsSummary: () => details.details ? /* @__PURE__ */ jsx(DetailsLine, {
139
+ details: (principalId) => {
140
+ const principal = principalId === void 0 ? void 0 : seen.get(principalId);
141
+ return principal?.kind === "user" ? /* @__PURE__ */ jsx(PersonDetailsPane, {
142
+ person: principal,
143
+ model,
144
+ granting,
145
+ t
146
+ }) : /* @__PURE__ */ jsx(GroupDetailsPane, {
147
+ group: principal,
148
+ state: details,
149
+ model,
150
+ granting,
151
+ t
152
+ });
153
+ },
154
+ detailsSummary: () => highlightedPrincipal?.kind === "user" ? highlightedPrincipal.detail ?? null : details.details ? /* @__PURE__ */ jsx(DetailsLine, {
116
155
  details: details.details,
117
156
  t
118
157
  }) : null
119
158
  }), /* @__PURE__ */ jsx("p", {
120
159
  id: describedBy,
121
160
  className: "m-0 mt-1.5 text-xs text-(--theme-muted-text-color)",
122
- children: everyoneChosen ? t.everyoneCoversGroups : t.groupsHint(groupCount, model.everyone !== void 0)
161
+ children: everyoneChosen ? t.everyoneCoversGroups : t.pickerHint(peopleCount, groupCount, model.everyone !== void 0)
123
162
  })] });
124
163
  }
125
164
  /** The highlighted group's details: from the cache, or asked for once the highlight rests on
@@ -156,21 +195,21 @@ function useDetails(groupId, describe, cache) {
156
195
  };
157
196
  }
158
197
  /** A search on the host's side, run once typing pauses; a newer query drops the older one's. */
159
- function useSearch(searchGroups) {
198
+ function useSearch(searchPrincipals) {
160
199
  const [query, setQuery] = useState("");
161
200
  const [found, setFound] = useState();
162
201
  const trimmed = query.trim();
163
202
  useEffect(() => {
164
- if (!searchGroups || trimmed === "") return;
203
+ if (!searchPrincipals || trimmed === "") return;
165
204
  const controller = new AbortController();
166
205
  const timer = setTimeout(() => {
167
- searchGroups(trimmed, controller.signal).then((groups) => setFound({
206
+ searchPrincipals(trimmed, controller.signal).then((principals) => setFound({
168
207
  query: trimmed,
169
- groups
208
+ principals
170
209
  }), () => {
171
210
  if (!controller.signal.aborted) setFound({
172
211
  query: trimmed,
173
- groups: []
212
+ principals: []
174
213
  });
175
214
  });
176
215
  }, SEARCH_DELAY);
@@ -178,12 +217,12 @@ function useSearch(searchGroups) {
178
217
  clearTimeout(timer);
179
218
  controller.abort();
180
219
  };
181
- }, [searchGroups, trimmed]);
182
- const active = searchGroups !== void 0 && trimmed !== "";
220
+ }, [searchPrincipals, trimmed]);
221
+ const active = searchPrincipals !== void 0 && trimmed !== "";
183
222
  return {
184
223
  query: trimmed,
185
224
  setQuery,
186
- results: active ? found?.groups ?? [] : void 0,
225
+ results: active ? found?.principals ?? [] : void 0,
187
226
  loading: active && found?.query !== trimmed
188
227
  };
189
228
  }
@@ -223,7 +262,78 @@ function Updated({ value, t }) {
223
262
  function DetailsLine({ details, t }) {
224
263
  return /* @__PURE__ */ jsx(Fragment, { children: [details.source === "synced" && t.syncedShort, memberLine(details, 2, t)].filter(Boolean).join(" · ") });
225
264
  }
226
- function GroupDetailsPane({ group, state, model, granting, resourceName, t }) {
265
+ /** What the highlighted one holds here, and what granting the picked roles would change. */
266
+ function OnResource({ principalId, model, granting, t }) {
267
+ const held = model.held.get(principalId) ?? [];
268
+ const owner = isOwner(model, principalId);
269
+ const heldIds = new Set(held.map((role) => role.id));
270
+ const fresh = owner ? [] : granting.filter((role) => !heldIds.has(role.id));
271
+ const redundant = held.filter((role) => fresh.some((by) => covers(by, role)));
272
+ return /* @__PURE__ */ jsxs("div", { children: [
273
+ /* @__PURE__ */ jsx(PaneHeading, { children: t.currentRoles }),
274
+ owner || held.length > 0 ? /* @__PURE__ */ jsxs("div", {
275
+ className: "flex flex-wrap gap-1",
276
+ children: [owner && /* @__PURE__ */ jsx(StatusBadge, {
277
+ size: "md",
278
+ variant: "info",
279
+ children: t.owner
280
+ }), held.map((role) => /* @__PURE__ */ jsx(StatusBadge, {
281
+ size: "md",
282
+ children: role.name
283
+ }, role.id))]
284
+ }) : /* @__PURE__ */ jsx("p", {
285
+ className: "m-0 text-xs text-(--theme-muted-text-color)",
286
+ children: t.noRolesHere
287
+ }),
288
+ owner && /* @__PURE__ */ jsx("p", {
289
+ className: "m-0 mt-2 text-xs leading-snug text-(--theme-muted-text-color)",
290
+ children: t.ownerDescription
291
+ }),
292
+ redundant.length > 0 && /* @__PURE__ */ jsx("p", {
293
+ className: "m-0 mt-2 text-xs leading-snug text-(--theme-muted-text-color)",
294
+ children: t.becomesRedundant(fresh.filter((by) => redundant.some((role) => covers(by, role))).map((r) => r.name), redundant.map((role) => role.name))
295
+ }),
296
+ !owner && granting.length > 0 && fresh.length === 0 && /* @__PURE__ */ jsx("p", {
297
+ className: "m-0 mt-2 text-xs leading-snug text-(--theme-muted-text-color)",
298
+ children: t.alreadyHas(granting.map((role) => role.name))
299
+ })
300
+ ] });
301
+ }
302
+ function PersonDetailsPane({ person, model, granting, t }) {
303
+ const groups = (person.groupIds ?? []).map((id) => model.principalById.get(id)).filter((group) => group?.kind === "group");
304
+ return /* @__PURE__ */ jsxs("div", {
305
+ className: "flex min-h-full flex-col gap-4 p-4 text-sm",
306
+ children: [
307
+ /* @__PURE__ */ jsxs("div", {
308
+ className: "flex items-start gap-3",
309
+ children: [/* @__PURE__ */ jsx(PrincipalTile, { principal: person }), /* @__PURE__ */ jsxs("div", {
310
+ className: "min-w-0",
311
+ children: [/* @__PURE__ */ jsx("p", {
312
+ className: "m-0 font-semibold wrap-break-word",
313
+ children: person.name
314
+ }), person.detail && /* @__PURE__ */ jsx("p", {
315
+ className: "m-0 text-xs wrap-break-word text-(--theme-muted-text-color)",
316
+ children: person.detail
317
+ })]
318
+ })]
319
+ }),
320
+ groups.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(PaneHeading, { children: t.inGroups }), /* @__PURE__ */ jsx("div", {
321
+ className: "flex flex-wrap gap-1",
322
+ children: groups.map((group) => /* @__PURE__ */ jsx(StatusBadge, {
323
+ size: "md",
324
+ children: group.name
325
+ }, group.id))
326
+ })] }),
327
+ /* @__PURE__ */ jsx(OnResource, {
328
+ principalId: person.id,
329
+ model,
330
+ granting,
331
+ t
332
+ })
333
+ ]
334
+ });
335
+ }
336
+ function GroupDetailsPane({ group, state, model, granting, t }) {
227
337
  if (!group) return /* @__PURE__ */ jsx("p", {
228
338
  className: "m-0 p-4 text-xs text-(--theme-muted-text-color)",
229
339
  children: t.noHighlight
@@ -234,14 +344,13 @@ function GroupDetailsPane({ group, state, model, granting, resourceName, t }) {
234
344
  const count = details?.memberCount ?? group.memberCount;
235
345
  const heldIds = new Set(held.map((role) => role.id));
236
346
  const fresh = granting.filter((role) => !heldIds.has(role.id));
237
- const redundant = held.filter((role) => fresh.some((by) => covers(by, role)));
238
347
  const source = everyone ? t.wholeOrganization : details?.source === "synced" ? t.synced : details?.source === "manual" ? t.manual : void 0;
239
348
  return /* @__PURE__ */ jsxs("div", {
240
349
  className: "flex min-h-full flex-col gap-4 p-4 text-sm",
241
350
  children: [
242
351
  /* @__PURE__ */ jsxs("div", {
243
352
  className: "flex items-start gap-3",
244
- children: [/* @__PURE__ */ jsx(GroupTile, { everyone }), /* @__PURE__ */ jsxs("div", {
353
+ children: [/* @__PURE__ */ jsx(PrincipalTile, { principal: group }), /* @__PURE__ */ jsxs("div", {
245
354
  className: "min-w-0",
246
355
  children: [/* @__PURE__ */ jsx("p", {
247
356
  className: "m-0 font-semibold wrap-break-word",
@@ -303,28 +412,12 @@ function GroupDetailsPane({ group, state, model, granting, resourceName, t }) {
303
412
  icon: false,
304
413
  children: t.everyoneReach(fresh.map((role) => role.name), count)
305
414
  }),
306
- /* @__PURE__ */ jsxs("div", { children: [
307
- /* @__PURE__ */ jsx(PaneHeading, { children: t.onResource(resourceName) }),
308
- held.length > 0 ? /* @__PURE__ */ jsx("div", {
309
- className: "flex flex-wrap gap-1",
310
- children: held.map((role) => /* @__PURE__ */ jsx(StatusBadge, {
311
- size: "md",
312
- variant: role.highImpact ? "warning" : "default",
313
- children: role.name
314
- }, role.id))
315
- }) : /* @__PURE__ */ jsx("p", {
316
- className: "m-0 text-xs text-(--theme-muted-text-color)",
317
- children: t.noRolesHere
318
- }),
319
- redundant.length > 0 && /* @__PURE__ */ jsx("p", {
320
- className: "m-0 mt-2 text-xs leading-snug text-(--theme-muted-text-color)",
321
- children: t.becomesRedundant(fresh.filter((by) => redundant.some((role) => covers(by, role))).map((r) => r.name), redundant.map((role) => role.name))
322
- }),
323
- granting.length > 0 && fresh.length === 0 && /* @__PURE__ */ jsx("p", {
324
- className: "m-0 mt-2 text-xs leading-snug text-(--theme-muted-text-color)",
325
- children: t.alreadyHas(granting.map((role) => role.name))
326
- })
327
- ] }),
415
+ /* @__PURE__ */ jsx(OnResource, {
416
+ principalId: group.id,
417
+ model,
418
+ granting,
419
+ t
420
+ }),
328
421
  details?.updatedAt !== void 0 && details.updatedAt !== null && /* @__PURE__ */ jsx(Updated, {
329
422
  value: details.updatedAt,
330
423
  t
@@ -333,4 +426,4 @@ function GroupDetailsPane({ group, state, model, granting, resourceName, t }) {
333
426
  });
334
427
  }
335
428
  //#endregion
336
- export { GroupPicker, GroupTile };
429
+ export { PrincipalPicker, PrincipalTile };
@@ -1,3 +1,3 @@
1
1
  export { type AccessArrangement, AccessDrawer, type AccessDrawerProps } from './AccessDrawer.js';
2
2
  export { ACCESS_STRINGS, type AccessStrings, type AccessText, useAccessStrings, } from './strings.js';
3
- export type { AccessChange, AccessGrant, AccessGroup, AccessGroupDetails, AccessMember, AccessPermission, AccessRole, } from './types.js';
3
+ export type { AccessChange, AccessGrant, AccessGroupDetails, AccessInheritedGrant, AccessMember, AccessPermission, AccessPrincipal, AccessRole, } from './types.js';