Skip to content

Commit

Permalink
feat(FR-513): NEO quota per storage volume card
Browse files Browse the repository at this point in the history
  • Loading branch information
agatha197 committed Feb 24, 2025
1 parent cf549da commit 6abcf54
Show file tree
Hide file tree
Showing 4 changed files with 343 additions and 2 deletions.
1 change: 1 addition & 0 deletions react/src/components/BAIPanelItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const BAIPanelItem: React.FC<BAIPanelItemProps> = ({
direction="column"
style={{ maxWidth: 88, textAlign: 'center', height: '100%' }}
justify="between"
wrap="wrap"
>
{_.isString(title) ? (
<Typography.Text strong style={{ fontSize: token.fontSizeHeading5 }}>
Expand Down
125 changes: 125 additions & 0 deletions react/src/components/BAIProgress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import Flex from './Flex';
import { ProgressProps, theme, Typography } from 'antd';
import _ from 'lodash';
import React, { ReactNode } from 'react';

interface BAIProgressProps extends ProgressProps {
title?: ReactNode;
used?: number | string;
total?: number | string;
progressStyle?: React.CSSProperties;
}

const BAIProgress: React.FC<BAIProgressProps> = ({
title,
used,
total,
progressStyle,
...baiProgressProps
}) => {
const { token } = theme.useToken();

return (
<Flex direction="column" align="stretch" gap={'xs'}>
<Flex align="stretch" justify={title ? 'between' : 'end'}>
<Typography.Text style={{ alignContent: 'end' }}>
{title}
</Typography.Text>
<Typography.Text
style={{
fontSize: token.fontSizeHeading3,
color: _.isString(baiProgressProps.strokeColor)
? baiProgressProps.strokeColor
: token.Layout?.headerBg,
alignContent: 'end',
}}
>
{baiProgressProps.percent ?? 0}%
</Typography.Text>
</Flex>
<Flex
style={{
padding: 1,
backgroundColor: token.colorFillSecondary,
height: _.isNumber(baiProgressProps.size)
? baiProgressProps.size
: token.size,
...progressStyle,
}}
direction="column"
align="stretch"
>
<Flex
style={{
height: _.isNumber(baiProgressProps.size)
? baiProgressProps.size
: token.size,
width: `${!baiProgressProps.percent || _.isNaN(baiProgressProps.percent) ? 0 : _.min([baiProgressProps.percent, 100])}%`,
position: 'absolute',
left: 0,
top: 0,
backgroundColor: _.isString(baiProgressProps.strokeColor)
? (baiProgressProps.strokeColor ??
token.Layout?.headerBg ??
token.colorPrimary)
: (token.Layout?.headerBg ?? token.colorPrimary),
zIndex: 0,
overflow: 'hidden',
}}
></Flex>
{/* Hide used text to avoid overlapping */}
{used && baiProgressProps.percent && baiProgressProps.percent < 70 ? (
<div
style={{
position: 'absolute',
left:
!baiProgressProps.percent || _.isNaN(baiProgressProps.percent)
? 0
: `calc(${_.min([baiProgressProps.percent, 100])}% - ${token.size}px)`,
bottom: -(token.size + token.fontSize),
textAlign: 'center',
}}
>
<Typography.Text
style={{
color: _.isString(baiProgressProps.strokeColor)
? (baiProgressProps.strokeColor ??
token.Layout?.headerBg ??
token.colorPrimary)
: (token.Layout?.headerBg ?? token.colorPrimary),
}}
>
{used}
</Typography.Text>
</div>
) : null}
</Flex>
<Flex justify="end">
{used &&
total &&
baiProgressProps.percent &&
baiProgressProps.percent >= 70 ? (
<Flex gap={'xxs'}>
<Typography.Text
style={{
color: _.isString(baiProgressProps.strokeColor)
? (baiProgressProps.strokeColor ??
token.Layout?.headerBg ??
token.colorPrimary)
: (token.Layout?.headerBg ?? token.colorPrimary),
}}
>
{used}
</Typography.Text>
<Typography.Text>/</Typography.Text>
<Typography.Text>{total}</Typography.Text>
</Flex>
) : total ? (
<Typography.Text>{total}</Typography.Text>
) : null}
</Flex>
</Flex>
);
};

export default BAIProgress;
213 changes: 213 additions & 0 deletions react/src/components/QuotaPerStorageVolumePanelCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { addQuotaScopeTypePrefix, convertDecimalSizeUnit } from '../helper';
import { useCurrentDomainValue, useSuspendedBackendaiClient } from '../hooks';
import { useCurrentProjectValue } from '../hooks/useCurrentProject';
import BAICard, { BAICardProps } from './BAICard';
import BAIProgress from './BAIProgress';
import Flex from './Flex';
import FlexActivityIndicator from './FlexActivityIndicator';
import StorageSelect from './StorageSelect';
import { QuotaPerStorageVolumePanelCardQuery } from './__generated__/QuotaPerStorageVolumePanelCardQuery.graphql';
import { QuotaPerStorageVolumePanelCardUserQuery } from './__generated__/QuotaPerStorageVolumePanelCardUserQuery.graphql';
import { QuestionCircleOutlined } from '@ant-design/icons';
import { Col, Empty, Row, theme, Tooltip, Typography } from 'antd';
import graphql from 'babel-plugin-relay/macro';
import _ from 'lodash';
import React, { useDeferredValue, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLazyLoadQuery } from 'react-relay';

export type VolumeInfo = {
id: string;
backend: string;
capabilities: string[];
usage: {
percentage: number;
};
sftp_scaling_groups: string[];
};

interface QuotaPerStorageVolumePanelCardProps extends BAICardProps {}

const QuotaPerStorageVolumePanelCard: React.FC<
QuotaPerStorageVolumePanelCardProps
> = ({ ...baiCardProps }) => {
const { t } = useTranslation();
const { token } = theme.useToken();
const [selectedVolumeInfo, setSelectedVolumeInfo] = useState<VolumeInfo>();
const deferredSelectedVolumeInfo = useDeferredValue(selectedVolumeInfo);
const currentProject = useCurrentProjectValue();
const baiClient = useSuspendedBackendaiClient();

// TODO: Add resolver to enable subquery and modify to call useLazyLoadQuery only once.
const { user } = useLazyLoadQuery<QuotaPerStorageVolumePanelCardUserQuery>(
graphql`
query QuotaPerStorageVolumePanelCardUserQuery(
$domain_name: String
$email: String
) {
user(domain_name: $domain_name, email: $email) {
id
}
}
`,
{
domain_name: useCurrentDomainValue(),
email: baiClient?.email,
},
);
const { project_quota_scope, user_quota_scope } =
useLazyLoadQuery<QuotaPerStorageVolumePanelCardQuery>(
graphql`
query QuotaPerStorageVolumePanelCardQuery(
$project_quota_scope_id: String!
$user_quota_scope_id: String!
$storage_host_name: String!
$skipQuotaScope: Boolean!
) {
project_quota_scope: quota_scope(
quota_scope_id: $project_quota_scope_id
storage_host_name: $storage_host_name
) @skip(if: $skipQuotaScope) {
details {
usage_bytes
hard_limit_bytes
}
}
user_quota_scope: quota_scope(
quota_scope_id: $user_quota_scope_id
storage_host_name: $storage_host_name
) @skip(if: $skipQuotaScope) {
details {
usage_bytes
hard_limit_bytes
}
}
}
`,
{
project_quota_scope_id: addQuotaScopeTypePrefix(
'project',
currentProject?.id,
),
user_quota_scope_id: addQuotaScopeTypePrefix('user', user?.id || ''),
storage_host_name: deferredSelectedVolumeInfo?.id || '',
skipQuotaScope:
currentProject?.id === undefined ||
user?.id === undefined ||
!deferredSelectedVolumeInfo?.id,
},
);

const projectUsageBytes = _.toFinite(
project_quota_scope?.details?.usage_bytes,
);
const projectHardLimitBytes = _.toFinite(
project_quota_scope?.details?.hard_limit_bytes,
);
const projectPercent = projectHardLimitBytes
? _.toFinite(
((projectUsageBytes / projectHardLimitBytes) * 100)?.toFixed(2),
)
: 0;

const userUsageBytes = _.toFinite(user_quota_scope?.details?.usage_bytes);
const userHardLimitBytes = _.toFinite(
user_quota_scope?.details?.hard_limit_bytes,
);
const userPercent = userHardLimitBytes
? _.toFinite(((userUsageBytes / userHardLimitBytes) * 100)?.toFixed(2))
: 0;

return (
<BAICard
{...baiCardProps}
title={
<Flex gap={'xs'} align="center">
{t('data.QuotaPerStorageVolume')}
<Tooltip title={t('data.HostDetails')}>
<QuestionCircleOutlined
style={{ color: token.colorTextDescription }}
/>
</Tooltip>
</Flex>
}
extra={
<StorageSelect
value={selectedVolumeInfo?.id}
onChange={(__, vInfo) => {
setSelectedVolumeInfo(vInfo);
}}
autoSelectType="usage"
showUsageStatus
showSearch
allowClear
/>
}
styles={{
body: {
paddingTop: token.paddingLG,
},
}}
>
{selectedVolumeInfo !== deferredSelectedVolumeInfo ? (
<FlexActivityIndicator style={{ minHeight: 120 }} />
) : selectedVolumeInfo?.capabilities?.includes('quota') ? (
<Row gutter={[24, 16]}>
<Col
span={12}
style={{
borderRight: `1px solid ${token.colorBorderSecondary}`,
}}
>
<BAIProgress
title={
<Flex direction="column" align="start">
<Typography.Text
type="secondary"
style={{ fontSize: token.fontSizeSM }}
>
{t('data.Project')}
</Typography.Text>
<Typography.Text style={{ fontSize: token.fontSize }}>
{currentProject?.name}
</Typography.Text>
</Flex>
}
percent={projectPercent}
used={`${convertDecimalSizeUnit(_.toString(projectUsageBytes), 'G')?.numberUnit}B`}
total={`${convertDecimalSizeUnit(_.toString(projectUsageBytes), 'G')?.numberUnit}B`}
/>
</Col>
<Col span={12}>
<BAIProgress
percent={userPercent}
title={
<Flex direction="column" align="start">
<Typography.Text
type="secondary"
style={{ fontSize: token.fontSizeSM }}
>
{t('data.User')}
</Typography.Text>
<Typography.Text style={{ fontSize: token.fontSize }}>
{baiClient?.full_name}
</Typography.Text>
</Flex>
}
used={`${convertDecimalSizeUnit(_.toString(userUsageBytes), 'G')?.numberUnit}B`}
total={`${convertDecimalSizeUnit(_.toString(userHardLimitBytes), 'G')?.numberUnit}B`}
/>
</Col>
</Row>
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={t('storageHost.QuotaDoesNotSupported')}
style={{ margin: '25px auto' }}
/>
)}
</BAICard>
);
};

export default QuotaPerStorageVolumePanelCard;
6 changes: 4 additions & 2 deletions resources/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"colorLink": "#FF7A00",
"colorText": "#141414",
"colorInfo": "#028DF2",
"colorError": "#FF4D4F"
"colorError": "#FF4D4F",
"colorFillSecondary": "#D9D9D9"
},
"components": {
"Tag": {
Expand All @@ -30,7 +31,8 @@
"colorLink": "#DC6B03",
"colorText": "#FFF",
"colorInfo": "#009BDD",
"colorError": "#DC4446"
"colorError": "#DC4446",
"colorFillSecondary": "#262626"
},
"components": {
"Tag": {
Expand Down

0 comments on commit 6abcf54

Please sign in to comment.