1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-30 22:50:10 +03:00

F #5422: Add showback to update Template form (#1923)

This commit is contained in:
Sergio Betanzos 2022-04-08 15:08:30 +02:00 committed by GitHub
parent 7aaec867e7
commit 705e9779ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 207 additions and 60 deletions

View File

@ -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:

View File

@ -74,6 +74,7 @@ dialogs:
information: true
ownership: true
capacity: true
showback: true
vm_group: true
vcenter:
enabled: true

View File

@ -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 = {

View File

@ -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 */

View File

@ -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(',')
}),
})

View File

@ -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]

View File

@ -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 },
})

View File

@ -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,

View File

@ -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({

View File

@ -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',

View File

@ -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,

View File

@ -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>

View File

@ -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',