Skip to content

Commit 2e72edf

Browse files
committed
feat(FR-1691): migrate project table to the webui (#4698)
resolves #4659 (FR-1691) This PR adds a new Project page to the UI and introduces several reusable components for displaying resource information: - Creates a new `BAIResourceNumberWithIcon` component to display resource values with appropriate icons - Adds `BAIProjectTable` component for displaying project information - Implements device-specific icons for various accelerators (NVIDIA, ROCm, TPU, IPU, Gaudi, Furiosa, Rebel) - Moves `NumberWithUnit` and `AllowedVfolderHostsWithPermission` components to the UI package - Adds a `BAIMetaDataProvider` to share device metadata across components - Adds translations for the new components across all supported languages - Implements the Project page with filtering and sorting capabilities The PR also enhances the table component to support column groups and improves resource display with appropriate icons based on resource type. The test cases in BAIDomainSelect.test.ts will fail. We need to configure Babel, but we’ll address that later in a different issue. The BAIAllowedVfolderHostsWithPermission component needs updates to its icons and functionality. Please recommend some icons. mutations features are implemented in another issue.
1 parent c666e48 commit 2e72edf

89 files changed

Lines changed: 2436 additions & 329 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/backend.ai-ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@
148148
"@storybook/react-vite": "^9.1.1",
149149
"@testing-library/jest-dom": "^6.6.3",
150150
"@testing-library/react": "^16.2.0",
151+
"@testing-library/user-event": "^14.6.1",
151152
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
152153
"@types/big.js": "^6.2.2",
153154
"@types/lodash": "^4.17.20",

react/src/components/NumberWithUnit.tsx renamed to packages/backend.ai-ui/src/components/BAINumberWithUnit.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import { convertToBinaryUnit, convertToDecimalUnit, SizeUnit } from '../helper';
2+
import BAIFlex from './BAIFlex';
23
import { Typography } from 'antd';
3-
import { BAIFlex } from 'backend.ai-ui';
44

5-
interface NumberWithUnitProps {
5+
interface BAINumberWithUnitProps {
66
numberUnit: string;
77
targetUnit: SizeUnit;
88
unitType: 'binary' | 'decimal';
99
postfix?: string;
1010
}
1111

12-
const NumberWithUnit = ({
12+
const BAINumberWithUnit = ({
1313
numberUnit,
1414
targetUnit,
1515
unitType,
1616
postfix,
17-
}: NumberWithUnitProps) => {
17+
}: BAINumberWithUnitProps) => {
1818
const convertedByTargetUnit =
1919
unitType === 'binary'
2020
? convertToBinaryUnit(numberUnit, targetUnit, 2, true)
@@ -41,4 +41,4 @@ const NumberWithUnit = ({
4141
);
4242
};
4343

44-
export default NumberWithUnit;
44+
export default BAINumberWithUnit;
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { convertToBinaryUnit } from '../helper';
2+
import { BAINvidiaIcon } from '../icons';
3+
import BAIFuriosaIcon from '../icons/BAIFuriosaIcon';
4+
import BAIGaudiIcon from '../icons/BAIGaudiIcon';
5+
import BAIIpuIcon from '../icons/BAIIpuIcon';
6+
import BAIRebelIcon from '../icons/BAIRebelIcon';
7+
import BAIRocmIcon from '../icons/BAIRocmIcon';
8+
import BAITpuIcon from '../icons/BAITpuIcon';
9+
import BAIFlex from './BAIFlex';
10+
import NumberWithUnit from './BAINumberWithUnit';
11+
import BAIText from './BAIText';
12+
import { ResourceSlotName, useBAIDeviceMetaData } from './provider';
13+
import { theme, Tooltip } from 'antd';
14+
import _ from 'lodash';
15+
import { CpuIcon, MemoryStickIcon, MicrochipIcon } from 'lucide-react';
16+
import { ReactNode } from 'react';
17+
18+
export type ResourceOpts = {
19+
shmem?: number;
20+
};
21+
22+
export interface BAIResourceNumberWithIconProps {
23+
type: string;
24+
extra?: ReactNode;
25+
opts?: ResourceOpts;
26+
value: string;
27+
hideTooltip?: boolean;
28+
max?: string;
29+
}
30+
31+
const BAIResourceNumberWithIcon = ({
32+
type,
33+
extra,
34+
opts,
35+
value: amount,
36+
max,
37+
hideTooltip = false,
38+
}: BAIResourceNumberWithIconProps) => {
39+
'use memo';
40+
41+
const deviceMetaData = useBAIDeviceMetaData();
42+
const { token } = theme.useToken();
43+
44+
const formatAmount = (amount: string) => {
45+
return deviceMetaData?.[type]?.number_format.binary
46+
? Number(
47+
convertToBinaryUnit(amount, 'g', 2, true)?.numberFixed,
48+
).toString()
49+
: (deviceMetaData?.[type]?.number_format.round_length || 0) > 0
50+
? parseFloat(amount).toFixed(2)
51+
: amount;
52+
};
53+
54+
return (
55+
<BAIFlex direction="row" gap="xxs">
56+
{deviceMetaData?.[type] ? (
57+
<ResourceTypeIcon type={type} showTooltip={!hideTooltip} />
58+
) : (
59+
type
60+
)}
61+
{deviceMetaData?.[type]?.number_format.binary ? (
62+
<NumberWithUnit
63+
numberUnit={amount}
64+
targetUnit="g"
65+
unitType="binary"
66+
postfix={
67+
_.isUndefined(max)
68+
? ''
69+
: max === 'Infinity'
70+
? '~∞'
71+
: `~${formatAmount(max)}`
72+
}
73+
/>
74+
) : (
75+
<>
76+
<BAIText>
77+
{formatAmount(amount)}
78+
{_.isUndefined(max)
79+
? null
80+
: max === 'Infinity'
81+
? '~∞'
82+
: `~${formatAmount(max)}`}
83+
</BAIText>
84+
<BAIText type="secondary" style={{ whiteSpace: 'nowrap' }}>
85+
{deviceMetaData?.[type]?.display_unit || ''}
86+
</BAIText>
87+
</>
88+
)}
89+
90+
{type === 'mem' && opts?.shmem && opts?.shmem > 0 ? (
91+
<BAIText type="secondary" style={{ fontSize: token.fontSizeSM }}>
92+
(SHM: {convertToBinaryUnit(opts.shmem, 'g', 2, true)?.numberFixed}
93+
GiB)
94+
</BAIText>
95+
) : null}
96+
{extra}
97+
</BAIFlex>
98+
);
99+
};
100+
101+
const knownDeviceIcons = {
102+
gaudi: <BAIGaudiIcon />,
103+
furiosa: <BAIFuriosaIcon />,
104+
tpu: <BAITpuIcon />,
105+
ipu: <BAIIpuIcon />,
106+
nvidia: <BAINvidiaIcon />,
107+
rocm: <BAIRocmIcon />,
108+
rebel: <BAIRebelIcon />,
109+
} as const;
110+
111+
interface ResourceTypeIconProps {
112+
type: ResourceSlotName | string;
113+
showTooltip?: boolean;
114+
size?: number;
115+
}
116+
117+
export const ResourceTypeIcon = ({
118+
type,
119+
showTooltip = true,
120+
size = 16,
121+
}: ResourceTypeIconProps) => {
122+
'use memo';
123+
124+
const deviceMetaData = useBAIDeviceMetaData();
125+
126+
const getIconContent = () => {
127+
if (type === 'cpu') {
128+
return (
129+
<BAIFlex style={{ width: size, height: size }}>
130+
<CpuIcon />
131+
</BAIFlex>
132+
);
133+
}
134+
if (type === 'mem') {
135+
return (
136+
<BAIFlex style={{ width: size, height: size }}>
137+
<MemoryStickIcon />
138+
</BAIFlex>
139+
);
140+
}
141+
142+
const displayIcon = deviceMetaData[type]?.display_icon;
143+
144+
if (displayIcon && _.keys(knownDeviceIcons).includes(displayIcon)) {
145+
return (
146+
knownDeviceIcons[displayIcon as keyof typeof knownDeviceIcons] ?? null
147+
);
148+
}
149+
150+
return (
151+
<BAIFlex style={{ width: size, height: size }}>
152+
<MicrochipIcon />
153+
</BAIFlex>
154+
);
155+
};
156+
157+
const content = getIconContent();
158+
159+
return showTooltip ? (
160+
<Tooltip title={deviceMetaData[type]?.description || type}>
161+
{content}
162+
</Tooltip>
163+
) : (
164+
<BAIFlex>{content}</BAIFlex>
165+
);
166+
};
167+
168+
export default BAIResourceNumberWithIcon;

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,18 @@ export interface BAIColumnType<RecordType = any>
7979
required?: boolean;
8080
}
8181

82+
export interface BAIColumnGroupType<RecordType = AnyObject>
83+
extends Omit<BAIColumnType<RecordType>, 'dataIndex'> {
84+
children: ColumnsType<RecordType>;
85+
}
86+
8287
/**
8388
* Array type for BAI table columns
8489
*/
85-
export type BAIColumnsType<RecordType = any> = BAIColumnType<RecordType>[];
90+
export type BAIColumnsType<RecordType = any> = (
91+
| BAIColumnGroupType<RecordType>
92+
| BAIColumnType<RecordType>
93+
)[];
8694

8795
/**
8896
* Utility function to determine if a column should be visible

react/src/components/AllowedVfolderHostsWithPermission.tsx renamed to packages/backend.ai-ui/src/components/fragments/BAIAllowedVfolderHostsWithPermission.tsx

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,67 @@
1-
import { AllowedVfolderHostsWithPermissionFragment$key } from '../__generated__/AllowedVfolderHostsWithPermissionFragment.graphql';
2-
import { AllowedVfolderHostsWithPermissionQuery } from '../__generated__/AllowedVfolderHostsWithPermissionQuery.graphql';
1+
import { BAIAllowedVfolderHostsWithPermissionFromGroupFragment$key } from '../../__generated__/BAIAllowedVfolderHostsWithPermissionFromGroupFragment.graphql';
2+
import { BAIAllowedVfolderHostsWithPermissionFromKeyPairResourcePolicyFragment$key } from '../../__generated__/BAIAllowedVfolderHostsWithPermissionFromKeyPairResourcePolicyFragment.graphql';
3+
import { BAIAllowedVfolderHostsWithPermissionQuery } from '../../__generated__/BAIAllowedVfolderHostsWithPermissionQuery.graphql';
4+
import BAIFlex from '../BAIFlex';
5+
import BAILink from '../BAILink';
6+
import BAIModal from '../BAIModal';
7+
import { BAITable } from '../Table';
38
import { CheckCircleFilled, StopFilled } from '@ant-design/icons';
49
import { Badge, theme } from 'antd';
5-
import { BAITable, BAIFlex, BAILink, BAIModal } from 'backend.ai-ui';
610
import _ from 'lodash';
711
import React from 'react';
812
import { useTranslation } from 'react-i18next';
913
import { graphql, useFragment, useLazyLoadQuery } from 'react-relay';
1014

11-
interface AllowedVfolderHostsWithPermissionProps {
12-
allowedVfolderHostsWithPermissionFrgmt: AllowedVfolderHostsWithPermissionFragment$key;
13-
}
15+
export type BAIAllowedVfolderHostsWithPermissionProps =
16+
| {
17+
allowedHostPermissionFrgmtFromKeyPair: BAIAllowedVfolderHostsWithPermissionFromKeyPairResourcePolicyFragment$key;
18+
allowedHostPermissionFrgmtFromGroup?: never;
19+
}
20+
| {
21+
allowedHostPermissionFrgmtFromKeyPair?: never;
22+
allowedHostPermissionFrgmtFromGroup: BAIAllowedVfolderHostsWithPermissionFromGroupFragment$key;
23+
};
1424

15-
const AllowedVfolderHostsWithPermission: React.FC<
16-
AllowedVfolderHostsWithPermissionProps
17-
> = ({ allowedVfolderHostsWithPermissionFrgmt }) => {
25+
const BAIAllowedVfolderHostsWithPermission: React.FC<
26+
BAIAllowedVfolderHostsWithPermissionProps
27+
> = ({
28+
allowedHostPermissionFrgmtFromKeyPair,
29+
allowedHostPermissionFrgmtFromGroup,
30+
}) => {
1831
const { t } = useTranslation();
1932
const { token } = theme.useToken();
2033
const [storageHost, setStorageHost] = React.useState<string | null>();
2134

22-
const keypairResourcePolicy = useFragment(
23-
graphql`
24-
fragment AllowedVfolderHostsWithPermissionFragment on KeyPairResourcePolicy {
25-
allowed_vfolder_hosts
26-
}
27-
`,
28-
allowedVfolderHostsWithPermissionFrgmt,
29-
);
35+
const keypairResourcePolicy =
36+
useFragment<BAIAllowedVfolderHostsWithPermissionFromKeyPairResourcePolicyFragment$key>(
37+
graphql`
38+
fragment BAIAllowedVfolderHostsWithPermissionFromKeyPairResourcePolicyFragment on KeyPairResourcePolicy {
39+
allowed_vfolder_hosts
40+
}
41+
`,
42+
allowedHostPermissionFrgmtFromKeyPair,
43+
);
44+
45+
const groupNode =
46+
useFragment<BAIAllowedVfolderHostsWithPermissionFromGroupFragment$key>(
47+
graphql`
48+
fragment BAIAllowedVfolderHostsWithPermissionFromGroupFragment on GroupNode {
49+
allowed_vfolder_hosts
50+
}
51+
`,
52+
allowedHostPermissionFrgmtFromGroup,
53+
);
3054

3155
const allowedVfolderHosts = JSON.parse(
32-
keypairResourcePolicy?.allowed_vfolder_hosts || '{}',
56+
keypairResourcePolicy?.allowed_vfolder_hosts ||
57+
groupNode?.allowed_vfolder_hosts ||
58+
'{}',
3359
);
3460

3561
const { vfolder_host_permissions } =
36-
useLazyLoadQuery<AllowedVfolderHostsWithPermissionQuery>(
62+
useLazyLoadQuery<BAIAllowedVfolderHostsWithPermissionQuery>(
3763
graphql`
38-
query AllowedVfolderHostsWithPermissionQuery {
64+
query BAIAllowedVfolderHostsWithPermissionQuery {
3965
vfolder_host_permissions {
4066
vfolder_host_permission_list
4167
}
@@ -77,7 +103,7 @@ const AllowedVfolderHostsWithPermission: React.FC<
77103
</BAIFlex>
78104
<BAIModal
79105
centered
80-
title={`${storageHost} ${t('data.explorer.Permission')}`}
106+
title={`${storageHost} ${t('comp:AllowedVfolderHostsWithPermission.Permission')}`}
81107
open={!_.isEmpty(storageHost)}
82108
onCancel={() => setStorageHost(null)}
83109
footer={null}
@@ -116,12 +142,12 @@ const AllowedVfolderHostsWithPermission: React.FC<
116142
)}
117143
columns={[
118144
{
119-
title: t('data.explorer.Permission'),
145+
title: t('comp:AllowedVfolderHostsWithPermission.Permission'),
120146
dataIndex: 'permission',
121147
key: 'permission',
122148
},
123149
{
124-
title: t('data.explorer.Allowed'),
150+
title: t('comp:AllowedVfolderHostsWithPermission.Allowed'),
125151
dataIndex: 'isAllowed',
126152
key: 'isAllowed',
127153
},
@@ -132,4 +158,4 @@ const AllowedVfolderHostsWithPermission: React.FC<
132158
);
133159
};
134160

135-
export default AllowedVfolderHostsWithPermission;
161+
export default BAIAllowedVfolderHostsWithPermission;

0 commit comments

Comments
 (0)