@tidecloak/ui-framework 0.0.1

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 +377 -0
  2. package/dist/index.d.mts +2739 -0
  3. package/dist/index.d.ts +2739 -0
  4. package/dist/index.js +12869 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/index.mjs +12703 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +54 -0
  9. package/src/components/common/ActionButton.tsx +234 -0
  10. package/src/components/common/EmptyState.tsx +140 -0
  11. package/src/components/common/LoadingSkeleton.tsx +121 -0
  12. package/src/components/common/RefreshButton.tsx +127 -0
  13. package/src/components/common/StatusBadge.tsx +177 -0
  14. package/src/components/common/index.ts +31 -0
  15. package/src/components/data-table/DataTable.tsx +201 -0
  16. package/src/components/data-table/PaginatedTable.tsx +247 -0
  17. package/src/components/data-table/index.ts +2 -0
  18. package/src/components/dialogs/CollapsibleSection.tsx +184 -0
  19. package/src/components/dialogs/ConfirmDialog.tsx +264 -0
  20. package/src/components/dialogs/DetailDialog.tsx +228 -0
  21. package/src/components/dialogs/index.ts +3 -0
  22. package/src/components/index.ts +5 -0
  23. package/src/components/pages/base/ApprovalsPageBase.tsx +680 -0
  24. package/src/components/pages/base/LogsPageBase.tsx +581 -0
  25. package/src/components/pages/base/RolesPageBase.tsx +1470 -0
  26. package/src/components/pages/base/TemplatesPageBase.tsx +761 -0
  27. package/src/components/pages/base/UsersPageBase.tsx +843 -0
  28. package/src/components/pages/base/index.ts +58 -0
  29. package/src/components/pages/connected/ApprovalsPage.tsx +797 -0
  30. package/src/components/pages/connected/LogsPage.tsx +267 -0
  31. package/src/components/pages/connected/RolesPage.tsx +525 -0
  32. package/src/components/pages/connected/TemplatesPage.tsx +181 -0
  33. package/src/components/pages/connected/UsersPage.tsx +237 -0
  34. package/src/components/pages/connected/index.ts +36 -0
  35. package/src/components/pages/index.ts +5 -0
  36. package/src/components/tabs/TabsView.tsx +300 -0
  37. package/src/components/tabs/index.ts +1 -0
  38. package/src/components/ui/index.tsx +1001 -0
  39. package/src/hooks/index.ts +3 -0
  40. package/src/hooks/useAutoRefresh.ts +119 -0
  41. package/src/hooks/usePagination.ts +152 -0
  42. package/src/hooks/useSelection.ts +81 -0
  43. package/src/index.ts +256 -0
  44. package/src/theme.ts +185 -0
  45. package/src/tide/index.ts +19 -0
  46. package/src/tide/tidePolicy.ts +270 -0
  47. package/src/types/index.ts +484 -0
  48. package/src/utils/index.ts +121 -0
