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,