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