// 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.
// Fameworks
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 {
Checkbox,
CheckboxProps,
Container,
FormField,
Header,
Select,
SpaceBetween,
} from '@cloudscape-design/components'
// State / Model
import {getState, setState, useState, clearState} from '../../store'
import {LoadAwsConfig} from '../../model'
// Components
import {CustomAMISettings} from './Components'
import {useFeatureFlag} from '../../feature-flags/useFeatureFlag'
import {createComputeResource as singleCreate} from './Queues/SingleInstanceComputeResource'
import {createComputeResource as multiCreate} from './Queues/MultiInstanceComputeResource'
import {MultiUser, multiUserValidate} from './MultiUser'
import {NonCancelableEventHandler} from '@cloudscape-design/components/internal/events'
import TitleDescriptionHelpPanel from '../../components/help-panel/TitleDescriptionHelpPanel'
import {useHelpPanel} from '../../components/help-panel/HelpPanel'
import {useCallback, useMemo} from 'react'
import {OptionDefinition} from '@cloudscape-design/components/internal/components/option/interfaces'
import {SelectProps} from '@cloudscape-design/components/select/interfaces'
// Constants
const errorsPath = ['app', 'wizard', 'errors', 'cluster']
const selectQueues = (state: any) =>
getState(state, ['app', 'wizard', 'config', 'Scheduling', 'SlurmQueues'])
const selectVpc = (state: any) => getState(state, ['app', 'wizard', 'vpc'])
const selectAwsSubnets = (state: any) => getState(state, ['aws', 'subnets'])
function clusterValidate() {
const vpc = getState(['app', 'wizard', 'vpc'])
const editing = getState(['app', 'wizard', 'editing'])
const customAmiEnabled = getState(['app', 'wizard', 'customAMI', 'enabled'])
const customAmi = getState(['app', 'wizard', 'config', 'Image', 'CustomAmi'])
const multiUserEnabled = getState(['app', 'wizard', 'multiUser']) || false
let valid = true
setState([...errorsPath, 'validated'], true)
if (!editing && !vpc) {
setState(
[...errorsPath, 'vpc'],
i18next.t('wizard.cluster.validation.VpcSelect'),
)
valid = false
} else {
clearState([...errorsPath, 'vpc'])
}
if (customAmiEnabled && !customAmi) {
setState(
[...errorsPath, 'customAmi'],
i18next.t('wizard.cluster.validation.customAmiSelect'),
)
valid = false
} else {
clearState([...errorsPath, 'customAmi'])
}
if (multiUserEnabled && !multiUserValidate()) {
valid = false
} else {
clearState([...errorsPath, 'multiUser'])
}
return valid
}
function itemToOption(
item: string | [string, string] | null,
): SelectProps.Option | null {
if (!item) return null
let label, value
if (typeof item == 'string') {
label = item
value = item
} else {
;[value, label] = item
}
return {label, value}
}
function RegionSelect() {
const {t} = useTranslation()
const region =
useState(['app', 'wizard', 'config', 'Region']) || 'Please select a region.'
const queues = useSelector(selectQueues)
const editing = useState(['app', 'wizard', 'editing'])
const handleChange = ({detail}: any) => {
const chosenRegion =
detail.selectedOption.value === 'Default'
? null
: detail.selectedOption.value
LoadAwsConfig(chosenRegion)
setState(['app', 'wizard', 'vpc'], null)
setState(['app', 'wizard', 'headNode', 'subnet'], null)
if (queues)
queues.forEach((_queue: any, i: any) => {
clearState(['app', 'wizard', 'queues', i, 'subnet'])
})
setState(['app', 'wizard', 'config', 'Region'], chosenRegion)
}
const supportedRegions = [
'af-south-1',
'ap-east-1',
'ap-northeast-1',
'ap-northeast-2',
'ap-south-1',
'ap-southeast-1',
'ap-southeast-2',
'ca-central-1',
'cn-north-1',
'cn-northwest-1',
'eu-central-1',
'eu-north-1',
'eu-south-1',
'eu-west-1',
'eu-west-2',
'eu-west-3',
'me-south-1',
'sa-east-1',
'us-east-1',
'us-east-2',
'us-gov-east-1',
'us-gov-west-1',
'us-west-1',
'us-west-2',
]
return (
<>
>
)
}
function OsSelect() {
const {t} = useTranslation()
const oses: [string, string][] = [
['alinux2', 'Amazon Linux 2'],
['centos7', 'CentOS 7'],
['ubuntu1804', 'Ubuntu 18.04'],
['ubuntu2004', 'Ubuntu 20.04'],
]
const osPath = ['app', 'wizard', 'config', 'Image', 'Os']
const os = useState(osPath) || 'alinux2'
const editing = useState(['app', 'wizard', 'editing'])
const osesOptions = useMemo(() => oses.map(itemToOption), [oses])
const selectedOs: OptionDefinition | null = useMemo(() => {
const selectedOsTuple = findFirst(oses, (x: any) => x[0] === os) || null
return itemToOption(selectedOsTuple)
}, [os, oses])
const handleChange = useCallback(({detail}: any) => {
setState(osPath, detail.selectedOption.value)
}, [])
return (
<>
>
)
}
function VpcSelect() {
const {t} = useTranslation()
const vpcs = useState(['aws', 'vpcs']) || []
const vpc = useSelector(selectVpc) || ''
const error = useState([...errorsPath, 'vpc'])
const subnets = useSelector(selectAwsSubnets)
const queues = useSelector(selectQueues)
const editing = useState(['app', 'wizard', 'editing'])
const VpcName = (vpc: any) => {
if (!vpc) return null
var tags = vpc.Tags
if (!tags) {
return null
}
tags = vpc.Tags.filter((t: any) => {
return t.Key === 'Name'
})
return tags.length > 0 ? tags[0].Value : null
}
const vpcToDisplayOption = (vpc: any) => {
return vpc
? {
label: (
{VpcName(vpc) ? VpcName(vpc) : vpc.VpcId}
),
value: vpc.VpcId,
}
: {
label: Select a VPC
,
value: null,
}
}
const vpcToOption = (vpc: any) => {
return vpc
? {
label: (
{vpc.VpcId} {VpcName(vpc) && `(${VpcName(vpc)})`}
),
value: vpc.VpcId,
}
: {
label: Select a VPC
,
value: null,
}
}
const setVpc = (vpcId: any) => {
setState(['app', 'wizard', 'vpc'], vpcId)
setState([...errorsPath, 'vpc'], null)
const headNodeSubnetPath = [
'app',
'wizard',
'config',
'HeadNode',
'Networking',
'SubnetId',
]
const filteredSubnets =
subnets &&
subnets.filter((s: any) => {
return s.VpcId === vpcId
})
if (filteredSubnets.length > 0) {
const subnetSet = new Set(filteredSubnets)
var subnet = filteredSubnets[0]
if (!subnetSet.has(getState(headNodeSubnetPath)))
setState(headNodeSubnetPath, subnet.SubnetId)
if (queues)
queues.forEach((_queue: any, i: any) => {
const queueSubnetPath = [
'app',
'wizard',
'config',
'Scheduling',
'SlurmQueues',
i,
'Networking',
'SubnetIds',
]
if (!subnetSet.has(getState(queueSubnetPath)))
setState(queueSubnetPath, [subnet.SubnetId])
})
}
}
return (
)
}
function Cluster() {
const {t} = useTranslation()
const editing = useState(['app', 'wizard', 'editing'])
const configPath = ['app', 'wizard', 'config']
let config = useState(configPath)
let clusterConfig = useState(['app', 'wizard', 'clusterConfigYaml']) || ''
let wizardLoaded = useState(['app', 'wizard', 'loaded'])
let multiUserEnabled = useState(['app', 'wizard', 'multiUser']) || false
let awsConfig = useState(['aws'])
let defaultRegion = useState(['aws', 'region']) || ''
const region = useState(['app', 'selectedRegion']) || defaultRegion
const isMultiuserClusterActive = useFeatureFlag('multiuser_cluster')
const isMultipleInstanceTypesActive = useFeatureFlag(
'queues_multiple_instance_types',
)
useHelpPanel()
React.useEffect(() => {
const configPath = ['app', 'wizard', 'config']
// Don't overwrite the config if we go back, still gets overwritten
// after going forward so need to consider better way of handling this
if (clusterConfig) return
// Load these values once when creating the component
if (!wizardLoaded) {
setState(['app', 'wizard', 'loaded'], true)
if (!config) {
const customAMIEnabled = getIn(config, ['Image', 'CustomAmi'])
? true
: false
setState(['app', 'wizard', 'customAMI', 'enabled'], customAMIEnabled)
setState([...configPath, 'HeadNode', 'InstanceType'], 't2.micro')
setState([...configPath, 'Scheduling', 'Scheduler'], 'slurm')
setState([...configPath, 'Region'], region)
setState([...configPath, 'Image', 'Os'], 'alinux2')
setState(
[...configPath, 'Scheduling', 'SlurmQueues'],
[
{
Name: 'queue0',
AllocationStrategy: isMultipleInstanceTypesActive
? 'lowest-price'
: undefined,
ComputeResources: [
isMultipleInstanceTypesActive
? multiCreate(0, 0)
: singleCreate(0, 0),
],
},
],
)
}
}
// Load these values when we get a new config as well (e.g. changing region)
if (awsConfig && awsConfig.keypairs && awsConfig.keypairs.length > 0) {
const keypairs = getState(['aws', 'keypairs']) || []
const keypairNames = new Set(keypairs.map((kp: any) => kp.KeyName))
const headNodeKPPath = [...configPath, 'HeadNode', 'Ssh', 'KeyName']
if (keypairs.length > 0 && !keypairNames.has(getState(headNodeKPPath))) {
setState(headNodeKPPath, awsConfig.keypairs[0].KeyName)
}
}
}, [
region,
config,
awsConfig,
clusterConfig,
wizardLoaded,
isMultipleInstanceTypesActive,
])
const handleMultiUserChange: NonCancelableEventHandler<
CheckboxProps.ChangeDetail
> = ({detail}) => {
if (!detail.checked) {
clearState(['app', 'wizard', 'config', 'DirectoryService'])
}
setState(['app', 'wizard', 'multiUser'], detail.checked)
}
return (
{isMultiuserClusterActive && (
)}
{multiUserEnabled && }
)
}
const ClusterPropertiesHelpPanel = () => {
const {t} = useTranslation()
const footerLinks = React.useMemo(
() => [
{
title: t('wizard.cluster.help.networkLink.title'),
href: t('wizard.cluster.help.networkLink.href'),
},
{
title: t('wizard.cluster.help.adLink.title'),
href: t('wizard.cluster.help.adLink.href'),
},
{
title: t('wizard.cluster.help.amiLink.title'),
href: t('wizard.cluster.help.amiLink.href'),
},
{
title: t('global.help.configurationProperties.title'),
href: t('global.help.configurationProperties.href'),
},
],
[t],
)
return (
}
footerLinks={footerLinks}
/>
)
}
export {Cluster, clusterValidate, ClusterPropertiesHelpPanel, itemToOption}