You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1910 lines
57 KiB
1910 lines
57 KiB
import { |
|
Dispatch, |
|
ReactNode, |
|
SetStateAction, |
|
useCallback, |
|
useEffect, |
|
useMemo, |
|
useState, |
|
} from 'react'; |
|
import { Box, Dialog, DialogProps, Grid } from '@mui/material'; |
|
import { Close as CloseIcon } from '@mui/icons-material'; |
|
import { DataSizeUnit } from 'format-data-size'; |
|
import { v4 as uuidv4 } from 'uuid'; |
|
|
|
import { BLUE, RED, TEXT } from '../lib/consts/DEFAULT_THEME'; |
|
|
|
import api from '../lib/api'; |
|
import Autocomplete from './Autocomplete'; |
|
import ConfirmDialog from './ConfirmDialog'; |
|
import ContainedButton from './ContainedButton'; |
|
import FlexBox from './FlexBox'; |
|
import { dsize, dsizeToByte } from '../lib/format_data_size_wrappers'; |
|
import IconButton, { IconButtonProps } from './IconButton'; |
|
import MessageBox, { MessageBoxProps } from './MessageBox'; |
|
import OutlinedInputWithLabel from './OutlinedInputWithLabel'; |
|
import OutlinedLabeledInputWithSelect from './OutlinedLabeledInputWithSelect'; |
|
import { Panel, PanelHeader } from './Panels'; |
|
import SelectWithLabel from './SelectWithLabel'; |
|
import Spinner from './Spinner'; |
|
import { |
|
testInput as baseTestInput, |
|
testMax, |
|
testNotBlank, |
|
testRange, |
|
} from '../lib/test_input'; |
|
import { BodyText, HeaderText, InlineMonoText } from './Text'; |
|
|
|
type InputMessage = Partial<Pick<MessageBoxProps, 'type' | 'text'>>; |
|
|
|
type ProvisionServerDialogProps = { |
|
dialogProps: DialogProps; |
|
onClose: IconButtonProps['onClick']; |
|
}; |
|
|
|
type HostMetadataForProvisionServerHost = { |
|
hostUUID: string; |
|
hostName: string; |
|
hostCPUCores: number; |
|
hostMemory: string; |
|
}; |
|
|
|
type ServerMetadataForProvisionServer = { |
|
serverUUID: string; |
|
serverName: string; |
|
serverCPUCores: number; |
|
serverMemory: string; |
|
}; |
|
|
|
type StorageGroupMetadataForProvisionServer = { |
|
storageGroupUUID: string; |
|
storageGroupName: string; |
|
storageGroupSize: string; |
|
storageGroupFree: string; |
|
}; |
|
|
|
type FileMetadataForProvisionServer = { |
|
fileUUID: string; |
|
fileName: string; |
|
}; |
|
|
|
type OrganizedServerMetadataForProvisionServer = Omit< |
|
ServerMetadataForProvisionServer, |
|
'serverMemory' |
|
> & { |
|
serverMemory: bigint; |
|
}; |
|
|
|
type OrganizedStorageGroupMetadataForProvisionServer = Omit< |
|
StorageGroupMetadataForProvisionServer, |
|
'storageGroupSize' | 'storageGroupFree' |
|
> & { |
|
anvilUUID: string; |
|
anvilName: string; |
|
storageGroupSize: bigint; |
|
storageGroupFree: bigint; |
|
}; |
|
|
|
type AnvilDetailMetadataForProvisionServer = { |
|
anvilUUID: string; |
|
anvilName: string; |
|
anvilDescription?: string; |
|
anvilTotalCPUCores: number; |
|
anvilTotalMemory: string; |
|
anvilTotalAllocatedCPUCores: number; |
|
anvilTotalAllocatedMemory: string; |
|
anvilTotalAvailableCPUCores: number; |
|
anvilTotalAvailableMemory: string; |
|
hosts: Array<HostMetadataForProvisionServerHost>; |
|
servers: Array<ServerMetadataForProvisionServer>; |
|
storageGroups: Array<StorageGroupMetadataForProvisionServer>; |
|
files: Array<FileMetadataForProvisionServer>; |
|
}; |
|
|
|
type OrganizedAnvilDetailMetadataForProvisionServer = Omit< |
|
AnvilDetailMetadataForProvisionServer, |
|
| 'anvilTotalMemory' |
|
| 'anvilTotalAllocatedMemory' |
|
| 'anvilTotalAvailableMemory' |
|
| 'hosts' |
|
| 'servers' |
|
| 'storageGroups' |
|
> & { |
|
anvilTotalMemory: bigint; |
|
anvilTotalAllocatedMemory: bigint; |
|
anvilTotalAvailableMemory: bigint; |
|
hosts: Array< |
|
Omit<HostMetadataForProvisionServerHost, 'hostMemory'> & { |
|
hostMemory: bigint; |
|
} |
|
>; |
|
servers: Array<OrganizedServerMetadataForProvisionServer>; |
|
storageGroupUUIDs: string[]; |
|
storageGroups: Array<OrganizedStorageGroupMetadataForProvisionServer>; |
|
fileUUIDs: string[]; |
|
}; |
|
|
|
type AnvilUUIDMapToData = { |
|
[uuid: string]: OrganizedAnvilDetailMetadataForProvisionServer; |
|
}; |
|
|
|
type FileUUIDMapToData = { |
|
[uuid: string]: FileMetadataForProvisionServer; |
|
}; |
|
|
|
type ServerNameMapToData = { |
|
[name: string]: OrganizedServerMetadataForProvisionServer; |
|
}; |
|
|
|
type StorageGroupUUIDMapToData = { |
|
[uuid: string]: OrganizedStorageGroupMetadataForProvisionServer; |
|
}; |
|
|
|
type OSAutoCompleteOption = { label: string; key: string }; |
|
|
|
type FilterAnvilsFunction = ( |
|
allAnvils: OrganizedAnvilDetailMetadataForProvisionServer[], |
|
storageGroupUUIDMapToData: StorageGroupUUIDMapToData, |
|
cpuCores: number, |
|
memory: bigint, |
|
vdSizes: bigint[], |
|
storageGroupUUIDs: string[], |
|
fileUUIDs: string[], |
|
options?: { |
|
includeAnvilUUIDs?: string[]; |
|
includeFileUUIDs?: string[]; |
|
includeStorageGroupUUIDs?: string[]; |
|
}, |
|
) => { |
|
anvils: OrganizedAnvilDetailMetadataForProvisionServer[]; |
|
anvilUUIDs: string[]; |
|
fileUUIDs: string[]; |
|
maxCPUCores: number; |
|
maxMemory: bigint; |
|
maxVirtualDiskSizes: bigint[]; |
|
storageGroupUUIDs: string[]; |
|
}; |
|
|
|
type VirtualDiskStates = { |
|
stateIds: string[]; |
|
inputMaxes: string[]; |
|
inputSizeMessages: Array<InputMessage | undefined>; |
|
inputSizes: string[]; |
|
inputStorageGroupUUIDMessages: Array<InputMessage | undefined>; |
|
inputStorageGroupUUIDs: string[]; |
|
inputUnits: DataSizeUnit[]; |
|
maxes: bigint[]; |
|
sizes: bigint[]; |
|
}; |
|
|
|
type UpdateLimitsFunction = (options?: { |
|
allAnvils?: OrganizedAnvilDetailMetadataForProvisionServer[]; |
|
cpuCores?: number; |
|
fileUUIDs?: string[]; |
|
includeAnvilUUIDs?: string[]; |
|
includeFileUUIDs?: string[]; |
|
includeStorageGroupUUIDs?: string[]; |
|
inputMemoryUnit?: DataSizeUnit; |
|
memory?: bigint; |
|
storageGroupUUIDMapToData?: StorageGroupUUIDMapToData; |
|
virtualDisks?: VirtualDiskStates; |
|
}) => Pick< |
|
ReturnType<FilterAnvilsFunction>, |
|
'maxCPUCores' | 'maxMemory' | 'maxVirtualDiskSizes' |
|
> & { |
|
formattedMaxMemory: string; |
|
formattedMaxVDSizes: string[]; |
|
}; |
|
|
|
const BIGINT_ZERO = BigInt(0); |
|
|
|
const DATA_SIZE_UNIT_SELECT_ITEMS: SelectItem<DataSizeUnit>[] = [ |
|
{ value: 'B' }, |
|
{ value: 'KiB' }, |
|
{ value: 'MiB' }, |
|
{ value: 'GiB' }, |
|
{ value: 'TiB' }, |
|
]; |
|
|
|
const INITIAL_DATA_SIZE_UNIT: DataSizeUnit = 'GiB'; |
|
|
|
const CPU_CORES_MIN = 1; |
|
// Unit: bytes; 64 KiB |
|
const MEMORY_MIN = BigInt(65536); |
|
// Unit: bytes; 100 MiB |
|
const VIRTUAL_DISK_SIZE_MIN = BigInt(104857600); |
|
|
|
const PROVISION_BUTTON_STYLES = { |
|
backgroundColor: BLUE, |
|
color: TEXT, |
|
|
|
'&:hover': { |
|
backgroundColor: BLUE, |
|
}, |
|
}; |
|
|
|
const createMaxValueButton = ( |
|
maxValue: string, |
|
{ |
|
onButtonClick, |
|
}: { |
|
onButtonClick?: ContainedButtonProps['onClick']; |
|
}, |
|
) => ( |
|
<ContainedButton |
|
disabled={onButtonClick === undefined} |
|
onClick={onButtonClick} |
|
sx={{ |
|
minWidth: 'unset', |
|
whiteSpace: 'nowrap', |
|
}} |
|
>{`Max: ${maxValue}`}</ContainedButton> |
|
); |
|
|
|
const createSelectItemDisplay = ({ |
|
endAdornment, |
|
mainLabel, |
|
subLabel, |
|
}: { |
|
endAdornment?: ReactNode; |
|
mainLabel?: string; |
|
subLabel?: string; |
|
} = {}) => ( |
|
<Box |
|
sx={{ |
|
alignItems: 'center', |
|
display: 'flex', |
|
flexDirection: 'row', |
|
width: '100%', |
|
|
|
'& > :first-child': { flexGrow: 1 }, |
|
}} |
|
> |
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}> |
|
{mainLabel && <BodyText inverted text={mainLabel} />} |
|
{subLabel && <BodyText inverted text={subLabel} />} |
|
</Box> |
|
{endAdornment} |
|
</Box> |
|
); |
|
|
|
const organizeAnvils = (data: AnvilDetailMetadataForProvisionServer[]) => { |
|
const allFiles: Record<string, FileMetadataForProvisionServer> = {}; |
|
const result = data.reduce<{ |
|
anvils: OrganizedAnvilDetailMetadataForProvisionServer[]; |
|
anvilSelectItems: SelectItem[]; |
|
anvilUUIDMapToData: AnvilUUIDMapToData; |
|
files: FileMetadataForProvisionServer[]; |
|
fileSelectItems: SelectItem[]; |
|
fileUUIDMapToData: FileUUIDMapToData; |
|
serverNameMapToData: ServerNameMapToData; |
|
storageGroups: OrganizedStorageGroupMetadataForProvisionServer[]; |
|
storageGroupSelectItems: SelectItem[]; |
|
storageGroupUUIDMapToData: StorageGroupUUIDMapToData; |
|
}>( |
|
(reduceContainer, anvil) => { |
|
const { |
|
anvilUUID, |
|
anvilName, |
|
anvilTotalMemory, |
|
anvilTotalAllocatedMemory, |
|
anvilTotalAvailableMemory, |
|
hosts, |
|
servers, |
|
storageGroups, |
|
files, |
|
} = anvil; |
|
|
|
const { anvilStorageGroups, anvilStorageGroupUUIDs } = |
|
storageGroups.reduce<{ |
|
anvilStorageGroups: OrganizedStorageGroupMetadataForProvisionServer[]; |
|
anvilStorageGroupUUIDs: string[]; |
|
}>( |
|
(reducedStorageGroups, storageGroup) => { |
|
const resultStorageGroup = { |
|
...storageGroup, |
|
anvilUUID, |
|
anvilName, |
|
storageGroupSize: BigInt(storageGroup.storageGroupSize), |
|
storageGroupFree: BigInt(storageGroup.storageGroupFree), |
|
humanizedStorageGroupFree: '', |
|
}; |
|
|
|
dsize(storageGroup.storageGroupFree, { |
|
fromUnit: 'B', |
|
onSuccess: { |
|
string: (value, unit) => { |
|
resultStorageGroup.humanizedStorageGroupFree = `${value} ${unit}`; |
|
}, |
|
}, |
|
precision: 0, |
|
toUnit: 'ibyte', |
|
}); |
|
|
|
reducedStorageGroups.anvilStorageGroupUUIDs.push( |
|
storageGroup.storageGroupUUID, |
|
); |
|
reducedStorageGroups.anvilStorageGroups.push(resultStorageGroup); |
|
|
|
reduceContainer.storageGroups.push(resultStorageGroup); |
|
reduceContainer.storageGroupSelectItems.push({ |
|
displayValue: createSelectItemDisplay({ |
|
endAdornment: ( |
|
<BodyText |
|
inverted |
|
text={`~${resultStorageGroup.humanizedStorageGroupFree} free`} |
|
/> |
|
), |
|
mainLabel: storageGroup.storageGroupName, |
|
subLabel: anvilName, |
|
}), |
|
value: storageGroup.storageGroupUUID, |
|
}); |
|
reduceContainer.storageGroupUUIDMapToData[ |
|
storageGroup.storageGroupUUID |
|
] = resultStorageGroup; |
|
|
|
return reducedStorageGroups; |
|
}, |
|
{ |
|
anvilStorageGroups: [], |
|
anvilStorageGroupUUIDs: [], |
|
}, |
|
); |
|
|
|
const fileUUIDs: string[] = []; |
|
|
|
files.forEach((file) => { |
|
const { fileUUID } = file; |
|
|
|
fileUUIDs.push(fileUUID); |
|
|
|
allFiles[fileUUID] = file; |
|
}); |
|
|
|
const resultAnvil = { |
|
...anvil, |
|
anvilTotalMemory: BigInt(anvilTotalMemory), |
|
anvilTotalAllocatedMemory: BigInt(anvilTotalAllocatedMemory), |
|
anvilTotalAvailableMemory: BigInt(anvilTotalAvailableMemory), |
|
humanizedAnvilTotalAvailableMemory: '', |
|
hosts: hosts.map((host) => ({ |
|
...host, |
|
hostMemory: BigInt(host.hostMemory), |
|
})), |
|
servers: servers.map(({ serverMemory, serverName, ...serverRest }) => { |
|
const resultServer = { |
|
...serverRest, |
|
serverMemory: BigInt(serverMemory), |
|
serverName, |
|
}; |
|
|
|
reduceContainer.serverNameMapToData[serverName] = resultServer; |
|
|
|
return resultServer; |
|
}), |
|
storageGroupUUIDs: anvilStorageGroupUUIDs, |
|
storageGroups: anvilStorageGroups, |
|
fileUUIDs, |
|
}; |
|
|
|
dsize(anvilTotalAvailableMemory, { |
|
fromUnit: 'B', |
|
onSuccess: { |
|
string: (value, unit) => { |
|
resultAnvil.humanizedAnvilTotalAvailableMemory = `${value} ${unit}`; |
|
}, |
|
}, |
|
precision: 0, |
|
toUnit: 'ibyte', |
|
}); |
|
|
|
reduceContainer.anvils.push(resultAnvil); |
|
reduceContainer.anvilSelectItems.push({ |
|
displayValue: createSelectItemDisplay({ |
|
endAdornment: ( |
|
<Box |
|
sx={{ display: 'flex', flexDirection: 'column', width: '8rem' }} |
|
> |
|
<BodyText |
|
inverted |
|
text={`CPU: ${resultAnvil.anvilTotalCPUCores} cores`} |
|
/> |
|
<BodyText |
|
inverted |
|
text={`Memory: ~${resultAnvil.humanizedAnvilTotalAvailableMemory}`} |
|
/> |
|
</Box> |
|
), |
|
mainLabel: resultAnvil.anvilName, |
|
subLabel: resultAnvil.anvilDescription, |
|
}), |
|
value: anvilUUID, |
|
}); |
|
reduceContainer.anvilUUIDMapToData[anvilUUID] = resultAnvil; |
|
|
|
return reduceContainer; |
|
}, |
|
{ |
|
anvils: [], |
|
anvilSelectItems: [], |
|
anvilUUIDMapToData: {}, |
|
files: [], |
|
fileSelectItems: [], |
|
fileUUIDMapToData: {}, |
|
serverNameMapToData: {}, |
|
storageGroups: [], |
|
storageGroupSelectItems: [], |
|
storageGroupUUIDMapToData: {}, |
|
}, |
|
); |
|
|
|
Object.values(allFiles).forEach((distinctFile) => { |
|
result.files.push(distinctFile); |
|
result.fileSelectItems.push({ |
|
displayValue: distinctFile.fileName, |
|
value: distinctFile.fileUUID, |
|
}); |
|
result.fileUUIDMapToData[distinctFile.fileUUID] = distinctFile; |
|
}); |
|
|
|
return result; |
|
}; |
|
|
|
const filterAnvils: FilterAnvilsFunction = ( |
|
organizedAnvils: OrganizedAnvilDetailMetadataForProvisionServer[], |
|
storageGroupUUIDMapToData: StorageGroupUUIDMapToData, |
|
cpuCores: number, |
|
memory: bigint, |
|
vdSizes: bigint[], |
|
storageGroupUUIDs: string[], |
|
fileUUIDs: string[], |
|
{ |
|
includeAnvilUUIDs = [], |
|
includeFileUUIDs = [], |
|
includeStorageGroupUUIDs = [], |
|
} = {}, |
|
) => { |
|
let testIncludeAnvil: (uuid: string) => boolean = () => true; |
|
let testIncludeFile: (uuid: string) => boolean = () => true; |
|
let testIncludeStorageGroup: (uuid: string) => boolean = () => true; |
|
|
|
if (includeAnvilUUIDs.length > 0) { |
|
testIncludeAnvil = (uuid: string) => includeAnvilUUIDs.includes(uuid); |
|
} |
|
|
|
if (includeFileUUIDs.length > 0) { |
|
testIncludeFile = (uuid: string) => includeFileUUIDs.includes(uuid); |
|
} |
|
|
|
if (includeStorageGroupUUIDs.length > 0) { |
|
testIncludeStorageGroup = (uuid: string) => |
|
includeStorageGroupUUIDs.includes(uuid); |
|
} |
|
|
|
const resultFileUUIDs: Record<string, boolean> = {}; |
|
|
|
const storageGroupTotals = storageGroupUUIDs.reduce<{ |
|
all: bigint; |
|
[uuid: string]: bigint; |
|
}>( |
|
(totals, uuid, index) => { |
|
const vdSize: bigint = vdSizes[index] ?? BIGINT_ZERO; |
|
|
|
totals.all += vdSize; |
|
|
|
if (uuid === '') { |
|
return totals; |
|
} |
|
|
|
if (totals[uuid] === undefined) { |
|
totals[uuid] = BIGINT_ZERO; |
|
} |
|
|
|
totals[uuid] += vdSize; |
|
|
|
return totals; |
|
}, |
|
{ all: BIGINT_ZERO }, |
|
); |
|
|
|
const result = organizedAnvils.reduce<{ |
|
anvils: OrganizedAnvilDetailMetadataForProvisionServer[]; |
|
anvilUUIDs: string[]; |
|
fileUUIDs: string[]; |
|
maxCPUCores: number; |
|
maxMemory: bigint; |
|
maxVirtualDiskSizes: bigint[]; |
|
storageGroupUUIDs: string[]; |
|
}>( |
|
(reduceContainer, organizedAnvil) => { |
|
const { anvilUUID } = organizedAnvil; |
|
|
|
if (testIncludeAnvil(anvilUUID)) { |
|
const { |
|
anvilTotalCPUCores, |
|
anvilTotalAvailableMemory, |
|
files, |
|
fileUUIDs: anvilFileUUIDs, |
|
storageGroups, |
|
} = organizedAnvil; |
|
|
|
const anvilStorageGroupUUIDs: string[] = []; |
|
let anvilStorageGroupFreeMax: bigint = BIGINT_ZERO; |
|
let anvilStorageGroupFreeTotal: bigint = BIGINT_ZERO; |
|
|
|
// Summarize storage groups in this anvil node to produce all UUIDs, max |
|
// free space, and total free space. |
|
storageGroups.forEach(({ storageGroupUUID, storageGroupFree }) => { |
|
if (testIncludeStorageGroup(storageGroupUUID)) { |
|
anvilStorageGroupUUIDs.push(storageGroupUUID); |
|
anvilStorageGroupFreeTotal += storageGroupFree; |
|
|
|
if (storageGroupFree > anvilStorageGroupFreeMax) { |
|
anvilStorageGroupFreeMax = storageGroupFree; |
|
} |
|
} |
|
}); |
|
|
|
const usableTests: (() => boolean)[] = [ |
|
// Does this anvil node have at least one storage group? |
|
() => storageGroups.length > 0, |
|
// Does this anvil node have enough CPU cores? |
|
() => cpuCores <= anvilTotalCPUCores, |
|
// Does this anvil node have enough memory? |
|
() => memory <= anvilTotalAvailableMemory, |
|
// For every virtual disk: |
|
// 1. Does this anvil node have the selected storage group which |
|
// will contain the VD? |
|
// 2. Does the selected storage group OR any storage group on this |
|
// anvil node have enough free space? |
|
() => |
|
storageGroupUUIDs.every((uuid, index) => { |
|
const vdSize = vdSizes[index] ?? BIGINT_ZERO; |
|
let hasStorageGroup = true; |
|
let hasEnoughStorage = vdSize <= anvilStorageGroupFreeMax; |
|
|
|
if (uuid !== '') { |
|
hasStorageGroup = anvilStorageGroupUUIDs.includes(uuid); |
|
hasEnoughStorage = |
|
vdSize <= storageGroupUUIDMapToData[uuid].storageGroupFree; |
|
} |
|
|
|
return hasStorageGroup && hasEnoughStorage; |
|
}), |
|
// Do storage groups on this anvil node have enough free space to |
|
// contain multiple VDs? |
|
() => |
|
Object.entries(storageGroupTotals).every(([uuid, total]) => |
|
uuid === 'all' |
|
? total <= anvilStorageGroupFreeTotal |
|
: total <= storageGroupUUIDMapToData[uuid].storageGroupFree, |
|
), |
|
// Does this anvil node have access to selected files? |
|
() => |
|
fileUUIDs.every( |
|
(fileUUID) => |
|
fileUUID === '' || anvilFileUUIDs.includes(fileUUID), |
|
), |
|
]; |
|
|
|
// If an anvil doesn't pass all tests, then it and its parts shouldn't be used. |
|
if (usableTests.every((test) => test())) { |
|
reduceContainer.anvils.push(organizedAnvil); |
|
reduceContainer.anvilUUIDs.push(anvilUUID); |
|
|
|
reduceContainer.maxCPUCores = Math.max( |
|
anvilTotalCPUCores, |
|
reduceContainer.maxCPUCores, |
|
); |
|
|
|
if (anvilTotalAvailableMemory > reduceContainer.maxMemory) { |
|
reduceContainer.maxMemory = anvilTotalAvailableMemory; |
|
} |
|
|
|
files.forEach(({ fileUUID }) => { |
|
if (testIncludeFile(fileUUID)) { |
|
resultFileUUIDs[fileUUID] = true; |
|
} |
|
}); |
|
|
|
reduceContainer.storageGroupUUIDs.push(...anvilStorageGroupUUIDs); |
|
reduceContainer.maxVirtualDiskSizes.fill(anvilStorageGroupFreeMax); |
|
} |
|
} |
|
|
|
return reduceContainer; |
|
}, |
|
{ |
|
anvils: [], |
|
anvilUUIDs: [], |
|
fileUUIDs: [], |
|
maxCPUCores: 0, |
|
maxMemory: BIGINT_ZERO, |
|
maxVirtualDiskSizes: storageGroupUUIDs.map(() => BIGINT_ZERO), |
|
storageGroupUUIDs: [], |
|
}, |
|
); |
|
|
|
result.fileUUIDs = Object.keys(resultFileUUIDs); |
|
|
|
storageGroupUUIDs.forEach((uuid: string, uuidIndex: number) => { |
|
if (uuid !== '') { |
|
result.maxVirtualDiskSizes[uuidIndex] = |
|
storageGroupUUIDMapToData[uuid].storageGroupFree; |
|
} |
|
}); |
|
|
|
return result; |
|
}; |
|
|
|
// const convertSelectValueToArray = (value: unknown) => |
|
// typeof value === 'string' ? value.split(',') : (value as string[]); |
|
|
|
const createVirtualDiskForm = ( |
|
virtualDisks: VirtualDiskStates, |
|
vdIndex: number, |
|
setVirtualDisks: Dispatch<SetStateAction<VirtualDiskStates>>, |
|
storageGroupSelectItems: SelectItem[], |
|
includeStorageGroupUUIDs: string[], |
|
updateLimits: UpdateLimitsFunction, |
|
storageGroupUUIDMapToData: StorageGroupUUIDMapToData, |
|
testInput: TestInputFunction, |
|
) => { |
|
const get = <Key extends keyof VirtualDiskStates>( |
|
key: Key, |
|
gIndex: number = vdIndex, |
|
) => virtualDisks[key][gIndex] as VirtualDiskStates[Key][number]; |
|
|
|
const set = <Key extends keyof VirtualDiskStates>( |
|
key: Key, |
|
value: VirtualDiskStates[Key][number], |
|
sIndex: number = vdIndex, |
|
) => { |
|
virtualDisks[key][sIndex] = value; |
|
setVirtualDisks({ ...virtualDisks }); |
|
}; |
|
|
|
const changeVDSize = (cvsValue: bigint = BIGINT_ZERO) => { |
|
set('sizes', cvsValue); |
|
|
|
const { formattedMaxVDSizes, maxVirtualDiskSizes } = updateLimits({ |
|
virtualDisks, |
|
}); |
|
|
|
testInput({ |
|
inputs: { |
|
[`vd${vdIndex}Size`]: { |
|
displayMax: `${formattedMaxVDSizes[vdIndex]}`, |
|
max: maxVirtualDiskSizes[vdIndex], |
|
value: cvsValue, |
|
}, |
|
}, |
|
}); |
|
}; |
|
|
|
const handleVDSizeChange = ({ |
|
value = get('inputSizes'), |
|
unit = get('inputUnits'), |
|
}: { |
|
value?: string; |
|
unit?: DataSizeUnit; |
|
}) => { |
|
if (value !== get('inputSizes')) { |
|
set('inputSizes', value); |
|
} |
|
|
|
if (unit !== get('inputUnits')) { |
|
set('inputUnits', unit); |
|
} |
|
|
|
dsizeToByte( |
|
value, |
|
unit, |
|
(convertedVDSize) => changeVDSize(convertedVDSize), |
|
() => changeVDSize(), |
|
); |
|
}; |
|
|
|
const handleVDStorageGroupChange = (uuid = get('inputStorageGroupUUIDs')) => { |
|
if (uuid !== get('inputStorageGroupUUIDs')) { |
|
set('inputStorageGroupUUIDs', uuid); |
|
} |
|
|
|
updateLimits({ virtualDisks }); |
|
}; |
|
|
|
return ( |
|
<Box |
|
key={`ps-virtual-disk-${get('stateIds')}`} |
|
sx={{ |
|
display: 'flex', |
|
flexDirection: 'column', |
|
|
|
'& > :not(:first-child)': { |
|
marginTop: '1em', |
|
}, |
|
}} |
|
> |
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}> |
|
<OutlinedLabeledInputWithSelect |
|
id={`ps-virtual-disk-size-${vdIndex}`} |
|
label="Disk size" |
|
messageBoxProps={get('inputSizeMessages')} |
|
inputWithLabelProps={{ |
|
inputProps: { |
|
endAdornment: createMaxValueButton( |
|
`${get('inputMaxes')} ${get('inputUnits')}`, |
|
{ |
|
onButtonClick: () => { |
|
set('inputSizes', get('inputMaxes')); |
|
changeVDSize(get('maxes')); |
|
}, |
|
}, |
|
), |
|
onChange: ({ target: { value } }) => { |
|
handleVDSizeChange({ value }); |
|
}, |
|
type: 'number', |
|
value: get('inputSizes'), |
|
}, |
|
inputLabelProps: { |
|
isNotifyRequired: get('sizes') === BIGINT_ZERO, |
|
}, |
|
}} |
|
selectItems={DATA_SIZE_UNIT_SELECT_ITEMS} |
|
selectWithLabelProps={{ |
|
selectProps: { |
|
onChange: ({ target: { value } }) => { |
|
const selectedUnit = value as DataSizeUnit; |
|
|
|
handleVDSizeChange({ unit: selectedUnit }); |
|
}, |
|
value: get('inputUnits'), |
|
}, |
|
}} |
|
/> |
|
</Box> |
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}> |
|
<SelectWithLabel |
|
id={`ps-storage-group-${vdIndex}`} |
|
label="Storage group" |
|
disableItem={(value) => |
|
!( |
|
includeStorageGroupUUIDs.includes(value) && |
|
get('sizes') <= storageGroupUUIDMapToData[value].storageGroupFree |
|
) |
|
} |
|
inputLabelProps={{ |
|
isNotifyRequired: get('inputStorageGroupUUIDs').length === 0, |
|
}} |
|
messageBoxProps={get('inputStorageGroupUUIDMessages')} |
|
selectItems={storageGroupSelectItems} |
|
selectProps={{ |
|
onChange: ({ target: { value } }) => { |
|
const selectedStorageGroupUUID = value as string; |
|
|
|
handleVDStorageGroupChange(selectedStorageGroupUUID); |
|
}, |
|
onClearIndicatorClick: () => handleVDStorageGroupChange(''), |
|
renderValue: (value) => { |
|
const { |
|
anvilName: rvAnvilName = '?', |
|
storageGroupName: rvStorageGroupName = `Unknown (${value})`, |
|
} = storageGroupUUIDMapToData[value as string] ?? {}; |
|
|
|
return `${rvStorageGroupName} (${rvAnvilName})`; |
|
}, |
|
value: get('inputStorageGroupUUIDs'), |
|
}} |
|
/> |
|
</Box> |
|
</Box> |
|
); |
|
}; |
|
|
|
const addVirtualDisk = ({ |
|
existingVirtualDisks: virtualDisks = { |
|
stateIds: [], |
|
inputMaxes: [], |
|
inputSizeMessages: [], |
|
inputSizes: [], |
|
inputStorageGroupUUIDMessages: [], |
|
inputStorageGroupUUIDs: [], |
|
inputUnits: [], |
|
maxes: [], |
|
sizes: [], |
|
}, |
|
stateId = uuidv4(), |
|
inputMax = '0', |
|
inputSize = '', |
|
inputSizeMessage = undefined, |
|
inputStorageGroupUUID = '', |
|
inputStorageGroupUUIDMessage = undefined, |
|
inputUnit = INITIAL_DATA_SIZE_UNIT, |
|
max = BIGINT_ZERO, |
|
setVirtualDisks, |
|
size = BIGINT_ZERO, |
|
}: { |
|
existingVirtualDisks?: VirtualDiskStates; |
|
stateId?: string; |
|
inputMax?: string; |
|
inputSize?: string; |
|
inputSizeMessage?: InputMessage | undefined; |
|
inputStorageGroupUUID?: string; |
|
inputStorageGroupUUIDMessage?: InputMessage | undefined; |
|
inputUnit?: DataSizeUnit; |
|
max?: bigint; |
|
setVirtualDisks?: Dispatch<SetStateAction<VirtualDiskStates>>; |
|
size?: bigint; |
|
} = {}) => { |
|
const { |
|
stateIds, |
|
inputMaxes, |
|
inputSizeMessages, |
|
inputSizes, |
|
inputStorageGroupUUIDMessages, |
|
inputStorageGroupUUIDs, |
|
inputUnits, |
|
maxes, |
|
sizes, |
|
} = virtualDisks; |
|
|
|
stateIds.push(stateId); |
|
inputMaxes.push(inputMax); |
|
inputSizeMessages.push(inputSizeMessage); |
|
inputSizes.push(inputSize); |
|
inputStorageGroupUUIDMessages.push(inputStorageGroupUUIDMessage); |
|
inputStorageGroupUUIDs.push(inputStorageGroupUUID); |
|
inputUnits.push(inputUnit); |
|
maxes.push(max); |
|
sizes.push(size); |
|
|
|
setVirtualDisks?.call(null, { ...virtualDisks }); |
|
|
|
return virtualDisks; |
|
}; |
|
|
|
const filterBlanks: (array: string[]) => string[] = (array: string[]) => |
|
array.filter((value) => value !== ''); |
|
|
|
const getDisplayDsizeOptions = ( |
|
onSuccessString: (value: string, unit: DataSizeUnit) => void, |
|
): Parameters<typeof dsize>[1] => ({ |
|
fromUnit: 'B', |
|
onSuccess: { |
|
string: onSuccessString, |
|
}, |
|
precision: 0, |
|
toUnit: 'ibyte', |
|
}); |
|
let displayMemoryMin: string; |
|
let displayVirtualDiskSizeMin: string; |
|
|
|
dsize( |
|
MEMORY_MIN, |
|
getDisplayDsizeOptions((value, unit) => { |
|
displayMemoryMin = `${value} ${unit}`; |
|
}), |
|
); |
|
|
|
dsize( |
|
VIRTUAL_DISK_SIZE_MIN, |
|
getDisplayDsizeOptions((value, unit) => { |
|
displayVirtualDiskSizeMin = `${value} ${unit}`; |
|
}), |
|
); |
|
|
|
const ProvisionServerDialog = ({ |
|
dialogProps: { open }, |
|
onClose: onCloseProvisionServerDialog, |
|
}: ProvisionServerDialogProps): JSX.Element => { |
|
const [allAnvils, setAllAnvils] = useState< |
|
OrganizedAnvilDetailMetadataForProvisionServer[] |
|
>([]); |
|
// Provision is impossible when one of anvil node list, file list, or storage |
|
// group list is empty. |
|
const [anvilUUIDMapToData, setAnvilUUIDMapToData] = |
|
useState<AnvilUUIDMapToData>({}); |
|
const [fileUUIDMapToData, setFileUUIDMapToData] = useState<FileUUIDMapToData>( |
|
{}, |
|
); |
|
const [serverNameMapToData, setServerNameMapToData] = |
|
useState<ServerNameMapToData>({}); |
|
const [storageGroupUUIDMapToData, setStorageGroupUUIDMapToData] = |
|
useState<StorageGroupUUIDMapToData>({}); |
|
|
|
const [anvilSelectItems, setAnvilSelectItems] = useState<SelectItem[]>([]); |
|
const [fileSelectItems, setFileSelectItems] = useState<SelectItem[]>([]); |
|
const [osAutocompleteOptions, setOSAutocompleteOptions] = useState< |
|
OSAutoCompleteOption[] |
|
>([]); |
|
const [storageGroupSelectItems, setStorageGroupSelectItems] = useState< |
|
SelectItem[] |
|
>([]); |
|
|
|
const [inputServerNameValue, setInputServerNameValue] = useState<string>(''); |
|
const [inputServerNameMessage, setInputServerNameMessage] = useState< |
|
InputMessage | undefined |
|
>(); |
|
|
|
const [inputCPUCoresValue, setInputCPUCoresValue] = useState<number>(1); |
|
const [inputCPUCoresMax, setInputCPUCoresMax] = useState<number>(0); |
|
const [inputCPUCoresMessage, setInputCPUCoresMessage] = useState< |
|
InputMessage | undefined |
|
>(); |
|
|
|
const [memory, setMemory] = useState<bigint>(BIGINT_ZERO); |
|
const [memoryMax, setMemoryMax] = useState<bigint>(BIGINT_ZERO); |
|
const [inputMemoryMessage, setInputMemoryMessage] = useState< |
|
InputMessage | undefined |
|
>(); |
|
const [inputMemoryMax, setInputMemoryMax] = useState<string>('0'); |
|
const [inputMemoryValue, setInputMemoryValue] = useState<string>(''); |
|
const [inputMemoryUnit, setInputMemoryUnit] = useState<DataSizeUnit>( |
|
INITIAL_DATA_SIZE_UNIT, |
|
); |
|
|
|
const [virtualDisks, setVirtualDisks] = useState<VirtualDiskStates>( |
|
addVirtualDisk(), |
|
); |
|
|
|
const [inputInstallISOFileUUID, setInputInstallISOFileUUID] = |
|
useState<string>(''); |
|
const [inputInstallISOMessage, setInputInstallISOMessage] = useState< |
|
InputMessage | undefined |
|
>(); |
|
const [inputDriverISOFileUUID, setInputDriverISOFileUUID] = |
|
useState<string>(''); |
|
const [inputDriverISOMessage] = useState<InputMessage | undefined>(); |
|
|
|
const [inputAnvilValue, setInputAnvilValue] = useState<string>(''); |
|
const [inputAnvilMessage, setInputAnvilMessage] = useState< |
|
InputMessage | undefined |
|
>(); |
|
|
|
const [inputOptimizeForOSValue, setInputOptimizeForOSValue] = |
|
useState<OSAutoCompleteOption | null>(null); |
|
const [inputOptimizeForOSMessage, setInputOptimizeForOSMessage] = useState< |
|
InputMessage | undefined |
|
>(); |
|
|
|
const [includeAnvilUUIDs, setIncludeAnvilUUIDs] = useState<string[]>([]); |
|
const [includeFileUUIDs, setIncludeFileUUIDs] = useState<string[]>([]); |
|
const [includeStorageGroupUUIDs, setIncludeStorageGroupUUIDs] = useState< |
|
string[] |
|
>([]); |
|
|
|
const [isProvisionServerDataReady, setIsProvisionServerDataReady] = |
|
useState<boolean>(false); |
|
const [isOpenProvisionConfirmDialog, setIsOpenProvisionConfirmDialog] = |
|
useState<boolean>(false); |
|
const [isProvisionRequestInProgress, setIsProvisionRequestInProgress] = |
|
useState<boolean>(false); |
|
|
|
const [successfulProvisionCount, setSuccessfulProvisionCount] = |
|
useState<number>(0); |
|
|
|
const inputCpuCoresOptions = useMemo(() => { |
|
const result: number[] = []; |
|
|
|
for (let i = CPU_CORES_MIN; i <= inputCPUCoresMax; i += 1) { |
|
result.push(i); |
|
} |
|
|
|
return result; |
|
}, [inputCPUCoresMax]); |
|
|
|
const inputTests: InputTestBatches = { |
|
serverName: { |
|
defaults: { |
|
onSuccess: () => { |
|
setInputServerNameMessage(undefined); |
|
}, |
|
value: inputServerNameValue, |
|
}, |
|
isRequired: true, |
|
tests: [ |
|
{ |
|
onFailure: () => { |
|
setInputServerNameMessage({ |
|
text: 'The server name length must be 1 to 16 characters.', |
|
type: 'warning', |
|
}); |
|
}, |
|
test: ({ value }) => { |
|
const { length } = value as string; |
|
|
|
return length >= 1 && length <= 16; |
|
}, |
|
}, |
|
{ |
|
onFailure: () => { |
|
setInputServerNameMessage({ |
|
text: 'The server name is expected to only contain alphanumeric, hyphen, or underscore characters.', |
|
type: 'warning', |
|
}); |
|
}, |
|
test: ({ value }) => /^[a-zA-Z0-9_-]+$/.test(value as string), |
|
}, |
|
{ |
|
onFailure: () => { |
|
setInputServerNameMessage({ |
|
text: `This server name already exists, please choose another name.`, |
|
type: 'warning', |
|
}); |
|
}, |
|
test: ({ value }) => |
|
serverNameMapToData[value as string] === undefined, |
|
}, |
|
], |
|
}, |
|
cpuCores: { |
|
defaults: { |
|
max: inputCPUCoresMax, |
|
min: CPU_CORES_MIN, |
|
onSuccess: () => { |
|
setInputCPUCoresMessage(undefined); |
|
}, |
|
value: inputCPUCoresValue, |
|
}, |
|
isRequired: true, |
|
tests: [ |
|
{ |
|
onFailure: () => { |
|
setInputCPUCoresMessage({ |
|
text: 'Non available.', |
|
type: 'warning', |
|
}); |
|
}, |
|
test: testMax, |
|
}, |
|
{ |
|
onFailure: ({ displayMax, displayMin }) => { |
|
setInputCPUCoresMessage({ |
|
text: `The number of CPU cores is expected to be between ${displayMin} and ${displayMax}.`, |
|
type: 'warning', |
|
}); |
|
}, |
|
test: testRange, |
|
}, |
|
], |
|
}, |
|
memory: { |
|
defaults: { |
|
displayMax: `${inputMemoryMax} ${inputMemoryUnit}`, |
|
displayMin: displayMemoryMin, |
|
max: memoryMax, |
|
min: MEMORY_MIN, |
|
onSuccess: () => { |
|
setInputMemoryMessage(undefined); |
|
}, |
|
value: memory, |
|
}, |
|
isRequired: true, |
|
tests: [ |
|
{ |
|
onFailure: () => { |
|
setInputMemoryMessage({ text: 'Non available.', type: 'warning' }); |
|
}, |
|
test: testMax, |
|
}, |
|
{ |
|
onFailure: ({ displayMax, displayMin }) => { |
|
setInputMemoryMessage({ |
|
text: `Memory is expected to be between ${displayMin} and ${displayMax}.`, |
|
type: 'warning', |
|
}); |
|
}, |
|
test: testRange, |
|
}, |
|
], |
|
}, |
|
installISO: { |
|
defaults: { |
|
onSuccess: () => { |
|
setInputInstallISOMessage(undefined); |
|
}, |
|
value: inputInstallISOFileUUID, |
|
}, |
|
isRequired: true, |
|
tests: [{ test: testNotBlank }], |
|
}, |
|
anvil: { |
|
defaults: { |
|
onSuccess: () => { |
|
setInputAnvilMessage(undefined); |
|
}, |
|
value: inputAnvilValue, |
|
}, |
|
isRequired: true, |
|
tests: [{ test: testNotBlank }], |
|
}, |
|
optimizeForOS: { |
|
defaults: { |
|
onSuccess: () => { |
|
setInputOptimizeForOSMessage(undefined); |
|
}, |
|
value: inputOptimizeForOSValue?.key, |
|
}, |
|
isRequired: true, |
|
tests: [{ test: testNotBlank }], |
|
}, |
|
}; |
|
virtualDisks.inputSizeMessages.forEach((message, vdIndex) => { |
|
inputTests[`vd${vdIndex}Size`] = { |
|
defaults: { |
|
displayMax: `${virtualDisks.inputMaxes[vdIndex]} ${virtualDisks.inputUnits[vdIndex]}`, |
|
displayMin: displayVirtualDiskSizeMin, |
|
max: virtualDisks.maxes[vdIndex], |
|
min: VIRTUAL_DISK_SIZE_MIN, |
|
onSuccess: () => { |
|
virtualDisks.inputSizeMessages[vdIndex] = undefined; |
|
}, |
|
value: virtualDisks.sizes[vdIndex], |
|
}, |
|
isRequired: true, |
|
onFinishBatch: () => { |
|
setVirtualDisks({ ...virtualDisks }); |
|
}, |
|
tests: [ |
|
{ |
|
onFailure: () => { |
|
virtualDisks.inputSizeMessages[vdIndex] = { |
|
text: 'Non available.', |
|
type: 'warning', |
|
}; |
|
}, |
|
test: testMax, |
|
}, |
|
{ |
|
onFailure: ({ displayMax, displayMin }) => { |
|
virtualDisks.inputSizeMessages[vdIndex] = { |
|
text: `Virtual disk ${vdIndex} size is expected to be between ${displayMin} and ${displayMax}.`, |
|
type: 'warning', |
|
}; |
|
}, |
|
test: testRange, |
|
}, |
|
], |
|
}; |
|
|
|
inputTests[`vd${vdIndex}StorageGroup`] = { |
|
defaults: { |
|
onSuccess: () => { |
|
virtualDisks.inputStorageGroupUUIDMessages[vdIndex] = undefined; |
|
}, |
|
value: virtualDisks.inputStorageGroupUUIDs[vdIndex], |
|
}, |
|
isRequired: true, |
|
onFinishBatch: () => { |
|
setVirtualDisks({ ...virtualDisks }); |
|
}, |
|
tests: [{ test: testNotBlank }], |
|
}; |
|
}); |
|
|
|
const updateLimits: UpdateLimitsFunction = ({ |
|
allAnvils: ulAllAnvils = allAnvils, |
|
cpuCores: ulCPUCores = inputCPUCoresValue, |
|
fileUUIDs: ulFileUUIDs = [inputInstallISOFileUUID, inputDriverISOFileUUID], |
|
includeAnvilUUIDs: ulIncludeAnvilUUIDs = filterBlanks([inputAnvilValue]), |
|
includeFileUUIDs: ulIncludeFileUUIDs, |
|
includeStorageGroupUUIDs: ulIncludeStorageGroupUUIDs, |
|
inputMemoryUnit: ulInputMemoryUnit = inputMemoryUnit, |
|
memory: ulMemory = memory, |
|
storageGroupUUIDMapToData: |
|
ulStorageGroupUUIDMapToData = storageGroupUUIDMapToData, |
|
virtualDisks: ulVirtualDisks = virtualDisks, |
|
} = {}) => { |
|
const { |
|
anvilUUIDs, |
|
fileUUIDs, |
|
maxCPUCores, |
|
maxMemory, |
|
maxVirtualDiskSizes, |
|
storageGroupUUIDs, |
|
} = filterAnvils( |
|
ulAllAnvils, |
|
ulStorageGroupUUIDMapToData, |
|
ulCPUCores, |
|
ulMemory, |
|
ulVirtualDisks.sizes, |
|
ulVirtualDisks.inputStorageGroupUUIDs, |
|
ulFileUUIDs, |
|
{ |
|
includeAnvilUUIDs: ulIncludeAnvilUUIDs, |
|
includeFileUUIDs: ulIncludeFileUUIDs, |
|
includeStorageGroupUUIDs: ulIncludeStorageGroupUUIDs, |
|
}, |
|
); |
|
|
|
setInputCPUCoresMax(maxCPUCores); |
|
setMemoryMax(maxMemory); |
|
|
|
const formattedMaxVDSizes: string[] = []; |
|
|
|
ulVirtualDisks.maxes = maxVirtualDiskSizes; |
|
ulVirtualDisks.maxes.forEach((vdMaxSize, vdIndex) => { |
|
dsize(vdMaxSize, { |
|
fromUnit: 'B', |
|
onSuccess: { |
|
string: (value, unit) => { |
|
ulVirtualDisks.inputMaxes[vdIndex] = value; |
|
formattedMaxVDSizes[vdIndex] = `${value} ${unit}`; |
|
}, |
|
}, |
|
toUnit: ulVirtualDisks.inputUnits[vdIndex], |
|
}); |
|
}); |
|
setVirtualDisks({ ...ulVirtualDisks }); |
|
|
|
setIncludeAnvilUUIDs(anvilUUIDs); |
|
setIncludeFileUUIDs(fileUUIDs); |
|
setIncludeStorageGroupUUIDs(storageGroupUUIDs); |
|
|
|
let formattedMaxMemory = ''; |
|
|
|
dsize(maxMemory, { |
|
fromUnit: 'B', |
|
onSuccess: { |
|
string: (value, unit) => { |
|
setInputMemoryMax(value); |
|
formattedMaxMemory = `${value} ${unit}`; |
|
}, |
|
}, |
|
toUnit: ulInputMemoryUnit, |
|
}); |
|
|
|
return { |
|
formattedMaxMemory, |
|
formattedMaxVDSizes, |
|
maxCPUCores, |
|
maxMemory, |
|
maxVirtualDiskSizes, |
|
}; |
|
}; |
|
// The memorized version of updateLimits() should only be called during first render. |
|
// eslint-disable-next-line react-hooks/exhaustive-deps |
|
const initLimits = useCallback(updateLimits, []); |
|
|
|
const testInput = ( |
|
...[options, ...restArgs]: Parameters<TestInputFunction> |
|
) => baseTestInput({ tests: inputTests, ...options }, ...restArgs); |
|
|
|
const changeMemory = ({ |
|
cmValue = BIGINT_ZERO, |
|
cmUnit = inputMemoryUnit, |
|
}: { cmValue?: bigint; cmUnit?: DataSizeUnit } = {}) => { |
|
setMemory(cmValue); |
|
|
|
const { formattedMaxMemory, maxMemory } = updateLimits({ |
|
inputMemoryUnit: cmUnit, |
|
memory: cmValue, |
|
}); |
|
|
|
testInput({ |
|
inputs: { |
|
memory: { |
|
displayMax: formattedMaxMemory, |
|
max: maxMemory, |
|
value: cmValue, |
|
}, |
|
}, |
|
}); |
|
}; |
|
|
|
const handleInputMemoryValueChange = ({ |
|
value = inputMemoryValue, |
|
unit = inputMemoryUnit, |
|
}: { |
|
value?: string; |
|
unit?: DataSizeUnit; |
|
} = {}) => { |
|
if (value !== inputMemoryValue) { |
|
setInputMemoryValue(value); |
|
} |
|
|
|
if (unit !== inputMemoryUnit) { |
|
setInputMemoryUnit(unit); |
|
} |
|
|
|
dsizeToByte( |
|
value, |
|
unit, |
|
(convertedMemory) => |
|
changeMemory({ cmValue: convertedMemory, cmUnit: unit }), |
|
() => changeMemory({ cmUnit: unit }), |
|
); |
|
}; |
|
|
|
const handleInputInstallISOFileUUIDChange = (uuid: string) => { |
|
setInputInstallISOFileUUID(uuid); |
|
|
|
updateLimits({ |
|
fileUUIDs: [uuid, inputDriverISOFileUUID], |
|
}); |
|
}; |
|
|
|
const handleInputDriverISOFileUUIDChange = (uuid: string) => { |
|
setInputDriverISOFileUUID(uuid); |
|
|
|
updateLimits({ |
|
fileUUIDs: [inputInstallISOFileUUID, uuid], |
|
}); |
|
}; |
|
|
|
const handleInputAnvilValueChange = (uuid: string) => { |
|
const havcIncludeAnvilUUIDs = filterBlanks([uuid]); |
|
|
|
setInputAnvilValue(uuid); |
|
|
|
updateLimits({ |
|
includeAnvilUUIDs: havcIncludeAnvilUUIDs, |
|
}); |
|
}; |
|
|
|
const createConfirmDialogContent = () => { |
|
const gridColumns = 10; |
|
const c1 = 2; |
|
const c2 = 5; |
|
const c3 = 3; |
|
const c2n3 = c2 + c3; |
|
|
|
return ( |
|
<Grid container columns={gridColumns} direction="column"> |
|
<Grid item xs={gridColumns}> |
|
<BodyText> |
|
Server <InlineMonoText text={inputServerNameValue} /> will be |
|
created on anvil node{' '} |
|
<InlineMonoText |
|
text={anvilUUIDMapToData[inputAnvilValue].anvilName} |
|
/>{' '} |
|
with the following properties: |
|
</BodyText> |
|
</Grid> |
|
<Grid container direction="row" item xs={gridColumns}> |
|
<Grid item xs={c1}> |
|
<BodyText text="CPU" /> |
|
</Grid> |
|
<Grid item xs={c2}> |
|
<BodyText> |
|
<InlineMonoText edge="start">{inputCPUCoresValue}</InlineMonoText>{' '} |
|
core(s) |
|
</BodyText> |
|
</Grid> |
|
<Grid item xs={c3}> |
|
<BodyText> |
|
<InlineMonoText edge="start">{inputCPUCoresMax}</InlineMonoText>{' '} |
|
core(s) available |
|
</BodyText> |
|
</Grid> |
|
</Grid> |
|
<Grid container direction="row" item xs={gridColumns}> |
|
<Grid item xs={c1}> |
|
<BodyText text="Memory" /> |
|
</Grid> |
|
<Grid item xs={c2}> |
|
<BodyText> |
|
<InlineMonoText edge="start"> |
|
{inputMemoryValue} {inputMemoryUnit} |
|
</InlineMonoText> |
|
</BodyText> |
|
</Grid> |
|
<Grid item xs={c3}> |
|
<BodyText> |
|
<InlineMonoText edge="start"> |
|
{inputMemoryMax} {inputMemoryUnit} |
|
</InlineMonoText>{' '} |
|
available |
|
</BodyText> |
|
</Grid> |
|
</Grid> |
|
{virtualDisks.stateIds.map((vdStateId, vdIndex) => { |
|
const vdInputMax = virtualDisks.inputMaxes[vdIndex]; |
|
const vdInputSize = virtualDisks.inputSizes[vdIndex]; |
|
const vdInputUnit = virtualDisks.inputUnits[vdIndex]; |
|
const vdStorageGroupName = |
|
storageGroupUUIDMapToData[ |
|
virtualDisks.inputStorageGroupUUIDs[vdIndex] |
|
].storageGroupName; |
|
|
|
return ( |
|
<Grid |
|
container |
|
direction="row" |
|
key={`ps-virtual-disk-${vdStateId}-summary`} |
|
item |
|
xs={gridColumns} |
|
> |
|
<Grid item xs={c1}> |
|
<BodyText> |
|
Disk <InlineMonoText text={vdIndex} /> |
|
</BodyText> |
|
</Grid> |
|
<Grid item xs={c2}> |
|
<BodyText> |
|
<InlineMonoText edge="start"> |
|
{vdInputSize} {vdInputUnit} |
|
</InlineMonoText>{' '} |
|
on <InlineMonoText>{vdStorageGroupName}</InlineMonoText> |
|
</BodyText> |
|
</Grid> |
|
<Grid item xs={c3}> |
|
<BodyText> |
|
<InlineMonoText edge="start"> |
|
{vdInputMax} {vdInputUnit} |
|
</InlineMonoText>{' '} |
|
available |
|
</BodyText> |
|
</Grid> |
|
</Grid> |
|
); |
|
})} |
|
<Grid container direction="row" item xs={gridColumns}> |
|
<Grid item xs={c1}> |
|
<BodyText text="Install ISO" /> |
|
</Grid> |
|
<Grid item xs={c2n3}> |
|
<BodyText> |
|
<InlineMonoText edge="start"> |
|
{fileUUIDMapToData[inputInstallISOFileUUID].fileName} |
|
</InlineMonoText> |
|
</BodyText> |
|
</Grid> |
|
</Grid> |
|
<Grid container direction="row" item xs={gridColumns}> |
|
<Grid item xs={c1}> |
|
<BodyText text="Driver ISO" /> |
|
</Grid> |
|
<Grid item xs={c2n3}> |
|
<BodyText> |
|
{fileUUIDMapToData[inputDriverISOFileUUID] ? ( |
|
<InlineMonoText edge="start"> |
|
{fileUUIDMapToData[inputDriverISOFileUUID].fileName} |
|
</InlineMonoText> |
|
) : ( |
|
'none' |
|
)} |
|
</BodyText> |
|
</Grid> |
|
</Grid> |
|
<Grid container direction="row" item xs={gridColumns}> |
|
<Grid item xs={c1}> |
|
<BodyText text="Optimize for OS" /> |
|
</Grid> |
|
<Grid item xs={c2n3}> |
|
<BodyText> |
|
<InlineMonoText edge="start">{`${inputOptimizeForOSValue?.label}`}</InlineMonoText> |
|
</BodyText> |
|
</Grid> |
|
</Grid> |
|
</Grid> |
|
); |
|
}; |
|
|
|
const hasResource = useMemo<Record<string, boolean>>( |
|
() => ({ |
|
'anvil node': Boolean(Object.keys(anvilUUIDMapToData).length), |
|
file: Boolean(Object.keys(fileUUIDMapToData).length), |
|
'storage group': Boolean(Object.keys(storageGroupUUIDMapToData).length), |
|
}), |
|
[anvilUUIDMapToData, fileUUIDMapToData, storageGroupUUIDMapToData], |
|
); |
|
|
|
useEffect(() => { |
|
api |
|
.get('/anvil', { |
|
params: { |
|
anvilUUIDs: 'all', |
|
isForProvisionServer: true, |
|
}, |
|
}) |
|
.then(({ data }) => { |
|
const { |
|
anvils: ueAllAnvils, |
|
anvilSelectItems: ueAnvilSelectItems, |
|
anvilUUIDMapToData: ueAnvilUUIDMapToData, |
|
fileSelectItems: ueFileSelectItems, |
|
fileUUIDMapToData: ueFileUUIDMapToData, |
|
serverNameMapToData: ueServerNameMapToData, |
|
storageGroupSelectItems: ueStorageGroupSelectItems, |
|
storageGroupUUIDMapToData: ueStorageGroupUUIDMapToData, |
|
} = organizeAnvils(data.anvils); |
|
|
|
setAllAnvils(ueAllAnvils); |
|
setAnvilUUIDMapToData(ueAnvilUUIDMapToData); |
|
setFileUUIDMapToData(ueFileUUIDMapToData); |
|
setServerNameMapToData(ueServerNameMapToData); |
|
setStorageGroupUUIDMapToData(ueStorageGroupUUIDMapToData); |
|
|
|
setAnvilSelectItems(ueAnvilSelectItems); |
|
setFileSelectItems(ueFileSelectItems); |
|
setStorageGroupSelectItems(ueStorageGroupSelectItems); |
|
|
|
const limits: Parameters<UpdateLimitsFunction>[0] = { |
|
allAnvils: ueAllAnvils, |
|
storageGroupUUIDMapToData: ueStorageGroupUUIDMapToData, |
|
}; |
|
|
|
// Auto-select the only option when there's only 1. |
|
// Reminder to update the form limits after changing any value. |
|
|
|
if (ueAnvilSelectItems.length === 1) { |
|
const { |
|
0: { value: uuid }, |
|
} = ueAnvilSelectItems; |
|
|
|
setInputAnvilValue(uuid); |
|
|
|
limits.includeAnvilUUIDs = [uuid]; |
|
} |
|
|
|
if (ueFileSelectItems.length === 1) { |
|
const { |
|
0: { value: uuid }, |
|
} = ueFileSelectItems; |
|
|
|
setInputInstallISOFileUUID(uuid); |
|
|
|
limits.fileUUIDs = [uuid, '']; |
|
} |
|
|
|
if (ueStorageGroupSelectItems.length === 1) { |
|
const { |
|
0: { value: uuid }, |
|
} = ueStorageGroupSelectItems; |
|
|
|
setVirtualDisks((previous) => { |
|
const current = { ...previous }; |
|
|
|
current.inputStorageGroupUUIDs[0] = uuid; |
|
|
|
limits.virtualDisks = current; |
|
|
|
return current; |
|
}); |
|
} |
|
|
|
initLimits(limits); |
|
|
|
setOSAutocompleteOptions( |
|
Object.entries(data.oses as Record<string, string>).map( |
|
([key, label]) => ({ |
|
key, |
|
label, |
|
}), |
|
), |
|
); |
|
|
|
setIsProvisionServerDataReady(true); |
|
}); |
|
}, [initLimits]); |
|
|
|
return ( |
|
<> |
|
<Dialog |
|
{...{ |
|
fullWidth: true, |
|
maxWidth: 'sm', |
|
open, |
|
PaperComponent: Panel, |
|
PaperProps: { sx: { overflow: 'visible' } }, |
|
}} |
|
> |
|
<PanelHeader> |
|
<HeaderText text="Provision a Server" /> |
|
<IconButton |
|
onClick={onCloseProvisionServerDialog} |
|
sx={{ |
|
backgroundColor: RED, |
|
color: TEXT, |
|
|
|
'&:hover': { backgroundColor: RED }, |
|
}} |
|
> |
|
<CloseIcon /> |
|
</IconButton> |
|
</PanelHeader> |
|
<FlexBox spacing=".6em"> |
|
{Object.entries(hasResource).map( |
|
([resource, has]) => |
|
!has && ( |
|
<MessageBox type="warning"> |
|
No {resource} available yet. Try refreshing after the resource |
|
gets created. |
|
</MessageBox> |
|
), |
|
)} |
|
</FlexBox> |
|
{isProvisionServerDataReady ? ( |
|
<Box |
|
sx={{ |
|
display: 'flex', |
|
flexDirection: 'column', |
|
maxHeight: '50vh', |
|
overflowY: 'scroll', |
|
paddingTop: '.6em', |
|
|
|
'& > :not(:first-child)': { |
|
marginTop: '1em', |
|
}, |
|
}} |
|
> |
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}> |
|
<OutlinedInputWithLabel |
|
id="ps-server-name" |
|
label="Server name" |
|
inputProps={{ |
|
onChange: ({ target: { value } }) => { |
|
setInputServerNameValue(value); |
|
|
|
testInput({ inputs: { serverName: { value } } }); |
|
}, |
|
value: inputServerNameValue, |
|
}} |
|
inputLabelProps={{ |
|
isNotifyRequired: inputServerNameValue.length === 0, |
|
}} |
|
messageBoxProps={inputServerNameMessage} |
|
/> |
|
</Box> |
|
<Autocomplete |
|
id="ps-cpu-cores" |
|
disableClearable |
|
extendRenderInput={({ inputLabelProps = {} }) => { |
|
inputLabelProps.isNotifyRequired = inputCPUCoresValue <= 0; |
|
}} |
|
getOptionLabel={(option) => String(option)} |
|
label="CPU cores" |
|
messageBoxProps={inputCPUCoresMessage} |
|
noOptionsText="No available number of cores." |
|
onChange={(event, value) => { |
|
if (!value || value === inputCPUCoresValue) return; |
|
|
|
setInputCPUCoresValue(value); |
|
|
|
const { maxCPUCores: newCPUCoresMax } = updateLimits({ |
|
cpuCores: value, |
|
}); |
|
|
|
testInput({ |
|
inputs: { |
|
cpuCores: { |
|
max: newCPUCoresMax, |
|
value, |
|
}, |
|
}, |
|
}); |
|
}} |
|
openOnFocus |
|
options={inputCpuCoresOptions} |
|
renderOption={(optionProps, option) => ( |
|
<li {...optionProps} key={`ps-cpu-cores-${option}`}> |
|
{option} |
|
</li> |
|
)} |
|
value={inputCPUCoresValue} |
|
/> |
|
<OutlinedLabeledInputWithSelect |
|
id="ps-memory" |
|
label="Memory" |
|
messageBoxProps={inputMemoryMessage} |
|
inputWithLabelProps={{ |
|
inputProps: { |
|
endAdornment: createMaxValueButton( |
|
`${inputMemoryMax} ${inputMemoryUnit}`, |
|
{ |
|
onButtonClick: () => { |
|
setInputMemoryValue(inputMemoryMax); |
|
changeMemory({ cmValue: memoryMax }); |
|
}, |
|
}, |
|
), |
|
onChange: ({ target: { value } }) => { |
|
handleInputMemoryValueChange({ value }); |
|
}, |
|
type: 'number', |
|
value: inputMemoryValue, |
|
}, |
|
inputLabelProps: { |
|
isNotifyRequired: memory === BIGINT_ZERO, |
|
}, |
|
}} |
|
selectItems={DATA_SIZE_UNIT_SELECT_ITEMS} |
|
selectWithLabelProps={{ |
|
selectProps: { |
|
onChange: ({ target: { value } }) => { |
|
const selectedUnit = value as DataSizeUnit; |
|
|
|
handleInputMemoryValueChange({ unit: selectedUnit }); |
|
}, |
|
value: inputMemoryUnit, |
|
}, |
|
}} |
|
/> |
|
{virtualDisks.stateIds.map((vdStateId, vdIndex) => |
|
createVirtualDiskForm( |
|
virtualDisks, |
|
vdIndex, |
|
setVirtualDisks, |
|
storageGroupSelectItems, |
|
includeStorageGroupUUIDs, |
|
updateLimits, |
|
storageGroupUUIDMapToData, |
|
testInput, |
|
), |
|
)} |
|
<SelectWithLabel |
|
disableItem={(value) => value === inputDriverISOFileUUID} |
|
hideItem={(value) => !includeFileUUIDs.includes(value)} |
|
id="ps-install-image" |
|
inputLabelProps={{ |
|
isNotifyRequired: inputInstallISOFileUUID.length === 0, |
|
}} |
|
label="Install ISO" |
|
messageBoxProps={inputInstallISOMessage} |
|
selectItems={fileSelectItems} |
|
selectProps={{ |
|
onChange: ({ target: { value } }) => { |
|
const newInstallISOFileUUID = value as string; |
|
|
|
handleInputInstallISOFileUUIDChange(newInstallISOFileUUID); |
|
}, |
|
onClearIndicatorClick: () => |
|
handleInputInstallISOFileUUIDChange(''), |
|
value: inputInstallISOFileUUID, |
|
}} |
|
/> |
|
<SelectWithLabel |
|
disableItem={(value) => value === inputInstallISOFileUUID} |
|
hideItem={(value) => !includeFileUUIDs.includes(value)} |
|
id="ps-driver-image" |
|
label="Driver ISO" |
|
messageBoxProps={inputDriverISOMessage} |
|
selectItems={fileSelectItems} |
|
selectProps={{ |
|
onChange: ({ target: { value } }) => { |
|
const newDriverISOFileUUID = value as string; |
|
|
|
handleInputDriverISOFileUUIDChange(newDriverISOFileUUID); |
|
}, |
|
onClearIndicatorClick: () => |
|
handleInputDriverISOFileUUIDChange(''), |
|
value: inputDriverISOFileUUID, |
|
}} |
|
/> |
|
<SelectWithLabel |
|
disableItem={(value) => !includeAnvilUUIDs.includes(value)} |
|
id="ps-anvil" |
|
inputLabelProps={{ |
|
isNotifyRequired: inputAnvilValue.length === 0, |
|
}} |
|
label="Anvil node" |
|
messageBoxProps={inputAnvilMessage} |
|
selectItems={anvilSelectItems} |
|
selectProps={{ |
|
onChange: ({ target: { value } }) => { |
|
const newAnvilUUID: string = value as string; |
|
|
|
handleInputAnvilValueChange(newAnvilUUID); |
|
}, |
|
onClearIndicatorClick: () => handleInputAnvilValueChange(''), |
|
renderValue: (value) => { |
|
const { anvilName: rvAnvilName = `Unknown ${value}` } = |
|
anvilUUIDMapToData[value as string] ?? {}; |
|
|
|
return rvAnvilName; |
|
}, |
|
value: inputAnvilValue, |
|
}} |
|
/> |
|
<Autocomplete |
|
id="ps-optimize-for-os" |
|
extendRenderInput={({ inputLabelProps = {} }) => { |
|
inputLabelProps.isNotifyRequired = |
|
inputOptimizeForOSValue === null; |
|
}} |
|
isOptionEqualToValue={(option, value) => option.key === value.key} |
|
label="Optimize for OS" |
|
messageBoxProps={inputOptimizeForOSMessage} |
|
noOptionsText="No matching OS" |
|
onChange={(event, value) => { |
|
setInputOptimizeForOSValue(value); |
|
}} |
|
openOnFocus |
|
options={osAutocompleteOptions} |
|
renderOption={(optionProps, option) => ( |
|
<li {...optionProps} key={`ps-optimize-for-os-${option.key}`}> |
|
{option.label} ({option.key}) |
|
</li> |
|
)} |
|
value={inputOptimizeForOSValue} |
|
/> |
|
</Box> |
|
) : ( |
|
<Spinner /> |
|
)} |
|
|
|
<Box |
|
sx={{ |
|
display: 'flex', |
|
flexDirection: 'column', |
|
marginTop: '1em', |
|
|
|
'& > :not(:first-child)': { |
|
marginTop: '1em', |
|
}, |
|
}} |
|
> |
|
{successfulProvisionCount > 0 && ( |
|
<MessageBox |
|
isAllowClose |
|
text="Provision server job registered. You can provision another server, or exit; it won't affect the registered job." |
|
/> |
|
)} |
|
{isProvisionRequestInProgress ? ( |
|
<Spinner mt={0} /> |
|
) : ( |
|
<Box |
|
sx={{ |
|
display: 'flex', |
|
flexDirection: 'row', |
|
justifyContent: 'flex-end', |
|
width: '100%', |
|
}} |
|
> |
|
<ContainedButton |
|
disabled={!testInput({ isIgnoreOnCallbacks: true })} |
|
onClick={() => { |
|
setIsOpenProvisionConfirmDialog(true); |
|
}} |
|
sx={PROVISION_BUTTON_STYLES} |
|
> |
|
Provision |
|
</ContainedButton> |
|
</Box> |
|
)} |
|
</Box> |
|
</Dialog> |
|
{isOpenProvisionConfirmDialog && ( |
|
<ConfirmDialog |
|
actionProceedText="Provision" |
|
content={createConfirmDialogContent()} |
|
dialogProps={{ open: isOpenProvisionConfirmDialog }} |
|
onCancelAppend={() => { |
|
setIsOpenProvisionConfirmDialog(false); |
|
}} |
|
onProceedAppend={() => { |
|
const requestBody = { |
|
serverName: inputServerNameValue, |
|
cpuCores: inputCPUCoresValue, |
|
memory: memory.toString(), |
|
virtualDisks: virtualDisks.stateIds.map((vdStateId, vdIndex) => ({ |
|
storageSize: virtualDisks.sizes[vdIndex].toString(), |
|
storageGroupUUID: virtualDisks.inputStorageGroupUUIDs[vdIndex], |
|
})), |
|
installISOFileUUID: inputInstallISOFileUUID, |
|
driverISOFileUUID: inputDriverISOFileUUID, |
|
anvilUUID: inputAnvilValue, |
|
optimizeForOS: inputOptimizeForOSValue?.key, |
|
}; |
|
|
|
setIsProvisionRequestInProgress(true); |
|
|
|
api.post('/server', requestBody).then(() => { |
|
setIsProvisionRequestInProgress(false); |
|
setSuccessfulProvisionCount(successfulProvisionCount + 1); |
|
}); |
|
|
|
setIsOpenProvisionConfirmDialog(false); |
|
}} |
|
proceedButtonProps={{ sx: PROVISION_BUTTON_STYLES }} |
|
titleText={`Provision ${inputServerNameValue}?`} |
|
/> |
|
)} |
|
</> |
|
); |
|
}; |
|
|
|
export default ProvisionServerDialog;
|
|
|