@ibdop/platform-kit 1.0.10 → 1.0.11

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.
@@ -0,0 +1,445 @@
1
+ /**
2
+ * Hook for working with feature toggles
3
+ */
4
+
5
+ import { useState, useEffect, useCallback, useMemo } from 'react'
6
+ import { useShellAuth } from './useShellAuth'
7
+ import type {
8
+ FeatureToggleInfo,
9
+ FeaturesResponse,
10
+ FeatureToggleAdmin,
11
+ FeatureAdminResponse,
12
+ FeatureCreateRequest,
13
+ FeatureUpdateRequest,
14
+ MicrofrontendWithFeatures,
15
+ MicrofrontendsFeaturesResponse,
16
+ } from '../types'
17
+
18
+ /**
19
+ * Result of useFeatures hook
20
+ */
21
+ export interface UseFeaturesResult {
22
+ features: FeatureToggleInfo[]
23
+ totalCount: number
24
+ userRoles: string[]
25
+ isLoading: boolean
26
+ error: string | null
27
+ refetch: () => Promise<void>
28
+ isFeatureEnabled: (featureName: string) => boolean
29
+ getFeaturesByMf: (mfName: string) => FeatureToggleInfo[]
30
+ }
31
+
32
+ /**
33
+ * Hook to get feature toggles for the current user
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * import { useFeatures } from '@ib-dop/platform-kit'
38
+ *
39
+ * function FeatureFlagComponent() {
40
+ * const { features, isFeatureEnabled, isLoading } = useFeatures()
41
+ *
42
+ * if (isLoading) return <Spinner />
43
+ *
44
+ * return (
45
+ * <div>
46
+ * {isFeatureEnabled('new-dashboard') && <NewDashboard />}
47
+ * {!isFeatureEnabled('new-dashboard') && <OldDashboard />}
48
+ * </div>
49
+ * )
50
+ * }
51
+ * ```
52
+ */
53
+ export function useFeatures(): UseFeaturesResult {
54
+ const auth = useShellAuth()
55
+ const [features, setFeatures] = useState<FeatureToggleInfo[]>([])
56
+ const [totalCount, setTotalCount] = useState(0)
57
+ const [userRoles, setUserRoles] = useState<string[]>([])
58
+ const [isLoading, setIsLoading] = useState(true)
59
+ const [error, setError] = useState<string | null>(null)
60
+
61
+ const fetchFeatures = useCallback(async () => {
62
+ if (!auth.isAuthenticated) {
63
+ console.debug('[useFeatures] Not authenticated')
64
+ setIsLoading(false)
65
+ return
66
+ }
67
+
68
+ setIsLoading(true)
69
+ setError(null)
70
+
71
+ try {
72
+ const headers: Record<string, string> = {
73
+ 'Content-Type': 'application/json',
74
+ }
75
+
76
+ const token = auth.user?.access_token
77
+ if (token) {
78
+ headers['Authorization'] = `Bearer ${token}`
79
+ }
80
+
81
+ const response = await fetch('/api/features', { headers })
82
+
83
+ if (!response.ok) {
84
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
85
+ }
86
+
87
+ const result: FeaturesResponse = await response.json()
88
+ setFeatures(result.features || [])
89
+ setTotalCount(result.totalCount || 0)
90
+ setUserRoles(result.userRoles || [])
91
+ } catch (err) {
92
+ console.debug('Features fetch error:', err)
93
+ setError(err instanceof Error ? err.message : String(err))
94
+ setFeatures([])
95
+ setUserRoles([])
96
+ } finally {
97
+ setIsLoading(false)
98
+ }
99
+ }, [auth.isAuthenticated, auth.user?.access_token])
100
+
101
+ useEffect(() => {
102
+ fetchFeatures()
103
+ }, [fetchFeatures])
104
+
105
+ const isFeatureEnabled = useCallback(
106
+ (featureName: string): boolean => {
107
+ const feature = features.find((f) => f.name === featureName)
108
+ return feature?.userEnabled ?? false
109
+ },
110
+ [features]
111
+ )
112
+
113
+ const getFeaturesByMf = useCallback(
114
+ (mfName: string): FeatureToggleInfo[] => {
115
+ return features.filter((f) => f.mfDependencies?.includes(mfName))
116
+ },
117
+ [features]
118
+ )
119
+
120
+ return {
121
+ features,
122
+ totalCount,
123
+ userRoles,
124
+ isLoading,
125
+ error,
126
+ refetch: fetchFeatures,
127
+ isFeatureEnabled,
128
+ getFeaturesByMf,
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Result of useFeatureAdmin hook
134
+ */
135
+ export interface UseFeatureAdminResult {
136
+ features: FeatureToggleAdmin[]
137
+ microfrontends: string[]
138
+ isAdmin: boolean
139
+ isLoading: boolean
140
+ error: string | null
141
+ refetch: () => Promise<void>
142
+ createFeature: (feature: FeatureCreateRequest) => Promise<boolean>
143
+ updateFeature: (name: string, feature: FeatureUpdateRequest) => Promise<boolean>
144
+ toggleFeature: (name: string, enabled: boolean) => Promise<boolean>
145
+ deleteFeature: (name: string) => Promise<boolean>
146
+ }
147
+
148
+ /**
149
+ * Hook for admin operations with feature toggles
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * import { useFeatureAdmin } from '@ib-dop/platform-kit'
154
+ *
155
+ * function AdminPanel() {
156
+ * const {
157
+ * features,
158
+ * isAdmin,
159
+ * isLoading,
160
+ * toggleFeature
161
+ * } = useFeatureAdmin()
162
+ *
163
+ * if (isLoading) return <Spinner />
164
+ * if (!isAdmin) return <AccessDenied />
165
+ *
166
+ * return (
167
+ * <table>
168
+ * {features.map(f => (
169
+ * <tr key={f.name}>
170
+ * <td>{f.name}</td>
171
+ * <td>{f.enabled ? 'ON' : 'OFF'}</td>
172
+ * <button onClick={() => toggleFeature(f.name, !f.enabled)}>
173
+ * Toggle
174
+ * </button>
175
+ * </tr>
176
+ * ))}
177
+ * </table>
178
+ * )
179
+ * }
180
+ * ```
181
+ */
182
+ export function useFeatureAdmin(): UseFeatureAdminResult {
183
+ const auth = useShellAuth()
184
+ const [features, setFeatures] = useState<FeatureToggleAdmin[]>([])
185
+ const [microfrontends, setMicrofrontends] = useState<string[]>([])
186
+ const [isAdmin, setIsAdmin] = useState(false)
187
+ const [isLoading, setIsLoading] = useState(true)
188
+ const [error, setError] = useState<string | null>(null)
189
+
190
+ const fetchFeatures = useCallback(async () => {
191
+ if (!auth.isAuthenticated) {
192
+ console.debug('[useFeatureAdmin] Not authenticated')
193
+ setIsLoading(false)
194
+ return
195
+ }
196
+
197
+ setIsLoading(true)
198
+ setError(null)
199
+
200
+ try {
201
+ const headers: Record<string, string> = {
202
+ 'Content-Type': 'application/json',
203
+ }
204
+
205
+ const token = auth.user?.access_token
206
+ if (token) {
207
+ headers['Authorization'] = `Bearer ${token}`
208
+ }
209
+
210
+ const response = await fetch('/api/features/admin', { headers })
211
+
212
+ if (!response.ok) {
213
+ if (response.status === 403) {
214
+ console.warn('[useFeatureAdmin] 403 Forbidden - checking if token has admin role')
215
+ setIsAdmin(false)
216
+ setFeatures([])
217
+ setMicrofrontends([])
218
+ setIsLoading(false)
219
+ return
220
+ }
221
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
222
+ }
223
+
224
+ const result: FeatureAdminResponse = await response.json()
225
+ setFeatures(result.featureToggles || [])
226
+ setMicrofrontends(result.microfrontends || [])
227
+ setIsAdmin(result.isAdmin || false)
228
+ } catch (err) {
229
+ console.debug('FeatureAdmin fetch error:', err)
230
+ setFeatures([])
231
+ setMicrofrontends([])
232
+ setIsAdmin(false)
233
+ } finally {
234
+ setIsLoading(false)
235
+ }
236
+ }, [auth.isAuthenticated, auth.user?.access_token])
237
+
238
+ const createFeature = useCallback(
239
+ async (feature: FeatureCreateRequest): Promise<boolean> => {
240
+ try {
241
+ const headers: Record<string, string> = {
242
+ 'Content-Type': 'application/json',
243
+ }
244
+ const token = auth.user?.access_token
245
+ if (token) {
246
+ headers['Authorization'] = `Bearer ${token}`
247
+ }
248
+
249
+ const response = await fetch('/api/features/admin', {
250
+ method: 'POST',
251
+ headers,
252
+ body: JSON.stringify(feature),
253
+ })
254
+
255
+ if (!response.ok) {
256
+ const errorData = await response.json().catch(() => ({}))
257
+ throw new Error(errorData.error || `HTTP ${response.status}`)
258
+ }
259
+
260
+ await fetchFeatures()
261
+ return true
262
+ } catch (err) {
263
+ setError(err instanceof Error ? err.message : String(err))
264
+ return false
265
+ }
266
+ },
267
+ [fetchFeatures]
268
+ )
269
+
270
+ const updateFeature = useCallback(
271
+ async (name: string, feature: FeatureUpdateRequest): Promise<boolean> => {
272
+ try {
273
+ const headers: Record<string, string> = {
274
+ 'Content-Type': 'application/json',
275
+ }
276
+ const token = auth.user?.access_token
277
+ if (token) {
278
+ headers['Authorization'] = `Bearer ${token}`
279
+ }
280
+
281
+ const response = await fetch(`/api/features/admin/${encodeURIComponent(name)}`, {
282
+ method: 'PUT',
283
+ headers,
284
+ body: JSON.stringify(feature),
285
+ })
286
+
287
+ if (!response.ok) {
288
+ const errorData = await response.json().catch(() => ({}))
289
+ throw new Error(errorData.error || `HTTP ${response.status}`)
290
+ }
291
+
292
+ await fetchFeatures()
293
+ return true
294
+ } catch (err) {
295
+ setError(err instanceof Error ? err.message : String(err))
296
+ return false
297
+ }
298
+ },
299
+ [fetchFeatures]
300
+ )
301
+
302
+ const toggleFeature = useCallback(
303
+ async (name: string, enabled: boolean): Promise<boolean> => {
304
+ try {
305
+ const headers: Record<string, string> = {}
306
+ const token = auth.user?.access_token
307
+ if (token) {
308
+ headers['Authorization'] = `Bearer ${token}`
309
+ }
310
+
311
+ const response = await fetch(
312
+ `/api/features/admin/${encodeURIComponent(name)}/toggle?enabled=${enabled}`,
313
+ {
314
+ method: 'POST',
315
+ headers,
316
+ }
317
+ )
318
+
319
+ if (!response.ok) {
320
+ const errorData = await response.json().catch(() => ({}))
321
+ throw new Error(errorData.error || `HTTP ${response.status}`)
322
+ }
323
+
324
+ await fetchFeatures()
325
+ return true
326
+ } catch (err) {
327
+ setError(err instanceof Error ? err.message : String(err))
328
+ return false
329
+ }
330
+ },
331
+ [fetchFeatures]
332
+ )
333
+
334
+ const deleteFeature = useCallback(
335
+ async (name: string): Promise<boolean> => {
336
+ try {
337
+ const headers: Record<string, string> = {}
338
+ const token = auth.user?.access_token
339
+ if (token) {
340
+ headers['Authorization'] = `Bearer ${token}`
341
+ }
342
+
343
+ const response = await fetch(`/api/features/admin/${encodeURIComponent(name)}`, {
344
+ method: 'DELETE',
345
+ headers,
346
+ })
347
+
348
+ if (!response.ok) {
349
+ const errorData = await response.json().catch(() => ({}))
350
+ throw new Error(errorData.error || `HTTP ${response.status}`)
351
+ }
352
+
353
+ await fetchFeatures()
354
+ return true
355
+ } catch (err) {
356
+ setError(err instanceof Error ? err.message : String(err))
357
+ return false
358
+ }
359
+ },
360
+ [fetchFeatures]
361
+ )
362
+
363
+ useEffect(() => {
364
+ fetchFeatures()
365
+ }, [fetchFeatures])
366
+
367
+ return {
368
+ features,
369
+ microfrontends,
370
+ isAdmin,
371
+ isLoading,
372
+ error,
373
+ refetch: fetchFeatures,
374
+ createFeature,
375
+ updateFeature,
376
+ toggleFeature,
377
+ deleteFeature,
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Hook to get microfrontends with their features
383
+ */
384
+ export interface UseMicrofrontendsFeaturesResult {
385
+ microfrontends: MicrofrontendWithFeatures[]
386
+ totalCount: number
387
+ isLoading: boolean
388
+ error: string | null
389
+ refetch: () => Promise<void>
390
+ }
391
+
392
+ export function useMicrofrontendsFeatures(): UseMicrofrontendsFeaturesResult {
393
+ const auth = useShellAuth()
394
+ const [microfrontends, setMicrofrontends] = useState<MicrofrontendWithFeatures[]>([])
395
+ const [totalCount, setTotalCount] = useState(0)
396
+ const [isLoading, setIsLoading] = useState(true)
397
+ const [error, setError] = useState<string | null>(null)
398
+
399
+ const fetchData = useCallback(async () => {
400
+ if (!auth.isAuthenticated) {
401
+ setIsLoading(false)
402
+ return
403
+ }
404
+
405
+ setIsLoading(true)
406
+ setError(null)
407
+
408
+ try {
409
+ const headers: Record<string, string> = {
410
+ 'Content-Type': 'application/json',
411
+ }
412
+
413
+ const token = auth.user?.access_token
414
+ if (token) {
415
+ headers['Authorization'] = `Bearer ${token}`
416
+ }
417
+
418
+ const response = await fetch('/api/features/microfrontends', { headers })
419
+
420
+ if (!response.ok) {
421
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
422
+ }
423
+
424
+ const result: MicrofrontendsFeaturesResponse = await response.json()
425
+ setMicrofrontends(result.microfrontends || [])
426
+ setTotalCount(result.totalCount || 0)
427
+ } catch (err) {
428
+ setError(err instanceof Error ? err.message : String(err))
429
+ } finally {
430
+ setIsLoading(false)
431
+ }
432
+ }, [auth.isAuthenticated, auth.user?.access_token])
433
+
434
+ useEffect(() => {
435
+ fetchData()
436
+ }, [fetchData])
437
+
438
+ return {
439
+ microfrontends,
440
+ totalCount,
441
+ isLoading,
442
+ error,
443
+ refetch: fetchData,
444
+ }
445
+ }
@@ -225,3 +225,102 @@ export type MfeNameSource =
225
225
  | 'sessionStorage.mf-config'
226
226
  | 'import.meta.env.VITE_MFE_NAME'
227
227
  | 'import.meta.env.MFE_NAME'
228
+
229
+ // ==================== Feature Types ====================
230
+
231
+ /**
232
+ * Feature toggle for a user
233
+ */
234
+ export interface FeatureToggleInfo {
235
+ name: string
236
+ enabled: boolean
237
+ description?: string
238
+ roles?: string[]
239
+ percentage?: number
240
+ microfrontends?: string[]
241
+ userEnabled: boolean
242
+ mfDependencies?: string[]
243
+ }
244
+
245
+ /**
246
+ * Response from /api/features endpoint
247
+ */
248
+ export interface FeaturesResponse {
249
+ features: FeatureToggleInfo[]
250
+ totalCount: number
251
+ userRoles: string[]
252
+ }
253
+
254
+ /**
255
+ * Admin feature toggle (full info)
256
+ */
257
+ export interface FeatureToggleAdmin {
258
+ name: string
259
+ enabled: boolean
260
+ description: string
261
+ roles: string[]
262
+ percentage: number
263
+ microfrontends: string[]
264
+ mfDependencies: string[]
265
+ }
266
+
267
+ /**
268
+ * Admin response from /api/features/admin endpoint
269
+ */
270
+ export interface FeatureAdminResponse {
271
+ featureToggles: FeatureToggleAdmin[]
272
+ isAdmin: boolean
273
+ microfrontends: string[]
274
+ totalCount: number
275
+ }
276
+
277
+ /**
278
+ * Feature creation request
279
+ */
280
+ export interface FeatureCreateRequest {
281
+ name: string
282
+ enabled?: boolean
283
+ description?: string
284
+ roles?: string[]
285
+ percentage?: number
286
+ microfrontends?: string[]
287
+ }
288
+
289
+ /**
290
+ * Feature update request
291
+ */
292
+ export interface FeatureUpdateRequest {
293
+ enabled?: boolean
294
+ description?: string
295
+ roles?: string[]
296
+ percentage?: number
297
+ microfrontends?: string[]
298
+ }
299
+
300
+ /**
301
+ * Microfrontend with its features
302
+ */
303
+ export interface MicrofrontendWithFeatures {
304
+ name: string
305
+ description?: string
306
+ featureToggles: string[]
307
+ features: FeatureInfo[]
308
+ }
309
+
310
+ /**
311
+ * Feature info for a microfrontend
312
+ */
313
+ export interface FeatureInfo {
314
+ name: string
315
+ enabled: boolean
316
+ global: boolean
317
+ required: boolean
318
+ }
319
+
320
+ /**
321
+ * Response from /api/features/microfrontends endpoint
322
+ */
323
+ export interface MicrofrontendsFeaturesResponse {
324
+ microfrontends: MicrofrontendWithFeatures[]
325
+ totalCount: number
326
+ }