@echothink-ui/identity 0.1.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 +5 -0
- package/dist/components/AccessReviewPanel.d.ts +9 -0
- package/dist/components/AccountMenu.d.ts +8 -0
- package/dist/components/ApprovalPolicyEditor.d.ts +8 -0
- package/dist/components/AuditTrail.d.ts +6 -0
- package/dist/components/GroupPicker.d.ts +12 -0
- package/dist/components/IdentityCard.d.ts +14 -0
- package/dist/components/InviteUserPanel.d.ts +14 -0
- package/dist/components/OrganizationSwitcher.d.ts +9 -0
- package/dist/components/PermissionMatrix.d.ts +10 -0
- package/dist/components/PolicyRuleViewer.d.ts +6 -0
- package/dist/components/RoleBadge.d.ts +6 -0
- package/dist/components/SessionStatus.d.ts +9 -0
- package/dist/components/TeamList.d.ts +13 -0
- package/dist/components/UserPicker.d.ts +12 -0
- package/dist/components/types.d.ts +77 -0
- package/dist/index.cjs +1629 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +2238 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +1610 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
- package/src/components/AccessReviewPanel.tsx +169 -0
- package/src/components/AccountMenu.tsx +144 -0
- package/src/components/ApprovalPolicyEditor.tsx +131 -0
- package/src/components/AuditTrail.tsx +105 -0
- package/src/components/GroupPicker.tsx +175 -0
- package/src/components/IdentityCard.tsx +78 -0
- package/src/components/InviteUserPanel.tsx +162 -0
- package/src/components/OrganizationSwitcher.test.tsx +59 -0
- package/src/components/OrganizationSwitcher.tsx +161 -0
- package/src/components/PermissionMatrix.test.tsx +96 -0
- package/src/components/PermissionMatrix.tsx +271 -0
- package/src/components/PolicyRuleViewer.test.tsx +29 -0
- package/src/components/PolicyRuleViewer.tsx +78 -0
- package/src/components/RoleBadge.test.tsx +35 -0
- package/src/components/RoleBadge.tsx +38 -0
- package/src/components/SessionStatus.test.tsx +40 -0
- package/src/components/SessionStatus.tsx +194 -0
- package/src/components/TeamList.test.tsx +48 -0
- package/src/components/TeamList.tsx +98 -0
- package/src/components/UserPicker.test.tsx +52 -0
- package/src/components/UserPicker.tsx +174 -0
- package/src/components/types.ts +89 -0
- package/src/index.tsx +35 -0
- package/src/styles.css +2578 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Tag, TextInput, type SurfaceComponentProps } from "@echothink-ui/core";
|
|
3
|
+
import type { IdentityRef } from "./types";
|
|
4
|
+
|
|
5
|
+
export interface UserPickerProps
|
|
6
|
+
extends Omit<SurfaceComponentProps, "children" | "onChange" | "value"> {
|
|
7
|
+
value: string[];
|
|
8
|
+
onChange?: (value: string[]) => void;
|
|
9
|
+
onSearch?: (query: string) => void;
|
|
10
|
+
suggestions?: IdentityRef[];
|
|
11
|
+
multiple?: boolean;
|
|
12
|
+
defaultOpen?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function UserPicker({
|
|
17
|
+
value,
|
|
18
|
+
onChange,
|
|
19
|
+
onSearch,
|
|
20
|
+
suggestions = [],
|
|
21
|
+
multiple = true,
|
|
22
|
+
defaultOpen = false,
|
|
23
|
+
disabled = false,
|
|
24
|
+
loading = false,
|
|
25
|
+
className
|
|
26
|
+
}: UserPickerProps) {
|
|
27
|
+
const [query, setQuery] = React.useState("");
|
|
28
|
+
const [open, setOpen] = React.useState(defaultOpen);
|
|
29
|
+
const searchRef = React.useRef<HTMLInputElement>(null);
|
|
30
|
+
const selected = value.map(
|
|
31
|
+
(id) => suggestions.find((item) => item.id === id) ?? { id, label: id }
|
|
32
|
+
);
|
|
33
|
+
const normalizedQuery = query.trim().toLocaleLowerCase();
|
|
34
|
+
const visibleSuggestions = normalizedQuery
|
|
35
|
+
? suggestions.filter((suggestion) =>
|
|
36
|
+
[suggestion.label, suggestion.email, suggestion.role, suggestion.kind]
|
|
37
|
+
.filter((field): field is string => typeof field === "string")
|
|
38
|
+
.some((field) => field.toLocaleLowerCase().includes(normalizedQuery))
|
|
39
|
+
)
|
|
40
|
+
: suggestions;
|
|
41
|
+
const selectedLabel = selected.length
|
|
42
|
+
? `${selected.length} selected`
|
|
43
|
+
: multiple
|
|
44
|
+
? "No users selected"
|
|
45
|
+
: "No user selected";
|
|
46
|
+
const triggerLabel = multiple ? "Add users" : selected.length ? "Change user" : "Choose user";
|
|
47
|
+
|
|
48
|
+
const toggle = (id: string) => {
|
|
49
|
+
if (disabled || loading) return;
|
|
50
|
+
|
|
51
|
+
if (!multiple) {
|
|
52
|
+
onChange?.([id]);
|
|
53
|
+
setOpen(false);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onChange?.(value.includes(id) ? value.filter((item) => item !== id) : [...value, id]);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
className={["eth-identity-user-picker", className].filter(Boolean).join(" ")}
|
|
63
|
+
data-eth-component="UserPicker"
|
|
64
|
+
data-disabled={disabled || loading ? "true" : undefined}
|
|
65
|
+
data-open={open ? "true" : undefined}
|
|
66
|
+
>
|
|
67
|
+
<div className="eth-identity-user-picker__control">
|
|
68
|
+
<div className="eth-identity-user-picker__chips" aria-label="Selected users">
|
|
69
|
+
{selected.length ? (
|
|
70
|
+
selected.map((item) => (
|
|
71
|
+
<Tag
|
|
72
|
+
key={item.id}
|
|
73
|
+
removable={!disabled && !loading}
|
|
74
|
+
onRemove={() => onChange?.(value.filter((id) => id !== item.id))}
|
|
75
|
+
>
|
|
76
|
+
{item.label}
|
|
77
|
+
</Tag>
|
|
78
|
+
))
|
|
79
|
+
) : (
|
|
80
|
+
<span className="eth-identity-user-picker__placeholder">
|
|
81
|
+
{multiple ? "Select users" : "Select a user"}
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
<details
|
|
86
|
+
open={open}
|
|
87
|
+
className="eth-popover eth-identity-user-picker__popover"
|
|
88
|
+
onToggle={(event) => {
|
|
89
|
+
const nextOpen = event.currentTarget.open;
|
|
90
|
+
setOpen(nextOpen);
|
|
91
|
+
|
|
92
|
+
if (nextOpen) window.setTimeout(() => searchRef.current?.focus(), 0);
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<summary
|
|
96
|
+
aria-label={`${triggerLabel}. ${selectedLabel}`}
|
|
97
|
+
aria-disabled={disabled || loading ? true : undefined}
|
|
98
|
+
tabIndex={disabled || loading ? -1 : undefined}
|
|
99
|
+
onClick={(event) => {
|
|
100
|
+
if (disabled || loading) event.preventDefault();
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<span className="eth-identity-user-picker__trigger">
|
|
104
|
+
<span>{triggerLabel}</span>
|
|
105
|
+
<span className="eth-identity-user-picker__count">{selectedLabel}</span>
|
|
106
|
+
<span className="eth-identity-user-picker__chevron" aria-hidden="true" />
|
|
107
|
+
</span>
|
|
108
|
+
</summary>
|
|
109
|
+
<div className="eth-popover__content eth-identity-user-picker__content">
|
|
110
|
+
<TextInput
|
|
111
|
+
ref={searchRef}
|
|
112
|
+
type="search"
|
|
113
|
+
labelText="Search users"
|
|
114
|
+
hideLabel
|
|
115
|
+
placeholder="Search users"
|
|
116
|
+
value={query}
|
|
117
|
+
disabled={disabled || loading}
|
|
118
|
+
onChange={(event) => {
|
|
119
|
+
setQuery(event.currentTarget.value);
|
|
120
|
+
onSearch?.(event.currentTarget.value);
|
|
121
|
+
}}
|
|
122
|
+
/>
|
|
123
|
+
<div
|
|
124
|
+
className="eth-identity-user-picker__suggestions"
|
|
125
|
+
role="listbox"
|
|
126
|
+
aria-label="User suggestions"
|
|
127
|
+
aria-multiselectable={multiple ? true : undefined}
|
|
128
|
+
>
|
|
129
|
+
{loading ? (
|
|
130
|
+
<div className="eth-identity-user-picker__empty" role="status">
|
|
131
|
+
Loading users...
|
|
132
|
+
</div>
|
|
133
|
+
) : visibleSuggestions.length ? (
|
|
134
|
+
visibleSuggestions.map((suggestion) => {
|
|
135
|
+
const isSelected = value.includes(suggestion.id);
|
|
136
|
+
const meta =
|
|
137
|
+
suggestion.email ??
|
|
138
|
+
suggestion.role ??
|
|
139
|
+
(suggestion.kind === "service-account" ? "Service account" : undefined);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<button
|
|
143
|
+
key={suggestion.id}
|
|
144
|
+
type="button"
|
|
145
|
+
className="eth-identity-user-picker__option"
|
|
146
|
+
role="option"
|
|
147
|
+
aria-selected={isSelected}
|
|
148
|
+
disabled={disabled || loading}
|
|
149
|
+
onClick={() => toggle(suggestion.id)}
|
|
150
|
+
>
|
|
151
|
+
<span className="eth-identity-user-picker__option-main">
|
|
152
|
+
<span className="eth-identity-user-picker__option-label">
|
|
153
|
+
{suggestion.label}
|
|
154
|
+
</span>
|
|
155
|
+
{meta ? (
|
|
156
|
+
<span className="eth-identity-user-picker__option-meta">{meta}</span>
|
|
157
|
+
) : null}
|
|
158
|
+
</span>
|
|
159
|
+
<span className="eth-identity-user-picker__option-check" aria-hidden="true" />
|
|
160
|
+
</button>
|
|
161
|
+
);
|
|
162
|
+
})
|
|
163
|
+
) : (
|
|
164
|
+
<div className="eth-identity-user-picker__empty" role="status">
|
|
165
|
+
{query ? "No matching users" : "No users available"}
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</details>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { EthAction, EthOperationalStatus } from "@echothink-ui/core";
|
|
2
|
+
import type { JsonSchema, RuleDefinition } from "@echothink-ui/forms";
|
|
3
|
+
|
|
4
|
+
export interface IdentityRef {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
avatar?: string;
|
|
9
|
+
role?: string;
|
|
10
|
+
kind?: "user" | "service-account" | "group";
|
|
11
|
+
status?: EthOperationalStatus;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface IdentitySubject {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
kind: string;
|
|
18
|
+
role?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PermissionAction {
|
|
23
|
+
id: string;
|
|
24
|
+
label: string;
|
|
25
|
+
group?: string;
|
|
26
|
+
resource?: string;
|
|
27
|
+
scope?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type PermissionValue = "allow" | "deny" | "inherit";
|
|
32
|
+
|
|
33
|
+
export interface AccessReviewResource {
|
|
34
|
+
resourceId: string;
|
|
35
|
+
resourceLabel: string;
|
|
36
|
+
permissions: string[];
|
|
37
|
+
lastUsedAt?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AccessReviewSubject extends IdentityRef {
|
|
41
|
+
accessTo: AccessReviewResource[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AccessFinding {
|
|
45
|
+
id: string;
|
|
46
|
+
subjectId: string;
|
|
47
|
+
reason: string;
|
|
48
|
+
severity: "info" | "warning" | "error" | "critical";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface OrganizationRef {
|
|
52
|
+
id: string;
|
|
53
|
+
label: string;
|
|
54
|
+
description?: string;
|
|
55
|
+
status?: EthOperationalStatus;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AuditTrailEntry {
|
|
59
|
+
id: string;
|
|
60
|
+
timestamp: string;
|
|
61
|
+
actor: string;
|
|
62
|
+
action: string;
|
|
63
|
+
resource: string;
|
|
64
|
+
outcome: "success" | "failure";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface PolicyRule {
|
|
68
|
+
id: string;
|
|
69
|
+
effect: "allow" | "deny";
|
|
70
|
+
subjects: string[];
|
|
71
|
+
actions: string[];
|
|
72
|
+
resources: string[];
|
|
73
|
+
conditions?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ApprovalPolicy {
|
|
77
|
+
id?: string;
|
|
78
|
+
name?: string;
|
|
79
|
+
description?: string;
|
|
80
|
+
requiredApprovals?: number;
|
|
81
|
+
rules?: RuleDefinition[];
|
|
82
|
+
approvers?: IdentityRef[];
|
|
83
|
+
actions?: EthAction[];
|
|
84
|
+
[key: string]: unknown;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ApprovalPolicySchema extends JsonSchema {
|
|
88
|
+
variables?: string[];
|
|
89
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import "./styles.css";
|
|
2
|
+
|
|
3
|
+
export * from "./components/types";
|
|
4
|
+
export * from "./components/IdentityCard";
|
|
5
|
+
export * from "./components/UserPicker";
|
|
6
|
+
export * from "./components/GroupPicker";
|
|
7
|
+
export * from "./components/TeamList";
|
|
8
|
+
export * from "./components/RoleBadge";
|
|
9
|
+
export * from "./components/PermissionMatrix";
|
|
10
|
+
export * from "./components/AccessReviewPanel";
|
|
11
|
+
export * from "./components/InviteUserPanel";
|
|
12
|
+
export * from "./components/OrganizationSwitcher";
|
|
13
|
+
export * from "./components/AccountMenu";
|
|
14
|
+
export * from "./components/SessionStatus";
|
|
15
|
+
export * from "./components/AuditTrail";
|
|
16
|
+
export * from "./components/PolicyRuleViewer";
|
|
17
|
+
export * from "./components/ApprovalPolicyEditor";
|
|
18
|
+
|
|
19
|
+
export const IdentityComponentNames = [
|
|
20
|
+
"IdentityCard",
|
|
21
|
+
"UserPicker",
|
|
22
|
+
"GroupPicker",
|
|
23
|
+
"TeamList",
|
|
24
|
+
"RoleBadge",
|
|
25
|
+
"PermissionMatrix",
|
|
26
|
+
"AccessReviewPanel",
|
|
27
|
+
"InviteUserPanel",
|
|
28
|
+
"OrganizationSwitcher",
|
|
29
|
+
"AccountMenu",
|
|
30
|
+
"SessionStatus",
|
|
31
|
+
"AuditTrail",
|
|
32
|
+
"PolicyRuleViewer",
|
|
33
|
+
"ApprovalPolicyEditor"
|
|
34
|
+
] as const;
|
|
35
|
+
export type IdentityComponentName = (typeof IdentityComponentNames)[number];
|