diff --git a/src/components/Form/index.jsx b/src/components/Form/index.jsx
index 59fd6c7c..5ee223e2 100644
--- a/src/components/Form/index.jsx
+++ b/src/components/Form/index.jsx
@@ -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 ;
+ }
+
+ renderRightTopExtra() {
+ const content = this.renderQuota();
+ if (!content) {
+ return null;
+ }
+ const checkValue = JSON.stringify(this.quotaInfo);
+ return (
+
+
+
+ );
+ }
+
render() {
const wrapperPadding =
this.listUrl || this.isStep || (this.isModal && this.tips)
@@ -646,6 +685,7 @@ export default class BaseForm extends React.Component {
>
{tips}
+ {this.renderRightTopExtra()}
{this.renderForms()}
diff --git a/src/components/Form/index.less b/src/components/Form/index.less
index 30e16713..96d0e692 100644
--- a/src/components/Form/index.less
+++ b/src/components/Form/index.less
@@ -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;
+ }
+ }
+ }
+}
diff --git a/src/components/QuotaChart/Info.jsx b/src/components/QuotaChart/Info.jsx
index fa5a5c47..5eb2e097 100644
--- a/src/components/QuotaChart/Info.jsx
+++ b/src/components/QuotaChart/Info.jsx
@@ -52,6 +52,7 @@ export default function QuotaInfo(props) {
pagination={false}
title={() => fullTitle}
bordered
+ size="small"
/>
);
diff --git a/src/components/QuotaChart/Line.jsx b/src/components/QuotaChart/Line.jsx
index 7871d3d8..0ebfaac8 100644
--- a/src/components/QuotaChart/Line.jsx
+++ b/src/components/QuotaChart/Line.jsx
@@ -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 = (
{`${title} ${secondTitle}: `}{' '}
- {`${allCount}/${limit}`}
+ {`${allCount}/${limitStr}`}
);
- const progress = (
+ const progress = isLimit ? (
+ ) : (
+
);
return (
diff --git a/src/components/QuotaChart/Ring.jsx b/src/components/QuotaChart/Ring.jsx
index 038b99f0..4986ae31 100644
--- a/src/components/QuotaChart/Ring.jsx
+++ b/src/components/QuotaChart/Ring.jsx
@@ -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 (
-
+
+
{/* 绘制图形 */}
@@ -122,7 +129,7 @@ export default function Ring(props) {
/>
;
}
if (type === 'ring') {
diff --git a/src/locales/en.json b/src/locales/en.json
index 74428d8e..68b37024 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -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.",
diff --git a/src/locales/zh.json b/src/locales/zh.json
index bf325f90..da495752 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -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} 无法获取",
diff --git a/src/pages/storage/containers/Volume/actions/Create/index.jsx b/src/pages/storage/containers/Volume/actions/Create/index.jsx
index 9734bce6..fd074c62 100644
--- a/src/pages/storage/containers/Volume/actions/Create/index.jsx
+++ b/src/pages/storage/containers/Volume/actions/Create/index.jsx
@@ -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 ;
+ if (msg && this.msg !== msg) {
+ $message.error(msg);
+ this.msg = msg;
+ }
+ return ;
}
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,