@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.
- package/README.md +52 -22
- 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/Tooltip.js +16 -11
- package/dist/components/fieldBox.js +1 -1
- 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/styles/theme.css +1 -1
- package/dist/styles.css +1 -1
- package/dist/theme/derive.d.ts +2 -0
- package/dist/theme/derive.js +12 -13
- package/package.json +1 -1
- package/dist/access/GroupPicker.d.ts +0 -35
package/dist/access/GrantView.js
CHANGED
|
@@ -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,
|
|
6
|
-
import {
|
|
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,
|
|
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 {
|
|
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
|
|
20
|
-
const
|
|
21
|
-
return
|
|
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,
|
|
23
|
+
const plan = grantPlan(model, principalIds, roleIds);
|
|
24
24
|
const everyone = model.everyone;
|
|
25
|
-
const toEveryone = everyone !== void 0 &&
|
|
25
|
+
const toEveryone = everyone !== void 0 && principalIds.includes(everyone.id);
|
|
26
26
|
const people = everyone?.memberCount;
|
|
27
|
-
const
|
|
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 =
|
|
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 && !
|
|
32
|
+
const everyoneHas = everyone !== void 0 && !principalIds.includes(everyone.id) && everyoneHolds(role);
|
|
32
33
|
const hints = [
|
|
33
|
-
holders.length > 0 && t.alreadyHave(holders.map(
|
|
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,
|
|
41
|
-
badge: role.
|
|
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.
|
|
48
|
-
const status =
|
|
49
|
-
text: t.
|
|
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
|
-
} :
|
|
52
|
-
text: t.
|
|
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(
|
|
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}
|
|
74
|
+
htmlFor: `${uid}principals`,
|
|
77
75
|
className: "mb-1.5 block text-sm font-medium",
|
|
78
|
-
children: t.
|
|
79
|
-
}), /* @__PURE__ */ jsx(
|
|
80
|
-
id: `${uid}
|
|
81
|
-
describedBy: `${uid}
|
|
76
|
+
children: t.principalsLabel
|
|
77
|
+
}), /* @__PURE__ */ jsx(PrincipalPicker, {
|
|
78
|
+
id: `${uid}principals`,
|
|
79
|
+
describedBy: `${uid}principalsHint`,
|
|
82
80
|
model,
|
|
83
|
-
value:
|
|
81
|
+
value: principalIds,
|
|
84
82
|
onValueChange: (next) => onSelectionChange({
|
|
85
|
-
|
|
83
|
+
principalIds: next,
|
|
86
84
|
roleIds
|
|
87
85
|
}),
|
|
88
86
|
granting: roles,
|
|
89
|
-
resourceName,
|
|
90
87
|
describeGroup,
|
|
91
|
-
|
|
88
|
+
searchPrincipals,
|
|
92
89
|
groupCount,
|
|
93
|
-
|
|
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
|
-
|
|
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.
|
|
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/
|
|
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
|
-
|
|
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
|
|
32
|
-
* the list says
|
|
33
|
-
function
|
|
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
|
|
36
|
-
const search = useSearch(searchGroups);
|
|
46
|
+
const search = useSearch(searchPrincipals);
|
|
37
47
|
const match = useQueryMatch();
|
|
38
|
-
for (const
|
|
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 = (
|
|
41
|
-
const item = (
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
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:
|
|
47
|
-
label: everyone ? t.everyone :
|
|
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(
|
|
50
|
-
icon
|
|
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 =
|
|
58
|
-
const listed = search.results ?? [...model.
|
|
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.
|
|
73
|
-
description: (
|
|
74
|
-
items:
|
|
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((
|
|
106
|
+
].filter((section) => section.items.length > 0);
|
|
77
107
|
}, [
|
|
78
108
|
model,
|
|
79
|
-
|
|
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 = (
|
|
91
|
-
const
|
|
92
|
-
return
|
|
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.
|
|
103
|
-
onQueryChange:
|
|
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: (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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.
|
|
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(
|
|
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 (!
|
|
203
|
+
if (!searchPrincipals || trimmed === "") return;
|
|
165
204
|
const controller = new AbortController();
|
|
166
205
|
const timer = setTimeout(() => {
|
|
167
|
-
|
|
206
|
+
searchPrincipals(trimmed, controller.signal).then((principals) => setFound({
|
|
168
207
|
query: trimmed,
|
|
169
|
-
|
|
208
|
+
principals
|
|
170
209
|
}), () => {
|
|
171
210
|
if (!controller.signal.aborted) setFound({
|
|
172
211
|
query: trimmed,
|
|
173
|
-
|
|
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
|
-
}, [
|
|
182
|
-
const active =
|
|
220
|
+
}, [searchPrincipals, trimmed]);
|
|
221
|
+
const active = searchPrincipals !== void 0 && trimmed !== "";
|
|
183
222
|
return {
|
|
184
223
|
query: trimmed,
|
|
185
224
|
setQuery,
|
|
186
|
-
results: active ? found?.
|
|
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
|
-
|
|
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(
|
|
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__ */
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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 {
|
|
429
|
+
export { PrincipalPicker, PrincipalTile };
|
package/dist/access/index.d.ts
CHANGED
|
@@ -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,
|
|
3
|
+
export type { AccessChange, AccessGrant, AccessGroupDetails, AccessInheritedGrant, AccessMember, AccessPermission, AccessPrincipal, AccessRole, } from './types.js';
|