mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 03:36:04 +08:00
Add Dashboard page
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
export function hashSHA512(value, getters, errors, validatorArgs, displayKey) {
|
||||
if (!/^[a-f0-9]{128}$/i.test(value)) {
|
||||
errors.push(getters['i18n/t']('harvester.validation.hash.sha512'));
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { imageUrl, fileRequired } from './vm-image';
|
||||
import { vmNetworks, vmDisks } from './vm';
|
||||
import { dataVolumeSize } from './vm-datavolumes';
|
||||
import { backupTarget, ntpServers } from './setting';
|
||||
import { volumeSize } from './volume';
|
||||
import { rancherMonitoring, rancherLogging } from './monitoringAndLogging';
|
||||
import { ranges } from './network';
|
||||
import { hashSHA512 } from './hash';
|
||||
|
||||
export default {
|
||||
imageUrl,
|
||||
dataVolumeSize,
|
||||
vmNetworks,
|
||||
vmDisks,
|
||||
fileRequired,
|
||||
backupTarget,
|
||||
ntpServers,
|
||||
volumeSize,
|
||||
rancherMonitoring,
|
||||
rancherLogging,
|
||||
ranges,
|
||||
hashSHA512,
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
import jsyaml from 'js-yaml';
|
||||
import { get } from '@shell/utils/object';
|
||||
|
||||
export function rancherMonitoring(valuesContent, getters, errors, validatorArgs) {
|
||||
const valueJson = jsyaml.load(valuesContent);
|
||||
const requiredFields = [
|
||||
{
|
||||
path: 'prometheus.prometheusSpec.scrapeInterval',
|
||||
translationKey: 'monitoring.prometheus.config.scrape'
|
||||
},
|
||||
{
|
||||
path: 'prometheus.prometheusSpec.evaluationInterval',
|
||||
translationKey: 'monitoring.prometheus.config.evaluation'
|
||||
},
|
||||
{
|
||||
path: 'prometheus.prometheusSpec.retention',
|
||||
translationKey: 'monitoring.prometheus.config.retention'
|
||||
},
|
||||
{
|
||||
path: 'prometheus.prometheusSpec.retentionSize',
|
||||
translationKey: 'monitoring.prometheus.config.retentionSize'
|
||||
},
|
||||
{
|
||||
path: 'prometheus.prometheusSpec.resources.requests.cpu',
|
||||
translationKey: 'monitoring.prometheus.config.requests.cpu'
|
||||
},
|
||||
{
|
||||
path: 'prometheus.prometheusSpec.resources.requests.memory',
|
||||
translationKey: 'monitoring.prometheus.config.requests.memory'
|
||||
},
|
||||
{
|
||||
path: 'prometheus.prometheusSpec.resources.limits.cpu',
|
||||
translationKey: 'monitoring.prometheus.config.limits.cpu'
|
||||
},
|
||||
{
|
||||
path: 'prometheus.prometheusSpec.resources.limits.memory',
|
||||
translationKey: 'monitoring.prometheus.config.limits.memory'
|
||||
},
|
||||
{
|
||||
path: 'prometheus-node-exporter.resources.requests.cpu',
|
||||
translationKey: 'monitoring.prometheus.config.requests.cpu'
|
||||
},
|
||||
{
|
||||
path: 'prometheus-node-exporter.resources.requests.memory',
|
||||
translationKey: 'monitoring.prometheus.config.requests.memory'
|
||||
},
|
||||
{
|
||||
path: 'prometheus-node-exporter.resources.limits.cpu',
|
||||
translationKey: 'monitoring.prometheus.config.limits.cpu'
|
||||
},
|
||||
{
|
||||
path: 'prometheus-node-exporter.resources.limits.memory',
|
||||
translationKey: 'monitoring.prometheus.config.limits.memory'
|
||||
},
|
||||
{
|
||||
path: 'grafana.resources.requests.cpu',
|
||||
translationKey: 'monitoring.prometheus.config.requests.cpu'
|
||||
},
|
||||
{
|
||||
path: 'grafana.resources.requests.memory',
|
||||
translationKey: 'monitoring.prometheus.config.requests.memory'
|
||||
},
|
||||
{
|
||||
path: 'grafana.resources.limits.cpu',
|
||||
translationKey: 'monitoring.prometheus.config.limits.cpu'
|
||||
},
|
||||
{
|
||||
path: 'grafana.resources.limits.memory',
|
||||
translationKey: 'monitoring.prometheus.config.limits.memory'
|
||||
},
|
||||
{
|
||||
path: 'alertmanager.alertmanagerSpec.retention',
|
||||
translationKey: 'monitoring.prometheus.config.retention'
|
||||
},
|
||||
{
|
||||
path: 'alertmanager.alertmanagerSpec.resources.requests.cpu',
|
||||
translationKey: 'monitoring.prometheus.config.requests.cpu'
|
||||
},
|
||||
{
|
||||
path: 'alertmanager.alertmanagerSpec.resources.requests.memory',
|
||||
translationKey: 'monitoring.prometheus.config.requests.memory'
|
||||
},
|
||||
{
|
||||
path: 'alertmanager.alertmanagerSpec.resources.limits.cpu',
|
||||
translationKey: 'monitoring.prometheus.config.limits.cpu'
|
||||
},
|
||||
{
|
||||
path: 'alertmanager.alertmanagerSpec.resources.limits.memory',
|
||||
translationKey: 'monitoring.prometheus.config.limits.memory'
|
||||
},
|
||||
];
|
||||
|
||||
requiredFields.forEach((rule) => {
|
||||
if (!get(valueJson, rule.path)) {
|
||||
errors.push(getters['i18n/t']('validation.required', { key: getters['i18n/t'](rule.translationKey) }));
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function rancherLogging(valuesContent, getters, errors, validatorArgs) {
|
||||
const valueJson = jsyaml.load(valuesContent);
|
||||
const requiredFields = [
|
||||
{
|
||||
path: 'fluentbit.resources.requests.cpu',
|
||||
translationKey: 'monitoring.prometheus.config.requests.cpu'
|
||||
},
|
||||
{
|
||||
path: 'fluentbit.resources.requests.memory',
|
||||
translationKey: 'monitoring.prometheus.config.requests.memory'
|
||||
},
|
||||
{
|
||||
path: 'fluentbit.resources.limits.cpu',
|
||||
translationKey: 'monitoring.prometheus.config.limits.cpu'
|
||||
},
|
||||
{
|
||||
path: 'fluentbit.resources.limits.memory',
|
||||
translationKey: 'monitoring.prometheus.config.limits.memory'
|
||||
},
|
||||
{
|
||||
path: 'fluentd.resources.requests.cpu',
|
||||
translationKey: 'monitoring.prometheus.config.requests.cpu'
|
||||
},
|
||||
{
|
||||
path: 'fluentd.resources.requests.memory',
|
||||
translationKey: 'monitoring.prometheus.config.requests.memory'
|
||||
},
|
||||
{
|
||||
path: 'fluentd.resources.limits.cpu',
|
||||
translationKey: 'monitoring.prometheus.config.limits.cpu'
|
||||
},
|
||||
{
|
||||
path: 'fluentd.resources.limits.memory',
|
||||
translationKey: 'monitoring.prometheus.config.limits.memory'
|
||||
},
|
||||
];
|
||||
|
||||
requiredFields.forEach((rule) => {
|
||||
if (!get(valueJson, rule.path)) {
|
||||
errors.push(getters['i18n/t']('validation.required', { key: getters['i18n/t'](rule.translationKey) }));
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export function ranges(ranges = [], getters, errors, validatorArgs) {
|
||||
const t = getters['i18n/t'];
|
||||
|
||||
if (ranges.length === 0) {
|
||||
errors.push(t('validation.required', { key: t('harvester.ipPool.tabs.range') }, true));
|
||||
}
|
||||
|
||||
ranges.map((r) => {
|
||||
if (!r.subnet) {
|
||||
errors.push(t('validation.required', { key: t('harvester.ipPool.subnet.label') }, true));
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
export function backupTarget(value, getters, errors, validatorArgs) {
|
||||
const t = getters['i18n/t'];
|
||||
|
||||
if (!value) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
const parseValue = JSON.parse(value);
|
||||
const type = parseValue.type;
|
||||
|
||||
if (!type) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (type === 's3') {
|
||||
if (!parseValue.accessKeyId) {
|
||||
errors.push(t('validation.required', { key: 'accessKeyId' }));
|
||||
}
|
||||
|
||||
if (!parseValue.secretAccessKey) {
|
||||
errors.push(t('validation.required', { key: 'secretAccessKey' }));
|
||||
}
|
||||
|
||||
if (!parseValue.bucketRegion) {
|
||||
errors.push(t('validation.required', { key: 'bucketRegion' }));
|
||||
}
|
||||
|
||||
if (!parseValue.bucketName) {
|
||||
errors.push(t('validation.required', { key: 'bucketName' }));
|
||||
}
|
||||
}
|
||||
|
||||
if (!parseValue.endpoint && type !== 's3') {
|
||||
errors.push(t('validation.required', { key: 'endpoint' }));
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function ntpServers(value, getters, errors, validatorArgs) {
|
||||
const { ntpServers } = JSON.parse(value);
|
||||
const t = getters['i18n/t'];
|
||||
const ipv4Regex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
||||
const hostRegex = /^(?!:\/\/)(?:[a-zA-Z0-9-]{1,63}\.)+[a-zA-Z]{2,63}$/;
|
||||
|
||||
if (!ntpServers) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
const ntpServersSet = new Set(ntpServers);
|
||||
|
||||
if (ntpServers.length !== ntpServersSet.size) {
|
||||
errors.push(t('harvester.setting.ntpServers.isDuplicate'));
|
||||
}
|
||||
|
||||
if (ntpServers.find(V => !ipv4Regex.test(V) && !hostRegex.test(V))) {
|
||||
errors.push(t('harvester.setting.ntpServers.isNotIPV4'));
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { formatSi, parseSi } from '@shell/utils/units';
|
||||
|
||||
export function dataVolumeSize(storage, getters, errors, validatorArgs) {
|
||||
const t = getters['i18n/t'];
|
||||
|
||||
if (!storage || storage === '') {
|
||||
const key = t('harvester.volume.size');
|
||||
|
||||
errors.push(t('validation.required', { key }));
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
const size = getSize(storage);
|
||||
const max = 999999;
|
||||
const integerRegex = /^[1-9]\d*$/;
|
||||
|
||||
if (!integerRegex.test(size) || size > max) {
|
||||
errors.push(t('harvester.validation.volume.sizeRange'));
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function getSize(storage) {
|
||||
if (!storage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const kibUnitSize = parseSi(storage);
|
||||
|
||||
return formatSi(kibUnitSize, {
|
||||
addSuffix: false,
|
||||
increment: 1024,
|
||||
minExponent: 3,
|
||||
maxExponent: 3
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { HCI } from '@pkg/harvester/config/labels-annotations';
|
||||
|
||||
export const VM_IMAGE_FILE_FORMAT = ['qcow', 'qcow2', 'raw', 'img', 'iso'];
|
||||
|
||||
export function imageUrl(url, getters, errors, validatorArgs, type) {
|
||||
const t = getters['i18n/t'];
|
||||
|
||||
if (!url || url === '') {
|
||||
return errors;
|
||||
}
|
||||
|
||||
const suffixName = url.split('/').pop();
|
||||
const fileSuffix = suffixName.split('.').pop().toLowerCase();
|
||||
|
||||
if (!VM_IMAGE_FILE_FORMAT.includes(fileSuffix)) {
|
||||
const tipString = type === 'file' ? 'harvester.validation.image.ruleFileTip' : 'harvester.validation.image.ruleTip';
|
||||
|
||||
errors.push(t(tipString));
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function fileRequired(annotations = {}, getters, errors, validatorArgs, type) {
|
||||
const t = getters['i18n/t'];
|
||||
|
||||
if (!annotations[HCI.IMAGE_NAME]) {
|
||||
errors.push(t('validation.required', { key: t('harvester.image.fileName') }));
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import { PVC } from '@shell/config/types';
|
||||
import { isValidMac, isValidDNSLabelName } from '@pkg/utils/regular';
|
||||
import { SOURCE_TYPE } from '@pkg/config/harvester-map';
|
||||
import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
|
||||
|
||||
const maxNameLength = 63;
|
||||
|
||||
export function vmNetworks(spec, getters, errors, validatorArgs) {
|
||||
const { domain: { devices: { interfaces } }, networks } = spec;
|
||||
|
||||
const networkNames = [];
|
||||
|
||||
interfaces.map( (I, index) => {
|
||||
const N = networks.find( N => I.name === N.name);
|
||||
const prefix = (I.name || N.name) || `Network ${ index + 1 }`;
|
||||
|
||||
const type = getters['i18n/t']('harvester.fields.network');
|
||||
|
||||
const lowerType = getters['i18n/t']('harvester.validation.vm.network.lowerType');
|
||||
const upperType = getters['i18n/t']('harvester.validation.vm.network.upperType');
|
||||
|
||||
validName(getters, errors, I.name, networkNames, prefix, type, lowerType, upperType);
|
||||
|
||||
if (N.multus) {
|
||||
if (!N.multus.networkName) {
|
||||
const key = getters['i18n/t']('harvester.fields.network');
|
||||
const message = getters['i18n/t']('validation.required', { key });
|
||||
|
||||
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||
}
|
||||
}
|
||||
|
||||
if (I.macAddress && !isValidMac(I.macAddress) && !N.pod) {
|
||||
const message = getters['i18n/t']('harvester.validation.vm.network.macFormat');
|
||||
|
||||
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value) {
|
||||
const isVMTemplate = validatorArgs.includes('isVMTemplate');
|
||||
const data = isVMTemplate ? this.value.spec.vm : value;
|
||||
|
||||
const _volumeClaimTemplates = parseVolumeClaimTemplates(data);
|
||||
|
||||
const _volumes = spec.template.spec.volumes || [];
|
||||
const _disks = spec.template.spec.domain.devices.disks || [];
|
||||
|
||||
const diskNames = [];
|
||||
|
||||
_disks.forEach((D, idx) => {
|
||||
const prefix = D.name || _volumes[idx]?.name || `Volume ${ idx + 1 }`;
|
||||
|
||||
if (!D.disk && !D.cdrom) {
|
||||
const key = getters['i18n/t']('harvester.fields.type');
|
||||
const message = getters['i18n/t']('validation.required', { key });
|
||||
|
||||
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||
}
|
||||
|
||||
const type = getters['i18n/t']('harvester.fields.volume');
|
||||
const lowerType = getters['i18n/t']('harvester.validation.vm.volume.lowerType');
|
||||
const upperType = getters['i18n/t']('harvester.validation.vm.volume.upperType');
|
||||
|
||||
validName(getters, errors, D.name, diskNames, prefix, type, lowerType, upperType);
|
||||
});
|
||||
|
||||
let requiredVolume = false;
|
||||
|
||||
_volumes.forEach((V, idx) => {
|
||||
const { type, typeValue } = getVolumeType(getters, V, _volumeClaimTemplates, value);
|
||||
|
||||
const prefix = V.name || idx + 1;
|
||||
|
||||
if ([SOURCE_TYPE.IMAGE, SOURCE_TYPE.ATTACH_VOLUME, SOURCE_TYPE.CONTAINER].includes(type)) {
|
||||
requiredVolume = true;
|
||||
}
|
||||
|
||||
if (type === SOURCE_TYPE.NEW || type === SOURCE_TYPE.IMAGE) {
|
||||
if (!/([1-9]|[1-9][0-9]+)[a-zA-Z]+/.test(typeValue?.spec?.resources?.requests?.storage)) {
|
||||
const key = getters['i18n/t']('harvester.fields.size');
|
||||
const message = getters['i18n/t']('validation.required', { key });
|
||||
|
||||
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||
}
|
||||
|
||||
if (typeValue?.spec?.resources?.requests?.storage && !/^([1-9][0-9]{0,8})[a-zA-Z]+$/.test(typeValue?.spec?.resources?.requests?.storage)) {
|
||||
const message = getters['i18n/t']('harvester.validation.generic.maximumSize', { max: '999999999 GiB' });
|
||||
|
||||
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||
}
|
||||
|
||||
if (type === SOURCE_TYPE.IMAGE && !typeValue?.spec?.storageClassName && !isVMTemplate) { // type === SOURCE_TYPE.IMAGE
|
||||
const key = getters['i18n/t']('harvester.fields.image');
|
||||
const message = getters['i18n/t']('validation.required', { key });
|
||||
|
||||
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||
}
|
||||
|
||||
if (!typeValue?.spec?.storageClassName && V?.persistentVolumeClaim?.claimName && type !== SOURCE_TYPE.IMAGE) {
|
||||
const key = getters['i18n/t']('harvester.fields.storageClass');
|
||||
const message = getters['i18n/t']('validation.required', { key });
|
||||
|
||||
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||
}
|
||||
}
|
||||
|
||||
if (type === SOURCE_TYPE.ATTACH_VOLUME) {
|
||||
const allPVCs = getters['harvester/all'](PVC);
|
||||
|
||||
const selectedVolumeName = V?.persistentVolumeClaim?.claimName;
|
||||
const hasExistingVolume = allPVCs.find(P => P.id === `${ value.metadata.namespace }/${ selectedVolumeName }`);
|
||||
|
||||
if (!hasExistingVolume && selectedVolumeName) { // selected volume may have been deleted. e.g: use template
|
||||
const type = getters['i18n/t']('harvester.fields.volume');
|
||||
|
||||
errors.push(getters['i18n/t']('harvester.validation.generic.hasDelete', { type, name: selectedVolumeName }));
|
||||
}
|
||||
|
||||
if (!selectedVolumeName) { // volume is not selected.
|
||||
const key = getters['i18n/t']('harvester.virtualMachine.volume.volume');
|
||||
|
||||
errors.push(getters['i18n/t']('validation.required', { key }));
|
||||
}
|
||||
}
|
||||
|
||||
if (type === SOURCE_TYPE.CONTAINER && !V.containerDisk.image) {
|
||||
const key = getters['i18n/t']('harvester.fields.dockerImage');
|
||||
const message = getters['i18n/t']('validation.required', { key });
|
||||
|
||||
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* At least one volume must be create. (Verify only when create.)
|
||||
*/
|
||||
if ((!requiredVolume || _volumes.length === 0) && !value.links) {
|
||||
errors.push(getters['i18n/t']('harvester.validation.vm.volume.needImageOrExisting'));
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function getVolumeType(getters, V, DVTS, value) {
|
||||
let outValue = null;
|
||||
const allPVCs = getters['harvester/all'](PVC);
|
||||
|
||||
if (V.persistentVolumeClaim) {
|
||||
const selectedVolumeName = V?.persistentVolumeClaim?.claimName;
|
||||
const hasExistingVolume = allPVCs.find(P => P.id === `${ value.metadata.namespace }/${ selectedVolumeName }`);
|
||||
|
||||
if (hasExistingVolume) {
|
||||
// In other cases, claimName will not be empty, so we can judge whether this is an exiting volume based on this attribute
|
||||
return {
|
||||
type: SOURCE_TYPE.ATTACH_VOLUME,
|
||||
typeValue: null
|
||||
};
|
||||
}
|
||||
|
||||
outValue = DVTS.find((DVT) => {
|
||||
return V.persistentVolumeClaim.claimName === DVT.metadata.name && DVT.metadata?.annotations && Object.prototype.hasOwnProperty.call(DVT.metadata.annotations, 'harvesterhci.io/imageId');
|
||||
});
|
||||
|
||||
if (outValue) {
|
||||
return {
|
||||
type: SOURCE_TYPE.IMAGE,
|
||||
typeValue: outValue
|
||||
};
|
||||
}
|
||||
|
||||
// new type
|
||||
outValue = DVTS.find(DVT => V.persistentVolumeClaim.claimName === DVT.metadata.name);
|
||||
|
||||
if (outValue) {
|
||||
return {
|
||||
type: SOURCE_TYPE.NEW,
|
||||
typeValue: outValue
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (V.containerDisk) {
|
||||
return {
|
||||
type: SOURCE_TYPE.CONTAINER,
|
||||
typeValue: null
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function validName(getters, errors, name, names = [], prefix, type, lowerType, upperType) {
|
||||
// Verify that the name is duplicate
|
||||
if (names.findIndex( N => name === N) !== -1) {
|
||||
errors.push(getters['i18n/t']('harvester.validation.vm.duplicatedName', { type, name }));
|
||||
}
|
||||
|
||||
names.push(name);
|
||||
|
||||
// The maximum length of volume name is 63 characters.
|
||||
if (name && name?.length > maxNameLength) {
|
||||
const key = getters['i18n/t']('harvester.fields.name');
|
||||
const message = getters['i18n/t']('harvester.validation.generic.maxLength', { key, max: maxNameLength });
|
||||
|
||||
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||
}
|
||||
|
||||
// name required
|
||||
if (!name) {
|
||||
const key = getters['i18n/t']('harvester.fields.name');
|
||||
const message = getters['i18n/t']('validation.required', { key });
|
||||
|
||||
errors.push(getters['i18n/t']('harvester.validation.generic.tabError', { prefix, message }));
|
||||
}
|
||||
|
||||
// valid RFC 1123
|
||||
if (!isValidDNSLabelName(name)) {
|
||||
const regex = '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$';
|
||||
|
||||
errors.push(getters['i18n/t']('harvester.validation.generic.regex', {
|
||||
lowerType, name, regex, upperType
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export function volumeSize(size, getters, errors, validatorArgs, displayKey, value) {
|
||||
if (!/^([1-9][0-9]{0,8})[a-zA-Z]+$/.test(size)) {
|
||||
const message = getters['i18n/t']('harvester.validation.generic.maximumSize', { max: '999999999 GiB' });
|
||||
|
||||
errors.push(message);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
Reference in New Issue
Block a user