// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance // with the License. A copy of the License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and // limitations under the License. // Frameworks import * as React from 'react' import i18next from 'i18next' import {Trans, useTranslation} from 'react-i18next' import {useSelector} from 'react-redux' import {findFirst, getIn} from '../../util' // UI Elements import { Box, Checkbox, ColumnLayout, Container, ExpandableSection, FormField, Input, Select, SpaceBetween, } from '@cloudscape-design/components' // State import { setState, getState, useState, clearState, updateState, ssmPolicy, } from '../../store' // Components import { HeadNodeActionsEditor, InstanceSelect, RootVolume, SecurityGroups, SubnetSelect, IamPoliciesEditor, ActionsEditor, } from './Components' import {useFeatureFlag} from '../../feature-flags/useFeatureFlag' import { SlurmSettings, validateSlurmSettings, } from './SlurmSettings/SlurmSettings' import InfoLink from '../../components/InfoLink' import TitleDescriptionHelpPanel from '../../components/help-panel/TitleDescriptionHelpPanel' import {useHelpPanel} from '../../components/help-panel/HelpPanel' // Constants const headNodePath = ['app', 'wizard', 'config', 'HeadNode'] const errorsPath = ['app', 'wizard', 'errors', 'headNode'] function headNodeValidate() { const subnetPath = [...headNodePath, 'Networking', 'SubnetId'] const subnetValue = getState(subnetPath) const rootVolumeSizePath = [ ...headNodePath, 'LocalStorage', 'RootVolume', 'Size', ] const rootVolumeValue = getState(rootVolumeSizePath) const instanceTypePath = [...headNodePath, 'InstanceType'] const instanceTypeValue = getState(instanceTypePath) const actionsPath = [...headNodePath, 'CustomActions'] const onStartPath = [...actionsPath, 'OnNodeStart'] const onStart = getState(onStartPath) const onConfiguredPath = [...actionsPath, 'OnNodeConfigured'] const onConfigured = getState(onConfiguredPath) const onUpdatedPath = [...actionsPath, 'OnNodeUpdated'] const onUpdated = getState(onUpdatedPath) let valid = true if (!subnetValue) { setState( [...errorsPath, 'subnet'], i18next.t('wizard.headNode.validation.selectSubnet'), ) valid = false } else { clearState([...errorsPath, 'subnet']) } if (!instanceTypeValue) { setState( [...errorsPath, 'instanceType'], i18next.t('wizard.headNode.validation.selectInstanceType'), ) valid = false } else { clearState([...errorsPath, 'instanceType']) } if (rootVolumeValue === '') { setState( [...errorsPath, 'rootVolume'], i18next.t('wizard.headNode.validation.setRootVolumeSize'), ) valid = false } else if ( rootVolumeValue && (!Number.isInteger(rootVolumeValue) || rootVolumeValue < 35) ) { setState( [...errorsPath, 'rootVolume'], i18next.t('wizard.headNode.validation.rootVolumeMinimum'), ) valid = false } else { clearState([...errorsPath, 'rootVolume']) } if ( onStart && getState([...onStartPath, 'Args']) && !getState([...onStartPath, 'Script']) ) { setState( [...errorsPath, 'onStart'], i18next.t('wizard.headNode.validation.scriptWithArgs'), ) valid = false } else { clearState([...errorsPath, 'onStart']) } if ( onConfigured && getState([...onConfiguredPath, 'Args']) && !getState([...onConfiguredPath, 'Script']) ) { setState( [...errorsPath, 'onConfigured'], i18next.t('wizard.headNode.validation.scriptWithArgs'), ) valid = false } else { clearState([...errorsPath, 'onConfigured']) } if ( onUpdated && getState([...onUpdatedPath, 'Args']) && !getState([...onUpdatedPath, 'Script']) ) { setState( [...errorsPath, 'onUpdated'], i18next.t('wizard.headNode.validation.scriptWithArgs'), ) valid = false } else { clearState([...errorsPath, 'onUpdated']) } valid = validateSlurmSettings() setState([...errorsPath, 'validated'], true) return valid } function enableSsm(enable: any) { const iamPolicies = getState([ ...headNodePath, 'Iam', 'AdditionalIamPolicies', ]) const defaultRegion = getState(['aws', 'region']) const region = getState(['app', 'selectedRegion']) || defaultRegion if (enable) { if (iamPolicies && findFirst(iamPolicies, isSsmPolicy)) return updateState( [...headNodePath, 'Iam', 'AdditionalIamPolicies'], (existing: any) => { return [...(existing || []), {Policy: ssmPolicy(region)}] }, ) } else { if (!iamPolicies || (iamPolicies && !findFirst(iamPolicies, isSsmPolicy))) return if (iamPolicies.length === 1) clearState([...headNodePath, 'Iam']) else { updateState( [...headNodePath, 'Iam', 'AdditionalIamPolicies'], (existing: any) => existing.filter((p: any) => { return !isSsmPolicy(p) }), ) } } } function KeypairSelect() { const {t} = useTranslation() const keypairs = useState(['aws', 'keypairs']) || [] const keypairPath = [...headNodePath, 'Ssh', 'KeyName'] const keypair = useState(keypairPath) || '' const editing = useState(['app', 'wizard', 'editing']) const keypairToOption = (kp: any) => { if (kp === 'None' || kp === null || kp === undefined) return {label: 'None', value: null} else return {label: kp.KeyName, value: kp.KeyName} } const keypairsWithNone = ['None', ...keypairs] const setKeyPair = (kpValue: any) => { if (kpValue) setState(keypairPath, kpValue) else { clearState([...headNodePath, 'Ssh']) enableSsm(true) } } return ( { setState( [...headNodePath, 'Dcv', 'AllowedIps'], detail.value, ) }} /> )} {dcvEnabled && ( setState( [...headNodePath, 'Dcv', 'Port'], parseInt(detail.value), ) } /> )} ) } function HeadNode() { const {t} = useTranslation() const imdsSecuredPath = [...headNodePath, 'Imds', 'Secured'] const imdsSecured = useState(imdsSecuredPath) const subnetPath = [...headNodePath, 'Networking', 'SubnetId'] const instanceTypeErrors = useState([...errorsPath, 'instanceType']) const subnetErrors = useState([...errorsPath, 'subnet']) const subnetValue = useState(subnetPath) || '' const editing = useState(['app', 'wizard', 'editing']) const isOnNodeUpdatedActive = useFeatureFlag('on_node_updated') useHelpPanel() const toggleImdsSecured = () => { const setImdsSecured = !imdsSecured if (setImdsSecured) setState(imdsSecuredPath, setImdsSecured) else { clearState(imdsSecuredPath) if (Object.keys(getState([...headNodePath, 'Imds'])).length === 0) clearState([...headNodePath, 'Imds']) } } return ( setState(subnetPath, subnetId)} /> } /> } /> } /> } > {isOnNodeUpdatedActive ? ( ) : ( )} ) } const HeadNodePropertiesHelpPanel = () => { const {t} = useTranslation() const footerLinks = React.useMemo( () => [ { title: t('wizard.headNode.help.instanceSelectionLink.title'), href: t('wizard.headNode.help.instanceSelectionLink.href'), }, { title: t('wizard.headNode.help.headNodePropertiesLink.title'), href: t('wizard.headNode.help.headNodePropertiesLink.href'), }, { title: t('wizard.headNode.help.ssmLink.title'), href: t('wizard.headNode.help.ssmLink.href'), }, { title: t('wizard.headNode.help.dcvLink.title'), href: t('wizard.headNode.help.dcvLink.href'), }, { title: t('wizard.headNode.help.customActionsLink.title'), href: t('wizard.headNode.help.customActionsLink.href'), }, ], [t], ) return ( } footerLinks={footerLinks} /> ) } export {HeadNode, headNodeValidate, HeadNodePropertiesHelpPanel}