@ibdop/platform-kit 1.0.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 (51) hide show
  1. package/README.md +63 -0
  2. package/dist/components/ErrorBoundary.d.ts +68 -0
  3. package/dist/components/ErrorBoundary.d.ts.map +1 -0
  4. package/dist/components/Notification.d.ts +46 -0
  5. package/dist/components/Notification.d.ts.map +1 -0
  6. package/dist/components/VersionInfo.d.ts +18 -0
  7. package/dist/components/VersionInfo.d.ts.map +1 -0
  8. package/dist/components/index.d.ts +8 -0
  9. package/dist/components/index.d.ts.map +1 -0
  10. package/dist/hooks/index.d.ts +9 -0
  11. package/dist/hooks/index.d.ts.map +1 -0
  12. package/dist/hooks/useApi.d.ts +65 -0
  13. package/dist/hooks/useApi.d.ts.map +1 -0
  14. package/dist/hooks/useInfoData.d.ts +35 -0
  15. package/dist/hooks/useInfoData.d.ts.map +1 -0
  16. package/dist/hooks/usePermissions.d.ts +23 -0
  17. package/dist/hooks/usePermissions.d.ts.map +1 -0
  18. package/dist/hooks/useShellAuth.d.ts +23 -0
  19. package/dist/hooks/useShellAuth.d.ts.map +1 -0
  20. package/dist/hooks/useV1Config.d.ts +27 -0
  21. package/dist/hooks/useV1Config.d.ts.map +1 -0
  22. package/dist/index.cjs.js +18 -0
  23. package/dist/index.d.ts +10 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.es.js +1500 -0
  26. package/dist/index.umd.js +18 -0
  27. package/dist/services/api.d.ts +59 -0
  28. package/dist/services/api.d.ts.map +1 -0
  29. package/dist/services/index.d.ts +8 -0
  30. package/dist/services/index.d.ts.map +1 -0
  31. package/dist/services/logger.d.ts +41 -0
  32. package/dist/services/logger.d.ts.map +1 -0
  33. package/dist/types/index.d.ts +178 -0
  34. package/dist/types/index.d.ts.map +1 -0
  35. package/package.json +90 -0
  36. package/src/components/ErrorBoundary.tsx +292 -0
  37. package/src/components/Notification.tsx +297 -0
  38. package/src/components/VersionInfo.tsx +271 -0
  39. package/src/components/index.ts +14 -0
  40. package/src/global.d.ts +40 -0
  41. package/src/hooks/index.ts +13 -0
  42. package/src/hooks/useApi.ts +314 -0
  43. package/src/hooks/useInfoData.ts +124 -0
  44. package/src/hooks/usePermissions.ts +88 -0
  45. package/src/hooks/useShellAuth.ts +145 -0
  46. package/src/hooks/useV1Config.ts +112 -0
  47. package/src/index.ts +17 -0
  48. package/src/services/api.ts +290 -0
  49. package/src/services/index.ts +9 -0
  50. package/src/services/logger.ts +71 -0
  51. package/src/types/index.ts +215 -0
