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