Skip to content

Commit 9c45759

Browse files
committed
feat(FR-2691): relocate /data header panels — Dashboard + host-capacity cell
Resolves #6937(FR-2691) Removes the three header panels from the /data VFolder list page (Create Folder action card, StorageStatusPanelCard, QuotaPerStorageVolumePanelCard) and redistributes their responsibilities: - Folder-status counts move to the Dashboard as a new `folderStatus` board item. StorageStatusPanelCard is unchanged functionally; a `TODO(FR-2691 v2-migration)` marker is added alongside its two legacy query surfaces (baiClient.vfolder.list REST call and the V1 user_resource_policy / project_resource_policy GraphQL fields) so a follow-up task can move them to myVfolders / projectVfolders V2 connections with server-side filter+count. - Per-volume quota visibility moves into the folder table's Location column via a new VFolderHostCell: the host name is now clickable and carries a StorageUsageBadge sourced from the shared vhostInfo tan-query cache key used by StorageSelect. Clicking opens QuotaPerStorageVolumePanelCard in a modal, pre-seeded on the clicked host through a new `defaultVolumeInfo` prop (auto-select is disabled when a default is passed; users can still switch volumes via the inline StorageSelect). The Create Folder affordance is retained through the existing primary button inside the folder table card header, so no replacement for the removed action card is needed. The cell suspense is scoped per-row so a cold hosts cache does not suspend the whole page — each row falls back to a plain host label until the shared payload resolves, after which the badge and click handler light up.
1 parent 42e2427 commit 9c45759

27 files changed

Lines changed: 447 additions & 315 deletions

react/src/components/QuotaPerStorageVolumePanelCard.tsx

