@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
package/src/theme.ts ADDED
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Theme constants for consistent styling across components.
3
+ * Use CSS variables for runtime theming, constants for TypeScript type safety.
4
+ */
5
+
6
+ // Color palette
7
+ export const colors = {
8
+ // Backgrounds
9
+ background: {
10
+ primary: "var(--tc-bg-primary, #ffffff)",
11
+ secondary: "var(--tc-bg-secondary, #f9fafb)",
12
+ muted: "var(--tc-bg-muted, #f3f4f6)",
13
+ overlay: "var(--tc-bg-overlay, rgba(0, 0, 0, 0.5))",
14
+ },
15
+ // Foregrounds (text)
16
+ foreground: {
17
+ primary: "var(--tc-fg-primary, #111827)",
18
+ secondary: "var(--tc-fg-secondary, #374151)",
19
+ muted: "var(--tc-fg-muted, #6b7280)",
20
+ disabled: "var(--tc-fg-disabled, #9ca3af)",
21
+ },
22
+ // Borders
23
+ border: {
24
+ default: "var(--tc-border, #e5e7eb)",
25
+ focus: "var(--tc-border-focus, #3b82f6)",
26
+ error: "var(--tc-border-error, #dc2626)",
27
+ },
28
+ // Status colors
29
+ status: {
30
+ success: "var(--tc-success, #16a34a)",
31
+ successBg: "var(--tc-success-bg, #dcfce7)",
32
+ error: "var(--tc-error, #dc2626)",
33
+ errorBg: "var(--tc-error-bg, #fef2f2)",
34
+ warning: "var(--tc-warning, #f59e0b)",
35
+ warningBg: "var(--tc-warning-bg, #fef3c7)",
36
+ info: "var(--tc-info, #3b82f6)",
37
+ infoBg: "var(--tc-info-bg, #eff6ff)",
38
+ pending: "var(--tc-pending, #6b7280)",
39
+ pendingBg: "var(--tc-pending-bg, #f3f4f6)",
40
+ },
41
+ // Action colors
42
+ action: {
43
+ primary: "var(--tc-action-primary, #3b82f6)",
44
+ primaryHover: "var(--tc-action-primary-hover, #2563eb)",
45
+ danger: "var(--tc-action-danger, #dc2626)",
46
+ dangerHover: "var(--tc-action-danger-hover, #b91c1c)",
47
+ },
48
+ } as const;
49
+
50
+ // Spacing scale
51
+ export const spacing = {
52
+ xs: "0.25rem",
53
+ sm: "0.5rem",
54
+ md: "1rem",
55
+ lg: "1.5rem",
56
+ xl: "2rem",
57
+ "2xl": "3rem",
58
+ } as const;
59
+
60
+ // Border radius
61
+ export const radius = {
62
+ sm: "0.25rem",
63
+ md: "0.375rem",
64
+ lg: "0.5rem",
65
+ full: "9999px",
66
+ } as const;
67
+
68
+ // Font sizes
69
+ export const fontSize = {
70
+ xs: "0.75rem",
71
+ sm: "0.875rem",
72
+ base: "1rem",
73
+ lg: "1.125rem",
74
+ xl: "1.25rem",
75
+ "2xl": "1.5rem",
76
+ } as const;
77
+
78
+ // Z-index scale
79
+ export const zIndex = {
80
+ dropdown: 50,
81
+ sticky: 100,
82
+ modal: 200,
83
+ overlay: 300,
84
+ toast: 400,
85
+ } as const;
86
+
87
+ // Animation durations
88
+ export const duration = {
89
+ fast: "150ms",
90
+ normal: "200ms",
91
+ slow: "300ms",
92
+ } as const;
93
+
94
+ // Focus ring style (consistent across all interactive elements)
95
+ export const focusRing = {
96
+ outline: `2px solid ${colors.border.focus}`,
97
+ outlineOffset: "2px",
98
+ } as const;
99
+
100
+ // Shadow styles
101
+ export const shadow = {
102
+ sm: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
103
+ md: "0 4px 6px -1px rgba(0, 0, 0, 0.1)",
104
+ lg: "0 10px 15px -3px rgba(0, 0, 0, 0.1)",
105
+ } as const;
106
+
107
+ // CSS to inject for theming support
108
+ export const themeCSS = `
109
+ :root {
110
+ --tc-bg-primary: #ffffff;
111
+ --tc-bg-secondary: #f9fafb;
112
+ --tc-bg-muted: #f3f4f6;
113
+ --tc-bg-overlay: rgba(0, 0, 0, 0.5);
114
+ --tc-fg-primary: #111827;
115
+ --tc-fg-secondary: #374151;
116
+ --tc-fg-muted: #6b7280;
117
+ --tc-fg-disabled: #9ca3af;
118
+ --tc-border: #e5e7eb;
119
+ --tc-border-focus: #3b82f6;
120
+ --tc-border-error: #dc2626;
121
+ --tc-success: #16a34a;
122
+ --tc-success-bg: #dcfce7;
123
+ --tc-error: #dc2626;
124
+ --tc-error-bg: #fef2f2;
125
+ --tc-warning: #f59e0b;
126
+ --tc-warning-bg: #fef3c7;
127
+ --tc-info: #3b82f6;
128
+ --tc-info-bg: #eff6ff;
129
+ --tc-pending: #6b7280;
130
+ --tc-pending-bg: #f3f4f6;
131
+ --tc-action-primary: #3b82f6;
132
+ --tc-action-primary-hover: #2563eb;
133
+ --tc-action-danger: #dc2626;
134
+ --tc-action-danger-hover: #b91c1c;
135
+ }
136
+
137
+ @media (prefers-color-scheme: dark) {
138
+ :root {
139
+ --tc-bg-primary: #1f2937;
140
+ --tc-bg-secondary: #111827;
141
+ --tc-bg-muted: #374151;
142
+ --tc-bg-overlay: rgba(0, 0, 0, 0.7);
143
+ --tc-fg-primary: #f9fafb;
144
+ --tc-fg-secondary: #e5e7eb;
145
+ --tc-fg-muted: #9ca3af;
146
+ --tc-fg-disabled: #6b7280;
147
+ --tc-border: #374151;
148
+ --tc-border-focus: #60a5fa;
149
+ --tc-border-error: #f87171;
150
+ --tc-success: #22c55e;
151
+ --tc-success-bg: #14532d;
152
+ --tc-error: #f87171;
153
+ --tc-error-bg: #7f1d1d;
154
+ --tc-warning: #fbbf24;
155
+ --tc-warning-bg: #78350f;
156
+ --tc-info: #60a5fa;
157
+ --tc-info-bg: #1e3a8a;
158
+ --tc-pending: #9ca3af;
159
+ --tc-pending-bg: #374151;
160
+ --tc-action-primary: #60a5fa;
161
+ --tc-action-primary-hover: #3b82f6;
162
+ --tc-action-danger: #f87171;
163
+ --tc-action-danger-hover: #dc2626;
164
+ }
165
+ }
166
+
167
+ @media (prefers-reduced-motion: reduce) {
168
+ *, *::before, *::after {
169
+ animation-duration: 0.01ms !important;
170
+ animation-iteration-count: 1 !important;
171
+ transition-duration: 0.01ms !important;
172
+ }
173
+ }
174
+ `;
175
+
176
+ // Helper to inject theme CSS (call once at app init)
177
+ export function injectThemeCSS(): void {
178
+ if (typeof document === "undefined") return;
179
+ if (document.getElementById("tidecloak-theme")) return;
180
+
181
+ const style = document.createElement("style");
182
+ style.id = "tidecloak-theme";
183
+ style.textContent = themeCSS;
184
+ document.head.appendChild(style);
185
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Tide Policy Workflow Exports
3
+ *
4
+ * These helpers provide the default Tide-based policy creation workflow.
5
+ * Requires heimdall-tide and asgard-tide libraries to be installed.
6
+ */
7
+
8
+ export {
9
+ loadTideLibs,
10
+ areTideLibsAvailable,
11
+ computeContractId,
12
+ createTidePolicyRequest,
13
+ createTidePolicyHandler,
14
+ rolePolicyToTideConfig,
15
+ DEFAULT_MODEL_IDS,
16
+ DEFAULT_ROLE_CONTRACT,
17
+ type TidePolicyConfig,
18
+ type TideContextMethods,
19
+ } from "./tidePolicy";
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Tide Policy Workflow Helpers
3
+ *
4
+ * Provides functions for creating Tide policy requests that can be signed
5
+ * by the TideCloak context. Uses heimdall-tide and asgard-tide libraries.
6
+ *
7
+ * This is the default policy creation workflow used by RolesPage when
8
+ * Tide libraries are available.
9
+ */
10
+
11
+ import type { RolePolicy } from "../components/pages/connected/RolesPage";
12
+
13
+ // Lazy imports for optional dependencies
14
+ let Policy: any;
15
+ let PolicySignRequest: any;
16
+ let TideMemory: any;
17
+ let ApprovalType: any;
18
+ let ExecutionType: any;
19
+
20
+ let tideLibsLoaded = false;
21
+ let tideLibsAvailable = false;
22
+
23
+ /**
24
+ * Attempts to load the Tide libraries (heimdall-tide and asgard-tide).
25
+ * Returns true if both libraries are available, false otherwise.
26
+ */
27
+ export async function loadTideLibs(): Promise<boolean> {
28
+ if (tideLibsLoaded) return tideLibsAvailable;
29
+
30
+ try {
31
+ const heimdall = await import("heimdall-tide");
32
+ const asgard = await import("asgard-tide");
33
+
34
+ Policy = heimdall.Policy;
35
+ PolicySignRequest = heimdall.PolicySignRequest;
36
+ TideMemory = heimdall.TideMemory;
37
+ ApprovalType = asgard.ApprovalType;
38
+ ExecutionType = asgard.ExecutionType;
39
+
40
+ tideLibsAvailable = true;
41
+ } catch {
42
+ tideLibsAvailable = false;
43
+ }
44
+
45
+ tideLibsLoaded = true;
46
+ return tideLibsAvailable;
47
+ }
48
+
49
+ /**
50
+ * Checks if Tide libraries are available without loading them.
51
+ * Must call loadTideLibs() first.
52
+ */
53
+ export function areTideLibsAvailable(): boolean {
54
+ return tideLibsAvailable;
55
+ }
56
+
57
+ // Default model IDs for different contract types
58
+ export const DEFAULT_MODEL_IDS = {
59
+ BASIC: "BasicCustom<Role>:BasicCustom<1>",
60
+ DYNAMIC: "DynamicCustom<Role>:DynamicCustom<1>",
61
+ DYNAMIC_APPROVED: "DynamicApprovedCustom<Role>:DynamicApprovedCustom<1>",
62
+ } as const;
63
+
64
+ /**
65
+ * Default Forseti contract for role-based access control.
66
+ * This is a simple contract that validates approvers and executors have the required role.
67
+ */
68
+ export const DEFAULT_ROLE_CONTRACT = `using Ork.Forseti.Sdk;
69
+ using System;
70
+ using System.Collections.Generic;
71
+
72
+ /// <summary>
73
+ /// Default Role-Based Access Policy.
74
+ /// Validates that approvers and executors have the required role for a resource.
75
+ /// </summary>
76
+ public class RolePolicy : IAccessPolicy
77
+ {
78
+ [PolicyParam(Required = true, Description = "Role required for access")]
79
+ public string Role { get; set; }
80
+
81
+ [PolicyParam(Required = true, Description = "Resource identifier for role check")]
82
+ public string Resource { get; set; }
83
+
84
+ public PolicyDecision ValidateData(DataContext ctx)
85
+ {
86
+ // No data validation needed for role-based policies
87
+ return PolicyDecision.Allow();
88
+ }
89
+
90
+ public PolicyDecision ValidateApprovers(ApproversContext ctx)
91
+ {
92
+ var approvers = DokenDto.WrapAll(ctx.Dokens);
93
+ return Decision
94
+ .Require(approvers != null && approvers.Count > 0, "No approver dokens provided")
95
+ .RequireAnyWithRole(approvers, Resource, Role);
96
+ }
97
+
98
+ public PolicyDecision ValidateExecutor(ExecutorContext ctx)
99
+ {
100
+ var executor = new DokenDto(ctx.Doken);
101
+ return Decision
102
+ .RequireNotExpired(executor)
103
+ .RequireRole(executor, Resource, Role);
104
+ }
105
+ }`;
106
+
107
+ export interface TidePolicyConfig {
108
+ roleName: string;
109
+ threshold: number;
110
+ approvalType: "implicit" | "explicit";
111
+ executionType: "public" | "private";
112
+ modelId?: string;
113
+ resource: string;
114
+ vendorId: string;
115
+ contractCode?: string;
116
+ }
117
+
118
+ export interface TideContextMethods {
119
+ initializeTideRequest: <T extends { encode: () => Uint8Array }>(request: T) => Promise<T>;
120
+ getVendorId: () => string;
121
+ getResource: () => string;
122
+ }
123
+
124
+ /**
125
+ * Computes the contract ID (SHA512 hash) for a Forseti contract source.
126
+ * This must match what the Ork server computes.
127
+ */
128
+ export async function computeContractId(source: string): Promise<string> {
129
+ const encoder = new TextEncoder();
130
+ const data = encoder.encode(source);
131
+ const hashBuffer = await crypto.subtle.digest("SHA-512", data);
132
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
133
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
134
+ }
135
+
136
+ /**
137
+ * Creates a Tide PolicySignRequest for a role policy.
138
+ *
139
+ * @param config - Policy configuration
140
+ * @param context - TideCloak context methods for signing
141
+ * @returns The signed PolicySignRequest
142
+ * @throws Error if Tide libraries are not available
143
+ */
144
+ export async function createTidePolicyRequest(
145
+ config: TidePolicyConfig,
146
+ context: TideContextMethods
147
+ ): Promise<any> {
148
+ if (!tideLibsAvailable) {
149
+ throw new Error(
150
+ "Tide libraries (heimdall-tide, asgard-tide) are not available. " +
151
+ "Install them or provide a custom onCreatePolicy handler."
152
+ );
153
+ }
154
+
155
+ const contractCode = config.contractCode || DEFAULT_ROLE_CONTRACT;
156
+ const contractId = await computeContractId(contractCode);
157
+
158
+ // Detect entry type from contract code
159
+ const entryType = detectEntryType(contractCode) || "RolePolicy";
160
+
161
+ // Create policy params
162
+ const policyParams = new Map<string, any>();
163
+ policyParams.set("role", config.roleName);
164
+ policyParams.set("threshold", config.threshold);
165
+ policyParams.set("resource", config.resource);
166
+ policyParams.set("approval_type", config.approvalType);
167
+ policyParams.set("execution_type", config.executionType);
168
+
169
+ // Create the Policy object
170
+ const policy = new Policy({
171
+ version: "2",
172
+ modelId: config.modelId || DEFAULT_MODEL_IDS.DYNAMIC_APPROVED,
173
+ contractId: contractId,
174
+ keyId: config.vendorId,
175
+ approvalType: config.approvalType === "explicit" ? ApprovalType.EXPLICIT : ApprovalType.IMPLICIT,
176
+ executionType: config.executionType === "private" ? ExecutionType.PRIVATE : ExecutionType.PUBLIC,
177
+ params: policyParams,
178
+ });
179
+
180
+ // Create the PolicySignRequest
181
+ const policyRequest = PolicySignRequest.New(policy);
182
+ const policyBytes = policy.toBytes();
183
+
184
+ // Create contract transport
185
+ // Structure: forsetiData[1] = innerPayload = [source, entryType]
186
+ const contractTypeBytes = new TextEncoder().encode("forseti");
187
+ const sourceCodeBytes = new TextEncoder().encode(contractCode);
188
+ const entryTypeBytes = new TextEncoder().encode(entryType);
189
+ const innerPayload = TideMemory.CreateFromArray([sourceCodeBytes, entryTypeBytes]);
190
+ const forsetiData = TideMemory.CreateFromArray([new Uint8Array(0), innerPayload]);
191
+ const contractTransport = TideMemory.CreateFromArray([contractTypeBytes, forsetiData]);
192
+
193
+ const draftWithContract = TideMemory.CreateFromArray([policyBytes, contractTransport]);
194
+ policyRequest.draft = draftWithContract;
195
+ policyRequest.setCustomExpiry(604800); // 7 days
196
+
197
+ // Initialize (sign) the request using TideCloak context
198
+ const signedRequest = await context.initializeTideRequest(policyRequest);
199
+
200
+ return signedRequest;
201
+ }
202
+
203
+ /**
204
+ * Detects the entry type (class name) from C# source code.
205
+ * Looks for a public class implementing IAccessPolicy.
206
+ */
207
+ function detectEntryType(source: string): string | null {
208
+ const match = source.match(/public\s+class\s+(\w+)\s*:\s*IAccessPolicy/);
209
+ return match ? match[1] : null;
210
+ }
211
+
212
+ /**
213
+ * Creates a policy creation handler that uses the Tide workflow.
214
+ * This is used as the default onCreatePolicy in RolesPage when Tide libs are available.
215
+ *
216
+ * @param context - TideCloak context methods
217
+ * @param onRequestCreated - Optional callback when request is created (for custom handling)
218
+ * @returns Policy creation handler function
219
+ */
220
+ export function createTidePolicyHandler(
221
+ context: TideContextMethods,
222
+ onRequestCreated?: (request: any, roleName: string) => Promise<void>
223
+ ) {
224
+ return async (params: {
225
+ roleName: string;
226
+ policyConfig: any;
227
+ templateId?: string;
228
+ templateParams?: Record<string, any>;
229
+ threshold: number;
230
+ contractCode?: string;
231
+ }) => {
232
+ const config: TidePolicyConfig = {
233
+ roleName: params.roleName,
234
+ threshold: params.threshold,
235
+ approvalType: params.policyConfig?.approvalType || "explicit",
236
+ executionType: params.policyConfig?.executionType || "private",
237
+ resource: context.getResource(),
238
+ vendorId: context.getVendorId(),
239
+ contractCode: params.contractCode,
240
+ modelId: params.policyConfig?.modelId,
241
+ };
242
+
243
+ const signedRequest = await createTidePolicyRequest(config, context);
244
+
245
+ // Call the callback if provided (e.g., to submit to backend)
246
+ if (onRequestCreated) {
247
+ await onRequestCreated(signedRequest, params.roleName);
248
+ }
249
+
250
+ return signedRequest;
251
+ };
252
+ }
253
+
254
+ /**
255
+ * Converts a RolePolicy to TidePolicyConfig.
256
+ * Useful when loading existing policies for re-signing or updates.
257
+ */
258
+ export function rolePolicyToTideConfig(
259
+ policy: RolePolicy,
260
+ context: TideContextMethods
261
+ ): TidePolicyConfig {
262
+ return {
263
+ roleName: policy.roleName,
264
+ threshold: policy.threshold,
265
+ approvalType: policy.approvalType,
266
+ executionType: policy.executionType,
267
+ resource: context.getResource(),
268
+ vendorId: context.getVendorId(),
269
+ };
270
+ }