feat: Add quota info when create volume
1. Add quota info display when create volume 2. Add volume type quota check when create volume 3. Update quota ring/line chart to support infinity value Change-Id: I0f300beb16bcf50008126afab9dac529c1749d09
This commit is contained in:
parent
d133705f6f
commit
f942c48352
@ -23,6 +23,8 @@ import { parse } from 'qs';
|
||||
import FormItem from 'components/FormItem';
|
||||
import { CancelToken } from 'axios';
|
||||
import { getPath, getLinkRender } from 'utils/route-map';
|
||||
import InfoButton from 'components/InfoButton';
|
||||
import QuotaChart from 'components/QuotaChart';
|
||||
import styles from './index.less';
|
||||
|
||||
export default class BaseForm extends React.Component {
|
||||
@ -253,6 +255,14 @@ export default class BaseForm extends React.Component {
|
||||
return false;
|
||||
}
|
||||
|
||||
get showQuota() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get quotaInfo() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getSubmitData(data) {
|
||||
return { ...data };
|
||||
}
|
||||
@ -624,6 +634,35 @@ export default class BaseForm extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderQuota() {
|
||||
if (!this.showQuota) {
|
||||
return null;
|
||||
}
|
||||
let props = {};
|
||||
if (!this.quotaInfo || !this.quotaInfo.length) {
|
||||
props.loading = true;
|
||||
} else {
|
||||
props = {
|
||||
loading: false,
|
||||
quotas: this.quotaInfo,
|
||||
};
|
||||
}
|
||||
return <QuotaChart {...props} />;
|
||||
}
|
||||
|
||||
renderRightTopExtra() {
|
||||
const content = this.renderQuota();
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
const checkValue = JSON.stringify(this.quotaInfo);
|
||||
return (
|
||||
<div className={styles['right-top-extra-wrapper']}>
|
||||
<InfoButton content={content} checkValue={checkValue} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const wrapperPadding =
|
||||
this.listUrl || this.isStep || (this.isModal && this.tips)
|
||||
@ -646,6 +685,7 @@ export default class BaseForm extends React.Component {
|
||||
>
|
||||
<Spin spinning={this.isSubmitting} tip={this.renderSubmittingTip()}>
|
||||
{tips}
|
||||
{this.renderRightTopExtra()}
|
||||
<div className={classnames(styles.form, 'sl-form')} style={formStyle}>
|
||||
{this.renderForms()}
|
||||
</div>
|
||||
|
@ -119,3 +119,23 @@
|
||||
.progress-wrapper {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
.right-top-extra-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 30px;
|
||||
z-index: 100;
|
||||
background-color: #fff;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 30px 0 rgba(0, 0, 0, 20%);
|
||||
|
||||
:global {
|
||||
.ant-card-head {
|
||||
min-width: 32px;
|
||||
|
||||
.ant-card-extra {
|
||||
padding: 8px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +52,7 @@ export default function QuotaInfo(props) {
|
||||
pagination={false}
|
||||
title={() => fullTitle}
|
||||
bordered
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -25,13 +25,15 @@ export default function Line(props) {
|
||||
title = '',
|
||||
secondTitle = t('Quota'),
|
||||
} = props;
|
||||
let left = limit - used - reserved - add;
|
||||
const isLimit = limit !== -1;
|
||||
const limitStr = !isLimit ? t('Infinity') : limit;
|
||||
let left = isLimit ? limit - used - reserved - add : 1;
|
||||
left = left < 0 ? 0 : left;
|
||||
const usedTip = `${t('Used')}: ${used}`;
|
||||
const reservedTip = reserved ? '' : `${t('Reserved')}: ${reserved}`;
|
||||
const newTip = `${t('New')}: ${add}`;
|
||||
const leftTip = `${t('Left')}: ${left}`;
|
||||
const tips = [usedTip, newTip, leftTip];
|
||||
const tips = isLimit ? [usedTip, newTip, leftTip] : [usedTip, newTip];
|
||||
if (reserved) {
|
||||
tips.splice(1, 0, reservedTip);
|
||||
}
|
||||
@ -43,17 +45,19 @@ export default function Line(props) {
|
||||
const resourceTitle = (
|
||||
<span>
|
||||
{`${title} ${secondTitle}: `}{' '}
|
||||
<span style={{ color: usedColor }}>{`${allCount}/${limit}`}</span>
|
||||
<span style={{ color: usedColor }}>{`${allCount}/${limitStr}`}</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
const progress = (
|
||||
const progress = isLimit ? (
|
||||
<Progress
|
||||
percent={allPercent}
|
||||
success={{ percent: usedPercent, strokeColor: typeColors.used }}
|
||||
strokeColor={typeColors.add}
|
||||
showInfo={false}
|
||||
/>
|
||||
) : (
|
||||
<Progress percent={0} showInfo={false} />
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
Legend,
|
||||
View,
|
||||
Annotation,
|
||||
Tooltip,
|
||||
} from 'bizcharts';
|
||||
|
||||
export const typeColors = {
|
||||
@ -54,24 +55,28 @@ export default function Ring(props) {
|
||||
secondTitle = t('Quota'),
|
||||
hasLabel = false,
|
||||
} = props;
|
||||
const left = limit - used - reserved - add;
|
||||
const isLimit = limit !== -1;
|
||||
const showTip = isLimit;
|
||||
const limitNumber = !isLimit ? Infinity : limit;
|
||||
const limitStr = !isLimit ? t('Infinity') : limit;
|
||||
const left = !isLimit ? 1 : limit - used - reserved - add;
|
||||
const data = [
|
||||
{
|
||||
type: t('Used'),
|
||||
value: used,
|
||||
value: isLimit ? used : 0,
|
||||
color: typeColors.used,
|
||||
},
|
||||
];
|
||||
if (reserved) {
|
||||
data.push({
|
||||
type: t('Reserved'),
|
||||
value: reserved,
|
||||
value: isLimit ? reserved : 0,
|
||||
color: typeColors.reserved,
|
||||
});
|
||||
}
|
||||
data.push({
|
||||
type: t('New'),
|
||||
value: add,
|
||||
value: isLimit ? add : 0,
|
||||
color: typeColors.add,
|
||||
});
|
||||
data.push({
|
||||
@ -79,18 +84,20 @@ export default function Ring(props) {
|
||||
value: left,
|
||||
color: typeColors.left,
|
||||
});
|
||||
|
||||
const colors = data.map((it) => it.color);
|
||||
|
||||
const width = hasLabel ? 200 : 120;
|
||||
const style = { width };
|
||||
const height = width;
|
||||
const allCount = used + add + reserved;
|
||||
const percent = (allCount / limit) * 100;
|
||||
const percent = isLimit ? (allCount / limitNumber) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<Chart placeholder={false} height={height} padding="auto" autoFit>
|
||||
<Legend visible={hasLabel} />
|
||||
<Legend visible={showTip && hasLabel} />
|
||||
<Tooltip visible={showTip} />
|
||||
{/* 绘制图形 */}
|
||||
<View data={data}>
|
||||
<Coordinate type="theta" innerRadius={0.75} />
|
||||
@ -122,7 +129,7 @@ export default function Ring(props) {
|
||||
/>
|
||||
<Annotation.Text
|
||||
position={['50%', '70%']}
|
||||
content={`${allCount}/${limit}`}
|
||||
content={`${allCount}/${limitStr}`}
|
||||
style={{
|
||||
lineHeight: '240px',
|
||||
fontSize: '14',
|
||||
|
@ -19,8 +19,8 @@ import Line from './Line';
|
||||
import QuotaInfo from './Info';
|
||||
|
||||
function renderItem(props) {
|
||||
const { type = 'ring', limit } = props;
|
||||
if (limit === -1) {
|
||||
const { type = 'ring', limit, unlimitByTable = false } = props;
|
||||
if (limit === -1 && unlimitByTable) {
|
||||
return <QuotaInfo {...props} />;
|
||||
}
|
||||
if (type === 'ring') {
|
||||
|
@ -1805,6 +1805,7 @@
|
||||
"Quota is not enough for extend share.": "Quota is not enough for extend share.",
|
||||
"Quota is not enough for extend volume.": "Quota is not enough for extend volume.",
|
||||
"Quota: Insufficient quota to create resources, please adjust resource quantity or quota(left { quota }, input { input }).": "Quota: Insufficient quota to create resources, please adjust resource quantity or quota(left { quota }, input { input }).",
|
||||
"Quota: Insufficient { name } quota to create resources, please adjust resource quantity or quota(left { left }, input { input }).": "Quota: Insufficient { name } quota to create resources, please adjust resource quantity or quota(left { left }, input { input }).",
|
||||
"Quota: Project quotas sufficient resources can be created": "Quota: Project quotas sufficient resources can be created",
|
||||
"RAM": "RAM",
|
||||
"RAM(MiB)": "RAM(MiB)",
|
||||
@ -2790,6 +2791,7 @@
|
||||
"{interval, plural, =1 {one week} other {# weeks} } later delete": "{interval, plural, =1 {one week} other {# weeks} } later delete",
|
||||
"{minutes} minutes {leftSeconds} seconds": "{minutes} minutes {leftSeconds} seconds",
|
||||
"{name} type": "{name} type",
|
||||
"{name} type gigabytes": "{name} type gigabytes",
|
||||
"{name} type gigabytes(GiB)": "{name} type gigabytes(GiB)",
|
||||
"{name} type snapshots": "{name} type snapshots",
|
||||
"{name} {id} could not be found.": "{name} {id} could not be found.",
|
||||
|
@ -1805,6 +1805,7 @@
|
||||
"Quota is not enough for extend share.": "配额不足以扩容共享。",
|
||||
"Quota is not enough for extend volume.": "配额不足以扩容云硬盘。",
|
||||
"Quota: Insufficient quota to create resources, please adjust resource quantity or quota(left { quota }, input { input }).": "配额:项目配额不足,无法创建资源,请进行资源数量或配额的调整(剩余{ quota },输入{ input })。",
|
||||
"Quota: Insufficient { name } quota to create resources, please adjust resource quantity or quota(left { left }, input { input }).": "配额:{ name } 配额不足,无法创建资源,请进行资源数量或配额的调整(剩余{ left },输入{ input })。",
|
||||
"Quota: Project quotas sufficient resources can be created": "配额:项目配额充足,可创建资源",
|
||||
"RAM": "内存",
|
||||
"RAM(MiB)": "内存(MiB)",
|
||||
@ -2790,6 +2791,7 @@
|
||||
"{interval, plural, =1 {one week} other {# weeks} } later delete": "{interval}周后删除",
|
||||
"{minutes} minutes {leftSeconds} seconds": "{minutes}分{leftSeconds}秒",
|
||||
"{name} type": "{name} 类型",
|
||||
"{name} type gigabytes": "{name} 类型容量",
|
||||
"{name} type gigabytes(GiB)": "{name} 类型容量(GiB)",
|
||||
"{name} type snapshots": "{name} 类型快照",
|
||||
"{name} {id} could not be found.": "您查看的资源{name} {id} 无法获取",
|
||||
|
@ -26,11 +26,11 @@ import globalImageStore from 'stores/glance/image';
|
||||
import globalVolumeStore from 'stores/cinder/volume';
|
||||
import globalVolumeTypeStore from 'stores/cinder/volume-type';
|
||||
import globalBackupStore from 'stores/cinder/backup';
|
||||
import { InputNumber, Badge } from 'antd';
|
||||
import { InputNumber, Badge, message as $message } from 'antd';
|
||||
import { toJS } from 'mobx';
|
||||
import { FormAction } from 'containers/Action';
|
||||
import classnames from 'classnames';
|
||||
import { isFinite } from 'lodash';
|
||||
import { isEmpty, isObject } from 'lodash';
|
||||
import {
|
||||
getImageSystemTabs,
|
||||
getImageOS,
|
||||
@ -94,8 +94,7 @@ export class Create extends FormAction {
|
||||
}
|
||||
|
||||
get errorText() {
|
||||
const { status } = this.state;
|
||||
if (status === 'error') {
|
||||
if (this.msg) {
|
||||
return t(
|
||||
'Unable to create volume: insufficient quota to create resources.'
|
||||
);
|
||||
@ -103,6 +102,78 @@ export class Create extends FormAction {
|
||||
return super.errorText;
|
||||
}
|
||||
|
||||
get showQuota() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getVolumeQuota() {
|
||||
const quotaAll = toJS(this.volumeStore.quotaSet) || {};
|
||||
if (isEmpty(quotaAll)) {
|
||||
return [];
|
||||
}
|
||||
Object.values(quotaAll).forEach((it) => {
|
||||
if (isObject(it)) {
|
||||
it.used = it.in_use;
|
||||
}
|
||||
});
|
||||
const { volume_type } = this.state;
|
||||
const { name } = volume_type || {};
|
||||
const result = {
|
||||
volumes: quotaAll.volumes,
|
||||
gigabytes: quotaAll.gigabytes,
|
||||
};
|
||||
if (name) {
|
||||
result[`volumes_${name}`] = quotaAll[`volumes_${name}`];
|
||||
result[`gigabytes_${name}`] = quotaAll[`gigabytes_${name}`];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
get quotaInfo() {
|
||||
const quota = this.getVolumeQuota();
|
||||
const { volumes = {}, gigabytes = {} } = quota;
|
||||
const { limit } = volumes || {};
|
||||
if (!limit) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { volume_type, size = 0, count = 1 } = this.state;
|
||||
const { name } = volume_type || {};
|
||||
const volume = {
|
||||
...volumes,
|
||||
add: count,
|
||||
name: 'volume',
|
||||
title: t('Volume'),
|
||||
};
|
||||
const sizeInfo = {
|
||||
...gigabytes,
|
||||
add: count * size,
|
||||
name: 'gigabytes',
|
||||
title: t('volume gigabytes'),
|
||||
type: 'line',
|
||||
};
|
||||
if (!name) {
|
||||
return [volume, sizeInfo];
|
||||
}
|
||||
const typeQuota = quota[`volumes_${name}`] || {};
|
||||
const typeSizeQuota = quota[`gigabytes_${name}`] || {};
|
||||
const detailInfo = {
|
||||
...typeQuota,
|
||||
add: count,
|
||||
name: `volumes_${name}`,
|
||||
title: t('{name} type', { name }),
|
||||
type: 'line',
|
||||
};
|
||||
const detailSizeInfo = {
|
||||
...typeSizeQuota,
|
||||
add: count * size,
|
||||
name: `gigabytes_${name}`,
|
||||
title: t('{name} type gigabytes', { name }),
|
||||
type: 'line',
|
||||
};
|
||||
return [volume, sizeInfo, detailInfo, detailSizeInfo];
|
||||
}
|
||||
|
||||
get defaultValue() {
|
||||
const size = this.quotaIsLimit && this.maxSize < 10 ? this.maxSize : 10;
|
||||
const { initVolumeType } = this.state;
|
||||
@ -167,23 +238,19 @@ export class Create extends FormAction {
|
||||
}
|
||||
|
||||
get quota() {
|
||||
const { volumes: { limit = 10, in_use = 0 } = {} } =
|
||||
toJS(this.volumeStore.quotaSet) || {};
|
||||
if (limit === -1) {
|
||||
return Infinity;
|
||||
}
|
||||
return limit - in_use;
|
||||
const { volumes = {} } = this.getVolumeQuota();
|
||||
return volumes;
|
||||
}
|
||||
|
||||
get quotaIsLimit() {
|
||||
const { gigabytes: { limit } = {} } = toJS(this.volumeStore.quotaSet) || {};
|
||||
const { gigabytes: { limit } = {} } = this.getVolumeQuota();
|
||||
return limit !== -1;
|
||||
}
|
||||
|
||||
get maxSize() {
|
||||
const { gigabytes: { limit = 10, in_use = 0 } = {} } =
|
||||
toJS(this.volumeStore.quotaSet) || {};
|
||||
return limit - in_use;
|
||||
const { gigabytes: { limit = 10, in_use = 0, reserved = 0 } = {} } =
|
||||
this.getVolumeQuota();
|
||||
return limit !== -1 ? limit - in_use - reserved : 1000;
|
||||
}
|
||||
|
||||
getAvailZones() {
|
||||
@ -204,6 +271,7 @@ export class Create extends FormAction {
|
||||
this.setState(
|
||||
{
|
||||
initVolumeType,
|
||||
volume_type: types[0],
|
||||
},
|
||||
() => {
|
||||
this.updateFormValue('volume_type', initVolumeType);
|
||||
@ -325,7 +393,7 @@ export class Create extends FormAction {
|
||||
};
|
||||
|
||||
get nameForStateUpdate() {
|
||||
return ['source', 'image', 'snapshot'];
|
||||
return ['source', 'image', 'snapshot', 'size', 'volume_type'];
|
||||
}
|
||||
|
||||
get formItems() {
|
||||
@ -481,20 +549,9 @@ export class Create extends FormAction {
|
||||
onCountChangeCallback() {}
|
||||
|
||||
onCountChange = (value) => {
|
||||
let msg = t('Quota: Project quotas sufficient resources can be created');
|
||||
let status = 'success';
|
||||
if (isFinite(this.quota) && value > this.quota) {
|
||||
msg = t(
|
||||
'Quota: Insufficient quota to create resources, please adjust resource quantity or quota(left { quota }, input { input }).',
|
||||
{ quota: this.quota, input: value }
|
||||
);
|
||||
status = 'error';
|
||||
}
|
||||
this.msg = msg;
|
||||
this.setState(
|
||||
{
|
||||
count: value,
|
||||
status,
|
||||
},
|
||||
() => {
|
||||
if (this.onCountChangeCallback) {
|
||||
@ -504,23 +561,77 @@ export class Create extends FormAction {
|
||||
);
|
||||
};
|
||||
|
||||
checkQuotaDetail = (quota) => {
|
||||
if (!quota || isEmpty(quota)) {
|
||||
return true;
|
||||
}
|
||||
const { limit, add, reserved = 0 } = quota || {};
|
||||
if (limit === -1) {
|
||||
return true;
|
||||
}
|
||||
return limit >= add + reserved;
|
||||
};
|
||||
|
||||
getQuotaErrorMsg = (quota) => {
|
||||
const { limit, used, reserved, add, title } = quota;
|
||||
const left = limit - used - reserved;
|
||||
return t(
|
||||
'Quota: Insufficient { name } quota to create resources, please adjust resource quantity or quota(left { left }, input { input }).',
|
||||
{
|
||||
name: title,
|
||||
left,
|
||||
input: add,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
checkQuotaAll = () => {
|
||||
const results = this.quotaInfo;
|
||||
if (!results.length) {
|
||||
return '';
|
||||
}
|
||||
const [quota = {}, sizeQuota = {}, typeQuota = {}, typeSizeQuota = {}] =
|
||||
results;
|
||||
let msg = '';
|
||||
const quotas = [quota, sizeQuota, typeQuota, typeSizeQuota];
|
||||
const errorQuota = quotas.find((it) => !this.checkQuotaDetail(it));
|
||||
if (errorQuota) {
|
||||
msg = this.getQuotaErrorMsg(errorQuota);
|
||||
}
|
||||
return msg;
|
||||
};
|
||||
|
||||
renderBadge() {
|
||||
const { status } = this.state;
|
||||
if (status === 'success') {
|
||||
const msg = this.checkQuotaAll();
|
||||
if (!msg) {
|
||||
this.msg = '';
|
||||
return null;
|
||||
}
|
||||
return <Badge status={status} text={this.msg} />;
|
||||
if (msg && this.msg !== msg) {
|
||||
$message.error(msg);
|
||||
this.msg = msg;
|
||||
}
|
||||
return <Badge status="error" text={msg} />;
|
||||
}
|
||||
|
||||
renderExtra() {
|
||||
return this.renderBadge();
|
||||
}
|
||||
|
||||
getCountMax = () => {
|
||||
const { limit, used, reserved } = this.quota;
|
||||
if (!limit || limit === -1) {
|
||||
return 100;
|
||||
}
|
||||
return limit - used - reserved;
|
||||
};
|
||||
|
||||
renderFooterLeft() {
|
||||
const { count = 1 } = this.state;
|
||||
const max = this.getCountMax();
|
||||
const configs = {
|
||||
min: 1,
|
||||
max: 100,
|
||||
max,
|
||||
precision: 0,
|
||||
onChange: this.onCountChange,
|
||||
formatter: (value) => `$ ${value}`.replace(/\D/g, ''),
|
||||
@ -539,9 +650,9 @@ export class Create extends FormAction {
|
||||
}
|
||||
|
||||
onSubmit = (data) => {
|
||||
const { count, status } = this.state;
|
||||
if (status === 'error') {
|
||||
return Promise.reject();
|
||||
const { count } = this.state;
|
||||
if (this.msg) {
|
||||
return Promise.reject(this.msg);
|
||||
}
|
||||
const {
|
||||
backup,
|
||||
|
Loading…
Reference in New Issue
Block a user