@@ -0,0 +1,292 @@
1
+ /**
2
+ * ErrorBoundary - Компонент для обработки ошибок React
3
+ *
4
+ * Перехватывает ошибки и dispatch'ит событие в shell для централизованной обработки
5
+ */
6
+
7
+ import React, { Component, ReactNode, ErrorInfo } from 'react'
8
+
9
+ // MF Name for logging
10
+ const MF_NAME = 'platform-kit'
11
+
12
+ // Development mode flag
13
+ const isDev = import.meta.env?.DEV === true || import.meta.env?.MODE === 'development'
14
+
15
+ /**
16
+ * Logger for ErrorBoundary
17
+ */
18
+ const logger = {
19
+ log: (...args: unknown[]) => {
20
+ if (isDev) console.log(`[${MF_NAME}]`, ...args)
21
+ },
22
+ warn: (...args: unknown[]) => {
23
+ if (isDev) console.warn(`[${MF_NAME}]`, ...args)
24
+ },
25
+ error: (...args: unknown[]) => {
26
+ console.error(`[${MF_NAME}]`, ...args)
27
+ },
28
+ }
29
+
30
+ /**
31
+ * Props для ErrorBoundary
32
+ */
33
+ interface ErrorBoundaryProps {
34
+ children: ReactNode
35
+ mfeName?: string
36
+ showDetails?: boolean
37
+ }
38
+
39
+ /**
40
+ * State для ErrorBoundary
41
+ */
42
+ interface ErrorBoundaryState {
43
+ hasError: boolean
44
+ error?: Error
45
+ }
46
+
47
+ /**
48
+ * ErrorBoundary - компонент для перехвата и обработки ошибок
49
+ *
50
+ * @example
51
+ * ```tsx
52
+ * <ErrorBoundary mfeName="@ib-dop/mf-example">
53
+ * <MyComponent />
54
+ * </ErrorBoundary>
55
+ * ```
56
+ */
57
+ export default class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
58
+ private hasDispatched = false
59
+
60
+ constructor(props: ErrorBoundaryProps) {
61
+ super(props)
62
+ this.state = { hasError: false }
63
+ }
64
+
65
+ /**
66
+ * Получить имя MF из props или window
67
+ */
68
+ private getMfeName(): string {
69
+ if (this.props.mfeName) return this.props.mfeName
70
+
71
+ if (typeof window !== 'undefined') {
72
+ const win = window as unknown as { __MF_NAME__?: string }
73
+ if (win.__MF_NAME__) return win.__MF_NAME__
74
+ }
75
+
76
+ return MF_NAME
77
+ }
78
+
79
+ /**
80
+ * Определить нужно ли показывать детали ошибки
81
+ */
82
+ private shouldShowDetails(): boolean {
83
+ // Priority: props > sessionStorage > NODE_ENV
84
+ if (this.props.showDetails !== undefined) {
85
+ return this.props.showDetails
86
+ }
87
+
88
+ // Check sessionStorage config
89
+ if (typeof sessionStorage !== 'undefined') {
90
+ try {
91
+ const storedConfig = sessionStorage.getItem('config')
92
+ if (storedConfig) {
93
+ const parsed = JSON.parse(storedConfig)
94
+ if (parsed.showErrorDetails !== undefined) {
95
+ return parsed.showErrorDetails
96
+ }
97
+ }
98
+ } catch {
99
+ // Ignore parse errors
100
+ }
101
+ }
102
+
103
+ // Fallback to NODE_ENV
104
+ return isDev
105
+ }
106
+
107
+ /**
108
+ * Dispatch ошибки в shell
109
+ */
110
+ private dispatchError(error: Error, errorInfo?: ErrorInfo): void {
111
+ if (this.hasDispatched || typeof window === 'undefined') return
112
+
113
+ this.hasDispatched = true
114
+ const mfeName = this.getMfeName()
115
+
116
+ logger.error('ErrorBoundary caught:', error)
117
+
118
+ try {
119
+ window.dispatchEvent(new CustomEvent('mfe-error', {
120
+ detail: {
121
+ mfeName,
122
+ error: error.message || String(error),
123
+ stack: error.stack,
124
+ componentStack: errorInfo?.componentStack,
125
+ timestamp: Date.now(),
126
+ },
127
+ bubbles: true,
128
+ }))
129
+ } catch (dispatchError) {
130
+ logger.error('Failed to dispatch mfe-error event:', dispatchError)
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Получить derived state из ошибки
136
+ */
137
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
138
+ return { hasError: true, error }
139
+ }
140
+
141
+ /**
142
+ * Обработать ошибку
143
+ */
144
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
145
+ this.dispatchError(error, errorInfo)
146
+ logger.error('Error info:', errorInfo.componentStack)
147
+ }
148
+
149
+ /**
150
+ * Копировать ошибку в буфер обмена
151
+ */
152
+ private handleCopy = (): void => {
153
+ const errorText = `Error in ${this.getMfeName()}:\n${this.state.error?.message}\n${this.state.error?.stack}`
154
+
155
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
156
+ navigator.clipboard.writeText(errorText).then(() => {
157
+ alert('Ошибка скопирована в буфер обмена')
158
+ }).catch(() => {
159
+ prompt('Скопируйте ошибку:', errorText)
160
+ })
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Повторить рендер
166
+ */
167
+ private handleRetry = (): void => {
168
+ this.setState({ hasError: false, error: undefined })
169
+ this.hasDispatched = false
170
+
171
+ if (typeof window !== 'undefined') {
172
+ window.location.reload()
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Перейти на главную
178
+ */
179
+ private handleGoHome = (): void => {
180
+ if (typeof window !== 'undefined') {
181
+ window.location.href = '/'
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Рендер
187
+ */
188
+ render(): ReactNode {
189
+ if (this.state.hasError) {
190
+ const errorMessage = this.state.error?.message || 'Unknown error'
191
+ const errorStack = this.state.error?.stack || ''
192
+ const showDetails = this.shouldShowDetails()
193
+ const mfeName = this.getMfeName()
194
+
195
+ return (
196
+ <div style={{
197
+ padding: '20px',
198
+ textAlign: 'center',
199
+ color: '#d32f2f',
200
+ fontFamily: 'monospace',
201
+ background: '#ffebee',
202
+ border: '1px solid #ef5350',
203
+ borderRadius: '4px',
204
+ margin: '10px',
205
+ }}>
206
+ <h2 style={{ fontSize: '16px', margin: '0 0 8px 0' }}>
207
+ ⚠️ Ошибка в {mfeName}
208
+ </h2>
209
+ <p style={{ fontSize: '12px', margin: 0 }}>
210
+ Произошла ошибка в микрофронтенде. Сообщение отправлено в shell.
211
+ </p>
212
+
213
+ {/* Error Details */}
214
+ {showDetails && (
215
+ <details style={{
216
+ whiteSpace: 'pre-wrap',
217
+ textAlign: 'left',
218
+ marginTop: '10px',
219
+ background: '#fff',
220
+ padding: '8px',
221
+ borderRadius: '4px',
222
+ }}>
223
+ <summary style={{ cursor: 'pointer', fontWeight: 'bold' }}>Детали ошибки</summary>
224
+ <pre style={{
225
+ fontSize: '11px',
226
+ overflow: 'auto',
227
+ maxHeight: '150px',
228
+ margin: '8px 0 0 0',
229
+ padding: '8px',
230
+ background: '#f5f5f5',
231
+ borderRadius: '4px',
232
+ }}>
233
+ {errorMessage}
234
+ {errorStack && `\n\n${errorStack}`}
235
+ </pre>
236
+ </details>
237
+ )}
238
+
239
+ {/* Actions */}
240
+ <div style={{ marginTop: '12px', display: 'flex', gap: '8px', justifyContent: 'center' }}>
241
+ <button
242
+ onClick={this.handleCopy}
243
+ style={{
244
+ padding: '8px 12px',
245
+ background: '#666',
246
+ color: 'white',
247
+ border: 'none',
248
+ borderRadius: '4px',
249
+ cursor: 'pointer',
250
+ }}
251
+ >
252
+ 📋 Копировать
253
+ </button>
254
+ <button
255
+ onClick={this.handleRetry}
256
+ style={{
257
+ padding: '8px 12px',
258
+ background: '#d32f2f',
259
+ color: 'white',
260
+ border: 'none',
261
+ borderRadius: '4px',
262
+ cursor: 'pointer',
263
+ }}
264
+ >
265
+ 🔄 Обновить
266
+ </button>
267
+ <button
268
+ onClick={this.handleGoHome}
269
+ style={{
270
+ padding: '8px 12px',
271
+ background: '#1976d2',
272
+ color: 'white',
273
+ border: 'none',
274
+ borderRadius: '4px',
275
+ cursor: 'pointer',
276
+ }}
277
+ >
278
+ 🏠 На главную
279
+ </button>
280
+ </div>
281
+ </div>
282
+ )
283
+ }
284
+
285
+ // Null-check для children
286
+ if (this.props.children == null) {
287
+ return null
288
+ }
289
+
290
+ return this.props.children
291
+ }
292
+ }
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Notification - Компонент и хук для отображения уведомлений
3
+ *
4
+ * Dispatch уведомления в shell через CustomEvent
5
+ */
6
+
7
+ import React, { createContext, useContext, useCallback, useState, useEffect, ReactNode } from 'react'
8
+ import type { NotificationPayload, NotificationType } from '../types'
9
+
10
+ // MF Name for logging
11
+ const MF_NAME = 'platform-kit'
12
+
13
+ // Development mode flag
14
+ const isDev = import.meta.env?.DEV === true || import.meta.env?.MODE === 'development'
15
+
16
+ /**
17
+ * Logger for Notification
18
+ */
19
+ const logger = {
20
+ log: (...args: unknown[]) => {
21
+ if (isDev) console.log(`[${MF_NAME}]`, ...args)
22
+ },
23
+ warn: (...args: unknown[]) => {
24
+ if (isDev) console.warn(`[${MF_NAME}]`, ...args)
25
+ },
26
+ error: (...args: unknown[]) => {
27
+ console.error(`[${MF_NAME}]`, ...args)
28
+ },
29
+ }
30
+
31
+ /**
32
+ * Notification item
33
+ */
34
+ interface NotificationItem extends NotificationPayload {
35
+ id: string
36
+ }
37
+
38
+ /**
39
+ * Context for notifications
40
+ */
41
+ interface NotificationContextValue {
42
+ notify: (payload: Omit<NotificationPayload, 'timestamp' | 'mfeName'>) => void
43
+ notifySuccess: (message: string, title?: string) => void
44
+ notifyError: (message: string, title?: string) => void
45
+ notifyWarning: (message: string, title?: string) => void
46
+ notifyInfo: (message: string, title?: string) => void
47
+ removeNotification: (id: string) => void
48
+ }
49
+
50
+ /**
51
+ * Context for notifications
52
+ */
53
+ const NotificationContext = createContext<NotificationContextValue | null>(null)
54
+
55
+ /**
56
+ * Provider for notifications
57
+ */
58
+ interface NotificationProviderProps {
59
+ children: ReactNode
60
+ }
61
+
62
+ export function NotificationProvider({ children }: NotificationProviderProps): React.ReactElement {
63
+ const [notifications, setNotifications] = useState<NotificationItem[]>([])
64
+
65
+ // Get mfeName
66
+ const mfeName = typeof window !== 'undefined'
67
+ ? ((window as unknown as { __MF_NAME__?: string }).__MF_NAME__ || MF_NAME)
68
+ : MF_NAME
69
+
70
+ // Create notification
71
+ const createNotification = useCallback((
72
+ type: NotificationType,
73
+ title: string,
74
+ message: string,
75
+ duration?: number
76
+ ): NotificationItem => ({
77
+ id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
78
+ type,
79
+ title,
80
+ message,
81
+ mfeName,
82
+ timestamp: Date.now(),
83
+ duration,
84
+ }), [mfeName])
85
+
86
+ // Add notification
87
+ const addNotification = useCallback((notification: NotificationItem) => {
88
+ setNotifications(prev => [...prev, notification].slice(0, 5))
89
+
90
+ // Auto remove after duration or 5 seconds
91
+ const duration = notification.duration || 5000
92
+ if (duration > 0) {
93
+ setTimeout(() => {
94
+ setNotifications(prev => prev.filter(n => n.id !== notification.id))
95
+ }, duration)
96
+ }
97
+ }, [])
98
+
99
+ // Notify
100
+ const notify = useCallback((payload: Omit<NotificationPayload, 'timestamp' | 'mfeName'>) => {
101
+ const notification = createNotification(payload.type, payload.title, payload.message, payload.duration)
102
+ addNotification(notification)
103
+
104
+ // Dispatch to shell
105
+ if (typeof window !== 'undefined') {
106
+ window.dispatchEvent(new CustomEvent('mfe-notification', {
107
+ detail: notification,
108
+ bubbles: true,
109
+ }))
110
+ }
111
+
112
+ logger.log('Notification:', notification)
113
+ }, [createNotification, addNotification])
114
+
115
+ // Convenience methods
116
+ const notifySuccess = useCallback((message: string, title = 'Успешно') => {
117
+ notify({ type: 'success', title, message })
118
+ }, [notify])
119
+
120
+ const notifyError = useCallback((message: string, title = 'Ошибка') => {
121
+ notify({ type: 'error', title, message })
122
+ }, [notify])
123
+
124
+ const notifyWarning = useCallback((message: string, title = 'Предупреждение') => {
125
+ notify({ type: 'warning', title, message })
126
+ }, [notify])
127
+
128
+ const notifyInfo = useCallback((message: string, title = 'Информация') => {
129
+ notify({ type: 'info', title, message })
130
+ }, [notify])
131
+
132
+ // Remove notification
133
+ const removeNotification = useCallback((id: string) => {
134
+ setNotifications(prev => prev.filter(n => n.id !== id))
135
+ }, [])
136
+
137
+ // Handle incoming notifications from other MFE
138
+ useEffect(() => {
139
+ if (typeof window === 'undefined') return
140
+
141
+ const handleNotification = (event: Event) => {
142
+ const detail = (event as CustomEvent).detail as NotificationPayload | undefined
143
+ if (detail && detail.type && detail.title && detail.message) {
144
+ const notification: NotificationItem = {
145
+ ...detail,
146
+ id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
147
+ }
148
+ addNotification(notification)
149
+ }
150
+ }
151
+
152
+ window.addEventListener('mfe-notification', handleNotification)
153
+
154
+ return () => {
155
+ window.removeEventListener('mfe-notification', handleNotification)
156
+ }
157
+ }, [addNotification])
158
+
159
+ return (
160
+ <NotificationContext.Provider value={{
161
+ notify,
162
+ notifySuccess,
163
+ notifyError,
164
+ notifyWarning,
165
+ notifyInfo,
166
+ removeNotification,
167
+ }}>
168
+ {children}
169
+
170
+ {/* Render notifications */}
171
+ {notifications.length > 0 && (
172
+ <div
173
+ style={{
174
+ position: 'fixed',
175
+ top: '80px',
176
+ right: '20px',
177
+ zIndex: 9998,
178
+ maxWidth: '400px',
179
+ width: '100%',
180
+ display: 'flex',
181
+ flexDirection: 'column',
182
+ gap: '8px',
183
+ pointerEvents: 'none',
184
+ }}
185
+ >
186
+ {notifications.map(notification => (
187
+ <div
188
+ key={notification.id}
189
+ className={`notification notification-${notification.type}`}
190
+ style={{
191
+ pointerEvents: 'auto',
192
+ padding: '12px 16px',
193
+ borderRadius: '8px',
194
+ background: notification.type === 'success' ? '#d4edda' :
195
+ notification.type === 'error' ? '#f8d7da' :
196
+ notification.type === 'warning' ? '#fff3cd' : '#d1ecf1',
197
+ color: notification.type === 'success' ? '#155724' :
198
+ notification.type === 'error' ? '#721c24' :
199
+ notification.type === 'warning' ? '#856404' : '#0c5460',
200
+ boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
201
+ display: 'flex',
202
+ alignItems: 'flex-start',
203
+ gap: '12px',
204
+ }}
205
+ >
206
+ <div style={{ flex: 1 }}>
207
+ <strong style={{ display: 'block', marginBottom: '4px' }}>
208
+ {notification.title}
209
+ </strong>
210
+ <p style={{ margin: 0, fontSize: '14px' }}>
211
+ {notification.message}
212
+ </p>
213
+ <small style={{ opacity: 0.7, fontSize: '12px' }}>
214
+ {notification.mfeName}
215
+ </small>
216
+ </div>
217
+ <button
218
+ onClick={() => removeNotification(notification.id)}
219
+ style={{
220
+ background: 'transparent',
221
+ border: 'none',
222
+ cursor: 'pointer',
223
+ fontSize: '18px',
224
+ lineHeight: 1,
225
+ opacity: 0.5,
226
+ }}
227
+ >
228
+ ×
229
+ </button>
230
+ </div>
231
+ ))}
232
+ </div>
233
+ )}
234
+ </NotificationContext.Provider>
235
+ )
236
+ }
237
+
238
+ /**
239
+ * Хук для использования уведомлений
240
+ *
241
+ * @returns NotificationContextValue
242
+ *
243
+ * @example
244
+ * ```tsx
245
+ * const { notifySuccess, notifyError } = useNotification()
246
+ *
247
+ * const handleSave = async () => {
248
+ * try {
249
+ * await saveData()
250
+ * notifySuccess('Данные сохранены')
251
+ * } catch (error) {
252
+ * notifyError('Ошибка сохранения')
253
+ * }
254
+ * }
255
+ * ```
256
+ */
257
+ export function useNotification(): NotificationContextValue {
258
+ const context = useContext(NotificationContext)
259
+
260
+ if (!context) {
261
+ logger.warn('useNotification called outside NotificationProvider')
262
+
263
+ // Return no-op functions
264
+ return {
265
+ notify: () => {},
266
+ notifySuccess: () => {},
267
+ notifyError: () => {},
268
+ notifyWarning: () => {},
269
+ notifyInfo: () => {},
270
+ removeNotification: () => {},
271
+ }
272
+ }
273
+
274
+ return context
275
+ }
276
+
277
+ /**
278
+ * Dispatch notification to shell (standalone function)
279
+ */
280
+ export function dispatchNotification(payload: Omit<NotificationPayload, 'timestamp' | 'mfeName'>): void {
281
+ if (typeof window === 'undefined') return
282
+
283
+ const mfeName = ((window as unknown) as { __MF_NAME__?: string }).__MF_NAME__ || MF_NAME
284
+
285
+ const fullPayload: NotificationPayload = {
286
+ ...payload,
287
+ mfeName,
288
+ timestamp: Date.now(),
289
+ }
290
+
291
+ window.dispatchEvent(new CustomEvent('mfe-notification', {
292
+ detail: fullPayload,
293
+ bubbles: true,
294
+ }))
295
+
296
+ logger.log('Dispatched notification:', fullPayload)
297
+ }