Lines changed: 49 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ import { useCurrentProjectValue } from '../hooks/useCurrentProject';
1010
import BAIProgress from './BAIProgress';
1111
import FlexActivityIndicator from './FlexActivityIndicator';
1212
import StorageSelect from './StorageSelect';
13-
import { QuestionCircleOutlined } from '@ant-design/icons';
14-
import { Col, Empty, Row, theme, Tooltip, Typography } from 'antd';
15-
import { BAICard, BAICardProps, BAIFlex } from 'backend.ai-ui';
13+
import { Col, Empty, Row, theme, Typography } from 'antd';
14+
import { BAIFlex } from 'backend.ai-ui';
1615
import * as _ from 'lodash-es';
1716
import React, { useDeferredValue, useState } from 'react';
1817
import { useTranslation } from 'react-i18next';
@@ -22,20 +21,50 @@ export type VolumeInfo = {
2221
id: string;
2322
backend: string;
2423
capabilities: string[];
25-
usage: {
26-
percentage: number;
24+
// `usage` is optional because `vfolder.list_hosts()` only attaches it for
25+
// hosts that can report capacity; `usage.percentage` is optional because
26+
// even a reporting host may omit the percentage (rendered as "Unknown").
27+
usage?: {
28+
percentage?: number;
2729
};
2830
sftp_scaling_groups: string[];
2931
};
3032

31-
interface QuotaPerStorageVolumePanelCardProps extends BAICardProps {}
33+
interface QuotaPerStorageVolumePanelCardProps {
34+
/**
35+
* Pre-selects a volume so the content renders that host's quota immediately
36+
* (e.g. when opened from a specific folder row). When provided, the built-in
37+
* usage-based auto-select is disabled; users can still switch volumes via
38+
* the inline `StorageSelect`.
39+
*/
40+
defaultVolumeInfo?: VolumeInfo;
41+
}
3242

43+
// Modal-body view for per-volume quota. Intentionally not wrapped in a BAICard
44+
// — the consuming Modal provides its own title and chrome, so a nested card
45+
// would duplicate the header and inflate the modal visually.
3346
const QuotaPerStorageVolumePanelCard: React.FC<
3447
QuotaPerStorageVolumePanelCardProps
35-
> = ({ ...baiCardProps }) => {
48+
> = ({ defaultVolumeInfo }) => {
49+
'use memo';
3650
const { t } = useTranslation();
3751
const { token } = theme.useToken();
38-
const [selectedVolumeInfo, setSelectedVolumeInfo] = useState<VolumeInfo>();
52+
const [selectedVolumeInfo, setSelectedVolumeInfo] = useState<
53+
VolumeInfo | undefined
54+
>(defaultVolumeInfo);
55+
// Reset the inline selection when the consumer passes a different
56+
// `defaultVolumeInfo` while the panel stays mounted (e.g., reopened for a
57+
// different host). Compare ids only — following the
58+
// "storing info from previous renders" pattern
59+
// (https://react.dev/reference/react/useState#storing-information-from-previous-renders),
60+
// so the badge reflects the latest prop without an effect.
61+
const [prevDefaultVolumeId, setPrevDefaultVolumeId] = useState(
62+
defaultVolumeInfo?.id,
63+
);
64+
if (prevDefaultVolumeId !== defaultVolumeInfo?.id) {
65+
setPrevDefaultVolumeId(defaultVolumeInfo?.id);
66+
setSelectedVolumeInfo(defaultVolumeInfo);
67+
}
3968
const deferredSelectedVolumeInfo = useDeferredValue(selectedVolumeInfo);
4069
const currentProject = useCurrentProjectValue();
4170
const baiClient = useSuspendedBackendaiClient();
@@ -121,42 +150,17 @@ const QuotaPerStorageVolumePanelCard: React.FC<
121150
: 0;
122151

123152
return (
124-
<BAICard
125-
{...baiCardProps}
126-
title={
127-
<BAIFlex gap={'xs'} align="center">
128-
{t('data.QuotaPerStorageVolume')}
129-
<Tooltip title={t('data.HostDetails')}>
130-
<QuestionCircleOutlined
131-
style={{ color: token.colorTextDescription }}
132-
/>
133-
</Tooltip>
134-
</BAIFlex>
135-
}
136-
extra={
137-
<BAIFlex
138-
style={{
139-
marginRight: -8,
140-
}}
141-
>
142-
<StorageSelect
143-
value={selectedVolumeInfo?.id}
144-
onChange={(__, vInfo) => {
145-
setSelectedVolumeInfo(vInfo);
146-
}}
147-
autoSelectType="usage"
148-
showUsageStatus
149-
showSearch
150-
variant="borderless"
151-
/>
152-
</BAIFlex>
153-
}
154-
styles={{
155-
body: {
156-
paddingTop: token.paddingLG,
157-
},
158-
}}
159-
>
153+
<BAIFlex direction="column" align="stretch" gap={'md'}>
154+
<StorageSelect
155+
value={selectedVolumeInfo?.id}
156+
onChange={(__, vInfo) => {
157+
setSelectedVolumeInfo(vInfo);
158+
}}
159+
autoSelectType={defaultVolumeInfo ? undefined : 'usage'}
160+
showUsageStatus
161+
showSearch
162+
style={{ alignSelf: 'flex-start', minWidth: 240 }}
163+
/>
160164
{selectedVolumeInfo !== deferredSelectedVolumeInfo ? (
161165
<FlexActivityIndicator style={{ minHeight: 120 }} />
162166
) : selectedVolumeInfo?.capabilities?.includes('quota') ? (
@@ -232,7 +236,7 @@ const QuotaPerStorageVolumePanelCard: React.FC<
232236
style={{ margin: 'auto 25px' }}
233237
/>
234238
)}
235-
</BAICard>
239+
</BAIFlex>
236240
);
237241
};
238242

react/src/components/StorageSelect.tsx

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ export type VolumeInfo = {
2121
id: string;
2222
backend: string;
2323
capabilities: string[];
24-
usage: {
25-
percentage: number;
24+
// `usage` is optional because `vfolder.list_hosts()` only attaches it for
25+
// hosts that can report capacity; `usage.percentage` is optional because
26+
// even a reporting host may omit the percentage.
27+
usage?: {
28+
percentage?: number;
2629
};
2730
sftp_scaling_groups: string[];
2831
};
@@ -118,38 +121,42 @@ const StorageSelect: React.FC<Props> = ({
118121
}
119122
}
120123
optionLabelProp={showUsageStatus ? 'label' : 'value'}
121-
options={_.map(vhostInfo?.allowed, (host) => ({
122-
label: showUsageStatus ? (
123-
<BAIFlex align="center">
124-
{vhostInfo?.volume_info?.[host]?.usage && (
125-
<Tooltip
126-
title={`${t('data.Host')} ${t('data.usage.Status')}:
127-
${
128-
vhostInfo?.volume_info[host]?.usage?.percentage < 70
129-
? t('data.usage.Adequate')
130-
: vhostInfo?.volume_info[host]?.usage?.percentage < 90
131-
? t('data.usage.Caution')
132-
: t('data.usage.Insufficient')
133-
}`}
134-
>
135-
<StorageUsageBadge
136-
percent={vhostInfo?.volume_info[host]?.usage?.percentage}
137-
/>
138-
{/* Use &nbsp; instead of Flex gap to fix Tooltip */}
139-
&nbsp;&nbsp;
140-
</Tooltip>
141-
)}
142-
<TextHighlighter keyword={controllableSearchValue}>
143-
{host}
144-
</TextHighlighter>
145-
{/* TODO: uncomment after implementing click action */}
146-
{/* <Button type="link" size="small" icon={<InfoCircleOutlined />} /> */}
147-
</BAIFlex>
148-
) : (
149-
host
150-
),
151-
value: host,
152-
}))}
124+
options={_.map(vhostInfo?.allowed, (host) => {
125+
const usagePercent = vhostInfo?.volume_info?.[host]?.usage?.percentage;
126+
const usageLabel =
127+
usagePercent === undefined
128+
? t('data.usage.Unknown')
129+
: usagePercent < 70
130+
? t('data.usage.Adequate')
131+
: usagePercent < 90
132+
? t('data.usage.Caution')
133+
: t('data.usage.Insufficient');
134+
return {
135+
label: showUsageStatus ? (
136+
<BAIFlex align="center">
137+
{vhostInfo?.volume_info?.[host]?.usage && (
138+
<Tooltip
139+
title={t('data.usage.HostStatusTooltip', {
140+
status: usageLabel,
141+
})}
142+
>
143+
<StorageUsageBadge percent={usagePercent} />
144+
{/* Use &nbsp; instead of Flex gap to fix Tooltip */}
145+
&nbsp;&nbsp;
146+
</Tooltip>
147+
)}
148+
<TextHighlighter keyword={controllableSearchValue}>
149+
{host}
150+
</TextHighlighter>
151+
{/* TODO: uncomment after implementing click action */}
152+
{/* <Button type="link" size="small" icon={<InfoCircleOutlined />} /> */}
153+
</BAIFlex>
154+
) : (
155+
host
156+
),
157+
value: host,
158+
};
159+
})}
153160
{...partialSelectProps}
154161
/>
155162
);

0 commit comments

Comments
 (0)