@ibdop/platform-kit 1.0.10 → 1.0.12
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/dist/hooks/index.d.ts +2 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useApi.d.ts +10 -7
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/hooks/useFeatures.d.ts +98 -0
- package/dist/hooks/useFeatures.d.ts.map +1 -0
- package/dist/hooks/useShellAuth.d.ts +2 -5
- package/dist/hooks/useShellAuth.d.ts.map +1 -1
- package/dist/index.js +10 -10
- package/dist/index.mjs +957 -709
- package/dist/index.umd.js +10 -10
- package/dist/services/api.d.ts +2 -0
- package/dist/services/api.d.ts.map +1 -1
- package/dist/types/index.d.ts +88 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/shellAuth.d.ts +43 -0
- package/dist/utils/shellAuth.d.ts.map +1 -0
- package/package.json +3 -5
- package/src/hooks/index.ts +7 -1
- package/src/hooks/useApi.ts +76 -17
- package/src/hooks/useFeatures.ts +445 -0
- package/src/hooks/useShellAuth.ts +6 -26
- package/src/services/api.ts +43 -43
- package/src/types/index.ts +99 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/shellAuth.ts +106 -0
package/src/hooks/useApi.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* обработкой ошибок и уведомлениями.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useState, useCallback } from 'react'
|
|
8
|
+
import { useState, useCallback, useRef, useEffect } from 'react'
|
|
9
9
|
import type { ApiError, ApiResponse, NotificationPayload } from '../types'
|
|
10
10
|
|
|
11
11
|
// Development mode flag
|
|
@@ -104,6 +104,8 @@ export interface UseApiConfig<T = never> {
|
|
|
104
104
|
errorContext?: string
|
|
105
105
|
onSuccess?: (data: T) => void
|
|
106
106
|
onError?: (error: ApiError) => void
|
|
107
|
+
/** Auto-execute on mount (default: false) */
|
|
108
|
+
immediate?: boolean
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
/**
|
|
@@ -117,6 +119,14 @@ export interface UseApiResult<T> {
|
|
|
117
119
|
isSuccess: boolean
|
|
118
120
|
execute: () => Promise<T | null>
|
|
119
121
|
reset: () => void
|
|
122
|
+
abort: () => void
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if error is an abort error
|
|
127
|
+
*/
|
|
128
|
+
function isAbortError(error: unknown): boolean {
|
|
129
|
+
return error instanceof Error && error.name === 'AbortError'
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
/**
|
|
@@ -128,14 +138,14 @@ export interface UseApiResult<T> {
|
|
|
128
138
|
*
|
|
129
139
|
* @example
|
|
130
140
|
* ```tsx
|
|
131
|
-
* const { data, isLoading, isError, execute } = useApi<User[]>(
|
|
132
|
-
* () => fetch('/api/users').then(r => r.json()),
|
|
141
|
+
* const { data, isLoading, isError, execute, abort } = useApi<User[]>(
|
|
142
|
+
* (signal) => fetch('/api/users', { signal }).then(r => r.json()),
|
|
133
143
|
* { notifyOnError: true, errorContext: 'загрузка пользователей' }
|
|
134
144
|
* )
|
|
135
145
|
* ```
|
|
136
146
|
*/
|
|
137
147
|
export function useApi<T>(
|
|
138
|
-
request: () => Promise<ApiResponse<T>>,
|
|
148
|
+
request: (signal?: AbortSignal) => Promise<ApiResponse<T>>,
|
|
139
149
|
config: UseApiConfig<T> = {}
|
|
140
150
|
): UseApiResult<T> {
|
|
141
151
|
const {
|
|
@@ -151,15 +161,50 @@ export function useApi<T>(
|
|
|
151
161
|
const [error, setError] = useState<ApiError | null>(null)
|
|
152
162
|
const [isLoading, setIsLoading] = useState(false)
|
|
153
163
|
|
|
164
|
+
// AbortController ref for request cancellation
|
|
165
|
+
const abortControllerRef = useRef<AbortController | null>(null)
|
|
166
|
+
|
|
154
167
|
const isError = error !== null
|
|
155
168
|
const isSuccess = data !== null && !isLoading && !isError
|
|
156
169
|
|
|
170
|
+
// Cleanup on unmount
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
return () => {
|
|
173
|
+
if (abortControllerRef.current) {
|
|
174
|
+
abortControllerRef.current.abort()
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}, [])
|
|
178
|
+
|
|
179
|
+
const abort = useCallback(() => {
|
|
180
|
+
if (abortControllerRef.current) {
|
|
181
|
+
abortControllerRef.current.abort()
|
|
182
|
+
abortControllerRef.current = null
|
|
183
|
+
setIsLoading(false)
|
|
184
|
+
logger.log('Request aborted')
|
|
185
|
+
}
|
|
186
|
+
}, [])
|
|
187
|
+
|
|
157
188
|
const execute = useCallback(async () => {
|
|
189
|
+
// Abort any pending request
|
|
190
|
+
if (abortControllerRef.current) {
|
|
191
|
+
abortControllerRef.current.abort()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Create new AbortController
|
|
195
|
+
abortControllerRef.current = new AbortController()
|
|
196
|
+
const signal = abortControllerRef.current.signal
|
|
197
|
+
|
|
158
198
|
setIsLoading(true)
|
|
159
199
|
setError(null)
|
|
160
200
|
|
|
161
201
|
try {
|
|
162
|
-
const response = await request()
|
|
202
|
+
const response = await request(signal)
|
|
203
|
+
|
|
204
|
+
// Check if aborted during request
|
|
205
|
+
if (signal.aborted) {
|
|
206
|
+
return null
|
|
207
|
+
}
|
|
163
208
|
|
|
164
209
|
if (response.ok) {
|
|
165
210
|
setData(response.data)
|
|
@@ -193,6 +238,12 @@ export function useApi<T>(
|
|
|
193
238
|
return null
|
|
194
239
|
}
|
|
195
240
|
} catch (err) {
|
|
241
|
+
// Ignore abort errors
|
|
242
|
+
if (isAbortError(err)) {
|
|
243
|
+
logger.log('Request was aborted')
|
|
244
|
+
return null
|
|
245
|
+
}
|
|
246
|
+
|
|
196
247
|
// Handle network/server errors
|
|
197
248
|
const apiError = err as ApiError
|
|
198
249
|
setError(apiError)
|
|
@@ -204,7 +255,10 @@ export function useApi<T>(
|
|
|
204
255
|
onError?.(apiError)
|
|
205
256
|
return null
|
|
206
257
|
} finally {
|
|
207
|
-
|
|
258
|
+
if (!signal.aborted) {
|
|
259
|
+
setIsLoading(false)
|
|
260
|
+
}
|
|
261
|
+
abortControllerRef.current = null
|
|
208
262
|
}
|
|
209
263
|
}, [request, notifyOnError, notifyOnSuccess, successMessage, errorContext, onSuccess, onError])
|
|
210
264
|
|
|
@@ -222,24 +276,26 @@ export function useApi<T>(
|
|
|
222
276
|
isSuccess,
|
|
223
277
|
execute,
|
|
224
278
|
reset,
|
|
279
|
+
abort,
|
|
225
280
|
}
|
|
226
281
|
}
|
|
227
282
|
|
|
228
283
|
// Convenience hooks for common HTTP methods
|
|
229
284
|
|
|
230
285
|
/**
|
|
231
|
-
* GET запрос
|
|
286
|
+
* GET запрос с AbortController
|
|
232
287
|
*/
|
|
233
288
|
export function useGet<T>(
|
|
234
289
|
url: string,
|
|
235
290
|
params?: Record<string, unknown>,
|
|
236
291
|
config: UseApiConfig<T> = {}
|
|
237
292
|
): UseApiResult<T> {
|
|
238
|
-
const request = () =>
|
|
239
|
-
fetch(url,
|
|
293
|
+
const request = (signal?: AbortSignal) =>
|
|
294
|
+
fetch(url, {
|
|
240
295
|
method: 'GET',
|
|
241
|
-
headers: { 'Content-Type': 'application/json' }
|
|
242
|
-
|
|
296
|
+
headers: { 'Content-Type': 'application/json' },
|
|
297
|
+
signal,
|
|
298
|
+
})
|
|
243
299
|
.then(async (response) => {
|
|
244
300
|
const data = await response.json()
|
|
245
301
|
return { data, status: response.status, ok: response.ok } as ApiResponse<T>
|
|
@@ -249,18 +305,19 @@ export function useGet<T>(
|
|
|
249
305
|
}
|
|
250
306
|
|
|
251
307
|
/**
|
|
252
|
-
* POST запрос
|
|
308
|
+
* POST запрос с AbortController
|
|
253
309
|
*/
|
|
254
310
|
export function usePost<T>(
|
|
255
311
|
url: string,
|
|
256
312
|
data?: unknown,
|
|
257
313
|
config: UseApiConfig<T> = {}
|
|
258
314
|
): UseApiResult<T> {
|
|
259
|
-
const request = () =>
|
|
315
|
+
const request = (signal?: AbortSignal) =>
|
|
260
316
|
fetch(url, {
|
|
261
317
|
method: 'POST',
|
|
262
318
|
headers: { 'Content-Type': 'application/json' },
|
|
263
319
|
body: data ? JSON.stringify(data) : undefined,
|
|
320
|
+
signal,
|
|
264
321
|
})
|
|
265
322
|
.then(async (response) => {
|
|
266
323
|
const responseData = await response.json()
|
|
@@ -271,18 +328,19 @@ export function usePost<T>(
|
|
|
271
328
|
}
|
|
272
329
|
|
|
273
330
|
/**
|
|
274
|
-
* PUT запрос
|
|
331
|
+
* PUT запрос с AbortController
|
|
275
332
|
*/
|
|
276
333
|
export function usePut<T>(
|
|
277
334
|
url: string,
|
|
278
335
|
data?: unknown,
|
|
279
336
|
config: UseApiConfig<T> = {}
|
|
280
337
|
): UseApiResult<T> {
|
|
281
|
-
const request = () =>
|
|
338
|
+
const request = (signal?: AbortSignal) =>
|
|
282
339
|
fetch(url, {
|
|
283
340
|
method: 'PUT',
|
|
284
341
|
headers: { 'Content-Type': 'application/json' },
|
|
285
342
|
body: data ? JSON.stringify(data) : undefined,
|
|
343
|
+
signal,
|
|
286
344
|
})
|
|
287
345
|
.then(async (response) => {
|
|
288
346
|
const responseData = await response.json()
|
|
@@ -293,16 +351,17 @@ export function usePut<T>(
|
|
|
293
351
|
}
|
|
294
352
|
|
|
295
353
|
/**
|
|
296
|
-
* DELETE запрос
|
|
354
|
+
* DELETE запрос с AbortController
|
|
297
355
|
*/
|
|
298
356
|
export function useDel<T>(
|
|
299
357
|
url: string,
|
|
300
358
|
config: UseApiConfig<T> = {}
|
|
301
359
|
): UseApiResult<T> {
|
|
302
|
-
const request = () =>
|
|
360
|
+
const request = (signal?: AbortSignal) =>
|
|
303
361
|
fetch(url, {
|
|
304
362
|
method: 'DELETE',
|
|
305
363
|
headers: { 'Content-Type': 'application/json' },
|
|
364
|
+
signal,
|
|
306
365
|
})
|
|
307
366
|
.then(async (response) => {
|
|
308
367
|
const responseData = await response.json()
|
|
@@ -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
|
+
}
|