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' ;
1213import { Typography , theme } from 'antd' ;
@@ -17,6 +18,7 @@ import {
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,48 +95,25 @@ 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 ;
160119}
@@ -163,15 +122,12 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
163122 deploymentsFrgmt,
164123 filter,
165124 setFilter,
166- sort,
167- setSort,
168- page,
169- setPage,
170- pageSize,
171- setPageSize,
125+ onChangeOrder,
126+ statusCategory = 'running' ,
127+ onStatusCategoryChange,
172128 mode,
173- loading,
174129 onRowClick,
130+ ...tableProps
175131} ) => {
176132 'use memo' ;
177133 const { t } = useTranslation ( ) ;
@@ -198,9 +154,7 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
198154 totalReplicas: replicas {
199155 count
200156 }
201- runningReplicas: replicas(
202- filter: { status: { equals: RUNNING } }
203- ) {
157+ runningReplicas: replicas(filter: { status: { equals: RUNNING } }) {
204158 count
205159 }
206160 currentRevision @since(version: "26.4.3") {
@@ -281,7 +235,7 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
281235 {
282236 key : 'name' ,
283237 title : t ( 'deployment.Name' ) ,
284- dataIndex : [ 'metadata' , ' name'] ,
238+ dataIndex : ' name',
285239 sorter : true ,
286240 fixed : 'left' as const ,
287241 render : ( _text , row ) => {
@@ -315,9 +269,6 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
315269 const running = row . runningReplicas ?. count ?? 0 ;
316270 const desired = row . replicaState ?. desiredReplicaCount ?? 0 ;
317271 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.
321272 const denominator = desired > 0 ? desired : total ;
322273 return (
323274 < Typography . Text >
@@ -335,7 +286,8 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
335286 render : ( _text , row ) => {
336287 const modelName =
337288 row . currentRevision ?. modelMountConfig ?. vfolder ?. name ?? null ;
338- if ( ! modelName ) return < Typography . Text type = "secondary" > -</ Typography . Text > ;
289+ if ( ! modelName )
290+ return < Typography . Text type = "secondary" > -</ Typography . Text > ;
339291 return (
340292 < Typography . Text
341293 ellipsis = { { tooltip : modelName } }
@@ -349,7 +301,7 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
349301 {
350302 key : 'createdAt' ,
351303 title : t ( 'deployment.CreatedAt' ) ,
352- dataIndex : [ 'metadata' , ' createdAt'] ,
304+ dataIndex : ' createdAt',
353305 sorter : true ,
354306 render : ( _text , row ) => {
355307 const createdAt = row . metadata ?. createdAt ;
@@ -363,38 +315,42 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
363315 } ,
364316 ] ) ;
365317
318+ // Merge fragment-derived total into the pagination config supplied by the
319+ // parent so callers don't need to separately query for count.
320+ const paginationWithTotal =
321+ tableProps . pagination === false
322+ ? ( false as const )
323+ : { ...tableProps . pagination , total : totalCount } ;
324+
366325 return (
367326 < BAIFlex direction = "column" align = "stretch" gap = "sm" >
327+ < BAIRadioGroup
328+ value = { statusCategory }
329+ onChange = { ( e ) => onStatusCategoryChange ?.( e . target . value ) }
330+ options = { [
331+ { label : t ( 'deployment.Running' ) , value : 'running' } ,
332+ { label : t ( 'deployment.status.Terminated' ) , value : 'finished' } ,
333+ ] }
334+ />
368335 < BAIGraphQLPropertyFilter
369336 style = { { marginBottom : token . marginXS } }
370337 filterProperties = { filterProperties }
371338 value = { filterValue }
372339 onChange = { ( next ) => {
373340 setFilter ( stringifyFilter ( next ) ) ;
374- // Reset pagination when filters change.
375- setPage ( 1 ) ;
376341 } }
377342 />
378343 < BAITable < DeploymentNode >
379344 rowKey = "id"
380345 scroll = { { x : 'max-content' } }
381- loading = { loading }
346+ showSorterTooltip = { false }
347+ { ...tableProps }
382348 dataSource = { deployments }
383349 columns = { columns }
384- showSorterTooltip = { false }
385- order = { sortToTableOrder ( sort ) }
386350 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- } ,
351+ onChangeOrder ?.( order || null ) ;
397352 } }
353+ pagination = { paginationWithTotal }
398354 />
399355 </ BAIFlex >
400356 ) ;
0 commit comments