@@ -7,16 +7,18 @@ import {
77 DeploymentList_modelDeploymentConnection$key ,
88} from '../__generated__/DeploymentList_modelDeploymentConnection.graphql' ;
99import { useSuspendedBackendaiClient } from '../hooks' ;
10+ import BAIRadioGroup from './BAIRadioGroup' ;
1011import DeploymentOwnerInfo from './DeploymentOwnerInfo' ;
1112import DeploymentStatusTag , { DeploymentStatus } from './DeploymentStatusTag' ;
12- import { Typography , theme } from 'antd' ;
13+ import { Tag , Typography } from 'antd' ;
1314import {
1415 BAIFlex ,
1516 BAIGraphQLPropertyFilter ,
1617 BAITable ,
1718 filterOutEmpty ,
1819 filterOutNullAndUndefined ,
1920 type BAIColumnType ,
21+ type BAITableProps ,
2022 type GraphQLFilter ,
2123} from 'backend.ai-ui' ;
2224import dayjs from 'dayjs' ;
@@ -26,26 +28,13 @@ import { useTranslation } from 'react-i18next';
2628import { graphql , useFragment } from 'react-relay' ;
2729
2830type DeploymentEdge = NonNullable <
29- NonNullable <
30- DeploymentList_modelDeploymentConnection$data [ 'edges' ]
31- > [ number ]
31+ NonNullable < DeploymentList_modelDeploymentConnection$data [ 'edges' ] > [ number ]
3232> ;
3333type DeploymentNode = NonNullable < DeploymentEdge [ 'node' ] > ;
3434
35- /**
36- * Deployment sort direction as emitted by the server-side `DeploymentOrderBy`
37- * input. Parent pages own the URL state and pass the current value through.
38- */
39- export type DeploymentSortOrder = 'ASC' | 'DESC' ;
40-
41- /**
42- * Structured sort value matching the server-side `DeploymentOrderBy` shape.
43- * `field` is one of the `DeploymentOrderField` enum values
44- * (`NAME`, `CREATED_AT`, `DOMAIN`, `PROJECT`, `RESOURCE_GROUP`, `TAG`, ...).
45- */
46- export interface DeploymentSort {
35+ interface DeploymentSort {
4736 field : string ;
48- order : DeploymentSortOrder ;
37+ order : 'ASC' | 'DESC' ;
4938}
5039
5140/** Maps BAITable column keys (camelCase) → server-side enum field. */
@@ -58,17 +47,24 @@ const COLUMN_KEY_TO_FIELD: Record<string, string> = {
5847 tag : 'TAG' ,
5948} ;
6049
61- const FIELD_TO_COLUMN_KEY : Record < string , string > = _ . invert (
62- COLUMN_KEY_TO_FIELD ,
63- ) ;
50+ /** All valid order strings accepted by BAITable for deployments. */
51+ export const availableDeploymentOrderValues = [
52+ 'name' ,
53+ '-name' ,
54+ 'createdAt' ,
55+ '-createdAt' ,
56+ ] as const ;
57+
58+ export type DeploymentOrderValue =
59+ ( typeof availableDeploymentOrderValues ) [ number ] ;
6460
6561/**
66- * BAITable exchanges sort state via a single string (e.g. `'name'`,
67- * `'-createdAt'`). Convert that string to the structured `DeploymentSort`
68- * shape the parent (and server-side `DeploymentOrderBy`) expects, and vice
69- * versa .
62+ * Convert a BAITable order string (e.g. `'-createdAt'`) to the structured
63+ * `DeploymentSort` shape expected by the server `DeploymentOrderBy` input.
64+ * Returns `undefined` for unrecognised keys so callers can safely skip
65+ * building the `orderBy` variable .
7066 */
71- const tableOrderToSort = (
67+ export const tableOrderToSort = (
7268 order : string | null | undefined ,
7369) : DeploymentSort | undefined => {
7470 if ( ! order ) return undefined ;
@@ -79,20 +75,6 @@ const tableOrderToSort = (
7975 return { field, order : descending ? 'DESC' : 'ASC' } ;
8076} ;
8177
82- const sortToTableOrder = (
83- sort : DeploymentSort | undefined ,
84- ) : string | undefined => {
85- if ( ! sort ) return undefined ;
86- const columnKey = FIELD_TO_COLUMN_KEY [ sort . field ] ;
87- if ( ! columnKey ) return undefined ;
88- return sort . order === 'DESC' ? `-${ columnKey } ` : columnKey ;
89- } ;
90-
91- /**
92- * Safely parse the stringified filter prop into a `GraphQLFilter` object.
93- * Invalid JSON and non-object values are treated as "no filter" so the
94- * component degrades gracefully when the URL state is malformed.
95- */
9678const parseFilterString = (
9779 filter : string | undefined ,
9880) : GraphQLFilter | undefined => {
@@ -113,69 +95,45 @@ const stringifyFilter = (filter: GraphQLFilter | undefined): string => {
11395 return JSON . stringify ( filter ) ;
11496} ;
11597
116- export interface DeploymentListProps {
117- /**
118- * Relay fragment reference for a `ModelDeploymentConnection`. The owning
119- * page (e.g. `DeploymentListPage` / `AdminDeploymentListPage`) passes the
120- * connection read from its own query.
121- */
122- deploymentsFrgmt : DeploymentList_modelDeploymentConnection$key ;
98+ export type DeploymentStatusCategory = 'running' | 'finished' ;
12399
124- /**
125- * Current filter value. Expected to be a JSON-serialized
126- * `GraphQLFilter` (as produced by `BAIGraphQLPropertyFilter`). Empty string
127- * or `undefined` means "no filter".
128- *
129- * The parent owns the URL state; this component parses on the way in and
130- * serializes on the way out.
131- */
100+ export interface DeploymentListProps extends Omit <
101+ BAITableProps < DeploymentNode > ,
102+ 'dataSource' | 'columns' | 'onChangeOrder'
103+ > {
104+ deploymentsFrgmt : DeploymentList_modelDeploymentConnection$key ;
132105 filter ?: string ;
133106 setFilter : ( value : string ) => void ;
134-
135- /** Current server-side sort (field + direction). */
136- sort ?: DeploymentSort ;
137- setSort : ( value : DeploymentSort | undefined ) => void ;
138-
139- /** 1-indexed page number. */
140- page : number ;
141- setPage : ( value : number ) => void ;
142-
143- /** Rows per page. */
144- pageSize : number ;
145- setPageSize : ( value : number ) => void ;
146-
107+ onChangeOrder ?: ( order : string | null ) => void ;
108+ statusCategory ?: DeploymentStatusCategory ;
109+ onStatusCategoryChange ?: ( value : DeploymentStatusCategory ) => void ;
147110 /**
148111 * `'user'` — standard user-owned list (myDeployments / projectDeployments).
149112 * `'admin'` — admin list. Shows the Owner column and — when the manager
150113 * supports `model-deployment-extended-filter` (>= 26.4.3) — exposes
151114 * Domain / Project / Resource Group filters.
152115 */
153116 mode : 'user' | 'admin' ;
154-
155- /** Whether the table body should show the loading spinner. */
156- loading ?: boolean ;
157-
158117 /** Called when a row name is clicked. Receives the deployment global ID. */
159118 onRowClick ?: ( deploymentId : string ) => void ;
119+ /** Extra elements rendered at the end of the toolbar row (e.g. refresh + create buttons). */
120+ toolbarEnd ?: React . ReactNode ;
160121}
161122
162123const DeploymentList : React . FC < DeploymentListProps > = ( {
163124 deploymentsFrgmt,
164125 filter,
165126 setFilter,
166- sort,
167- setSort,
168- page,
169- setPage,
170- pageSize,
171- setPageSize,
127+ onChangeOrder,
128+ statusCategory = 'running' ,
129+ onStatusCategoryChange,
172130 mode,
173- loading,
174131 onRowClick,
132+ toolbarEnd,
133+ ...tableProps
175134} ) => {
176135 'use memo' ;
177136 const { t } = useTranslation ( ) ;
178- const { token } = theme . useToken ( ) ;
179137 const baiClient = useSuspendedBackendaiClient ( ) ;
180138
181139 const connection = useFragment (
@@ -191,16 +149,18 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
191149 createdAt
192150 domainName
193151 projectId
152+ tags
153+ }
154+ networkAccess {
155+ endpointUrl
194156 }
195157 replicaState {
196158 desiredReplicaCount
197159 }
198160 totalReplicas: replicas {
199161 count
200162 }
201- runningReplicas: replicas(
202- filter: { status: { equals: RUNNING } }
203- ) {
163+ runningReplicas: replicas(filter: { status: { equals: RUNNING } }) {
204164 count
205165 }
206166 currentRevision @since(version: "26.4.3") {
@@ -281,7 +241,7 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
281241 {
282242 key : 'name' ,
283243 title : t ( 'deployment.Name' ) ,
284- dataIndex : [ 'metadata' , ' name'] ,
244+ dataIndex : ' name',
285245 sorter : true ,
286246 fixed : 'left' as const ,
287247 render : ( _text , row ) => {
@@ -315,9 +275,6 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
315275 const running = row . runningReplicas ?. count ?? 0 ;
316276 const desired = row . replicaState ?. desiredReplicaCount ?? 0 ;
317277 const total = row . totalReplicas ?. count ?? desired ;
318- // Prefer desired count as the denominator so ongoing (scaling)
319- // deployments still surface the intended replica target. Fall back
320- // to the observed total if desired is not reported.
321278 const denominator = desired > 0 ? desired : total ;
322279 return (
323280 < Typography . Text >
@@ -335,7 +292,8 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
335292 render : ( _text , row ) => {
336293 const modelName =
337294 row . currentRevision ?. modelMountConfig ?. vfolder ?. name ?? null ;
338- if ( ! modelName ) return < Typography . Text type = "secondary" > -</ Typography . Text > ;
295+ if ( ! modelName )
296+ return < Typography . Text type = "secondary" > -</ Typography . Text > ;
339297 return (
340298 < Typography . Text
341299 ellipsis = { { tooltip : modelName } }
@@ -346,56 +304,107 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
346304 ) ;
347305 } ,
348306 } ,
307+ {
308+ key : 'endpointUrl' ,
309+ title : t ( 'deployment.EndpointUrl' ) ,
310+ render : ( _text , row ) => {
311+ const url = row . networkAccess ?. endpointUrl ;
312+ if ( ! url ) return < Typography . Text type = "secondary" > -</ Typography . Text > ;
313+ return (
314+ < Typography . Link href = { url } target = "_blank" rel = "noreferrer" >
315+ { url }
316+ </ Typography . Link >
317+ ) ;
318+ } ,
319+ } ,
320+ {
321+ key : 'tags' ,
322+ title : t ( 'deployment.Tags' ) ,
323+ render : ( _text , row ) => {
324+ const tags = row . metadata ?. tags ?? [ ] ;
325+ if ( tags . length === 0 )
326+ return < Typography . Text type = "secondary" > -</ Typography . Text > ;
327+ return (
328+ < BAIFlex wrap = "wrap" gap = "xs" >
329+ { tags . map ( ( tag ) => (
330+ < Tag key = { tag } > { tag } </ Tag >
331+ ) ) }
332+ </ BAIFlex >
333+ ) ;
334+ } ,
335+ } ,
349336 {
350337 key : 'createdAt' ,
351338 title : t ( 'deployment.CreatedAt' ) ,
352- dataIndex : [ 'metadata' , ' createdAt'] ,
339+ dataIndex : ' createdAt',
353340 sorter : true ,
354341 render : ( _text , row ) => {
355342 const createdAt = row . metadata ?. createdAt ;
356343 return createdAt ? dayjs ( createdAt ) . format ( 'll LT' ) : '-' ;
357344 } ,
358345 } ,
346+ isAdminMode && {
347+ key : 'domainName' ,
348+ title : t ( 'deployment.Domain' ) ,
349+ render : ( _text , row ) => {
350+ const domain = row . metadata ?. domainName ;
351+ return domain ? (
352+ < Typography . Text > { domain } </ Typography . Text >
353+ ) : (
354+ < Typography . Text type = "secondary" > -</ Typography . Text >
355+ ) ;
356+ } ,
357+ } ,
359358 isAdminMode && {
360359 key : 'owner' ,
361360 title : t ( 'deployment.Owner' ) ,
362361 render : ( _text , row ) => < DeploymentOwnerInfo deploymentFrgmt = { row } /> ,
363362 } ,
364363 ] ) ;
365364
365+ // Merge fragment-derived total into the pagination config supplied by the
366+ // parent so callers don't need to separately query for count.
367+ const paginationWithTotal =
368+ tableProps . pagination === false
369+ ? ( false as const )
370+ : { ...tableProps . pagination , total : totalCount } ;
371+
366372 return (
367373 < BAIFlex direction = "column" align = "stretch" gap = "sm" >
368- < BAIGraphQLPropertyFilter
369- style = { { marginBottom : token . marginXS } }
370- filterProperties = { filterProperties }
371- value = { filterValue }
372- onChange = { ( next ) => {
373- setFilter ( stringifyFilter ( next ) ) ;
374- // Reset pagination when filters change.
375- setPage ( 1 ) ;
376- } }
377- />
378- < BAITable < DeploymentNode >
379- rowKey = "id"
380- scroll = { { x : 'max-content' } }
381- loading = { loading }
382- dataSource = { deployments }
383- columns = { columns }
384- showSorterTooltip = { false }
385- order = { sortToTableOrder ( sort ) }
386- onChangeOrder = { ( order ) => {
387- setSort ( tableOrderToSort ( order ) ) ;
388- } }
389- pagination = { {
390- current : page ,
391- pageSize,
392- total : totalCount ,
393- onChange : ( nextPage , nextPageSize ) => {
394- if ( nextPage !== page ) setPage ( nextPage ) ;
395- if ( nextPageSize !== pageSize ) setPageSize ( nextPageSize ) ;
396- } ,
397- } }
398- />
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 ) ) ;
389+ } }
390+ />
391+ </ BAIFlex >
392+ { toolbarEnd }
393+ </ 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 >
399408 </ BAIFlex >
400409 ) ;
401410} ;
0 commit comments