@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.
Files changed (48) hide show
  1. package/README.md +5 -0
  2. package/dist/components/AccessReviewPanel.d.ts +9 -0
  3. package/dist/components/AccountMenu.d.ts +8 -0
  4. package/dist/components/ApprovalPolicyEditor.d.ts +8 -0
  5. package/dist/components/AuditTrail.d.ts +6 -0
  6. package/dist/components/GroupPicker.d.ts +12 -0
  7. package/dist/components/IdentityCard.d.ts +14 -0
  8. package/dist/components/InviteUserPanel.d.ts +14 -0
  9. package/dist/components/OrganizationSwitcher.d.ts +9 -0
  10. package/dist/components/PermissionMatrix.d.ts +10 -0
  11. package/dist/components/PolicyRuleViewer.d.ts +6 -0
  12. package/dist/components/RoleBadge.d.ts +6 -0
  13. package/dist/components/SessionStatus.d.ts +9 -0
  14. package/dist/components/TeamList.d.ts +13 -0
  15. package/dist/components/UserPicker.d.ts +12 -0
  16. package/dist/components/types.d.ts +77 -0
  17. package/dist/index.cjs +1629 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.css +2238 -0
  20. package/dist/index.css.map +1 -0
  21. package/dist/index.d.ts +18 -0
  22. package/dist/index.js +1610 -0
  23. package/dist/index.js.map +1 -0
  24. package/package.json +43 -0
  25. package/src/components/AccessReviewPanel.tsx +169 -0
  26. package/src/components/AccountMenu.tsx +144 -0
  27. package/src/components/ApprovalPolicyEditor.tsx +131 -0
  28. package/src/components/AuditTrail.tsx +105 -0
  29. package/src/components/GroupPicker.tsx +175 -0
  30. package/src/components/IdentityCard.tsx +78 -0
  31. package/src/components/InviteUserPanel.tsx +162 -0
  32. package/src/components/OrganizationSwitcher.test.tsx +59 -0
  33. package/src/components/OrganizationSwitcher.tsx +161 -0
  34. package/src/components/PermissionMatrix.test.tsx +96 -0
  35. package/src/components/PermissionMatrix.tsx +271 -0
  36. package/src/components/PolicyRuleViewer.test.tsx +29 -0
  37. package/src/components/PolicyRuleViewer.tsx +78 -0
  38. package/src/components/RoleBadge.test.tsx +35 -0
  39. package/src/components/RoleBadge.tsx +38 -0
  40. package/src/components/SessionStatus.test.tsx +40 -0
  41. package/src/components/SessionStatus.tsx +194 -0
  42. package/src/components/TeamList.test.tsx +48 -0
  43. package/src/components/TeamList.tsx +98 -0
  44. package/src/components/UserPicker.test.tsx +52 -0
  45. package/src/components/UserPicker.tsx +174 -0
  46. package/src/components/types.ts +89 -0
  47. package/src/index.tsx +35 -0
  48. 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];