// 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, useState as reactUseState} from 'react'
import i18next from 'i18next'
import {Trans, useTranslation} from 'react-i18next'
import {clamp} from '../../util'
// UI Elements
import {
Button,
ColumnLayout,
Container,
FormField,
Header,
Input,
InputProps,
Select,
SpaceBetween,
Checkbox,
Alert,
Link,
} from '@cloudscape-design/components'
// State
import {getState, setState, useState, clearState} from '../../store'
// Components
import {
DeletionPolicy,
EbsDeletionPolicy,
EfsDeletionPolicy,
FsxLustreDeletionPolicy,
Storages,
Storage,
StorageType,
STORAGE_TYPE_PROPS,
UIStorageSettings,
EbsStorage,
EfsStorage,
} from './Storage.types'
import {useFeatureFlag} from '../../feature-flags/useFeatureFlag'
import InfoLink from '../../components/InfoLink'
import TitleDescriptionHelpPanel from '../../components/help-panel/TitleDescriptionHelpPanel'
import {useMemo} from 'react'
import {useHelpPanel} from '../../components/help-panel/HelpPanel'
import {
ebsErrorsMapping,
efsErrorsMapping,
externalFsErrorsMapping,
storageNameErrorsMapping,
STORAGE_NAME_MAX_LENGTH,
validateEbs,
validateEfs,
validateExternalFileSystem,
validateStorageName,
} from './Storage/storage.validators'
import {NonCancelableEventHandler} from '@cloudscape-design/components/internal/events'
import {BaseChangeDetail} from '@cloudscape-design/components/input/interfaces'
import {AddStorageForm} from './Storage/AddStorageForm'
import {buildStorageEntries} from './Storage/buildStorageEntries'
import {CheckboxWithHelpPanel} from './Components'
import {DeletionPolicyFormField} from './Storage/DeletionPolicyFormField'
// Constants
const storagePath = ['app', 'wizard', 'config', 'SharedStorage']
const errorsPath = ['app', 'wizard', 'errors', 'sharedStorage']
const uiSettingsPath = ['app', 'wizard', 'storage', 'ui']
// Types
export type StorageTypeOption = [StorageType, string]
// Helper Functions
export function itemToOption([value, label]: StorageTypeOption) {
return {value: value, label: label}
}
function strToOption(str: any) {
return {value: str, label: str.toString()}
}
function storageValidate() {
const storages: Storages = getState(storagePath)
let valid = true
if (storages) {
storages.forEach((storage: Storage, index: number) => {
const settings = `${storage.StorageType}Settings`
const idType = STORAGE_TYPE_PROPS[storage.StorageType].mountFilesystem
? 'FileSystemId'
: 'VolumeId'
const useExisting =
getState(['app', 'wizard', 'storage', 'ui', index, 'useExisting']) ||
!(STORAGE_TYPE_PROPS[storage.StorageType].maxToCreate > 0)
if (useExisting) {
const [externalFsValid, error] = validateExternalFileSystem(storage)
if (!externalFsValid) {
const errorMessage = i18next.t(externalFsErrorsMapping[error!])
setState([...errorsPath, index, settings, idType], errorMessage)
valid = false
} else {
clearState([...errorsPath, index, settings, idType])
}
} else {
if (storage.StorageType === 'Ebs') {
const [ebsValid, error] = validateEbs(storage as EbsStorage)
if (!ebsValid) {
const errorMessage = i18next.t(ebsErrorsMapping[error!])
setState(
[...errorsPath, index, 'EbsSettings', 'Size'],
errorMessage,
)
valid = false
} else {
clearState([...errorsPath, index, 'EbsSettings', 'Size'])
}
}
if (storage.StorageType === 'Efs') {
const [efsValid, error] = validateEfs(storage as EfsStorage)
if (!efsValid) {
const errorMessage = i18next.t(efsErrorsMapping[error!])
setState(
[...errorsPath, index, 'EfsSettings', 'ProvisionedThroughput'],
errorMessage,
)
valid = false
} else {
clearState([
...errorsPath,
index,
'EfsSettings',
'ProvisionedThroughput',
])
}
}
}
const name = getState([...storagePath, index, 'Name'])
const [nameValid, error] = validateStorageName(name)
if (!nameValid) {
let errorMessage: string
if (error === 'max_length') {
errorMessage = i18next.t(storageNameErrorsMapping[error], {
maxChars: STORAGE_NAME_MAX_LENGTH,
})
} else {
errorMessage = i18next.t(storageNameErrorsMapping[error!])
}
setState([...errorsPath, index, 'Name'], errorMessage)
valid = false
} else {
clearState([...errorsPath, index, 'Name'])
}
})
}
setState([...errorsPath, 'validated'], true)
return valid
}
const LUSTRE_PERSISTENT1_DEFAULT_THROUGHPUT = 200
const LUSTRE_PERSISTENT2_DEFAULT_THROUGHPUT = 125
const storageThroughputsP1 = [50, 100, LUSTRE_PERSISTENT1_DEFAULT_THROUGHPUT]
const storageThroughputsP2 = [
LUSTRE_PERSISTENT2_DEFAULT_THROUGHPUT,
250,
500,
1000,
]
const DEFAULT_DELETION_POLICY: DeletionPolicy = 'Retain'
function isPersistentFsx(lustreType: string): boolean {
return ['PERSISTENT_1', 'PERSISTENT_2'].includes(lustreType)
}
function setDefaultStorageThroughput(
lustreType: string,
storageThroughputPath: string[],
) {
if (isPersistentFsx(lustreType)) {
setState(
storageThroughputPath,
lustreType === 'PERSISTENT_1'
? LUSTRE_PERSISTENT1_DEFAULT_THROUGHPUT
: LUSTRE_PERSISTENT2_DEFAULT_THROUGHPUT,
)
} else {
clearState(storageThroughputPath)
}
}
export function FsxLustreSettings({index}: any) {
const {t} = useTranslation()
const isLustrePersistent2Active = useFeatureFlag('lustre_persistent2')
const isDeletionPolicyEnabled = useFeatureFlag('lustre_deletion_policy')
const useExisting =
useState(['app', 'wizard', 'storage', 'ui', index, 'useExisting']) || false
const fsxPath = useMemo(
() => [...storagePath, index, 'FsxLustreSettings'],
[index],
)
const storageCapacityPath = [...fsxPath, 'StorageCapacity']
const lustreTypePath = [...fsxPath, 'DeploymentType']
// support FSx Lustre PERSISTENT_2 only in >= 3.2.0
const lustreTypes = [
isLustrePersistent2Active ? 'PERSISTENT_2' : null,
'PERSISTENT_1',
'SCRATCH_1',
'SCRATCH_2',
].filter(Boolean)
const storageThroughputPath = [...fsxPath, 'PerUnitStorageThroughput']
const importPathPath = [...fsxPath, 'ImportPath']
const exportPathPath = [...fsxPath, 'ExportPath']
const compressionPath = [...fsxPath, 'DataCompressionType']
const deletionPolicyPath = [...fsxPath, 'DeletionPolicy']
const storageCapacity = useState(storageCapacityPath)
const lustreType = useState(lustreTypePath)
const storageThroughput = useState(storageThroughputPath)
const importPath = useState(importPathPath) || ''
const exportPath = useState(exportPathPath) || ''
const compression = useState(compressionPath)
const deletionPolicy = useState(deletionPolicyPath)
const supportedDeletionPolicies: FsxLustreDeletionPolicy[] = [
'Delete',
'Retain',
]
React.useEffect(() => {
const fsxPath = [...storagePath, index, 'FsxLustreSettings']
const storageCapacityPath = [...fsxPath, 'StorageCapacity']
const lustreTypePath = [...fsxPath, 'DeploymentType']
const deletionPolicyPath = [...fsxPath, 'DeletionPolicy']
const storageThroughputPath = [...fsxPath, 'PerUnitStorageThroughput']
if (isDeletionPolicyEnabled && deletionPolicy === null)
setState(deletionPolicyPath, DEFAULT_DELETION_POLICY)
if (storageCapacity === null && !useExisting)
setState(storageCapacityPath, 1200)
if (!storageThroughput && !useExisting) {
setDefaultStorageThroughput(lustreType, storageThroughputPath)
}
if (lustreType === null && !useExisting)
setState(
lustreTypePath,
isLustrePersistent2Active ? 'PERSISTENT_2' : 'PERSISTENT_1',
)
}, [
storageCapacity,
lustreType,
storageThroughput,
index,
useExisting,
isLustrePersistent2Active,
deletionPolicy,
isDeletionPolicyEnabled,
])
const toggleCompression = () => {
if (compression) clearState(compressionPath)
else setState(compressionPath, 'LZ4')
}
const setImportPath = (path: any) => {
if (path !== '') setState(importPathPath, path)
else clearState(importPathPath)
}
const setExportPath = (path: any) => {
if (path !== '') setState(exportPathPath, path)
else clearState(exportPathPath)
}
const capacityMin = 1200
const capacityMax = 100800
const capacityStep = 1200
const clampCapacity = (inCapacityStr: string) => {
return clamp(
parseInt(inCapacityStr),
capacityMin,
capacityMax,
capacityStep,
).toString()
}
const onDeletionPolicyChange = useCallback(
(selectedDeletionPolicy: DeletionPolicy) => {
const deletionPolicyPath = [...fsxPath, 'DeletionPolicy']
setState(deletionPolicyPath, selectedDeletionPolicy)
},
[fsxPath],
)
const throughputFooterLinks = useMemo(
() => [
{
title: t('wizard.storage.Fsx.throughput.link.title'),
href: t('wizard.storage.Fsx.throughput.link.href'),
},
],
[t],
)
const lustreTypeFooterLinks = useMemo(
() => [
{
title: t('wizard.storage.Fsx.lustreType.link.title'),
href: t('wizard.storage.Fsx.lustreType.link.href'),
},
],
[t],
)
const lustreCompressionFooterLinks = useMemo(
() => [
{
title: t('wizard.storage.Fsx.compression.link.title'),
href: t('wizard.storage.Fsx.compression.link.href'),
},
],
[t],
)
return (
}
info={
}
footerLinks={lustreTypeFooterLinks}
/>
}
/>
}
>
{
setState(storageCapacityPath, detail.value)
}}
onBlur={_e => {
setState(storageCapacityPath, clampCapacity(storageCapacity))
}}
type="number"
/>
{lustreType === 'PERSISTENT_1' && (
<>
}
/>
}
/>
}
>
setImportPath(detail.value)}
/>
}
/>
}
/>
}
>
{
setExportPath(detail.value)
}}
/>
>
)}
{isPersistentFsx(lustreType) && (
}
/>
}
>
)}
}
/>
{isDeletionPolicyEnabled && (
)}
)
}
export function EfsSettings({index}: any) {
const efsPath = useMemo(() => [...storagePath, index, 'EfsSettings'], [index])
const encryptedPath = [...efsPath, 'Encrypted']
const kmsPath = [...efsPath, 'KmsKeyId']
const performancePath = [...efsPath, 'PerformanceMode']
const performanceModes = ['generalPurpose', 'maxIO']
const deletionPolicyPath = [...efsPath, 'DeletionPolicy']
const {t} = useTranslation()
const throughputModePath = [...efsPath, 'ThroughputMode']
const provisionedThroughputPath = [...efsPath, 'ProvisionedThroughput']
let encrypted = useState(encryptedPath)
let kmsId = useState(kmsPath)
let performanceMode = useState(performancePath) || 'generalPurpose'
let throughputMode = useState(throughputModePath)
let provisionedThroughput = useState(provisionedThroughputPath)
const deletionPolicy = useState(deletionPolicyPath)
const isDeletionPolicyEnabled = useFeatureFlag('efs_deletion_policy')
const supportedDeletionPolicies: EfsDeletionPolicy[] = ['Delete', 'Retain']
const provisionedThroughputErrors = useState([
...errorsPath,
index,
'EfsSettings',
'ProvisionedThroughput',
])
React.useEffect(() => {
const efsPath = [...storagePath, index, 'EfsSettings']
const throughputModePath = [...efsPath, 'ThroughputMode']
const provisionedThroughputPath = [...efsPath, 'ProvisionedThroughput']
const deletionPolicyPath = [...efsPath, 'DeletionPolicy']
if (throughputMode === null) setState(throughputModePath, 'bursting')
else if (throughputMode === 'bursting')
clearState([provisionedThroughputPath])
if (isDeletionPolicyEnabled && deletionPolicy === null)
setState(deletionPolicyPath, DEFAULT_DELETION_POLICY)
}, [deletionPolicy, index, isDeletionPolicyEnabled, throughputMode])
const toggleEncrypted = () => {
const setEncrypted = !encrypted
setState(encryptedPath, setEncrypted)
if (!setEncrypted) clearState(kmsPath)
}
const encryptionFooterLinks = useMemo(
() => [
{
title: t('wizard.storage.Efs.encrypted.encryptionLink.title'),
href: t('wizard.storage.Efs.encrypted.encryptionLink.href'),
},
],
[t],
)
const provisionedFooterLinks = useMemo(
() => [
{
title: t('wizard.storage.Efs.provisioned.throughputLink.title'),
href: t('wizard.storage.Efs.provisioned.throughputLink.href'),
},
],
[t],
)
const onDeletionPolicyChange = useCallback(
(selectedDeletionPolicy: DeletionPolicy) => {
const deletionPolicyPath = [...efsPath, 'DeletionPolicy']
setState(deletionPolicyPath, selectedDeletionPolicy)
},
[efsPath],
)
return (
}
>
{t('wizard.storage.Efs.encrypted.label')}
{encrypted ? (
{
setState(kmsPath, detail.value)
}}
/>
) : null}
}
checked={throughputMode !== 'bursting'}
onChange={_event => {
const newThroughputMode =
throughputMode === 'bursting' ? 'provisioned' : 'bursting'
setState(throughputModePath, newThroughputMode)
newThroughputMode === 'provisioned'
? setState(provisionedThroughputPath, 128)
: clearState(provisionedThroughputPath)
}}
>
{t('wizard.storage.Efs.provisioned.label')}
{throughputMode === 'provisioned' && (
{
setState(
provisionedThroughputPath,
clamp(parseInt(detail.value), 1, 1024),
)
}}
/>
)}
{isDeletionPolicyEnabled && (
)}
)
}
export function EbsSettings({index}: any) {
const {t} = useTranslation()
const isDeletionPolicyEnabled = useFeatureFlag('ebs_deletion_policy')
const ebsPath = useMemo(() => [...storagePath, index, 'EbsSettings'], [index])
const volumeTypePath = [...ebsPath, 'VolumeType']
const volumeTypes = ['gp3', 'gp2', 'io1', 'io2', 'sc1', 'st1', 'standard']
const defaultVolumeType = 'gp3'
const volumeSizePath = [...ebsPath, 'Size']
const encryptedPath = [...ebsPath, 'Encrypted']
const kmsPath = [...ebsPath, 'KmsKeyId']
const snapshotIdPath = useMemo(() => [...ebsPath, 'SnapshotId'], [ebsPath])
const deletionPolicyPath = [...ebsPath, 'DeletionPolicy']
const supportedDeletionPolicies: EbsDeletionPolicy[] = [
'Delete',
'Retain',
'Snapshot',
]
const volumeErrors = useState([...errorsPath, index, 'EbsSettings', 'Size'])
let volumeType = useState(volumeTypePath)
let volumeSize = useState(volumeSizePath)
let encrypted = useState(encryptedPath)
let kmsId = useState(kmsPath)
let snapshotId = useState(snapshotIdPath)
const [snapshostVisible, setSnapshotVisible] = reactUseState(!!snapshotId)
let deletionPolicy = useState(deletionPolicyPath)
let validated = useState([...errorsPath, 'validated'])
React.useEffect(() => {
const volumeTypePath = [...ebsPath, 'VolumeType']
const deletionPolicyPath = [...ebsPath, 'DeletionPolicy']
const volumeSizePath = [...ebsPath, 'Size']
if (volumeType === null) setState(volumeTypePath, defaultVolumeType)
if (isDeletionPolicyEnabled && deletionPolicy === null)
setState(deletionPolicyPath, DEFAULT_DELETION_POLICY)
if (volumeSize === null) setState(volumeSizePath, 35)
}, [volumeType, volumeSize, deletionPolicy, isDeletionPolicyEnabled, ebsPath])
const toggleEncrypted = () => {
const setEncrypted = !encrypted
setState(encryptedPath, setEncrypted)
if (!setEncrypted) clearState(kmsPath)
}
const toggleSnapshotVisibility = useCallback(() => {
clearState(snapshotIdPath)
setSnapshotVisible(!snapshostVisible)
}, [setSnapshotVisible, snapshostVisible, snapshotIdPath])
const encryptionFooterLinks = useMemo(
() => [
{
title: t('wizard.storage.Ebs.encrypted.encryptionLink.title'),
href: t('wizard.storage.Ebs.encrypted.encryptionLink.href'),
},
],
[t],
)
const snapshotFooterLinks = useMemo(
() => [
{
title: t('wizard.storage.Ebs.snapshotId.snapshotLink.title'),
href: t('wizard.storage.Ebs.snapshotId.snapshotLink.href'),
},
],
[t],
)
const onDeletionPolicyChange = useCallback(
(selectedDeletionPolicy: DeletionPolicy) => {
const deletionPolicyPath = [...ebsPath, 'DeletionPolicy']
setState(deletionPolicyPath, selectedDeletionPolicy)
},
[ebsPath],
)
return (
{
setState(volumeSizePath, detail.value)
validated && storageValidate()
}}
/>
}
>
{t('wizard.storage.Ebs.encrypted.label')}
{encrypted ? (
{
setState(kmsPath, detail.value)
}}
/>
) : null}
}
>
{t('wizard.storage.Ebs.snapshotId.label')}
{snapshostVisible && (
{
setState(snapshotIdPath, detail.value)
}}
/>
)}
{isDeletionPolicyEnabled && (
)}
)
}
function StorageInstance({index}: any) {
const path = [...storagePath, index]
const uiSettingsForStorage = ['app', 'wizard', 'storage', 'ui', index]
const storageType: StorageType = useState([...path, 'StorageType'])
const storageName = useState([...path, 'Name']) || ''
const storageNameErrors = useState([...errorsPath, index, 'Name'])
const mountPoint = useState([...path, 'MountDir'])
const settingsPath = [...path, `${storageType}Settings`]
const errorsInstancePath = [...errorsPath, index, `${storageType}Settings`]
const useExisting =
useState([...uiSettingsForStorage, 'useExisting']) ||
!(STORAGE_TYPE_PROPS[storageType].maxToCreate > 0)
const existingPath = STORAGE_TYPE_PROPS[storageType].mountFilesystem
? [...settingsPath, 'FileSystemId']
: [...settingsPath, 'VolumeId']
const existingPathError = useState(
STORAGE_TYPE_PROPS[storageType].mountFilesystem
? [...errorsInstancePath, 'FileSystemId']
: [...errorsInstancePath, 'VolumeId'],
)
const existingId = useState(existingPath) || ''
const storages = useState(storagePath)
const uiSettings = useState(['app', 'wizard', 'storage', 'ui'])
const {t} = useTranslation()
const fsxFilesystems = useState(['aws', 'fsxFilesystems'])
const fsxVolumes = useState(['aws', 'fsxVolumes'])
const efsFilesystems = useState(['aws', 'efs_filesystems']) || []
const canEditFilesystems = useDynamicStorage()
const canToggle =
(useExisting && canCreateStorage(storageType, storages, uiSettings)) ||
(!useExisting &&
canAttachExistingStorage(storageType, storages, uiSettings))
const removeStorage = () => {
if (index === 0 && storages.length === 1) {
clearState(['app', 'wizard', 'storage', 'ui'])
clearState(storagePath)
} else {
clearState(uiSettingsForStorage)
clearState(path)
}
// Rename storages to keep indices correct and names unique
const updatedStorages = getState(storagePath)
if (updatedStorages)
for (let i = 0; i < updatedStorages.length; i++) {
const storage = getState([...storagePath, i])
setState([...storagePath, i, 'Name'], `${storage.StorageType}${i}`)
}
}
const toggleUseExisting = () => {
const value = !useExisting
clearState(settingsPath)
setState([...uiSettingsForStorage, 'useExisting'], value)
}
const idToOption = (id: any) => {
return {label: id, value: id}
}
const updateStorageName = useCallback<
NonCancelableEventHandler
>(
({detail}) => {
setState([...storagePath, index, 'Name'], detail.value)
},
[index],
)
const useExistingFooterLinks = useMemo(
() => [
{
title: t('wizard.storage.instance.useExisting.fsxLink.title'),
href: t('wizard.storage.instance.useExisting.fsxLink.href'),
},
{
title: t('wizard.storage.instance.useExisting.efsLink.title'),
href: t('wizard.storage.instance.useExisting.efsLink.href'),
},
{
title: t('wizard.storage.instance.useExisting.ebsLink.title'),
href: t('wizard.storage.instance.useExisting.ebsLink.href'),
},
],
[t],
)
const storageTypeDisplay = ALL_STORAGES.find(
([type]) => type === storageType,
)?.[1]
return (
{t('wizard.storage.container.removeStorage')}
}
>
{t('wizard.storage.container.sourceTitle', {
index: index + 1,
name: storageTypeDisplay,
})}
}
>
}
/>
}
>
{
setState([...storagePath, index, 'MountDir'], detail.value)
}}
/>
{STORAGE_TYPE_PROPS[storageType].maxToCreate > 0 ? (
}
/>
) : null}
{useExisting &&
{
Ebs: (
{
setState(existingPath, detail.value)
}}
/>
),
FsxLustre: (
),
FsxOpenZfs: (
),
FsxOntap: (
),
Efs: (
),
}[storageType]}
{!useExisting &&
{
FsxLustre: ,
Efs: ,
Ebs: ,
FsxOntap: null,
FsxOpenZfs: null,
}[storageType]}
)
}
const ALL_STORAGES: StorageTypeOption[] = [
['FsxLustre', 'Amazon FSx for Lustre (FSX)'],
['FsxOntap', 'Amazon FSx for NetApp ONTAP (FSX)'],
['FsxOpenZfs', 'Amazon FSx for OpenZFS (FSX)'],
['Efs', 'Amazon Elastic File System (EFS)'],
['Ebs', 'Amazon Elastic Block Store (EBS)'],
]
function Storage() {
const {t} = useTranslation()
const storages = useState(storagePath)
const uiStorageSettings = useState(['app', 'wizard', 'storage', 'ui'])
const isFsxOnTapActive = useFeatureFlag('fsx_ontap')
const isFsxOpenZsfActive = useFeatureFlag('fsx_openzsf')
const canEditFilesystems = useDynamicStorage()
const hasAddedStorage = storages?.length > 0
useHelpPanel()
const storageMaxes: Record = {
FsxLustre: 21,
FsxOntap: 20,
FsxOpenZfs: 20,
Efs: 21,
Ebs: 5,
}
const supportedStorages = ALL_STORAGES.filter(([storageType]) => {
if (storageType === 'FsxOntap' && !isFsxOnTapActive) {
return false
}
if (storageType === 'FsxOpenZfs' && !isFsxOpenZsfActive) {
return false
}
return true
})
const defaultCounts = {FsxLustre: 0, Efs: 0, Ebs: 0}
const storageReducer = (eax: any, item: any) => {
let ret = {...eax}
ret[item.StorageType] += 1
return ret
}
const storageCounts = storages
? storages.reduce(storageReducer, defaultCounts)
: defaultCounts
const storageTypes = supportedStorages.reduce(
(newStorages: StorageTypeOption[], storageType: StorageTypeOption) => {
const st = storageType[0]
return storageCounts[st] >= storageMaxes[st]
? newStorages
: [...newStorages, storageType]
},
[],
)
const onAddStorageSubmit = React.useCallback(
(selectedStorageTypes: StorageType[]) => {
const existingStorages = storages || []
const existingUiStorageSettings = uiStorageSettings || []
const [storageEntries, uiSettingsEntries] = buildStorageEntries(
existingStorages,
existingUiStorageSettings,
selectedStorageTypes,
)
setState(storagePath, [...existingStorages, ...storageEntries])
setState(uiSettingsPath, [
...existingUiStorageSettings,
...uiSettingsEntries,
])
},
[storages, uiStorageSettings],
)
return (
}
>
{t('wizard.storage.container.title')}
}
>
{!hasAddedStorage && (
{t('wizard.storage.container.noStorageSelected')}
)}
{canEditFilesystems && storageTypes.length > 0 && (
)}
{hasAddedStorage
? storages.map((_: any, i: any) => (
))
: null}
)
}
const StorageHelpPanel = () => {
const {t} = useTranslation()
const footerLinks = useMemo(
() => [
{
title: t('wizard.storage.helpPanel.link.sharedStorage.title'),
href: t('wizard.storage.helpPanel.link.sharedStorage.href'),
},
{
title: t('wizard.storage.helpPanel.link.sharedStorageSection.title'),
href: t('wizard.storage.helpPanel.link.sharedStorageSection.href'),
},
],
[t],
)
return (
}
description={
<>
>
}
footerLinks={footerLinks}
/>
)
}
function canCreateStorage(
storageType: StorageType,
storages: Storages,
uiStorageSettings: UIStorageSettings,
) {
if (!storageType) {
return false
}
if (!storages || !uiStorageSettings) {
return true
}
const maxToCreate = STORAGE_TYPE_PROPS[storageType].maxToCreate
const alreadyCreated = storages
.filter((_, index) => !uiStorageSettings[index].useExisting)
.filter(storage => storage.StorageType === storageType).length
return alreadyCreated < maxToCreate
}
function canAttachExistingStorage(
storageType: StorageType,
storages: Storages,
uiStorageSettings: UIStorageSettings,
) {
if (!storageType) {
return false
}
if (!storages || !uiStorageSettings) {
return true
}
const maxExistingToAttach =
STORAGE_TYPE_PROPS[storageType].maxExistingToAttach
const existingAlreadyAttached = storages
.filter((_, index) => uiStorageSettings[index].useExisting)
.filter(storage => storage.StorageType === storageType).length
return existingAlreadyAttached < maxExistingToAttach
}
function useDynamicStorage() {
const editingCluster = useState(['app', 'wizard', 'editing'])
const isDynamicFSMountActive = useFeatureFlag('dynamic_fs_mount')
return isDynamicFSMountActive || !editingCluster
}
export {
Storage,
storageValidate,
canCreateStorage,
canAttachExistingStorage,
useDynamicStorage,
StorageHelpPanel,
}