1
+ import React , { useState , useEffect } from 'react' ;
2
+ import { Card , Button , Divider , Alert , message , Table , Tag , Input , Space , Tooltip } from 'antd' ;
3
+ import { SyncOutlined , CloudUploadOutlined , SearchOutlined } from '@ant-design/icons' ;
4
+ import Title from 'antd/lib/typography/Title' ;
5
+ import { Environment } from '../types/environment.types' ;
6
+ import { Workspace } from '../types/workspace.types' ;
7
+ import { App , AppStats } from '../types/app.types' ;
8
+ import { getMergedWorkspaceApps } from '../services/apps.service' ;
9
+ import { Switch , Spin , Empty } from 'antd' ;
10
+ import { ManagedObjectType , setManagedObject , unsetManagedObject } from '../services/managed-objects.service' ;
11
+ import { useDeployModal } from '../context/DeployModalContext' ;
12
+ import { appsConfig } from '../config/apps.config' ;
13
+
14
+ const { Search } = Input ;
15
+
16
+ interface AppsTabProps {
17
+ environment : Environment ;
18
+ workspace : Workspace ;
19
+ }
20
+
21
+ const AppsTab : React . FC < AppsTabProps > = ( { environment, workspace } ) => {
22
+ const [ apps , setApps ] = useState < App [ ] > ( [ ] ) ;
23
+ const [ stats , setStats ] = useState < AppStats > ( {
24
+ total : 0 ,
25
+ published : 0 ,
26
+ managed : 0 ,
27
+ unmanaged : 0
28
+ } ) ;
29
+ const [ loading , setLoading ] = useState ( false ) ;
30
+ const [ refreshing , setRefreshing ] = useState ( false ) ;
31
+ const [ error , setError ] = useState < string | null > ( null ) ;
32
+ const [ searchText , setSearchText ] = useState ( '' ) ;
33
+ const { openDeployModal } = useDeployModal ( ) ;
34
+
35
+ // Fetch apps
36
+ const fetchApps = async ( ) => {
37
+ if ( ! workspace . id || ! environment ) return ;
38
+
39
+ setLoading ( true ) ;
40
+ setError ( null ) ;
41
+
42
+ try {
43
+ const result = await getMergedWorkspaceApps (
44
+ workspace . id ,
45
+ environment . environmentId ,
46
+ environment . environmentApikey ,
47
+ environment . environmentApiServiceUrl !
48
+ ) ;
49
+
50
+ setApps ( result . apps ) ;
51
+
52
+ // Calculate stats
53
+ const total = result . apps . length ;
54
+ const published = result . apps . filter ( app => app . published ) . length ;
55
+ const managed = result . apps . filter ( app => app . managed ) . length ;
56
+
57
+ setStats ( {
58
+ total,
59
+ published,
60
+ managed,
61
+ unmanaged : total - managed
62
+ } ) ;
63
+ } catch ( err ) {
64
+ setError ( err instanceof Error ? err . message : "Failed to fetch apps" ) ;
65
+ } finally {
66
+ setLoading ( false ) ;
67
+ setRefreshing ( false ) ;
68
+ }
69
+ } ;
70
+
71
+ useEffect ( ( ) => {
72
+ fetchApps ( ) ;
73
+ } , [ environment , workspace ] ) ;
74
+
75
+ // Handle refresh
76
+ const handleRefresh = ( ) => {
77
+ setRefreshing ( true ) ;
78
+ fetchApps ( ) ;
79
+ } ;
80
+
81
+ // Toggle managed status
82
+ const handleToggleManaged = async ( app : App , checked : boolean ) => {
83
+ setRefreshing ( true ) ;
84
+ try {
85
+ if ( checked ) {
86
+ await setManagedObject (
87
+ app . applicationGid ,
88
+ environment . environmentId ,
89
+ ManagedObjectType . APP ,
90
+ app . name
91
+ ) ;
92
+ } else {
93
+ await unsetManagedObject (
94
+ app . applicationGid ,
95
+ environment . environmentId ,
96
+ ManagedObjectType . APP
97
+ ) ;
98
+ }
99
+
100
+ // Update the app in state
101
+ const updatedApps = apps . map ( item => {
102
+ if ( item . applicationId === app . applicationId ) {
103
+ return { ...item , managed : checked } ;
104
+ }
105
+ return item ;
106
+ } ) ;
107
+
108
+ setApps ( updatedApps ) ;
109
+
110
+ // Update stats
111
+ const managed = updatedApps . filter ( app => app . managed ) . length ;
112
+ setStats ( prev => ( {
113
+ ...prev ,
114
+ managed,
115
+ unmanaged : prev . total - managed
116
+ } ) ) ;
117
+
118
+ message . success ( `${ app . name } is now ${ checked ? 'Managed' : 'Unmanaged' } ` ) ;
119
+ return true ;
120
+ } catch ( error ) {
121
+ message . error ( `Failed to change managed status for ${ app . name } ` ) ;
122
+ return false ;
123
+ } finally {
124
+ setRefreshing ( false ) ;
125
+ }
126
+ } ;
127
+
128
+ // Filter apps based on search
129
+ const filteredApps = searchText
130
+ ? apps . filter ( app =>
131
+ app . name . toLowerCase ( ) . includes ( searchText . toLowerCase ( ) ) ||
132
+ app . applicationId . toLowerCase ( ) . includes ( searchText . toLowerCase ( ) ) )
133
+ : apps ;
134
+
135
+ // Table columns
136
+ const columns = [
137
+ {
138
+ title : 'Name' ,
139
+ dataIndex : 'name' ,
140
+ key : 'name' ,
141
+ render : ( text : string ) => < span className = "app-name" > { text } </ span >
142
+ } ,
143
+ {
144
+ title : 'ID' ,
145
+ dataIndex : 'applicationId' ,
146
+ key : 'applicationId' ,
147
+ ellipsis : true ,
148
+ } ,
149
+ {
150
+ title : 'Published' ,
151
+ dataIndex : 'published' ,
152
+ key : 'published' ,
153
+ render : ( published : boolean ) => (
154
+ < Tag color = { published ? 'green' : 'default' } >
155
+ { published ? 'Published' : 'Draft' }
156
+ </ Tag >
157
+ ) ,
158
+ } ,
159
+ {
160
+ title : 'Managed' ,
161
+ key : 'managed' ,
162
+ render : ( _ : any , app : App ) => (
163
+ < Switch
164
+ checked = { ! ! app . managed }
165
+ onChange = { ( checked : boolean ) => handleToggleManaged ( app , checked ) }
166
+ loading = { refreshing }
167
+ size = "small"
168
+ />
169
+ ) ,
170
+ } ,
171
+ {
172
+ title : 'Actions' ,
173
+ key : 'actions' ,
174
+ render : ( _ : any , app : App ) => (
175
+ < Space onClick = { ( e ) => e . stopPropagation ( ) } >
176
+ < Tooltip title = { ! app . managed ? "App must be managed before it can be deployed" : "Deploy this app to another environment" } >
177
+ < Button
178
+ type = "primary"
179
+ size = "small"
180
+ icon = { < CloudUploadOutlined /> }
181
+ onClick = { ( ) => openDeployModal ( app , appsConfig , environment ) }
182
+ disabled = { ! app . managed }
183
+ >
184
+ Deploy
185
+ </ Button >
186
+ </ Tooltip >
187
+ </ Space >
188
+ ) ,
189
+ }
190
+ ] ;
191
+
192
+ return (
193
+ < Card >
194
+ { /* Header with refresh button */ }
195
+ < div style = { { display : "flex" , justifyContent : "space-between" , alignItems : "center" , marginBottom : "16px" } } >
196
+ < Title level = { 5 } > Apps in this Workspace</ Title >
197
+ < Button
198
+ icon = { < SyncOutlined spin = { refreshing } /> }
199
+ onClick = { handleRefresh }
200
+ loading = { loading }
201
+ >
202
+ Refresh
203
+ </ Button >
204
+ </ div >
205
+
206
+ { /* Stats display */ }
207
+ < div style = { { display : 'flex' , flexWrap : 'wrap' , gap : '24px' , marginBottom : '16px' } } >
208
+ < div >
209
+ < div style = { { fontSize : '14px' , color : '#8c8c8c' } } > Total Apps</ div >
210
+ < div style = { { fontSize : '24px' , fontWeight : 600 } } > { stats . total } </ div >
211
+ </ div >
212
+ < div >
213
+ < div style = { { fontSize : '14px' , color : '#8c8c8c' } } > Published Apps</ div >
214
+ < div style = { { fontSize : '24px' , fontWeight : 600 } } > { stats . published } </ div >
215
+ </ div >
216
+ < div >
217
+ < div style = { { fontSize : '14px' , color : '#8c8c8c' } } > Managed Apps</ div >
218
+ < div style = { { fontSize : '24px' , fontWeight : 600 } } > { stats . managed } </ div >
219
+ </ div >
220
+ < div >
221
+ < div style = { { fontSize : '14px' , color : '#8c8c8c' } } > Unmanaged Apps</ div >
222
+ < div style = { { fontSize : '24px' , fontWeight : 600 } } > { stats . unmanaged } </ div >
223
+ </ div >
224
+ </ div >
225
+
226
+ < Divider style = { { margin : "16px 0" } } />
227
+
228
+ { /* Error display */ }
229
+ { error && (
230
+ < Alert
231
+ message = "Error loading apps"
232
+ description = { error }
233
+ type = "error"
234
+ showIcon
235
+ style = { { marginBottom : "16px" } }
236
+ />
237
+ ) }
238
+
239
+ { /* Configuration warnings */ }
240
+ { ( ! environment . environmentApikey || ! environment . environmentApiServiceUrl ) && ! error && (
241
+ < Alert
242
+ message = "Configuration Issue"
243
+ description = "Missing required configuration: API key or API service URL"
244
+ type = "warning"
245
+ showIcon
246
+ style = { { marginBottom : "16px" } }
247
+ />
248
+ ) }
249
+
250
+ { /* Content */ }
251
+ { loading ? (
252
+ < div style = { { display : 'flex' , justifyContent : 'center' , padding : '20px' } } >
253
+ < Spin tip = "Loading apps..." />
254
+ </ div >
255
+ ) : apps . length === 0 ? (
256
+ < Empty
257
+ description = { error || "No apps found in this workspace" }
258
+ image = { Empty . PRESENTED_IMAGE_SIMPLE }
259
+ />
260
+ ) : (
261
+ < >
262
+ { /* Search Bar */ }
263
+ < div style = { { marginBottom : 16 } } >
264
+ < Search
265
+ placeholder = "Search apps by name or ID"
266
+ allowClear
267
+ onSearch = { value => setSearchText ( value ) }
268
+ onChange = { e => setSearchText ( e . target . value ) }
269
+ style = { { width : 300 } }
270
+ />
271
+ { searchText && filteredApps . length !== apps . length && (
272
+ < div style = { { marginTop : 8 } } >
273
+ Showing { filteredApps . length } of { apps . length } apps
274
+ </ div >
275
+ ) }
276
+ </ div >
277
+
278
+ < Table
279
+ columns = { columns }
280
+ dataSource = { filteredApps }
281
+ rowKey = "applicationId"
282
+ pagination = { { pageSize : 10 } }
283
+ size = "middle"
284
+ scroll = { { x : 'max-content' } }
285
+ />
286
+ </ >
287
+ ) }
288
+ </ Card >
289
+ ) ;
290
+ } ;
291
+
292
+ export default AppsTab ;
0 commit comments