diff --git a/src/components/organisms/NewResourceWizard/NewResourceWizard.tsx b/src/components/organisms/NewResourceWizard/NewResourceWizard.tsx index 2beb9f951c..687ec57438 100644 --- a/src/components/organisms/NewResourceWizard/NewResourceWizard.tsx +++ b/src/components/organisms/NewResourceWizard/NewResourceWizard.tsx @@ -12,13 +12,14 @@ import {first} from 'lodash'; import path from 'path'; import {useAppDispatch, useAppSelector} from '@redux/hooks'; +import {setAlert} from '@redux/reducers/alert'; import {closeNewResourceWizard} from '@redux/reducers/ui'; import {registeredKindHandlersSelector} from '@redux/selectors/resourceKindSelectors'; import {useResourceContentMapRef, useResourceMetaMap} from '@redux/selectors/resourceMapSelectors'; import {joinK8sResource} from '@redux/services/resource'; import {getResourceKindSchema} from '@redux/services/schema'; import {createTransientResource} from '@redux/services/transientResource'; -import {saveTransientResources} from '@redux/thunks/saveTransientResources'; +import {saveResourceToFileFolder} from '@redux/thunks/saveResourceToFileFolder'; import {useFileSelectOptions} from '@hooks/useFileSelectOptions'; import {useFileFolderTreeSelectData} from '@hooks/useFolderTreeSelectData'; @@ -30,12 +31,15 @@ import {getResourceKindHandler} from '@src/kindhandlers'; import {ROOT_FILE_ENTRY} from '@shared/constants/fileEntry'; import {hotkeys} from '@shared/constants/hotkeys'; +import {AlertEnum} from '@shared/models/alert'; import {FileMapType} from '@shared/models/appState'; import {FileEntry} from '@shared/models/fileEntry'; import {K8sResource, ResourceMeta} from '@shared/models/k8sResource'; +import {ResourceSavingDestination} from '@shared/models/resourceCreate'; import {ResourceKindHandler} from '@shared/models/resourceKindHandler'; import {NewResourceWizardInput} from '@shared/models/ui'; import {openNamespaceTopic, openUniqueObjectNameTopic} from '@shared/utils/shell'; +import {trackEvent} from '@shared/utils/telemetry'; import {FileCategoryLabel, FileNameLabel, SaveDestinationWrapper, StyledSelect} from './NewResourceWizard.styled'; @@ -93,7 +97,8 @@ const NewResourceWizard = () => { const [isResourceKindNamespaced, setIsResourceKindNamespaced] = useState(true); const [isSubmitDisabled, setSubmitDisabled] = useState(true); const [exportFileName, setExportFileName] = useState(''); - const [savingDestination, setSavingDestination, savingDestinationRef] = useStateWithRef('doNotSave'); + const [savingDestination, setSavingDestination, savingDestinationRef] = + useStateWithRef('doNotSave'); const [selectedFile, setSelectedFile, selectedFileRef] = useStateWithRef(undefined); const [selectedFolder, setSelectedFolder, selectedFolderRef] = useStateWithRef(ROOT_FILE_ENTRY); const [generateRandom, setGenerateRandom, generateRandomRef] = useStateWithRef(false); @@ -227,7 +232,7 @@ const NewResourceWizard = () => { setSavingDestination('saveToFolder'); setSelectedFolder(defaultInput.targetFolder); } else if (defaultInput?.targetFile && isFolderOpen) { - setSavingDestination('saveToFile'); + setSavingDestination('appendToFile'); setSelectedFile(defaultInput.targetFile); } else { setSavingDestination('doNotSave'); @@ -420,9 +425,12 @@ const NewResourceWizard = () => { }, dispatch, 'local', - jsonTemplate + jsonTemplate, + savingDestinationRef.current !== 'doNotSave' ? true : undefined ); + trackEvent('create/resource', {resourceKind: newResource.kind}); + if (savingDestinationRef.current !== 'doNotSave') { let absolutePath; @@ -432,20 +440,23 @@ const NewResourceWizard = () => { selectedFolderRef.current === ROOT_FILE_ENTRY ? path.join(rootFolderEntryRef.current.filePath, path.sep, fullFileName) : path.join(rootFolderEntryRef.current.filePath, selectedFolderRef.current, path.sep, fullFileName); - } else if (savingDestinationRef.current === 'saveToFile' && selectedFileRef.current) { + } else if (savingDestinationRef.current === 'appendToFile' && selectedFileRef.current) { absolutePath = path.join(rootFolderEntryRef.current.filePath, selectedFileRef.current); } else { absolutePath = path.join(rootFolderEntryRef.current.filePath, path.sep, fullFileName); } - dispatch( - saveTransientResources({ - resourcePayloads: [{resource: newResource, absolutePath}], - saveMode: savingDestinationRef.current === 'saveToFolder' ? savingDestinationRef.current : 'appendToFile', - }) - ); + dispatch(saveResourceToFileFolder({resource: newResource, absolutePath, saveMode: savingDestinationRef.current})); } + dispatch( + setAlert({ + title: 'Resource created', + message: `Successfully created ${newResource.name}`, + type: AlertEnum.Success, + }) + ); + setSavingDestination('doNotSave'); closeWizard(); }, [ @@ -649,7 +660,7 @@ const NewResourceWizard = () => { - + {savingDestination === 'saveToFolder' && ( @@ -665,7 +676,7 @@ const NewResourceWizard = () => { treeNodeLabelProp="label" /> )} - {savingDestination === 'saveToFile' && ( + {savingDestination === 'appendToFile' && ( setSelectedFile(value)} diff --git a/src/redux/services/transientResource.ts b/src/redux/services/transientResource.ts index 9d7bea5eab..b67f5f8f25 100644 --- a/src/redux/services/transientResource.ts +++ b/src/redux/services/transientResource.ts @@ -27,7 +27,8 @@ export function createTransientResource( input: {name: string; kind: string; apiVersion: string; namespace?: string}, dispatch: AppDispatch, createdIn: 'local' | 'cluster', - jsonTemplate?: Partial + jsonTemplate?: Partial, + saveToFileOrFolder?: boolean ) { const newResourceId = uuidv4(); let newResourceText: string; @@ -76,7 +77,10 @@ export function createTransientResource( object: newResourceObject, isClusterScoped: getResourceKindHandler(input.kind)?.isNamespaced || false, }; - dispatch(addResource(newResource)); + + if (!saveToFileOrFolder) { + dispatch(addResource(newResource)); + } return newResource; } diff --git a/src/redux/thunks/saveResourceToFileFolder.ts b/src/redux/thunks/saveResourceToFileFolder.ts new file mode 100644 index 0000000000..a7ba479435 --- /dev/null +++ b/src/redux/thunks/saveResourceToFileFolder.ts @@ -0,0 +1,75 @@ +import {createAsyncThunk} from '@reduxjs/toolkit'; + +import fs from 'fs'; +import util from 'util'; + +import {YAML_DOCUMENT_DELIMITER} from '@constants/constants'; + +import {getFileTimestamp, hasValidExtension} from '@utils/files'; + +import {ROOT_FILE_ENTRY} from '@shared/constants/fileEntry'; +import {AppDispatch} from '@shared/models/appDispatch'; +import {K8sResource} from '@shared/models/k8sResource'; +import {ResourceSavingDestination} from '@shared/models/resourceCreate'; +import {RootState} from '@shared/models/rootState'; + +const readFilePromise = util.promisify(fs.readFile); +const appendFilePromise = util.promisify(fs.appendFile); +const writeFilePromise = util.promisify(fs.writeFile); + +export type SaveResourceToFileFolderPayload = { + resourceRange: {start: number; length: number} | undefined; + fileTimestamp: number | undefined; +}; +type SaveResourceToFileFolderArgs = { + resource: K8sResource; + absolutePath: string; + saveMode: ResourceSavingDestination; +}; + +export const saveResourceToFileFolder = createAsyncThunk< + SaveResourceToFileFolderPayload, + SaveResourceToFileFolderArgs, + {dispatch: AppDispatch; state: RootState} +>('main/saveResourceToFileFolder', async (payload, thunkAPI) => { + const mainState = thunkAPI.getState().main; + const rootFolder = mainState.fileMap[ROOT_FILE_ENTRY]; + + const {absolutePath, resource, saveMode} = payload; + + let resourceRange: {start: number; length: number} | undefined; + + if (!rootFolder) { + throw new Error('Could not find the root folder.'); + } + + if (saveMode === 'saveToFolder') { + await writeFilePromise(absolutePath, resource.text); + } else if (saveMode === 'appendToFile') { + const fileName = absolutePath.split('\\').pop(); + + if (!hasValidExtension(fileName, ['.yaml', '.yml'])) { + throw new Error('The selected file does not have .yaml extension.'); + } + + const fileContent = await readFilePromise(absolutePath, 'utf-8'); + let contentToAppend = resource.text; + if (fileContent.trim().length > 0) { + if (fileContent.trim().endsWith(YAML_DOCUMENT_DELIMITER)) { + contentToAppend = `\n${resource.text}`; + } else { + contentToAppend = `\n${YAML_DOCUMENT_DELIMITER}\n${resource.text}`; + } + } + + resourceRange = { + start: fileContent.length, + length: contentToAppend.length, + }; + + await appendFilePromise(absolutePath, contentToAppend); + } + const fileTimestamp = getFileTimestamp(absolutePath); + + return {resourceRange, fileTimestamp}; +}); diff --git a/src/redux/thunks/saveTransientResources.ts b/src/redux/thunks/saveTransientResources.ts index 72202a4eae..b73b0446b7 100644 --- a/src/redux/thunks/saveTransientResources.ts +++ b/src/redux/thunks/saveTransientResources.ts @@ -1,21 +1,12 @@ import {createAsyncThunk} from '@reduxjs/toolkit'; -import fs from 'fs'; -import util from 'util'; - -import {YAML_DOCUMENT_DELIMITER} from '@constants/constants'; - import {fileIsExcluded} from '@redux/services/fileEntry'; -import {getFileTimestamp, hasValidExtension} from '@utils/files'; - -import {ROOT_FILE_ENTRY} from '@shared/constants/fileEntry'; import {AppDispatch} from '@shared/models/appDispatch'; -import {FileEntry} from '@shared/models/fileEntry'; import {K8sResource} from '@shared/models/k8sResource'; import {RootState} from '@shared/models/rootState'; -import {trackEvent} from '@shared/utils/telemetry'; +import {SaveResourceToFileFolderPayload, saveResourceToFileFolder} from './saveResourceToFileFolder'; import {createRejectionWithAlert} from './utils'; const ERROR_TITLE = 'Resource Save Failed'; @@ -34,54 +25,6 @@ type SaveMultipleTransientResourcesArgs = { saveMode: 'saveToFolder' | 'appendToFile'; }; -const readFilePromise = util.promisify(fs.readFile); -const appendFilePromise = util.promisify(fs.appendFile); -const writeFilePromise = util.promisify(fs.writeFile); - -const performSaveTransientResource = async ( - resource: K8sResource, - rootFolder: FileEntry | undefined, - absolutePath: string, - saveMode: 'saveToFolder' | 'appendToFile' -) => { - let resourceRange: {start: number; length: number} | undefined; - if (!rootFolder) { - throw new Error('Could not find the root folder.'); - } - - trackEvent('create/resource', {resourceKind: resource.kind}); - - if (saveMode === 'saveToFolder') { - await writeFilePromise(absolutePath, resource.text); - } else { - const fileName = absolutePath.split('\\').pop(); - - if (!hasValidExtension(fileName, ['.yaml', '.yml'])) { - throw new Error('The selected file does not have .yaml extension.'); - } - - const fileContent = await readFilePromise(absolutePath, 'utf-8'); - let contentToAppend = resource.text; - if (fileContent.trim().length > 0) { - if (fileContent.trim().endsWith(YAML_DOCUMENT_DELIMITER)) { - contentToAppend = `\n${resource.text}`; - } else { - contentToAppend = `\n${YAML_DOCUMENT_DELIMITER}\n${resource.text}`; - } - } - - resourceRange = { - start: fileContent.length, - length: contentToAppend.length, - }; - - await appendFilePromise(absolutePath, contentToAppend); - } - const fileTimestamp = getFileTimestamp(absolutePath); - - return {resourceRange, fileTimestamp}; -}; - export const saveTransientResources = createAsyncThunk< SaveMultipleTransientResourcesPayload, SaveMultipleTransientResourcesArgs, @@ -90,22 +33,15 @@ export const saveTransientResources = createAsyncThunk< state: RootState; } >('main/saveTransientResources', async (args, thunkAPI) => { - const mainState = thunkAPI.getState().main; - const rootFolder = mainState.fileMap[ROOT_FILE_ENTRY]; - let resourcePayloads: ResourcePayload[] = []; for (let i = 0; i < args.resourcePayloads.length; i += 1) { const {resource, absolutePath} = args.resourcePayloads[i]; try { - // eslint-disable-next-line no-await-in-loop - const {resourceRange, fileTimestamp} = await performSaveTransientResource( - resource, - rootFolder, - absolutePath, - args.saveMode - ); + const {resourceRange, fileTimestamp} = ( + await thunkAPI.dispatch(saveResourceToFileFolder({resource, absolutePath, saveMode: args.saveMode})) + ).payload as SaveResourceToFileFolderPayload; resourcePayloads.push({ resourceId: resource.id, diff --git a/src/shared/models/resourceCreate.ts b/src/shared/models/resourceCreate.ts index b9c9bb666d..e17ca13dd2 100644 --- a/src/shared/models/resourceCreate.ts +++ b/src/shared/models/resourceCreate.ts @@ -7,3 +7,5 @@ export type NewResourceAction = { fromTypeLabel: 'AI' | 'advanced template' | 'model'; onClick: () => void; }; + +export type ResourceSavingDestination = 'doNotSave' | 'saveToFolder' | 'appendToFile';