22 @license
33 Copyright (c) 2015-2026 Lablup Inc. All rights reserved.
44 */
5+ import type { DeploymentListDeleteMutation } from '../__generated__/DeploymentListDeleteMutation.graphql' ;
56import {
67 DeploymentList_modelDeploymentConnection$data ,
78 DeploymentList_modelDeploymentConnection$key ,
@@ -10,22 +11,28 @@ import { useSuspendedBackendaiClient } from '../hooks';
1011import BAIRadioGroup from './BAIRadioGroup' ;
1112import DeploymentOwnerInfo from './DeploymentOwnerInfo' ;
1213import DeploymentStatusTag , { DeploymentStatus } from './DeploymentStatusTag' ;
13- import { Tag , Typography } from 'antd' ;
14+ import { DeleteOutlined , EditOutlined } from '@ant-design/icons' ;
15+ import { Alert , App , Tag , Typography , theme } from 'antd' ;
1416import {
17+ BAIConfirmModalWithInput ,
1518 BAIFlex ,
1619 BAIGraphQLPropertyFilter ,
20+ BAINameActionCell ,
1721 BAITable ,
1822 filterOutEmpty ,
1923 filterOutNullAndUndefined ,
24+ toLocalId ,
25+ useBAILogger ,
2026 type BAIColumnType ,
27+ type BAINameActionCellAction ,
2128 type BAITableProps ,
2229 type GraphQLFilter ,
2330} from 'backend.ai-ui' ;
2431import dayjs from 'dayjs' ;
2532import * as _ from 'lodash-es' ;
26- import React from 'react' ;
33+ import React , { useState } from 'react' ;
2734import { useTranslation } from 'react-i18next' ;
28- import { graphql , useFragment } from 'react-relay' ;
35+ import { graphql , useFragment , useMutation } from 'react-relay' ;
2936
3037type DeploymentEdge = NonNullable <
3138 NonNullable < DeploymentList_modelDeploymentConnection$data [ 'edges' ] > [ number ]
@@ -116,6 +123,10 @@ export interface DeploymentListProps extends Omit<
116123 mode : 'user' | 'admin' ;
117124 /** Called when a row name is clicked. Receives the deployment global ID. */
118125 onRowClick ?: ( deploymentId : string ) => void ;
126+ /** Called when the edit action button is clicked. Receives the deployment global ID. */
127+ onEditClick ?: ( deploymentId : string ) => void ;
128+ /** Called after a deployment is successfully deleted. Use to refresh the list. */
129+ onDeleteComplete ?: ( ) => void ;
119130 /** Extra elements rendered at the end of the toolbar row (e.g. refresh + create buttons). */
120131 toolbarEnd ?: React . ReactNode ;
121132}
@@ -129,12 +140,30 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
129140 onStatusCategoryChange,
130141 mode,
131142 onRowClick,
143+ onEditClick,
144+ onDeleteComplete,
132145 toolbarEnd,
133146 ...tableProps
134147} ) => {
135148 'use memo' ;
136149 const { t } = useTranslation ( ) ;
150+ const { message } = App . useApp ( ) ;
151+ const { token } = theme . useToken ( ) ;
152+ const { logger } = useBAILogger ( ) ;
137153 const baiClient = useSuspendedBackendaiClient ( ) ;
154+ const [ deletingDeployment , setDeletingDeployment ] = useState < {
155+ id : string ;
156+ name : string ;
157+ } | null > ( null ) ;
158+
159+ const [ commitDeleteMutation , isInFlightDeleteMutation ] =
160+ useMutation < DeploymentListDeleteMutation > ( graphql `
161+ mutation DeploymentListDeleteMutation($input: DeleteDeploymentInput!) {
162+ deleteModelDeployment(input: $input) {
163+ id
164+ }
165+ }
166+ ` ) ;
138167
139168 const connection = useFragment (
140169 graphql `
@@ -246,16 +275,34 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
246275 fixed : 'left' as const ,
247276 render : ( _text , row ) => {
248277 const name = row . metadata ?. name ?? '-' ;
249- if ( ! onRowClick ) {
250- return < Typography . Text > { name } </ Typography . Text > ;
278+ const isDestroying = [ 'STOPPING' , 'STOPPED' , 'TERMINATED' ] . includes (
279+ row . metadata ?. status ?? '' ,
280+ ) ;
281+ const actions : BAINameActionCellAction [ ] = [ ] ;
282+ if ( onEditClick ) {
283+ actions . push ( {
284+ key : 'edit' ,
285+ title : t ( 'deployment.EditDeployment' ) ,
286+ icon : < EditOutlined /> ,
287+ disabled : isDestroying ,
288+ onClick : ( ) => onEditClick ( row . id ) ,
289+ } ) ;
251290 }
291+ actions . push ( {
292+ key : 'delete' ,
293+ title : t ( 'deployment.DeleteDeployment' ) ,
294+ icon : < DeleteOutlined /> ,
295+ type : 'danger' ,
296+ disabled : isDestroying ,
297+ onClick : ( ) => setDeletingDeployment ( { id : row . id , name } ) ,
298+ } ) ;
252299 return (
253- < Typography . Link
254- onClick = { ( ) => onRowClick ( row . id ) }
255- style = { { maxWidth : 240 } }
256- >
257- { name }
258- </ Typography . Link >
300+ < BAINameActionCell
301+ title = { name }
302+ onTitleClick = { onRowClick ? ( ) => onRowClick ( row . id ) : undefined }
303+ actions = { actions }
304+ showActions = "always"
305+ / >
259306 ) ;
260307 } ,
261308 } ,
@@ -370,42 +417,91 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
370417 : { ...tableProps . pagination , total : totalCount } ;
371418
372419 return (
373- < BAIFlex direction = "column" align = "stretch" gap = "sm" >
374- < BAIFlex justify = "between" wrap = "wrap" gap = "sm" >
375- < BAIFlex gap = "sm" align = "start" wrap = "wrap" style = { { flexShrink : 1 } } >
376- < BAIRadioGroup
377- value = { statusCategory }
378- onChange = { ( e ) => onStatusCategoryChange ?.( e . target . value ) }
379- options = { [
380- { label : t ( 'deployment.Running' ) , value : 'running' } ,
381- { label : t ( 'deployment.status.Terminated' ) , value : 'finished' } ,
382- ] }
383- />
384- < BAIGraphQLPropertyFilter
385- filterProperties = { filterProperties }
386- value = { filterValue }
387- onChange = { ( next ) => {
388- setFilter ( stringifyFilter ( next ) ) ;
420+ < >
421+ < BAIFlex direction = "column" align = "stretch" gap = "sm" >
422+ < BAIFlex justify = "between" wrap = "wrap" gap = "sm" >
423+ < BAIFlex gap = "sm" align = "start" wrap = "wrap" style = { { flexShrink : 1 } } >
424+ < BAIRadioGroup
425+ value = { statusCategory }
426+ onChange = { ( e ) => onStatusCategoryChange ?.( e . target . value ) }
427+ options = { [
428+ { label : t ( 'deployment.Running' ) , value : 'running' } ,
429+ { label : t ( 'deployment.status.Terminated' ) , value : 'finished' } ,
430+ ] }
431+ />
432+ < BAIGraphQLPropertyFilter
433+ filterProperties = { filterProperties }
434+ value = { filterValue }
435+ onChange = { ( next ) => {
436+ setFilter ( stringifyFilter ( next ) ) ;
437+ } }
438+ />
439+ </ BAIFlex >
440+ { toolbarEnd }
441+ </ BAIFlex >
442+ < div style = { { overflowX : 'auto' } } >
443+ < BAITable < DeploymentNode >
444+ rowKey = "id"
445+ scroll = { { x : 'max-content' } }
446+ showSorterTooltip = { false }
447+ locale = { { emptyText : t ( 'deployment.NoDeployments' ) } }
448+ { ...tableProps }
449+ dataSource = { deployments }
450+ columns = { columns }
451+ onChangeOrder = { ( order ) => {
452+ onChangeOrder ?.( order || null ) ;
389453 } }
454+ pagination = { paginationWithTotal }
390455 />
391- </ BAIFlex >
392- { toolbarEnd }
456+ </ div >
393457 </ BAIFlex >
394- < div style = { { overflowX : 'auto' } } >
395- < BAITable < DeploymentNode >
396- rowKey = "id"
397- scroll = { { x : 'max-content' } }
398- showSorterTooltip = { false }
399- { ...tableProps }
400- dataSource = { deployments }
401- columns = { columns }
402- onChangeOrder = { ( order ) => {
403- onChangeOrder ?.( order || null ) ;
404- } }
405- pagination = { paginationWithTotal }
406- />
407- </ div >
408- </ BAIFlex >
458+ < BAIConfirmModalWithInput
459+ open = { ! ! deletingDeployment }
460+ title = { t ( 'deployment.DeleteDeployment' ) }
461+ content = {
462+ < BAIFlex direction = "column" gap = "md" align = "stretch" >
463+ < Alert type = "warning" title = { t ( 'dialog.warning.CannotBeUndone' ) } />
464+ < BAIFlex >
465+ < Typography . Text style = { { marginRight : token . marginXXS } } >
466+ { t ( 'dialog.TypeNameToConfirmDeletion' ) }
467+ </ Typography . Text >
468+ (
469+ < Typography . Text code > { deletingDeployment ?. name } </ Typography . Text >
470+ )
471+ </ BAIFlex >
472+ </ BAIFlex >
473+ }
474+ confirmText = { deletingDeployment ?. name ?? '' }
475+ inputProps = { { placeholder : deletingDeployment ?. name ?? '' } }
476+ okText = { t ( 'button.Delete' ) }
477+ okButtonProps = { { loading : isInFlightDeleteMutation } }
478+ onOk = { ( ) => {
479+ if ( ! deletingDeployment ) return ;
480+ commitDeleteMutation ( {
481+ variables : {
482+ input : {
483+ id : toLocalId ( deletingDeployment . id ) ?? deletingDeployment . id ,
484+ } ,
485+ } ,
486+ onCompleted : ( _response , errors ) => {
487+ if ( errors && errors . length > 0 ) {
488+ logger . error ( 'Failed to delete deployment' , errors ) ;
489+ message . error ( t ( 'deployment.FailedToDeleteDeployment' ) ) ;
490+ return ;
491+ }
492+ message . success ( t ( 'deployment.DeploymentDeleted' ) ) ;
493+ setDeletingDeployment ( null ) ;
494+ onDeleteComplete ?.( ) ;
495+ } ,
496+ onError : ( error ) => {
497+ logger . error ( 'Failed to delete deployment' , error ) ;
498+ message . error ( t ( 'deployment.FailedToDeleteDeployment' ) ) ;
499+ } ,
500+ } ) ;
501+ } }
502+ onCancel = { ( ) => setDeletingDeployment ( null ) }
503+ />
504+ </ >
409505 ) ;
410506} ;
411507
0 commit comments