// 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, { ReactElement, useCallback, useMemo, useState as useStateReact, } from 'react' import {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, Select, InputProps, CheckboxProps, Multiselect, MultiselectProps, ColumnLayout, BreadcrumbGroupProps, BreadcrumbGroup, } 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' import { ActionsEditorProps, InstanceGroup, InstanceType, InstanceTypeOption, } from './Components.types' import {useNavigate} from 'react-router-dom' // Helper Functions function strToOption(str: any) { return {value: str, label: str} } // 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}
{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 { 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], ) const helpPanelFooter = useMemo( () => [ { title: t('wizard.components.customAmi.helpPanel.imageLink.title'), href: t('wizard.components.customAmi.helpPanel.imageLink.href'), }, ], [t], ) return ( } > {t('wizard.components.customAmi.label')} {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 ( { setState([...path, i], detail.value) }} placeholder={t('wizard.components.actionsEditor.argument.placeholder')} /> ) } function ActionEditor({ label, error, path, }: { label: string error: string path: string[] }) { const script = useState([...path, 'Script']) || '' const {t} = useTranslation() const args = useState([...path, 'Args']) || [] const [enabled, setEnabled] = useStateReact(!!script) 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) } const toggleCheckbox = useCallback(() => { clearState(path) setEnabled(!enabled) }, [enabled, setEnabled, path]) return ( {label} {enabled ? ( <> editScript([...path, 'Script'], detail.value) } /> {args.length > 0 ? ( {/* The title is styled as an empty FormField because the shortest heading is an h3, too big for this case */} {args.map((a: any, i: any) => ( ))} ) : null} ) : null} ) } 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 ( ) } const securityGroupToOption = (item: {GroupId: string; GroupName: string}) => { return { value: item.GroupId, label: item.GroupId, description: item.GroupName, } } function SecurityGroups({basePath}: {basePath: string[]}) { const {t} = useTranslation() const securityGroupsPath = useMemo( () => [...basePath, 'Networking', 'AdditionalSecurityGroups'], [basePath], ) const selectedSecurityGroups: string[] = useState(securityGroupsPath) || [] const availableSecurityGroups = useState(['aws', 'security_groups']) const options = useMemo( () => (availableSecurityGroups || []).map(securityGroupToOption), [availableSecurityGroups], ) const onChange: NonCancelableEventHandler = useCallback( ({detail}) => { setState( securityGroupsPath, detail.selectedOptions.map(option => option.value), ) }, [securityGroupsPath], ) return ( selectedSecurityGroups.includes(opt.value), )} placeholder={t('wizard.headNode.securityGroups.select')} onChange={onChange} options={options} /> ) } 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)} /> setState(policyPath, detail.value)} /> {policies.map( (p: any, i: any) => p.Policy !== ssmPolicy && (
{p.Policy}
), )} ) } 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 ( } /> } >
) } type CheckboxWithInfoLinkProps = CheckboxProps & { helpPanel: ReactElement } const CheckboxWithHelpPanel = ({ helpPanel, children, ...checkboxProps }: CheckboxWithInfoLinkProps) => { return ( {children} ) } export const BreadcrumbGroupNavigate = ({ onFollow, ...props }: BreadcrumbGroupProps) => { const navigate = useNavigate() const handleNavigate = React.useCallback( event => { event.preventDefault() if (onFollow) { onFollow(event) } else { navigate(event.detail.href) } }, [navigate, onFollow], ) return } export { SubnetSelect, SecurityGroups, InstanceSelect, LabeledIcon, ActionsEditor, ActionEditor, HeadNodeActionsEditor, CustomAMISettings, RootVolume, IamPoliciesEditor, HelpTextInput, CheckboxWithHelpPanel, }