1
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 , AuditOutlined } from '@ant-design/icons' ;
2
+ import { Card , Button , Divider , Alert , message , Table , Tag , Input , Space , Tooltip , Row , Col , Avatar } from 'antd' ;
3
+ import { SyncOutlined , AuditOutlined , TeamOutlined , CheckCircleFilled , CloudServerOutlined , DisconnectOutlined } from '@ant-design/icons' ;
4
4
import Title from 'antd/lib/typography/Title' ;
5
5
import { Environment } from '../types/environment.types' ;
6
6
import { Workspace } from '../types/workspace.types' ;
@@ -68,9 +68,6 @@ const WorkspacesTab: React.FC<WorkspacesTabProps> = ({ environment }) => {
68
68
fetchWorkspaces ( ) ;
69
69
} ;
70
70
71
- // Toggle managed status
72
-
73
-
74
71
// Handle row click for navigation
75
72
const handleRowClick = ( workspace : Workspace ) => {
76
73
history . push ( `/setting/environments/${ environment . environmentId } /workspaces/${ workspace . id } ` ) ;
@@ -83,19 +80,72 @@ const WorkspacesTab: React.FC<WorkspacesTabProps> = ({ environment }) => {
83
80
workspace . id . toLowerCase ( ) . includes ( searchText . toLowerCase ( ) ) )
84
81
: workspaces ;
85
82
83
+ // Helper function to generate colors from strings
84
+ const stringToColor = ( str : string ) => {
85
+ let hash = 0 ;
86
+ for ( let i = 0 ; i < str . length ; i ++ ) {
87
+ hash = str . charCodeAt ( i ) + ( ( hash << 5 ) - hash ) ;
88
+ }
89
+
90
+ const hue = Math . abs ( hash % 360 ) ;
91
+ return `hsl(${ hue } , 70%, 50%)` ;
92
+ } ;
93
+
94
+ // Stat card component
95
+ const StatCard = ( { title, value, icon } : { title : string ; value : number ; icon : React . ReactNode } ) => (
96
+ < Card
97
+ style = { {
98
+ height : '100%' ,
99
+ borderRadius : '8px' ,
100
+ boxShadow : '0 2px 8px rgba(0,0,0,0.05)'
101
+ } }
102
+ >
103
+ < div style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' } } >
104
+ < div >
105
+ < div style = { { fontSize : '14px' , color : '#8c8c8c' , marginBottom : '8px' } } > { title } </ div >
106
+ < div style = { { fontSize : '24px' , fontWeight : 600 } } > { value } </ div >
107
+ </ div >
108
+ < div style = { {
109
+ fontSize : '28px' ,
110
+ opacity : 0.8 ,
111
+ color : '#52c41a' ,
112
+ padding : '12px' ,
113
+ backgroundColor : 'rgba(82, 196, 26, 0.1)' ,
114
+ borderRadius : '50%' ,
115
+ display : 'flex' ,
116
+ alignItems : 'center' ,
117
+ justifyContent : 'center'
118
+ } } >
119
+ { icon }
120
+ </ div >
121
+ </ div >
122
+ </ Card >
123
+ ) ;
124
+
86
125
// Table columns
87
126
const columns = [
88
127
{
89
- title : 'Name' ,
90
- dataIndex : 'name' ,
91
- key : 'name' ,
92
- render : ( text : string ) => < span className = "workspace-name" > { text } </ span >
93
- } ,
94
- {
95
- title : 'ID' ,
96
- dataIndex : 'id' ,
97
- key : 'id' ,
98
- ellipsis : true ,
128
+ title : 'Workspace' ,
129
+ key : 'workspace' ,
130
+ render : ( workspace : Workspace ) => (
131
+ < div style = { { display : 'flex' , alignItems : 'center' } } >
132
+ < Avatar
133
+ style = { {
134
+ backgroundColor : stringToColor ( workspace . name ) ,
135
+ marginRight : 12
136
+ } }
137
+ shape = "square"
138
+ >
139
+ { workspace . name . charAt ( 0 ) . toUpperCase ( ) }
140
+ </ Avatar >
141
+ < div >
142
+ < div style = { { fontWeight : 500 } } > { workspace . name } </ div >
143
+ < div style = { { fontSize : 12 , color : '#8c8c8c' , marginTop : 4 } } >
144
+ { workspace . id }
145
+ </ div >
146
+ </ div >
147
+ </ div >
148
+ ) ,
99
149
} ,
100
150
{
101
151
title : 'Role' ,
@@ -107,7 +157,8 @@ const WorkspacesTab: React.FC<WorkspacesTabProps> = ({ environment }) => {
107
157
dataIndex : 'status' ,
108
158
key : 'status' ,
109
159
render : ( status : string ) => (
110
- < Tag color = { status === 'ACTIVE' ? 'green' : 'red' } >
160
+ < Tag color = { status === 'ACTIVE' ? 'green' : 'red' } style = { { borderRadius : '12px' } } >
161
+ { status === 'ACTIVE' ? < CheckCircleFilled style = { { marginRight : 4 } } /> : null }
111
162
{ status }
112
163
</ Tag >
113
164
) ,
@@ -116,7 +167,14 @@ const WorkspacesTab: React.FC<WorkspacesTabProps> = ({ environment }) => {
116
167
title : 'Managed' ,
117
168
key : 'managed' ,
118
169
render : ( _ : any , workspace : Workspace ) => (
119
- < Tag color = { workspace . managed ? 'blue' : 'default' } >
170
+ < Tag
171
+ color = { workspace . managed ? 'processing' : 'default' }
172
+ style = { { borderRadius : '12px' } }
173
+ >
174
+ { workspace . managed
175
+ ? < CloudServerOutlined style = { { marginRight : 4 } } />
176
+ : < DisconnectOutlined style = { { marginRight : 4 } } />
177
+ }
120
178
{ workspace . managed ? 'Managed' : 'Unmanaged' }
121
179
</ Tag >
122
180
) ,
@@ -129,7 +187,6 @@ const WorkspacesTab: React.FC<WorkspacesTabProps> = ({ environment }) => {
129
187
< Tooltip title = "View Audit Logs" >
130
188
< Button
131
189
icon = { < AuditOutlined /> }
132
- size = "small"
133
190
onClick = { ( e ) => {
134
191
e . stopPropagation ( ) ;
135
192
const auditUrl = `/setting/audit?environmentId=${ environment . environmentId } &orgId=${ workspace . id } &pageSize=100&pageNum=1` ;
@@ -145,45 +202,43 @@ const WorkspacesTab: React.FC<WorkspacesTabProps> = ({ environment }) => {
145
202
] ;
146
203
147
204
return (
148
- < Card >
149
- { /* Header with refresh button */ }
150
- < div style = { { display : "flex" , justifyContent : "space-between" , alignItems : "center" , marginBottom : "16px" } } >
151
- < Title level = { 5 } > Workspaces in this Environment</ Title >
205
+ < div style = { { padding : '16px 0' } } >
206
+ { /* Header */ }
207
+ < div style = { {
208
+ display : "flex" ,
209
+ justifyContent : "space-between" ,
210
+ alignItems : "center" ,
211
+ marginBottom : "24px" ,
212
+ background : 'linear-gradient(135deg, #52c41a 0%, #13c2c2 100%)' ,
213
+ padding : '20px 24px' ,
214
+ borderRadius : '8px' ,
215
+ color : 'white'
216
+ } } >
217
+ < div >
218
+ < Title level = { 4 } style = { { color : 'white' , margin : 0 } } >
219
+ < TeamOutlined style = { { marginRight : 10 } } /> Workspaces
220
+ </ Title >
221
+ < p style = { { marginBottom : 0 } } > Manage workspaces in this environment</ p >
222
+ </ div >
152
223
< Button
153
224
icon = { < SyncOutlined spin = { refreshing } /> }
154
225
onClick = { handleRefresh }
155
226
loading = { loading }
227
+ type = "primary"
228
+ ghost
156
229
>
157
230
Refresh
158
231
</ Button >
159
232
</ div >
160
233
161
- { /* Stats display */ }
162
- < div style = { { display : 'flex' , flexWrap : 'wrap' , gap : '24px' , marginBottom : '16px' } } >
163
- < div >
164
- < div style = { { fontSize : '14px' , color : '#8c8c8c' } } > Total Workspaces</ div >
165
- < div style = { { fontSize : '24px' , fontWeight : 600 } } > { stats . total } </ div >
166
- </ div >
167
- < div >
168
- < div style = { { fontSize : '14px' , color : '#8c8c8c' } } > Managed</ div >
169
- < div style = { { fontSize : '24px' , fontWeight : 600 } } > { stats . managed } </ div >
170
- </ div >
171
- < div >
172
- < div style = { { fontSize : '14px' , color : '#8c8c8c' } } > Unmanaged</ div >
173
- < div style = { { fontSize : '24px' , fontWeight : 600 } } > { stats . unmanaged } </ div >
174
- </ div >
175
- </ div >
176
-
177
- < Divider style = { { margin : "16px 0" } } />
178
-
179
234
{ /* Error display */ }
180
235
{ error && (
181
236
< Alert
182
237
message = "Error loading workspaces"
183
238
description = { error }
184
239
type = "error"
185
240
showIcon
186
- style = { { marginBottom : "16px " } }
241
+ style = { { marginBottom : "20px " } }
187
242
/>
188
243
) }
189
244
@@ -194,53 +249,92 @@ const WorkspacesTab: React.FC<WorkspacesTabProps> = ({ environment }) => {
194
249
description = "Missing required configuration: API key or API service URL"
195
250
type = "warning"
196
251
showIcon
197
- style = { { marginBottom : "16px " } }
252
+ style = { { marginBottom : "20px " } }
198
253
/>
199
254
) }
200
255
256
+ { /* Stats display */ }
257
+ < Row gutter = { [ 16 , 16 ] } style = { { marginBottom : '24px' } } >
258
+ < Col xs = { 24 } sm = { 8 } >
259
+ < StatCard
260
+ title = "Total Workspaces"
261
+ value = { stats . total }
262
+ icon = { < TeamOutlined /> }
263
+ />
264
+ </ Col >
265
+ < Col xs = { 24 } sm = { 8 } >
266
+ < StatCard
267
+ title = "Managed Workspaces"
268
+ value = { stats . managed }
269
+ icon = { < CloudServerOutlined /> }
270
+ />
271
+ </ Col >
272
+ < Col xs = { 24 } sm = { 8 } >
273
+ < StatCard
274
+ title = "Unmanaged Workspaces"
275
+ value = { stats . unmanaged }
276
+ icon = { < DisconnectOutlined /> }
277
+ />
278
+ </ Col >
279
+ </ Row >
280
+
201
281
{ /* Content */ }
202
- { loading ? (
203
- < div style = { { display : 'flex' , justifyContent : 'center' , padding : '20px' } } >
204
- < Spin tip = "Loading workspaces..." />
205
- </ div >
206
- ) : workspaces . length === 0 ? (
207
- < Empty
208
- description = { error || "No workspaces found in this environment" }
209
- image = { Empty . PRESENTED_IMAGE_SIMPLE }
210
- />
211
- ) : (
212
- < >
213
- { /* Search Bar */ }
214
- < div style = { { marginBottom : 16 } } >
215
- < Search
216
- placeholder = "Search workspaces by name or ID"
217
- allowClear
218
- onSearch = { value => setSearchText ( value ) }
219
- onChange = { e => setSearchText ( e . target . value ) }
220
- style = { { width : 300 } }
221
- />
222
- { searchText && filteredWorkspaces . length !== workspaces . length && (
223
- < div style = { { marginTop : 8 } } >
224
- Showing { filteredWorkspaces . length } of { workspaces . length } workspaces
225
- </ div >
226
- ) }
282
+ < Card
283
+ style = { {
284
+ borderRadius : '8px' ,
285
+ boxShadow : '0 2px 8px rgba(0,0,0,0.05)'
286
+ } }
287
+ >
288
+ { loading ? (
289
+ < div style = { { display : 'flex' , justifyContent : 'center' , padding : '40px' } } >
290
+ < Spin size = "large" tip = "Loading workspaces..." />
227
291
</ div >
228
-
229
- < Table
230
- columns = { columns }
231
- dataSource = { filteredWorkspaces }
232
- rowKey = "id"
233
- pagination = { { pageSize : 10 } }
234
- size = "middle"
235
- scroll = { { x : 'max-content' } }
236
- onRow = { ( record ) => ( {
237
- onClick : ( ) => handleRowClick ( record ) ,
238
- style : { cursor : 'pointer' }
239
- } ) }
292
+ ) : workspaces . length === 0 ? (
293
+ < Empty
294
+ description = { error || "No workspaces found in this environment" }
295
+ image = { Empty . PRESENTED_IMAGE_SIMPLE }
240
296
/>
241
- </ >
242
- ) }
243
- </ Card >
297
+ ) : (
298
+ < >
299
+ { /* Search Bar */ }
300
+ < div style = { { marginBottom : 20 } } >
301
+ < Search
302
+ placeholder = "Search workspaces by name or ID"
303
+ allowClear
304
+ onSearch = { value => setSearchText ( value ) }
305
+ onChange = { e => setSearchText ( e . target . value ) }
306
+ style = { { width : 300 } }
307
+ size = "large"
308
+ />
309
+ { searchText && filteredWorkspaces . length !== workspaces . length && (
310
+ < div style = { { marginTop : 8 , color : '#8c8c8c' } } >
311
+ Showing { filteredWorkspaces . length } of { workspaces . length } workspaces
312
+ </ div >
313
+ ) }
314
+ </ div >
315
+
316
+ < Table
317
+ columns = { columns }
318
+ dataSource = { filteredWorkspaces }
319
+ rowKey = "id"
320
+ pagination = { {
321
+ pageSize : 10 ,
322
+ showTotal : ( total , range ) => `${ range [ 0 ] } -${ range [ 1 ] } of ${ total } workspaces`
323
+ } }
324
+ style = { {
325
+ borderRadius : '8px' ,
326
+ overflow : 'hidden'
327
+ } }
328
+ onRow = { ( record ) => ( {
329
+ onClick : ( ) => handleRowClick ( record ) ,
330
+ style : { cursor : 'pointer' }
331
+ } ) }
332
+ rowClassName = { ( ) => 'workspace-row' }
333
+ />
334
+ </ >
335
+ ) }
336
+ </ Card >
337
+ </ div >
244
338
) ;
245
339
} ;
246
340
0 commit comments