@@ -0,0 +1,237 @@
1
+ /**
2
+ * UsersPage - Pre-made user management page
3
+ *
4
+ * Uses AdminAPI from @tidecloak/js for data fetching.
5
+ * Just drop it in and it works.
6
+ */
7
+
8
+ import { AdminAPI as DefaultAdminAPI } from "@tidecloak/js";
9
+ import { UsersPageBase, type UsersPageBaseProps, type UserItem } from "../base";
10
+ import type { AccessMetadataAPI, AccessMetadataRecord } from "./ApprovalsPage";
11
+
12
+ // Type for the AdminAPI instance
13
+ type AdminAPIInstance = {
14
+ setRealm?: (realm: string) => void;
15
+ getUsers: () => Promise<any>;
16
+ getRoles: () => Promise<any>;
17
+ getRole: (roleName: string) => Promise<any>;
18
+ createUser: (user: any) => Promise<any>;
19
+ updateUser: (userId: string, user: any) => Promise<any>;
20
+ deleteUser: (userId: string) => Promise<any>;
21
+ addUserRoles?: (userId: string, roles: any[]) => Promise<any>;
22
+ removeUserRoles?: (userId: string, roles: any[]) => Promise<any>;
23
+ setUserEnabled?: (userId: string, enabled: boolean) => Promise<any>;
24
+ getTideLinkUrl?: (userId: string, redirectUri: string, lifespan?: number) => Promise<string>;
25
+ };
26
+
27
+ export interface ChangeSetInfo {
28
+ changeSetId: string;
29
+ changeSetType: string;
30
+ actionType: string;
31
+ username: string;
32
+ userId: string;
33
+ roles: string[];
34
+ timestamp: number;
35
+ }
36
+
37
+ export interface UsersPageProps<T extends UserItem = UserItem>
38
+ extends Omit<
39
+ UsersPageBaseProps<T>,
40
+ | "fetchUsers"
41
+ | "fetchRoles"
42
+ | "onCreate"
43
+ | "onUpdateProfile"
44
+ | "onUpdateRoles"
45
+ | "onDelete"
46
+ | "onSetEnabled"
47
+ | "getTideLinkUrl"
48
+ > {
49
+ /** AdminAPI instance */
50
+ adminAPI?: AdminAPIInstance;
51
+ /** Realm name */
52
+ realm?: string;
53
+ /** Redirect URI for Tide link */
54
+ tideLinkRedirectUri?: string;
55
+ /** Access metadata adapter (saves metadata when role changes create change sets) */
56
+ accessMetadataAPI?: AccessMetadataAPI;
57
+ /** Callback when a change set is created */
58
+ onChangeSetCreated?: (info: ChangeSetInfo) => void | Promise<void>;
59
+ /** Override fetch users */
60
+ fetchUsers?: UsersPageBaseProps<T>["fetchUsers"];
61
+ /** Override create */
62
+ onCreate?: UsersPageBaseProps<T>["onCreate"];
63
+ /** Override delete */
64
+ onDelete?: UsersPageBaseProps<T>["onDelete"];
65
+ /** Override getTideLinkUrl */
66
+ getTideLinkUrl?: UsersPageBaseProps<T>["getTideLinkUrl"];
67
+ }
68
+
69
+ export function UsersPage<T extends UserItem = UserItem>({
70
+ adminAPI: adminAPIProp,
71
+ realm,
72
+ tideLinkRedirectUri,
73
+ accessMetadataAPI,
74
+ onChangeSetCreated,
75
+ fetchUsers: fetchUsersProp,
76
+ onCreate: onCreateProp,
77
+ onDelete: onDeleteProp,
78
+ getTideLinkUrl: getTideLinkUrlProp,
79
+ ...props
80
+ }: UsersPageProps<T>) {
81
+ // Use provided AdminAPI instance or fall back to default singleton
82
+ const api = adminAPIProp || DefaultAdminAPI;
83
+
84
+ // Set realm if provided
85
+ if (realm && api.setRealm) {
86
+ api.setRealm(realm);
87
+ }
88
+
89
+ const fetchUsers =
90
+ fetchUsersProp ||
91
+ (async () => {
92
+ const rawUsers = await api.getUsers();
93
+ // Transform users to include linked status based on vuid attribute
94
+ const users = (rawUsers || []).map((u: any) => ({
95
+ ...u,
96
+ // User is linked if they have a vuid attribute (Tide account linked)
97
+ linked: !!(u.attributes?.vuid?.[0]),
98
+ // Ensure role is an array
99
+ role: Array.isArray(u.role) ? u.role : u.role ? [u.role] : [],
100
+ })) as T[];
101
+ return users;
102
+ });
103
+
104
+ const fetchRoles = async () => {
105
+ const roles = await api.getRoles();
106
+ return { roles: roles || [] };
107
+ };
108
+
109
+ const onCreate =
110
+ onCreateProp ||
111
+ (async (data) => {
112
+ await api.createUser(data);
113
+ });
114
+
115
+ const onUpdateProfile = async (data: {
116
+ id: string;
117
+ firstName?: string;
118
+ lastName?: string;
119
+ email?: string;
120
+ }) => {
121
+ await api.updateUser(data.id, {
122
+ firstName: data.firstName,
123
+ lastName: data.lastName,
124
+ email: data.email,
125
+ });
126
+ };
127
+
128
+ // Helper to track change set metadata
129
+ const trackChangeSet = async (
130
+ response: any,
131
+ userId: string,
132
+ username: string,
133
+ roles: string[],
134
+ actionType: string
135
+ ) => {
136
+ // Extract changeSetId from response (AdminAPI returns this)
137
+ const changeSetId = response?.changeSetId || response?.draftRecordId || response?.id;
138
+ if (!changeSetId) {
139
+ return;
140
+ }
141
+
142
+ const timestamp = Date.now();
143
+ const info: ChangeSetInfo = {
144
+ changeSetId,
145
+ changeSetType: response?.changeSetType || "USER",
146
+ actionType,
147
+ username,
148
+ userId,
149
+ roles,
150
+ timestamp,
151
+ };
152
+
153
+ // Call the callback if provided
154
+ if (onChangeSetCreated) {
155
+ await onChangeSetCreated(info);
156
+ }
157
+
158
+ // Save to metadata API if provided
159
+ if (accessMetadataAPI) {
160
+ const record: AccessMetadataRecord = {
161
+ changeSetId,
162
+ username,
163
+ role: roles.join(", "),
164
+ timestamp,
165
+ actionType,
166
+ changeSetType: info.changeSetType,
167
+ };
168
+ await accessMetadataAPI.saveMetadata(record);
169
+ }
170
+ };
171
+
172
+ const onUpdateRoles = async (data: {
173
+ id: string;
174
+ username?: string;
175
+ rolesToAdd?: string[];
176
+ rolesToRemove?: string[];
177
+ }) => {
178
+ // Get username for metadata tracking
179
+ const username = data.username || data.id;
180
+
181
+ if (data.rolesToAdd?.length && api.addUserRoles) {
182
+ // Fetch role details first
183
+ const roles = await Promise.all(
184
+ data.rolesToAdd.map((name) => api.getRole(name))
185
+ );
186
+ const response = await api.addUserRoles(data.id, roles);
187
+ // Track the change set
188
+ await trackChangeSet(response, data.id, username, data.rolesToAdd, "ADD");
189
+ }
190
+ if (data.rolesToRemove?.length && api.removeUserRoles) {
191
+ const roles = await Promise.all(
192
+ data.rolesToRemove.map((name) => api.getRole(name))
193
+ );
194
+ const response = await api.removeUserRoles(data.id, roles);
195
+ // Track the change set
196
+ await trackChangeSet(response, data.id, username, data.rolesToRemove, "REMOVE");
197
+ }
198
+ };
199
+
200
+ const onDelete =
201
+ onDeleteProp ||
202
+ (async (userId: string) => {
203
+ await api.deleteUser(userId);
204
+ });
205
+
206
+ const onSetEnabled = api.setUserEnabled
207
+ ? async (userId: string, enabled: boolean) => {
208
+ await api.setUserEnabled!(userId, enabled);
209
+ }
210
+ : undefined;
211
+
212
+ // Get Tide link URL for user invitation
213
+ const getTideLinkUrl =
214
+ getTideLinkUrlProp ||
215
+ (api.getTideLinkUrl
216
+ ? async (userId: string) => {
217
+ // Default redirect to current origin if not specified
218
+ const redirectUri = tideLinkRedirectUri || (typeof window !== "undefined" ? window.location.origin : "");
219
+ const linkUrl = await api.getTideLinkUrl!(userId, redirectUri);
220
+ return { linkUrl };
221
+ }
222
+ : undefined);
223
+
224
+ return (
225
+ <UsersPageBase<T>
226
+ fetchUsers={fetchUsers}
227
+ fetchRoles={fetchRoles}
228
+ onCreate={onCreate}
229
+ onUpdateProfile={onUpdateProfile}
230
+ onUpdateRoles={onUpdateRoles}
231
+ onDelete={onDelete}
232
+ onSetEnabled={onSetEnabled}
233
+ getTideLinkUrl={getTideLinkUrl}
234
+ {...props}
235
+ />
236
+ );
237
+ }
@@ -0,0 +1,36 @@
1
+ // Pre-made page components - uses AdminAPI from @tidecloak/js
2
+
3
+ export {
4
+ RolesPage,
5
+ createLocalStoragePolicyAPI,
6
+ type RolesPageProps,
7
+ type PolicyAPI,
8
+ type RolePolicy,
9
+ } from "./RolesPage";
10
+ export { UsersPage, type UsersPageProps, type ChangeSetInfo } from "./UsersPage";
11
+ export {
12
+ TemplatesPage,
13
+ createLocalStorageTemplateAPI,
14
+ type TemplatesPageProps,
15
+ type TemplateAPI,
16
+ } from "./TemplatesPage";
17
+ export {
18
+ LogsPage,
19
+ createLocalStoragePolicyLogsAPI,
20
+ type LogsPageProps,
21
+ type PolicyLogsAPI,
22
+ type PolicyLogData,
23
+ } from "./LogsPage";
24
+ export {
25
+ ApprovalsPage,
26
+ createLocalStoragePolicyApprovalsAPI,
27
+ createLocalStorageAccessMetadataAPI,
28
+ type ApprovalsPageProps,
29
+ type PolicyApprovalsAPI,
30
+ type PolicyApprovalData,
31
+ type AccessMetadataAPI,
32
+ type AccessMetadataRecord,
33
+ type TideApprovalContext,
34
+ type TideApprovalResult,
35
+ type EnclaveApprovalTabConfig,
36
+ } from "./ApprovalsPage";
@@ -0,0 +1,5 @@
1
+ // Page components - headless/composable UI for custom backends
2
+ export * from "./base";
3
+
4
+ // Connected pages - pre-made pages that use @tidecloak/js AdminAPI
5
+ export * from "./connected";
@@ -0,0 +1,300 @@
1
+ import React from "react";
2
+ import type { TabDef } from "../../types";
3
+
4
+ export interface TabsViewProps {
5
+ /** Tab definitions */
6
+ tabs: TabDef[];
7
+ /** Currently active tab key */
8
+ activeTab: string;
9
+ /** Tab change callback */
10
+ onTabChange: (tabKey: string) => void;
11
+ /** Additional class name */
12
+ className?: string;
13
+ /** Tab list class name */
14
+ tabListClassName?: string;
15
+ /** Custom components */
16
+ components?: {
17
+ Tabs?: React.ComponentType<{
18
+ value: string;
19
+ onValueChange: (value: string) => void;
20
+ children: React.ReactNode;
21
+ className?: string;
22
+ }>;
23
+ TabsList?: React.ComponentType<{ children: React.ReactNode; className?: string }>;
24
+ TabsTrigger?: React.ComponentType<{
25
+ value: string;
26
+ disabled?: boolean;
27
+ className?: string;
28
+ children: React.ReactNode;
29
+ }>;
30
+ TabsContent?: React.ComponentType<{
31
+ value: string;
32
+ className?: string;
33
+ children: React.ReactNode;
34
+ }>;
35
+ Badge?: React.ComponentType<{
36
+ variant?: string;
37
+ className?: string;
38
+ children: React.ReactNode;
39
+ }>;
40
+ };
41
+ }
42
+
43
+ /**
44
+ * TabsView - Generic tabbed view component
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * <TabsView
49
+ * tabs={[
50
+ * { key: 'access', label: 'Access', badge: 5, content: <AccessTab /> },
51
+ * { key: 'roles', label: 'Roles', badge: 2, content: <RolesTab /> },
52
+ * { key: 'policies', label: 'Policies', content: <PoliciesTab /> },
53
+ * ]}
54
+ * activeTab={activeTab}
55
+ * onTabChange={setActiveTab}
56
+ * />
57
+ * ```
58
+ */
59
+ export function TabsView({
60
+ tabs,
61
+ activeTab,
62
+ onTabChange,
63
+ className,
64
+ tabListClassName,
65
+ components = {},
66
+ }: TabsViewProps) {
67
+ // Use custom components or defaults
68
+ const Tabs = components.Tabs || DefaultTabs;
69
+ const TabsList = components.TabsList || DefaultTabsList;
70
+ const TabsTrigger = components.TabsTrigger || DefaultTabsTrigger;
71
+ const TabsContent = components.TabsContent || DefaultTabsContent;
72
+ const Badge = components.Badge || DefaultBadge;
73
+
74
+ return (
75
+ <Tabs value={activeTab} onValueChange={onTabChange} className={className}>
76
+ <TabsList className={tabListClassName}>
77
+ {tabs.map((tab) => (
78
+ <TabsTrigger
79
+ key={tab.key}
80
+ value={tab.key}
81
+ disabled={tab.disabled}
82
+ >
83
+ <span style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
84
+ {tab.icon}
85
+ {tab.label}
86
+ {tab.badge !== undefined && tab.badge !== null && tab.badge > 0 && (
87
+ <Badge
88
+ variant={tab.badgeVariant || "destructive"}
89
+ >
90
+ {tab.badge > 99 ? "99+" : tab.badge}
91
+ </Badge>
92
+ )}
93
+ </span>
94
+ </TabsTrigger>
95
+ ))}
96
+ </TabsList>
97
+
98
+ {tabs.map((tab) => (
99
+ <TabsContent key={tab.key} value={tab.key}>
100
+ {tab.content}
101
+ </TabsContent>
102
+ ))}
103
+ </Tabs>
104
+ );
105
+ }
106
+
107
+ // Default component implementations
108
+ function DefaultTabs({
109
+ value,
110
+ onValueChange,
111
+ children,
112
+ className,
113
+ }: {
114
+ value: string;
115
+ onValueChange: (value: string) => void;
116
+ children: React.ReactNode;
117
+ className?: string;
118
+ }) {
119
+ return (
120
+ <div className={className} data-value={value}>
121
+ {React.Children.map(children, (child) => {
122
+ if (React.isValidElement(child)) {
123
+ return React.cloneElement(child as React.ReactElement<any>, {
124
+ activeTab: value,
125
+ onTabChange: onValueChange,
126
+ });
127
+ }
128
+ return child;
129
+ })}
130
+ </div>
131
+ );
132
+ }
133
+
134
+ function DefaultTabsList({
135
+ children,
136
+ className,
137
+ activeTab,
138
+ onTabChange,
139
+ }: {
140
+ children: React.ReactNode;
141
+ className?: string;
142
+ activeTab?: string;
143
+ onTabChange?: (value: string) => void;
144
+ }) {
145
+ return (
146
+ <div
147
+ style={{
148
+ display: "flex",
149
+ alignItems: "stretch",
150
+ borderBottom: "1px solid #e5e7eb",
151
+ backgroundColor: "#f9fafb",
152
+ padding: "0 1rem",
153
+ gap: "0",
154
+ }}
155
+ className={className}
156
+ >
157
+ {React.Children.map(children, (child) => {
158
+ if (React.isValidElement(child)) {
159
+ return React.cloneElement(child as React.ReactElement<any>, {
160
+ activeTab,
161
+ onTabChange,
162
+ });
163
+ }
164
+ return child;
165
+ })}
166
+ </div>
167
+ );
168
+ }
169
+
170
+ function DefaultTabsTrigger({
171
+ value,
172
+ disabled,
173
+ className,
174
+ children,
175
+ activeTab,
176
+ onTabChange,
177
+ }: {
178
+ value: string;
179
+ disabled?: boolean;
180
+ className?: string;
181
+ children: React.ReactNode;
182
+ activeTab?: string;
183
+ onTabChange?: (value: string) => void;
184
+ }) {
185
+ const isActive = activeTab === value;
186
+
187
+ const baseStyle: React.CSSProperties = {
188
+ display: "inline-flex",
189
+ alignItems: "center",
190
+ justifyContent: "center",
191
+ whiteSpace: "nowrap",
192
+ padding: "0.75rem 1rem",
193
+ fontSize: "0.875rem",
194
+ fontWeight: 500,
195
+ transition: "all 150ms ease",
196
+ border: "none",
197
+ borderBottom: isActive ? "2px solid #1f2937" : "2px solid transparent",
198
+ marginBottom: "-1px",
199
+ cursor: disabled ? "not-allowed" : "pointer",
200
+ opacity: disabled ? 0.5 : 1,
201
+ backgroundColor: isActive ? "#ffffff" : "transparent",
202
+ color: isActive ? "#111827" : "#6b7280",
203
+ };
204
+
205
+ return (
206
+ <button
207
+ type="button"
208
+ onClick={() => onTabChange?.(value)}
209
+ disabled={disabled}
210
+ style={baseStyle}
211
+ className={className}
212
+ >
213
+ {children}
214
+ </button>
215
+ );
216
+ }
217
+
218
+ function DefaultTabsContent({
219
+ value,
220
+ className,
221
+ children,
222
+ activeTab,
223
+ }: {
224
+ value: string;
225
+ className?: string;
226
+ children: React.ReactNode;
227
+ activeTab?: string;
228
+ }) {
229
+ if (activeTab !== value) return null;
230
+
231
+ return (
232
+ <div
233
+ style={{ marginTop: 0 }}
234
+ className={className}
235
+ >
236
+ {children}
237
+ </div>
238
+ );
239
+ }
240
+
241
+ function DefaultBadge({
242
+ variant,
243
+ className,
244
+ children,
245
+ }: {
246
+ variant?: string;
247
+ className?: string;
248
+ children: React.ReactNode;
249
+ }) {
250
+ const getVariantStyles = (): React.CSSProperties => {
251
+ switch (variant) {
252
+ case "destructive":
253
+ return {
254
+ backgroundColor: "#dc2626",
255
+ color: "#ffffff",
256
+ borderColor: "transparent",
257
+ };
258
+ case "secondary":
259
+ return {
260
+ backgroundColor: "#f3f4f6",
261
+ color: "#374151",
262
+ borderColor: "transparent",
263
+ };
264
+ case "outline":
265
+ return {
266
+ backgroundColor: "transparent",
267
+ color: "#111827",
268
+ borderColor: "#e5e7eb",
269
+ };
270
+ default:
271
+ return {
272
+ backgroundColor: "#3b82f6",
273
+ color: "#ffffff",
274
+ borderColor: "transparent",
275
+ };
276
+ }
277
+ };
278
+
279
+ return (
280
+ <span
281
+ style={{
282
+ display: "inline-flex",
283
+ alignItems: "center",
284
+ justifyContent: "center",
285
+ borderRadius: "9999px",
286
+ border: "1px solid",
287
+ padding: "0 0.5rem",
288
+ fontSize: "0.75rem",
289
+ fontWeight: 600,
290
+ height: "1.25rem",
291
+ minWidth: "1.25rem",
292
+ marginLeft: "0.25rem",
293
+ ...getVariantStyles(),
294
+ }}
295
+ className={className}
296
+ >
297
+ {children}
298
+ </span>
299
+ );
300
+ }
@@ -0,0 +1 @@
1
+ export { TabsView, type TabsViewProps } from "./TabsView";