Skip to content

Commit 8915989

Browse files
committed
feat(FR-1371): Implement model deployment UI components
1 parent 3e99110 commit 8915989

File tree

11 files changed

+638
-36
lines changed

11 files changed

+638
-36
lines changed

data/schema.graphql

Lines changed: 159 additions & 27 deletions
Large diffs are not rendered by default.

packages/backend.ai-ui/src/components/Table/BAITable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { Resizable, ResizeCallbackData } from 'react-resizable';
2424
* Configuration interface for BAITable pagination
2525
* Extends Ant Design's TablePaginationConfig but omits 'position' property
2626
*/
27-
interface BAITablePaginationConfig
27+
export interface BAITablePaginationConfig
2828
extends Omit<TablePaginationConfig, 'position'> {
2929
/** Additional content to display in the pagination area */
3030
extraContent?: ReactNode;

packages/backend.ai-ui/src/components/Table/BAITableSettingModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ const BAITableSettingModal: React.FC<TableSettingProps> = ({
442442
style={{
443443
height: 330,
444444
}}
445-
scroll={{ x: 'max-content' }}
445+
scroll={{ x: 'max-content', y: 330 }}
446446
/>
447447
</SortableContext>
448448
</DndContext>

packages/backend.ai-ui/src/components/Table/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type {
44
BAIColumnType,
55
BAIColumnsType,
66
BAITableSettings,
7+
BAITablePaginationConfig,
78
BAITableColumnOverrideItem,
89
} from './BAITable';
910
export {

react/src/App.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,11 @@ const ChatPage = React.lazy(() => import('./pages/ChatPage'));
8383
const AIAgentPage = React.lazy(() => import('./pages/AIAgentPage'));
8484

8585
// Deployment pages
86+
// const DeploymentListPage = React.lazy(
87+
// () => import('./pages/Deployments/DeploymentListPage'),
88+
// );
8689
const DeploymentListPage = React.lazy(
87-
() => import('./pages/Deployments/DeploymentListPage'),
90+
() => import('./pages/DeploymentListPage'),
8891
);
8992
const DeploymentDetailPage = React.lazy(
9093
() => import('./pages/Deployments/DeploymentDetailPage'),
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import ResourceNumber from './ResourceNumber';
2+
import WebUILink from './WebUILink';
3+
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
4+
import { Tag, theme, Tooltip, Typography } from 'antd';
5+
import {
6+
BAIColumnType,
7+
BAIFlex,
8+
BAITable,
9+
BAITablePaginationConfig,
10+
BAITableProps,
11+
filterOutEmpty,
12+
filterOutNullAndUndefined,
13+
} from 'backend.ai-ui';
14+
import dayjs from 'dayjs';
15+
import _ from 'lodash';
16+
import { ExternalLinkIcon } from 'lucide-react';
17+
import { useTranslation } from 'react-i18next';
18+
import { useFragment } from 'react-relay';
19+
import { graphql } from 'relay-runtime';
20+
import {
21+
DeploymentListFragment$data,
22+
DeploymentListFragment$key,
23+
} from 'src/__generated__/DeploymentListFragment.graphql';
24+
import { useSuspendedBackendaiClient } from 'src/hooks';
25+
26+
type ModelDeployment = NonNullable<
27+
NonNullable<DeploymentListFragment$data>[number]
28+
>;
29+
interface DeploymentListProps
30+
extends Omit<BAITableProps<ModelDeployment>, 'dataSource' | 'columns'> {
31+
deploymentsFragment: DeploymentListFragment$key;
32+
pagination: BAITablePaginationConfig;
33+
}
34+
35+
const DeploymentList: React.FC<DeploymentListProps> = ({
36+
deploymentsFragment,
37+
pagination,
38+
...tableProps
39+
}) => {
40+
const { t } = useTranslation();
41+
const { token } = theme.useToken();
42+
const baiClient = useSuspendedBackendaiClient();
43+
44+
const deployments = useFragment(
45+
graphql`
46+
fragment DeploymentListFragment on ModelDeployment @relay(plural: true) {
47+
id
48+
metadata {
49+
name
50+
createdAt
51+
updatedAt
52+
tags
53+
}
54+
networkAccess {
55+
endpointUrl
56+
openToPublic
57+
}
58+
revision {
59+
id
60+
name
61+
clusterConfig {
62+
size
63+
}
64+
resourceConfig {
65+
resourceGroup {
66+
name
67+
}
68+
resourceSlots
69+
resourceOpts
70+
}
71+
}
72+
replicaState {
73+
desiredReplicaCount
74+
}
75+
deploymentStrategy {
76+
type
77+
}
78+
createdUser {
79+
email
80+
}
81+
}
82+
`,
83+
deploymentsFragment,
84+
);
85+
86+
const filteredDeployments = filterOutNullAndUndefined(deployments);
87+
const columns = _.map(
88+
filterOutEmpty<BAIColumnType<ModelDeployment>>([
89+
{
90+
title: t('deployment.DeploymentName'),
91+
key: 'name',
92+
dataIndex: ['metadata', 'name'],
93+
fixed: 'left',
94+
render: (name, row) => (
95+
<WebUILink to={`/deployment/${row.id}`}>{name}</WebUILink>
96+
),
97+
},
98+
{
99+
title: t('deployment.EndpointURL'),
100+
key: 'endpointUrl',
101+
dataIndex: ['networkAccess', 'endpointUrl'],
102+
render: (url) => (
103+
<BAIFlex gap={'xxs'}>
104+
{url ? (
105+
<>
106+
<Typography.Text copyable>{url}</Typography.Text>
107+
<a
108+
href={url}
109+
title=""
110+
target="_blank"
111+
rel="noopener noreferrer"
112+
>
113+
<Tooltip title={t('common.OpenInNewTab')}>
114+
<ExternalLinkIcon />
115+
</Tooltip>
116+
</a>
117+
</>
118+
) : (
119+
'-'
120+
)}
121+
</BAIFlex>
122+
),
123+
},
124+
{
125+
title: t('deployment.NumberOfDesiredReplicas'),
126+
key: 'desiredReplicaCount',
127+
dataIndex: ['replicaState', 'desiredReplicaCount'],
128+
render: (count) => count,
129+
},
130+
{
131+
title: t('deployment.Public'),
132+
key: 'openToPublic',
133+
dataIndex: ['networkAccess', 'openToPublic'],
134+
render: (openToPublic) =>
135+
openToPublic ? (
136+
<CheckOutlined style={{ color: token.colorSuccess }} />
137+
) : (
138+
<CloseOutlined style={{ color: token.colorTextSecondary }} />
139+
),
140+
},
141+
{
142+
title: t('deployment.Resources'),
143+
dataIndex: ['revision', 'resourceConfig', 'resourceSlots'],
144+
key: 'resourceSlots',
145+
render: (resourceSlots) => (
146+
<BAIFlex direction="row" gap="xs">
147+
{_.map(JSON.parse(resourceSlots || '{}'), (value, key) => (
148+
<ResourceNumber key={key} type={key} value={value.toString()} />
149+
))}
150+
</BAIFlex>
151+
),
152+
defaultHidden: true,
153+
},
154+
{
155+
title: t('deployment.CreatedAt'),
156+
dataIndex: ['metadata', 'createdAt'],
157+
key: 'createdAt',
158+
render: (createdAt) => {
159+
return dayjs(createdAt).format('ll LT');
160+
},
161+
},
162+
{
163+
title: t('deployment.UpdatedAt'),
164+
dataIndex: ['metadata', 'updatedAt'],
165+
key: 'updatedAt',
166+
render: (updatedAt) => {
167+
return dayjs(updatedAt).format('ll LT');
168+
},
169+
170+
defaultHidden: true,
171+
},
172+
baiClient.is_admin && {
173+
title: t('deployment.CreatedBy'),
174+
dataIndex: ['createdUser', 'email'],
175+
key: 'createdBy',
176+
render: (email) => <Typography.Text>{email}</Typography.Text>,
177+
},
178+
{
179+
title: t('deployment.Tags'),
180+
dataIndex: ['metadata', 'tags'],
181+
key: 'tags',
182+
render: (tags) => _.map(tags, (tag) => <Tag key={tag}>{tag}</Tag>),
183+
defaultHidden: true,
184+
},
185+
{
186+
title: t('deployment.DeploymentStrategy'),
187+
dataIndex: ['deploymentStrategy', 'type'],
188+
key: 'type',
189+
render: (type) => (
190+
<Tag
191+
color={
192+
type === 'ROLLING'
193+
? 'default'
194+
: type === 'BLUE_GREEN'
195+
? 'blue'
196+
: 'yellow'
197+
}
198+
>
199+
{type}
200+
</Tag>
201+
),
202+
defaultHidden: true,
203+
},
204+
{
205+
title: t('deployment.RevisionName'),
206+
dataIndex: ['revision', 'name'],
207+
key: 'revisionName',
208+
render: (name, row) => (
209+
<WebUILink to={`/deployment/revision/${row.id}`}>{name}</WebUILink>
210+
),
211+
},
212+
{
213+
title: t('deployment.ClusterSize'),
214+
dataIndex: ['revision', 'clusterConfig', 'size'],
215+
key: 'clusterSize',
216+
render: (size) => <Typography.Text>{size}</Typography.Text>,
217+
218+
defaultHidden: true,
219+
},
220+
]),
221+
);
222+
223+
return (
224+
<BAITable
225+
resizable
226+
rowKey={'id'}
227+
size="small"
228+
dataSource={filteredDeployments}
229+
columns={columns}
230+
pagination={pagination}
231+
scroll={{ x: 'max-content' }}
232+
{...tableProps}
233+
/>
234+
);
235+
};
236+
237+
export default DeploymentList;

react/src/components/Deployments/DeploymentList.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ import ResourceNumber from '../ResourceNumber';
22
import WebUILink from '../WebUILink';
33
import { Typography, TablePaginationConfig, Tooltip } from 'antd';
44
import { ColumnType } from 'antd/lib/table';
5-
import { BAIFlex, BAITable, BAITableProps } from 'backend.ai-ui';
5+
import {
6+
BAIFlex,
7+
BAITable,
8+
BAITableProps,
9+
filterOutNullAndUndefined,
10+
} from 'backend.ai-ui';
611
import dayjs from 'dayjs';
712
import { ExternalLinkIcon } from 'lucide-react';
813
import React from 'react';
914
import { useTranslation } from 'react-i18next';
10-
import { filterOutNullAndUndefined } from 'src/helper';
1115

1216
interface Deployment {
1317
id: string;

react/src/helper/graphql-transformer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function manipulateGraphQLQueryWithClientDirectives(
1212
// Optionally normalize fragment type conditions from Query to Queries
1313
let newAst = ast;
1414
// Since the super graph, the query type has been changed from Queries to Query
15-
const shouldConvertFragmentTypeToQueries = isNotCompatibleWith('25.14.0');
15+
const shouldConvertFragmentTypeToQueries = isNotCompatibleWith('25.12.0');
1616
if (shouldConvertFragmentTypeToQueries) {
1717
function normalizeFragmentTypeCondition(node: any) {
1818
if (!node.typeCondition) return;

0 commit comments

Comments
 (0)