// 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 React, {useCallback, useMemo} from 'react'
import {Trans, useTranslation} from 'react-i18next'
import {useSelector} from 'react-redux'
import {findFirst} from '../../util'
// State / Model
import {
setState,
getState,
useState,
updateState,
clearState,
clearEmptyNest,
ssmPolicy,
} from '../../store'
// UI Elements
import {
Autosuggest,
Button,
FormField,
Input,
SpaceBetween,
Checkbox,
TokenGroup,
Select,
InputProps,
TextContent,
} from '@cloudscape-design/components'
// Components
import {NonCancelableEventHandler} from '@cloudscape-design/components/internal/events'
import TitleDescriptionHelpPanel from '../../components/help-panel/TitleDescriptionHelpPanel'
import InfoLink from '../../components/InfoLink'
import {subnetName} from './util'
// Helper Functions
function strToOption(str: any) {
return {value: str, label: str}
}
type Extension = {
name: string
path: string
description: string
args: {name: string; default?: string}[]
}
type ActionsEditorProps = {
basePath: string[]
errorsPath: string[]
}
// Selectors
const selectVpc = (state: any) => getState(state, ['app', 'wizard', 'vpc'])
const selectAwsSubnets = (state: any) => getState(state, ['aws', 'subnets'])
function LabeledIcon({label, icon}: any) {
return (
{/* eslint-disable-next-line @next/next/no-img-element*/}
{label}
)
}
function SubnetSelect({value, onChange, disabled}: any) {
const subnets = useSelector(selectAwsSubnets)
const vpc = useSelector(selectVpc)
var filteredSubnets =
subnets &&
subnets.filter((s: any) => {
return vpc ? s.VpcId === vpc : true
})
if (!subnets) {
return No Subnets Found.
}
const itemToOption = (item: any) => {
return {
value: item.SubnetId,
label: item.SubnetId,
description:
item.AvailabilityZone +
` - ${item.AvailabilityZoneId}` +
(subnetName(item) ? ` (${subnetName(item)})` : ''),
}
}
return (
{
return x.SubnetId === value
})
? itemToOption(
findFirst(filteredSubnets, (x: any) => {
return x.SubnetId === value
}),
)
: {label: 'Please Select A Subnet'}
}
onChange={({detail}) => {
onChange && onChange(detail.selectedOption.value)
}}
selectedAriaLabel="Selected"
options={filteredSubnets.map(itemToOption)}
/>
)
}
type InstanceGroup = [string, string][]
export function useInstanceGroups(): Record {
const instanceTypes = useState(['aws', 'instanceTypes']) || []
let groups: {[key: string]: [string, string][]} = {}
for (let instance of instanceTypes) {
let group = 'General Purpose'
if (instance.InstanceType.startsWith('c6g')) {
group = 'Graviton'
} else if (instance.InstanceType.startsWith('c')) {
group = 'Compute'
} else if (instance.InstanceType.startsWith('hpc')) {
group = 'HPC'
} else if (instance.InstanceType.startsWith('m')) {
group = 'Mixed'
} else if (instance.InstanceType.startsWith('r')) {
group = 'High Memory'
} else if (
instance.InstanceType.startsWith('p') ||
instance.InstanceType.startsWith('g')
) {
group = 'GPU'
}
if (!(group in groups)) groups[group] = []
let desc = `${instance.VCpuInfo.DefaultVCpus} vcpus, ${
instance.MemoryInfo.SizeInMiB / 1024
}GB memory`
if (Object.keys(instance.GpuInfo).length > 0)
desc = `${instance.GpuInfo.Count} x ${instance.GpuInfo.Name}, ${desc}`
groups[group].push([instance.InstanceType, desc])
}
return groups
}
function InstanceSelect({path, selectId, callback, disabled}: any) {
const value = useState(path) || ''
const instanceGroups = useInstanceGroups()
const instanceToOption = ([value, label]: string[]) => {
return {label: value, description: label, value: value}
}
return (
{
if (detail.value !== value) {
setState(path, detail.value)
callback && callback(detail.value)
}
}}
// @ts-expect-error TS(2322) FIXME: Type '(newValue: string) => void' is not assignabl... Remove this comment to see the full error message
enteredTextLabel={newValue => {
if (newValue !== value) {
setState(path, newValue)
callback && callback(newValue)
}
}}
ariaLabel="Instance Selector"
placeholder="Instance Type"
empty="No matches found"
options={Object.keys(instanceGroups).map(groupName => {
return {
label: groupName,
options: instanceGroups[groupName].map(instanceToOption),
}
})}
/>
)
}
function CustomAMISettings({basePath, appPath, errorsPath, validate}: any) {
const editing = useState(['app', 'wizard', 'editing'])
const customImages = useState(['app', 'wizard', 'customImages']) || []
const officialImages = useState(['app', 'wizard', 'officialImages']) || []
const error = useState([...errorsPath, 'customAmi'])
const customAmiPath = useMemo(
() => [...basePath, 'Image', 'CustomAmi'],
[basePath],
)
const customAmi = useState(customAmiPath)
const customAmiEnabled =
useState([...appPath, 'customAMI', 'enabled']) || false
const osPath = ['app', 'wizard', 'config', 'Image', 'Os']
const os = useState(osPath) || 'alinux2'
const {t} = useTranslation()
var suggestions = []
for (let image of customImages) {
suggestions.push({
value: image.ec2AmiInfo.amiId,
description: `${image.ec2AmiInfo.amiId} (${image.imageId})`,
})
}
for (let image of officialImages)
if (image.os === os) {
suggestions.push({
value: image.amiId,
description: `${image.amiId} (${image.name})`,
})
}
const toggleCustomAmi = () => {
const value = !customAmiEnabled
setState([...appPath, 'customAMI', 'enabled'], value)
if (!value) {
clearState(customAmiPath)
if (Object.keys(getState([...basePath, 'Image'])).length === 0)
clearState([...basePath, 'Image'])
}
}
const selectText = useCallback(
(value: string) => {
if (value !== customAmi) {
setState(customAmiPath, value)
}
return value
},
[customAmi, customAmiPath],
)
return (
<>
}
/>
}
/>
{customAmiEnabled && (
{
if (detail.value !== customAmi) {
setState(customAmiPath, detail.value)
}
}}
value={customAmi || ''}
enteredTextLabel={selectText}
ariaLabel="Custom AMI Selector"
placeholder="AMI ID"
empty="No matches found"
options={suggestions}
/>
)}
>
)
}
function ArgEditor({path, i}: any) {
const {t} = useTranslation()
const args = useState(path)
const arg = useState([...path, i])
const remove = () => {
if (args.length > 1)
setState([...path], [...args.slice(0, i), ...args.slice(i + 1)])
else clearState(path)
clearEmptyNest(path, 3)
}
return (
Arg:
{
setState([...path, i], detail.value)
}}
/>
{t('wizard.actions.remove')}
)
}
function ActionEditor({label, description, actionKey, errorPath, path}: any) {
const script = useState([...path, 'Script']) || ''
const args = useState([...path, 'Args']) || []
const addArg = (path: any) => {
updateState(path, (old: any) => [...(old || []), ''])
}
const editScript = (path: any, val: any) => {
if (val !== '') setState(path, val)
else clearState(path)
clearEmptyNest(path, 3)
}
return (
{args.map((a: any, i: any) => (
))}
)
}
function ActionsEditor({basePath, errorsPath}: ActionsEditorProps) {
const {t} = useTranslation()
const actionsPath = [...basePath, 'CustomActions']
const onStartPath = [...actionsPath, 'OnNodeStart']
const onConfiguredPath = [...actionsPath, 'OnNodeConfigured']
const onStartErrors = useState([...errorsPath, 'onStart'])
const onConfiguredErrors = useState([...errorsPath, 'onConfigured'])
return (
)
}
function HeadNodeActionsEditor({basePath, errorsPath}: ActionsEditorProps) {
const {t} = useTranslation()
const actionsPath = [...basePath, 'CustomActions']
const onStartPath = [...actionsPath, 'OnNodeStart']
const onConfiguredPath = [...actionsPath, 'OnNodeConfigured']
const onUpdatedPath = [...actionsPath, 'OnNodeUpdated']
const onStartErrors = useState([...errorsPath, 'onStart'])
const onConfiguredErrors = useState([...errorsPath, 'onConfigured'])
const onUpdatedErrors = useState([...errorsPath, 'onUpdated'])
return (
)
}
function SecurityGroups({basePath}: any) {
const {t} = useTranslation()
const sgPath = [...basePath, 'Networking', 'AdditionalSecurityGroups']
const selectedSgs = useState(sgPath) || []
const sgSelected = useState(['app', 'wizard', 'sg-selected'])
const sgs = useState(['aws', 'security_groups']) || []
const sgMap = sgs.reduce((acc: any, s: any) => {
acc[s.GroupId] = s.GroupName
return acc
}, {})
const itemToOption = (item: any) => {
return {
value: item.GroupId,
label: item.GroupId,
description: item.GroupName,
}
}
const removeSg = (i: any) => {
setState(sgPath, [...selectedSgs.slice(0, i), ...selectedSgs.slice(i + 1)])
if (getState(sgPath).length === 0) clearState(sgPath)
}
return (
x.GroupId === sgSelected.value)
? itemToOption(
findFirst(sgs, (x: any) => x.GroupId === sgSelected.value),
)
: {label: t('wizard.headNode.securityGroups.select')}
}
onChange={({detail}) => {
setState(['app', 'wizard', 'sg-selected'], detail.selectedOption)
}}
triggerVariant={'option'}
options={sgs.map(itemToOption)}
/>
setState(sgPath, [...selectedSgs, sgSelected.value])}
>
Add
{
removeSg(itemIndex)
}}
items={selectedSgs.map((s: any) => {
return {label: s, dismissLabel: `Remove ${s}`, description: sgMap[s]}
})}
/>
)
}
function RootVolume({basePath, errorsPath}: any) {
const {t} = useTranslation()
const rootVolumeSizePath = [...basePath, 'LocalStorage', 'RootVolume', 'Size']
const rootVolumeSize = useState(rootVolumeSizePath)
const rootVolumeEncryptedPath = [
...basePath,
'LocalStorage',
'RootVolume',
'Encrypted',
]
const rootVolumeEncrypted = useState(rootVolumeEncryptedPath)
const rootVolumeTypePath = useMemo(
() => [...basePath, 'LocalStorage', 'RootVolume', 'VolumeType'],
[basePath],
)
const rootVolumeType = useState(rootVolumeTypePath)
const defaultRootVolumeType = 'gp3'
const volumeTypes = ['gp3', 'gp2', 'io1', 'io2', 'sc1', 'st1', 'standard']
const rootVolumeErrors = useState([...errorsPath, 'rootVolume'])
const editing = useState(['app', 'wizard', 'editing'])
const setRootVolume = (size: any) => {
if (size === '') clearState(rootVolumeSizePath)
else setState(rootVolumeSizePath, parseInt(size))
clearEmptyNest(rootVolumeSizePath, 3)
}
const toggleEncrypted = () => {
const setEncrypted = !rootVolumeEncrypted
if (setEncrypted) setState(rootVolumeEncryptedPath, setEncrypted)
else clearState(rootVolumeEncryptedPath)
clearEmptyNest(rootVolumeSizePath, 3)
}
React.useEffect(() => {
if (rootVolumeType === null)
setState(rootVolumeTypePath, defaultRootVolumeType)
}, [rootVolumeType, rootVolumeTypePath])
return (
setRootVolume(detail.value)}
/>
{t('wizard.components.rootVolume.encrypted')}
:
{
setState(rootVolumeTypePath, detail.selectedOption.value)
}}
options={volumeTypes.map(strToOption)}
/>
)
}
function IamPoliciesEditor({basePath}: any) {
const {t} = useTranslation()
const policiesPath = [...basePath, 'Iam', 'AdditionalIamPolicies']
const policies = useState(policiesPath) || []
const policyPath = ['app', 'wizard', 'headNode', 'iamPolicy']
const policy = useState(policyPath) || ''
const addPolicy = () => {
updateState(policiesPath, (existing: any) => [
...(existing || []),
{Policy: policy},
])
setState(policyPath, '')
}
const removePolicy = (index: any) => {
setState(policiesPath, [
...policies.slice(0, index),
...policies.slice(index + 1),
])
if (policies.length === 0) clearState(policiesPath)
}
return (
{t('wizard.components.IamPoliciesEditor.title')}
x.Policy === policy)
? t('wizard.components.IamPoliciesEditor.policyAlreadyAdded')
: ''
}
>
setState(policyPath, detail.value)}
/>
x.Policy === policy)
}
>
{t('wizard.components.IamPoliciesEditor.addButtonLabel')}
{policies.map(
(p: any, i: any) =>
p.Policy !== ssmPolicy && (
{p.Policy}
removePolicy(i)}>Remove
),
)}
)
}
type HelpTextInputProps = {
name: string
path: string[]
errorsPath: string[]
configKey: string
description: string
help: string
placeholder: string
type?: InputProps.Type
onChange: NonCancelableEventHandler
}
function HelpTextInput({
name,
path,
errorsPath,
configKey,
description,
help,
placeholder,
type = 'text',
onChange,
}: HelpTextInputProps) {
let value = useState([...path, configKey])
let error = useState([...errorsPath, configKey])
return (
}
/>
}
>
)
}
export {
SubnetSelect,
SecurityGroups,
InstanceSelect,
LabeledIcon,
ActionsEditor,
HeadNodeActionsEditor,
CustomAMISettings,
RootVolume,
IamPoliciesEditor,
HelpTextInput,
}