mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-30 22:50:10 +03:00
parent
7aaec867e7
commit
705e9779ee
@ -40,6 +40,11 @@ oneprovision_optional_create_command: ''
|
||||
# port: 4822
|
||||
# host: '127.0.0.1'
|
||||
|
||||
# Currency formatting
|
||||
# Possible values are the ISO 4217 currency codes
|
||||
# https://www.six-group.com/en/products-services/financial-information/data-standards.html
|
||||
currency: 'EUR'
|
||||
|
||||
# Translations: use it if you want to use certain languages on the client.
|
||||
# Check that the language exists in client/assets/languages
|
||||
# langs:
|
||||
|
@ -74,6 +74,7 @@ dialogs:
|
||||
information: true
|
||||
ownership: true
|
||||
capacity: true
|
||||
showback: true
|
||||
vm_group: true
|
||||
vcenter:
|
||||
enabled: true
|
||||
|
@ -81,7 +81,9 @@ const TextController = memo(
|
||||
}),
|
||||
}}
|
||||
error={Boolean(error)}
|
||||
helperText={Boolean(error) && <ErrorHelper label={error?.message} />}
|
||||
helperText={
|
||||
error ? <ErrorHelper label={error?.message} /> : fieldProps.helperText
|
||||
}
|
||||
FormHelperTextProps={{ 'data-cy': `${cy}-error` }}
|
||||
{...fieldProps}
|
||||
/>
|
||||
@ -91,7 +93,8 @@ const TextController = memo(
|
||||
prevProps.type === nextProps.type &&
|
||||
prevProps.label === nextProps.label &&
|
||||
prevProps.tooltip === nextProps.tooltip &&
|
||||
prevProps.fieldProps?.value === nextProps.fieldProps?.value
|
||||
prevProps.fieldProps?.value === nextProps.fieldProps?.value &&
|
||||
prevProps.fieldProps?.helperText === nextProps.fieldProps?.helperText
|
||||
)
|
||||
|
||||
TextController.propTypes = {
|
||||
|
@ -20,93 +20,84 @@ import { T, INPUT_TYPES, HYPERVISORS } from 'client/constants'
|
||||
|
||||
const { vcenter, lxc, firecracker } = HYPERVISORS
|
||||
|
||||
const commonOptions = arrayToOptions([T.Yes, T.No], {
|
||||
getValue: (o) => o.toLowerCase(),
|
||||
})
|
||||
|
||||
const commonValidation = string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined)
|
||||
|
||||
/** @type {Field} ACPI field */
|
||||
export const ACPI = {
|
||||
name: 'OS.ACPI',
|
||||
name: 'FEATURES.ACPI',
|
||||
label: T.Acpi,
|
||||
tooltip: T.AcpiConcept,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions([T.Yes, T.No]),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
values: commonOptions,
|
||||
validation: commonValidation,
|
||||
}
|
||||
|
||||
/** @type {Field} PAE field */
|
||||
export const PAE = {
|
||||
name: 'OS.PAE',
|
||||
name: 'FEATURES.PAE',
|
||||
label: T.Pae,
|
||||
tooltip: T.PaeConcept,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions([T.Yes, T.No]),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
values: commonOptions,
|
||||
validation: commonValidation,
|
||||
}
|
||||
|
||||
/** @type {Field} APIC field */
|
||||
export const APIC = {
|
||||
name: 'OS.APIC',
|
||||
name: 'FEATURES.APIC',
|
||||
label: T.Apic,
|
||||
tooltip: T.ApicConcept,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions([T.Yes, T.No]),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
values: commonOptions,
|
||||
validation: commonValidation,
|
||||
}
|
||||
|
||||
/** @type {Field} HYPER-V field */
|
||||
export const HYPERV = {
|
||||
name: 'OS.HYPERV',
|
||||
name: 'FEATURES.HYPERV',
|
||||
label: T.Hyperv,
|
||||
tooltip: T.HypervConcept,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions([T.Yes, T.No]),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
values: commonOptions,
|
||||
validation: commonValidation,
|
||||
}
|
||||
|
||||
/** @type {Field} Local time field */
|
||||
export const LOCALTIME = {
|
||||
name: 'OS.LOCALTIME',
|
||||
name: 'FEATURES.LOCALTIME',
|
||||
label: T.Localtime,
|
||||
tooltip: T.LocaltimeConcept,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions([T.Yes, T.No]),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
values: commonOptions,
|
||||
validation: commonValidation,
|
||||
}
|
||||
|
||||
/** @type {Field} Guest agent field */
|
||||
export const GUEST_AGENT = {
|
||||
name: 'OS.GUEST_AGENT',
|
||||
name: 'FEATURES.GUEST_AGENT',
|
||||
label: T.GuestAgent,
|
||||
tooltip: T.GuestAgentConcept,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: arrayToOptions([T.Yes, T.No]),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
values: commonOptions,
|
||||
validation: commonValidation,
|
||||
}
|
||||
|
||||
/** @type {Field} Virtio-SCSI queues field */
|
||||
export const VIRTIO_SCSI_QUEUES = {
|
||||
name: 'OS.VIRTIO_SCSI_QUEUES',
|
||||
name: 'FEATURES.VIRTIO_SCSI_QUEUES',
|
||||
label: T.VirtioQueues,
|
||||
tooltip: T.VirtioQueuesConcept,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
@ -115,21 +106,20 @@ export const VIRTIO_SCSI_QUEUES = {
|
||||
Array.from({ length: 16 }, (_, i) => i + 1),
|
||||
OPTION_SORTERS.numeric
|
||||
),
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => undefined),
|
||||
validation: commonValidation,
|
||||
}
|
||||
|
||||
/** @type {Field} IO threads field */
|
||||
export const IO_THREADS = {
|
||||
name: 'OS.IOTHREADS',
|
||||
name: 'FEATURES.IOTHREADS',
|
||||
label: T.IoThreads,
|
||||
tooltip: T.IoThreadsConcept,
|
||||
notOnHypervisors: [vcenter, lxc, firecracker],
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: number().default(() => undefined),
|
||||
validation: number()
|
||||
.positive()
|
||||
.default(() => undefined),
|
||||
}
|
||||
|
||||
/** @type {Field[]} List of Features fields */
|
||||
|
@ -229,6 +229,9 @@ export const USER_INPUTS_SCHEMA = object({
|
||||
.afterSubmit((_inputsOrder_, { context }) => {
|
||||
const userInputs = context?.extra?.USER_INPUTS
|
||||
|
||||
return userInputs?.map(({ name }) => String(name).toUpperCase()).join(',')
|
||||
return userInputs
|
||||
?.filter(({ name }) => !['MEMORY', 'CPU', 'VCPU'].includes(name))
|
||||
?.map(({ name }) => String(name).toUpperCase())
|
||||
.join(',')
|
||||
}),
|
||||
})
|
||||
|
@ -19,7 +19,10 @@ import {
|
||||
generateModificationInputs,
|
||||
generateHotResizeInputs,
|
||||
generateCapacityInput,
|
||||
generateCostCapacityInput,
|
||||
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/capacityUtils'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { formatNumberByCurrency } from 'client/models/Helper'
|
||||
import { Field } from 'client/utils'
|
||||
import { T, HYPERVISORS } from 'client/constants'
|
||||
|
||||
@ -45,9 +48,9 @@ export const MEMORY = generateCapacityInput({
|
||||
|
||||
/** @type {Field[]} Hot resize on memory field */
|
||||
export const HR_MEMORY_FIELDS = generateHotResizeInputs(
|
||||
{ fieldName: MEMORY.name },
|
||||
{ name: 'MEMORY_HOT_ADD_ENABLED' },
|
||||
{
|
||||
name: `${MEMORY.name}_MAX`,
|
||||
name: 'MEMORY_MAX',
|
||||
label: T.MaxMemory,
|
||||
tooltip: T.MaxMemoryConcept,
|
||||
}
|
||||
@ -87,14 +90,13 @@ export const VIRTUAL_CPU = generateCapacityInput({
|
||||
label: T.VirtualCpu,
|
||||
tooltip: T.VirtualCpuConcept,
|
||||
validation: commonValidation,
|
||||
grid: { md: 3 },
|
||||
})
|
||||
|
||||
/** @type {Field[]} Hot resize on memory field */
|
||||
/** @type {Field[]} Hot resize on CPU field */
|
||||
export const HR_CPU_FIELDS = generateHotResizeInputs(
|
||||
{ name: PHYSICAL_CPU.name },
|
||||
{ name: 'CPU_HOT_ADD_ENABLED' },
|
||||
{
|
||||
name: `${VIRTUAL_CPU.name}_MAX`,
|
||||
name: 'VCPU_MAX',
|
||||
label: T.MaxVirtualCpu,
|
||||
tooltip: T.MaxVirtualCpuConcept,
|
||||
}
|
||||
@ -105,3 +107,83 @@ export const MOD_VCPU_FIELDS = generateModificationInputs(VIRTUAL_CPU.name)
|
||||
|
||||
/** @type {Field[]} List of Virtual CPU fields */
|
||||
export const VCPU_FIELDS = [VIRTUAL_CPU, ...HR_CPU_FIELDS, ...MOD_VCPU_FIELDS]
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Showback fields
|
||||
// --------------------------------------------------------
|
||||
|
||||
/** @type {Field} Memory cost field */
|
||||
export const MEMORY_COST = generateCostCapacityInput({
|
||||
name: 'MEMORY_COST',
|
||||
label: T.Memory,
|
||||
tooltip: T.CostMemoryConcept,
|
||||
dependOf: [MEMORY.name, 'MEMORY_COST'],
|
||||
validation: commonValidation,
|
||||
fieldProps: ([memory, cost] = []) => {
|
||||
const fieldProps = { step: 0.1 }
|
||||
|
||||
if (memory && cost) {
|
||||
const monthCost = formatNumberByCurrency(memory * cost * 24 * 30)
|
||||
fieldProps.helperText = (
|
||||
<Translate word={T.CostEachMonth} values={[monthCost]} />
|
||||
)
|
||||
}
|
||||
|
||||
return fieldProps
|
||||
},
|
||||
})
|
||||
|
||||
/** @type {Field} CPU cost field */
|
||||
export const CPU_COST = generateCostCapacityInput({
|
||||
name: 'CPU_COST',
|
||||
label: T.PhysicalCpu,
|
||||
tooltip: T.CostCpuConcept,
|
||||
dependOf: [PHYSICAL_CPU.name, 'CPU_COST'],
|
||||
validation: commonValidation,
|
||||
fieldProps: ([cpu, cost] = []) => {
|
||||
const fieldProps = { step: 0.1 }
|
||||
|
||||
if (cpu && cost) {
|
||||
const monthCost = formatNumberByCurrency(cpu * cost * 24 * 30)
|
||||
fieldProps.helperText = (
|
||||
<Translate word={T.CostEachMonth} values={[monthCost]} />
|
||||
)
|
||||
}
|
||||
|
||||
return fieldProps
|
||||
},
|
||||
})
|
||||
|
||||
/** @type {Field} Disk cost field */
|
||||
export const DISK_COST = generateCostCapacityInput({
|
||||
name: 'DISK_COST',
|
||||
label: T.Disk,
|
||||
tooltip: T.CostDiskConcept,
|
||||
dependOf: ['$extra.DISK', 'DISK_COST'],
|
||||
validation: (context) =>
|
||||
commonValidation
|
||||
.transform((value) =>
|
||||
// transform the initial value from MB to GB
|
||||
+context?.DISK_COST === +value ? context?.DISK_COST * 1024 : value
|
||||
)
|
||||
.afterSubmit((cost) => (cost ? cost / 1024 : undefined)),
|
||||
fieldProps: ([disks, cost] = []) => {
|
||||
const fieldProps = { step: 0.1 }
|
||||
|
||||
if (disks?.length && cost) {
|
||||
const getSize = (disk) => disk?.IMAGE?.SIZE ?? disk?.SIZE ?? 0
|
||||
const sizesInGB = disks.reduce((res, disk) => res + getSize(disk), 0)
|
||||
const sizesInMB = sizesInGB / 1024
|
||||
const monthCost = formatNumberByCurrency(sizesInMB * cost * 24 * 30)
|
||||
|
||||
fieldProps.helperText = (
|
||||
<Translate word={T.CostEachMonth} values={[monthCost]} />
|
||||
)
|
||||
}
|
||||
|
||||
return fieldProps
|
||||
},
|
||||
})
|
||||
|
||||
/** @type {Field[]} List of showback fields */
|
||||
export const SHOWBACK_FIELDS = [MEMORY_COST, CPU_COST, DISK_COST]
|
||||
|
@ -212,8 +212,8 @@ export const generateHotResizeInputs = (
|
||||
) => [
|
||||
{
|
||||
...hrField,
|
||||
name: `HOT_RESIZE.${hrFieldName}_HOT_ADD_ENABLED`,
|
||||
dependOf: `HOT_RESIZE.${hrFieldName}_HOT_ADD_ENABLED`,
|
||||
name: `HOT_RESIZE.${hrFieldName}`,
|
||||
dependOf: `HOT_RESIZE.${hrFieldName}`,
|
||||
label: T.EnableHotResize,
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().yesOrNo(),
|
||||
@ -222,13 +222,13 @@ export const generateHotResizeInputs = (
|
||||
},
|
||||
{
|
||||
...maxField,
|
||||
dependOf: `HOT_RESIZE.${hrFieldName}_HOT_ADD_ENABLED`,
|
||||
dependOf: `HOT_RESIZE.${hrFieldName}`,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: (enabledHr) => (enabledHr ? 'number' : INPUT_TYPES.HIDDEN),
|
||||
validation: number()
|
||||
.positive()
|
||||
.default(() => undefined)
|
||||
.when(`HOT_RESIZE.${hrFieldName}_HOT_ADD_ENABLED`, (enabledHr, schema) =>
|
||||
.when(`HOT_RESIZE.${hrFieldName}`, (enabledHr, schema) =>
|
||||
enabledHr ? schema.required() : schema.notRequired()
|
||||
),
|
||||
grid: { xs: 5, sm: 7, md: 6 },
|
||||
@ -264,3 +264,20 @@ export const generateCapacityInput = ({ validation, ...field }) => ({
|
||||
),
|
||||
grid: { md: 3 },
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {Field} config - Configuration
|
||||
* @param {CapacityFieldName} config.name - Capacity field name
|
||||
* @param {BaseSchema} config.validation - Validation schema
|
||||
* @returns {Field} - Field with validation modification conditions
|
||||
*/
|
||||
export const generateCostCapacityInput = ({ validation, grid, ...field }) => ({
|
||||
...field,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation:
|
||||
typeof validation === 'function'
|
||||
? lazy((_, { context }) => validation(context.extra))
|
||||
: validation,
|
||||
grid: { md: 4 },
|
||||
})
|
||||
|
@ -19,7 +19,12 @@ import {
|
||||
FIELDS as INFORMATION_FIELDS,
|
||||
HYPERVISOR_FIELD,
|
||||
} from './informationSchema'
|
||||
import { MEMORY_FIELDS, CPU_FIELDS, VCPU_FIELDS } from './capacitySchema'
|
||||
import {
|
||||
MEMORY_FIELDS,
|
||||
CPU_FIELDS,
|
||||
VCPU_FIELDS,
|
||||
SHOWBACK_FIELDS,
|
||||
} from './capacitySchema'
|
||||
import { FIELDS as VM_GROUP_FIELDS } from './vmGroupSchema'
|
||||
import { FIELDS as OWNERSHIP_FIELDS } from './ownershipSchema'
|
||||
import { FIELDS as VCENTER_FIELDS } from './vcenterSchema'
|
||||
@ -64,6 +69,11 @@ const SECTIONS = (hypervisor, isUpdate) => [
|
||||
legend: T.VirtualCpu,
|
||||
fields: filterFieldsByHypervisor(VCPU_FIELDS, hypervisor),
|
||||
},
|
||||
{
|
||||
id: 'showback',
|
||||
legend: T.Cost,
|
||||
fields: filterFieldsByHypervisor(SHOWBACK_FIELDS, hypervisor),
|
||||
},
|
||||
{
|
||||
id: 'ownership',
|
||||
legend: T.Ownership,
|
||||
|
@ -76,8 +76,9 @@ const Steps = createSteps([General, ExtraConfiguration, CustomVariables], {
|
||||
|
||||
// add user inputs to context
|
||||
Object.keys(extraTemplate?.USER_INPUTS ?? {}).forEach((name) => {
|
||||
const isCapacity = ['MEMORY', 'CPU', 'VCPU'].includes(name)
|
||||
const upperName = String(name).toUpperCase()
|
||||
context[upperName] = `$${upperName}`
|
||||
!isCapacity && (context[upperName] = `$${upperName}`)
|
||||
})
|
||||
|
||||
return jsonToXml({
|
||||
|
@ -412,8 +412,13 @@ module.exports = {
|
||||
Cores: 'Cores',
|
||||
Sockets: 'Sockets',
|
||||
Memory: 'Memory',
|
||||
Cost: 'Cost',
|
||||
CostEachMonth: '%s / month',
|
||||
CostCpu: 'Cost / CPU',
|
||||
CostCpuConcept: 'Cost of each CPU per hour',
|
||||
CostMByte: 'Cost / MByte',
|
||||
CostMemoryConcept: 'Cost of each memory MB per hour',
|
||||
CostDiskConcept: 'Cost of each disk GB per hour',
|
||||
/* VM schema - storage */
|
||||
Storage: 'Storage',
|
||||
Disk: 'Disk',
|
||||
|
@ -75,6 +75,32 @@ export const booleanToString = (bool) => (bool ? T.Yes : T.No)
|
||||
export const stringToBoolean = (str) =>
|
||||
String(str).toLowerCase() === 'yes' || +str === 1
|
||||
|
||||
/**
|
||||
* Formats a number into a string according to the currency configuration.
|
||||
*
|
||||
* @param {number|bigint} number - Number to format
|
||||
* @param {Intl.NumberFormatOptions} options - Options to format the number
|
||||
* @returns {string} - Number in string format with the currency symbol
|
||||
*/
|
||||
export const formatNumberByCurrency = (number, options) => {
|
||||
try {
|
||||
const currency = window?.currency ?? 'EUR'
|
||||
const locale = window?.lang?.replace('_', '-') ?? undefined
|
||||
|
||||
return Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
currencyDisplay: 'narrowSymbol',
|
||||
notation: 'compact',
|
||||
compactDisplay: 'long',
|
||||
maximumFractionDigits: 2,
|
||||
...options,
|
||||
}).format(number)
|
||||
} catch {
|
||||
return number.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the given value is an instance of Date.
|
||||
*
|
||||
@ -333,8 +359,8 @@ export const getUserInputParams = (userInputString) => {
|
||||
USER_INPUT_TYPES.range,
|
||||
USER_INPUT_TYPES.rangeFloat,
|
||||
].includes(params[1])
|
||||
? params[3].split(RANGE_SEPARATOR)
|
||||
: params[3].split(LIST_SEPARATOR)
|
||||
? params[3]?.split(RANGE_SEPARATOR)
|
||||
: params[3]?.split(LIST_SEPARATOR)
|
||||
|
||||
return {
|
||||
mandatory: params[0] === MANDATORY,
|
||||
|
@ -25,6 +25,7 @@ const rootReducer = require('client/store/reducers')
|
||||
const { getFireedgeConfig } = require('server/utils/yml')
|
||||
const {
|
||||
availableLanguages,
|
||||
defaultCurrency,
|
||||
defaultApps,
|
||||
} = require('server/utils/constants/defaults')
|
||||
const { APP_URL, STATIC_FILES_URL } = require('client/constants')
|
||||
@ -32,6 +33,7 @@ const { upperCaseFirst } = require('client/utils')
|
||||
|
||||
// settings
|
||||
const appConfig = getFireedgeConfig()
|
||||
const currency = appConfig.currency || defaultCurrency
|
||||
const langs = appConfig.langs || availableLanguages
|
||||
|
||||
const languages = Object.keys(langs)
|
||||
@ -89,6 +91,7 @@ router.get('*', (req, res) => {
|
||||
<div id="root">${rootComponent}</div>
|
||||
${storeRender}
|
||||
<script>${`langs = ${JSON.stringify(scriptLanguages)}`}</script>
|
||||
<script>${`currency = ${JSON.stringify(currency)}`}</script>
|
||||
<script src='${APP_URL}/client/bundle.${appName}.js'></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -163,6 +163,7 @@ const defaults = {
|
||||
defaultHost: '0.0.0.0',
|
||||
defaultPort: 2616,
|
||||
defaultEvents: ['SIGINT', 'SIGTERM'],
|
||||
defaultCurrency: 'EUR',
|
||||
availableLanguages: {
|
||||
bg_BG: 'Bulgarian (Bulgaria)',
|
||||
bg: 'Bulgarian',
|
||||
|
Loading…
x
Reference in New Issue
Block a user