diff --git a/electron/app/git/git.ts b/electron/app/git/git.ts index 16c20796ac..6391654a4f 100644 --- a/electron/app/git/git.ts +++ b/electron/app/git/git.ts @@ -23,8 +23,13 @@ export async function isGitInstalled(path: string) { export async function getGitRemoteUrl(path: string) { const git: SimpleGit = simpleGit({baseDir: path}); - const result = await git.raw('config', '--get', 'remote.origin.url'); - return result; + + try { + const result = await git.raw('config', '--get', 'remote.origin.url'); + return result; + } catch (e: any) { + return {error: e.message}; + } } export async function areFoldersGitRepos(paths: string[]) { @@ -57,8 +62,12 @@ export async function isFolderGitRepo(path: string) { export async function getRemotePath(localPath: string) { const git: SimpleGit = simpleGit({baseDir: localPath}); - const gitFolderPath = await git.revparse({'--show-toplevel': null}); - return gitFolderPath; + try { + const gitFolderPath = await git.revparse({'--show-toplevel': null}); + return gitFolderPath; + } catch (e: any) { + return {error: e.message}; + } } export async function cloneGitRepo(payload: {localPath: string; repoPath: string}) { @@ -103,7 +112,7 @@ export async function getGitRepoInfo(localPath: string) { remoteRepo: {exists: false, authRequired: false}, }; - if (remoteUrl) { + if (typeof remoteUrl === 'string') { gitRepo.remoteUrl = remoteUrl.replaceAll('.git', ''); } @@ -185,51 +194,59 @@ export async function checkoutGitBranch(payload: {localPath: string; branchName: export async function initGitRepo(localPath: string) { const git: SimpleGit = simpleGit({baseDir: localPath}); - await git.init(); - await git.commit('Initial commit', undefined, {'--allow-empty': null}); + + try { + await git.init(); + await git.commit('Initial commit', undefined, {'--allow-empty': null}); + } catch (e: any) { + return {error: e.message}; + } } export async function getChangedFiles(localPath: string, fileMap: FileMapType) { const git: SimpleGit = simpleGit({baseDir: localPath}); const projectFolderPath = fileMap[''].filePath; - const gitFolderPath = await git.revparse({'--show-toplevel': null}); - const currentBranch = (await git.branch()).current; - const branchStatus = await git.status({'-z': null, '-uall': null}); - const files = branchStatus.files; - - const changedFiles = formatGitChangedFiles(files, fileMap, projectFolderPath, gitFolderPath, git); + try { + const gitFolderPath = await git.revparse({'--show-toplevel': null}); + const currentBranch = (await git.branch()).current; - for (let i = 0; i < changedFiles.length; i += 1) { - if (!changedFiles[i].originalContent) { - let originalContent: string = ''; + const branchStatus = await git.status({'-z': null, '-uall': null}); + const files = branchStatus.files; - try { - // eslint-disable-next-line no-await-in-loop - originalContent = await git.show(`${currentBranch}:${changedFiles[i].gitPath}`); - } catch (error) { - originalContent = ''; - } + const changedFiles = formatGitChangedFiles(files, fileMap, projectFolderPath, gitFolderPath, git); - changedFiles[i].originalContent = originalContent; - } - } + for (let i = 0; i < changedFiles.length; i += 1) { + if (!changedFiles[i].originalContent) { + let originalContent: string = ''; - return changedFiles; -} + try { + // eslint-disable-next-line no-await-in-loop + originalContent = await git.show(`${currentBranch}:${changedFiles[i].gitPath}`); + } catch (error) { + originalContent = ''; + } -export async function getCurrentBranch(localPath: string) { - const git: SimpleGit = simpleGit({baseDir: localPath}); - const branchesSummary = await git.branch(); + changedFiles[i].originalContent = originalContent; + } + } - return branchesSummary.current; + return changedFiles; + } catch (e: any) { + return {error: e.message}; + } } export async function stageChangedFiles(localPath: string, filePaths: string[]) { const git: SimpleGit = simpleGit({baseDir: localPath}); - await git.add(filePaths); + try { + await git.add(filePaths); + return {}; + } catch (e: any) { + return {error: e.message}; + } } export async function unstageFiles(localPath: string, filePaths: string[]) { @@ -239,28 +256,57 @@ export async function unstageFiles(localPath: string, filePaths: string[]) { return {...prev, [current]: null}; }, {} as any); - await git.reset({'-q': null, HEAD: null, '--': null, ...unstageProperties}); + try { + await git.reset({'-q': null, HEAD: null, '--': null, ...unstageProperties}); + return {}; + } catch (e: any) { + return {error: e.message}; + } } export async function commitChanges(localPath: string, message: string) { const git: SimpleGit = simpleGit({baseDir: localPath}); - await git.commit(message); - trackEvent('git/commit'); + + try { + await git.commit(message); + trackEvent('git/commit'); + return {}; + } catch (e: any) { + return {error: e.message}; + } } export async function deleteLocalBranch(localPath: string, branchName: string) { const git: SimpleGit = simpleGit({baseDir: localPath}); - await git.deleteLocalBranch(branchName); + + try { + await git.deleteLocalBranch(branchName); + return {}; + } catch (e: any) { + return {error: e.message}; + } } export async function createLocalBranch(localPath: string, branchName: string) { const git: SimpleGit = simpleGit({baseDir: localPath}); - await git.checkoutLocalBranch(branchName); + + try { + await git.checkoutLocalBranch(branchName); + return {}; + } catch (e: any) { + return {error: e.message}; + } } export async function publishLocalBranch(localPath: string, branchName: string) { const git: SimpleGit = simpleGit({baseDir: localPath}); - await git.push({'-u': null, origin: null, [branchName]: null}); + + try { + await git.push({'-u': null, origin: null, [branchName]: null}); + return {}; + } catch (e: any) { + return {error: e.message}; + } } export async function pushChanges(localPath: string, branchName: string) { @@ -277,8 +323,14 @@ export async function pushChanges(localPath: string, branchName: string) { export async function setRemote(localPath: string, remoteURL: string) { const git: SimpleGit = simpleGit({baseDir: localPath}); - await git.addRemote('origin', remoteURL); - await git.fetch(); + + try { + await git.addRemote('origin', remoteURL); + await git.fetch(); + return {}; + } catch (e: any) { + return {error: e.message}; + } } export async function getCommitsCount(localPath: string, branchName: string) { @@ -299,7 +351,13 @@ export async function getCommitsCount(localPath: string, branchName: string) { export async function fetchRepo(localPath: string) { const git: SimpleGit = simpleGit({baseDir: localPath}); - await git.fetch(); + + try { + await git.fetch(); + return {}; + } catch (e: any) { + return {error: e.message}; + } } export async function pullChanges(localPath: string) { diff --git a/electron/app/git/ipc.ts b/electron/app/git/ipc.ts index 0f85aa5ad9..022b925339 100644 --- a/electron/app/git/ipc.ts +++ b/electron/app/git/ipc.ts @@ -14,8 +14,6 @@ import { getChangedFiles, getCommitResources, getCommitsCount, - getCurrentBranch, - getGitRemoteUrl, getGitRepoInfo, getRemotePath, initGitRepo, @@ -30,125 +28,203 @@ import { } from './git'; ipcMain.on('git.areFoldersGitRepos', async (event, paths: string[]) => { - const result = await areFoldersGitRepos(paths); - event.sender.send('git.areFoldersGitRepos.result', result); + try { + const result = await areFoldersGitRepos(paths); + event.sender.send('git.areFoldersGitRepos.result', result); + } catch (e: any) { + event.sender.send('git.areFoldersGitRepos.result', {error: e.message}); + } }); ipcMain.on('git.isFolderGitRepo', async (event, path: string) => { - const result = await isFolderGitRepo(path); - event.sender.send('git.isFolderGitRepo.result', result); + try { + const result = await isFolderGitRepo(path); + event.sender.send('git.isFolderGitRepo.result', result); + } catch (e: any) { + event.sender.send('git.isFolderGitRepo.result', {error: e.message}); + } }); ipcMain.on('git.isGitInstalled', async (event, path: string) => { - const result = await isGitInstalled(path); - event.sender.send('git.isGitInstalled.result', result); + try { + const result = await isGitInstalled(path); + event.sender.send('git.isGitInstalled.result', result); + } catch (e: any) { + event.sender.send('git.isGitInstalled.result', {error: e.message}); + } }); ipcMain.on('git.cloneGitRepo', async (event, payload: {localPath: string; repoPath: string}) => { - const result = await cloneGitRepo(payload); - event.sender.send('git.cloneGitRepo.result', result); -}); - -ipcMain.on('git.getGitRemoteUrl', async (event, path: string) => { - const result = await getGitRemoteUrl(path); - event.sender.send('git.getGitRemoteUrl.result', result); + try { + const result = await cloneGitRepo(payload); + event.sender.send('git.cloneGitRepo.result', result); + } catch (e: any) { + event.sender.send('git.cloneGitRepo.result', {error: e.message}); + } }); ipcMain.on('git.getGitRepoInfo', async (event, localPath: string) => { - const result = await getGitRepoInfo(localPath); - event.sender.send('git.getGitRepoInfo.result', result); + try { + const result = await getGitRepoInfo(localPath); + event.sender.send('git.getGitRepoInfo.result', result); + } catch (e: any) { + event.sender.send('git.getGitRepoInfo.result', {error: e.message}); + } }); ipcMain.on('git.checkoutGitBranch', async (event, payload: {localPath: string; branchName: string}) => { - const result = await checkoutGitBranch(payload); - event.sender.send('git.checkoutGitBranch.result', result); + try { + const result = await checkoutGitBranch(payload); + event.sender.send('git.checkoutGitBranch.result', result); + } catch (e: any) { + event.sender.send('git.checkoutGitBranch.result', {error: e.message}); + } }); ipcMain.on('git.initGitRepo', async (event, localPath: string) => { - await initGitRepo(localPath); - event.sender.send('git.initGitRepo.result'); + try { + const result = await initGitRepo(localPath); + event.sender.send('git.initGitRepo.result', result); + } catch (e: any) { + event.sender.send('git.initGitRepo.result', {error: e.message}); + } }); ipcMain.on('git.getChangedFiles', async (event, payload: {localPath: string; fileMap: FileMapType}) => { - const result = await getChangedFiles(payload.localPath, payload.fileMap); - event.sender.send('git.getChangedFiles.result', result); -}); - -ipcMain.on('git.getCurrentBranch', async (event, localPath: string) => { - const result = await getCurrentBranch(localPath); - event.sender.send('git.getCurrentBranch.result', result); + try { + const result = await getChangedFiles(payload.localPath, payload.fileMap); + event.sender.send('git.getChangedFiles.result', result); + } catch (e: any) { + event.sender.send('git.getChangedFiles.result', {error: e.message}); + } }); ipcMain.on('git.stageChangedFiles', async (event, payload: {localPath: string; filePaths: string[]}) => { const {filePaths, localPath} = payload; - await stageChangedFiles(localPath, filePaths); - event.sender.send('git.stageChangedFiles.result'); + try { + const result = await stageChangedFiles(localPath, filePaths); + event.sender.send('git.stageChangedFiles.result', result); + } catch (e: any) { + event.sender.send('git.stageChangedFiles.result', {error: e.message}); + } }); ipcMain.on('git.unstageFiles', async (event, payload: {localPath: string; filePaths: string[]}) => { const {filePaths, localPath} = payload; - await unstageFiles(localPath, filePaths); - event.sender.send('git.unstageFiles.result'); + try { + const result = await unstageFiles(localPath, filePaths); + event.sender.send('git.unstageFiles.result', result); + } catch (e: any) { + event.sender.send('git.unstageFiles.result', {error: e.message}); + } }); ipcMain.on('git.commitChanges', async (event, payload: {localPath: string; message: string}) => { - await commitChanges(payload.localPath, payload.message); - event.sender.send('git.commitChanges.result'); + try { + const result = await commitChanges(payload.localPath, payload.message); + event.sender.send('git.commitChanges.result', result); + } catch (e: any) { + event.sender.send('git.commitChanges.result', {error: e.message}); + } }); ipcMain.on('git.deleteLocalBranch', async (event, payload: {localPath: string; branchName: string}) => { - await deleteLocalBranch(payload.localPath, payload.branchName); - event.sender.send('git.deleteLocalBranch.result'); + try { + const result = await deleteLocalBranch(payload.localPath, payload.branchName); + event.sender.send('git.deleteLocalBranch.result', result); + } catch (e: any) { + event.sender.send('git.deleteLocalBranch.result', {error: e.message}); + } }); ipcMain.on('git.createLocalBranch', async (event, payload: {localPath: string; branchName: string}) => { - await createLocalBranch(payload.localPath, payload.branchName); - event.sender.send('git.createLocalBranch.result'); + try { + const result = await createLocalBranch(payload.localPath, payload.branchName); + event.sender.send('git.createLocalBranch.result', result); + } catch (e: any) { + event.sender.send('git.createLocalBranch.result', {error: e.message}); + } }); ipcMain.on('git.publishLocalBranch', async (event, payload: {localPath: string; branchName: string}) => { - await publishLocalBranch(payload.localPath, payload.branchName); - event.sender.send('git.publishLocalBranch.result'); + try { + const result = await publishLocalBranch(payload.localPath, payload.branchName); + event.sender.send('git.publishLocalBranch.result', result); + } catch (e: any) { + event.sender.send('git.publishLocalBranch.result', {error: e.message}); + } }); ipcMain.on('git.pushChanges', async (event, payload: {localPath: string; branchName: string}) => { - const result = await pushChanges(payload.localPath, payload.branchName); - event.sender.send('git.pushChanges.result', result); + try { + const result = await pushChanges(payload.localPath, payload.branchName); + event.sender.send('git.pushChanges.result', result); + } catch (e: any) { + event.sender.send('git.pushChanges.result', {error: e.message}); + } }); ipcMain.on('git.setRemote', async (event, payload: {localPath: string; remoteURL: string}) => { - await setRemote(payload.localPath, payload.remoteURL); - event.sender.send('git.setRemote.result'); + try { + const result = await setRemote(payload.localPath, payload.remoteURL); + event.sender.send('git.setRemote.result', result); + } catch (e: any) { + event.sender.send('git.setRemote.result', {error: e.message}); + } }); ipcMain.on('git.getRemotePath', async (event, localPath: string) => { - const result = await getRemotePath(localPath); - event.sender.send('git.getRemotePath.result', result); + try { + const result = await getRemotePath(localPath); + event.sender.send('git.getRemotePath.result', result); + } catch (e: any) { + event.sender.send('git.getRemotePath.result', {error: e.message}); + } }); ipcMain.on('git.getCommitsCount', async (event, payload: {localPath: string; branchName: string}) => { - const result = await getCommitsCount(payload.localPath, payload.branchName); - event.sender.send('git.getCommitsCount.result', result); + try { + const result = await getCommitsCount(payload.localPath, payload.branchName); + event.sender.send('git.getCommitsCount.result', result); + } catch (e: any) { + event.sender.send('git.getCommitsCount.result', {error: e.message}); + } }); ipcMain.on('git.fetchRepo', async (event, localPath: string) => { - await fetchRepo(localPath); - event.sender.send('git.fetchRepo.result'); + try { + const result = await fetchRepo(localPath); + event.sender.send('git.fetchRepo.result', result); + } catch (e: any) { + event.sender.send('git.fetchRepo.result', {error: e.message}); + } }); ipcMain.on('git.pullChanges', async (event, localPath: string) => { - const result = await pullChanges(localPath); - event.sender.send('git.pullChanges.result', result); + try { + const result = await pullChanges(localPath); + event.sender.send('git.pullChanges.result', result); + } catch (e: any) { + event.sender.send('git.pullChanges.result', {error: e.message}); + } }); ipcMain.on('git.getCommitResources', async (event, payload: {localPath: string; commitHash: string}) => { - const result = await getCommitResources(payload.localPath, payload.commitHash); - event.sender.send('git.getCommitResources.result', result); + try { + const result = await getCommitResources(payload.localPath, payload.commitHash); + event.sender.send('git.getCommitResources.result', result); + } catch (e: any) { + event.sender.send('git.getCommitResources.result', {error: e.message}); + } }); ipcMain.on('git.getBranchCommits', async (event, payload: {localPath: string; branchName: string}) => { - const result = await getBranchCommits(payload.localPath, payload.branchName); - event.sender.send('git.getBranchCommits.result', result); + try { + const result = await getBranchCommits(payload.localPath, payload.branchName); + event.sender.send('git.getBranchCommits.result', result); + } catch (e: any) { + event.sender.send('git.getBranchCommits.result', {error: e.message}); + } }); diff --git a/src/components/atoms/StyledComponents/SectionBlueprintList.tsx b/src/components/atoms/StyledComponents/SectionBlueprintList.tsx deleted file mode 100644 index f09780c84c..0000000000 --- a/src/components/atoms/StyledComponents/SectionBlueprintList.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import styled from 'styled-components'; - -export const SectionBlueprintList = styled.ol<{$width?: number}>` - width: ${({$width}) => ($width ? `${$width}px` : '100%')}; - height: 100%; - list-style-type: none; - padding: 0; - padding-bottom: 20px; - overflow-y: auto; - margin: 0; -`; diff --git a/src/components/atoms/StyledComponents/index.ts b/src/components/atoms/StyledComponents/index.ts index 8b18d42c02..27c7735c5f 100644 --- a/src/components/atoms/StyledComponents/index.ts +++ b/src/components/atoms/StyledComponents/index.ts @@ -1,5 +1,4 @@ export {IconButton} from './IconButton'; export {PrimaryButton} from './PrimaryButton'; export {SecondaryButton} from './SecondaryButton'; -export {SectionBlueprintList} from './SectionBlueprintList'; export {TitleBarWrapper} from './TitleBarWrapper'; diff --git a/src/components/atoms/index.ts b/src/components/atoms/index.ts index a80158b08f..0edf39131b 100644 --- a/src/components/atoms/index.ts +++ b/src/components/atoms/index.ts @@ -11,7 +11,7 @@ export {default as KeyValueInput} from './KeyValueInput'; export {default as ScrollIntoView} from './ScrollIntoView'; export {default as SearchInput} from './SearchInput'; export {default as Spinner} from './Spinner'; -export {IconButton, PrimaryButton, SecondaryButton, SectionBlueprintList, TitleBarWrapper} from './StyledComponents'; +export {IconButton, PrimaryButton, SecondaryButton, TitleBarWrapper} from './StyledComponents'; export {default as SelectItemImage} from './SelectItemImage'; export {default as TabHeader} from './TabHeader'; export {default as TableSelect} from './TableSelect'; diff --git a/src/components/molecules/BranchSelect/BranchCell.tsx b/src/components/molecules/BranchSelect/BranchCell.tsx index d620c3daa2..02ddcf9217 100644 --- a/src/components/molecules/BranchSelect/BranchCell.tsx +++ b/src/components/molecules/BranchSelect/BranchCell.tsx @@ -1,10 +1,11 @@ import {Modal, Space} from 'antd'; -import {useAppSelector} from '@redux/hooks'; +import {useAppDispatch, useAppSelector} from '@redux/hooks'; import {CopyButton} from '@components/atoms'; import {promiseFromIpcRenderer} from '@utils/promises'; +import {showGitErrorModal} from '@utils/terminal'; import {GitBranch} from '@shared/models/git'; @@ -17,6 +18,7 @@ type IProps = { const BranchCell: React.FC = props => { const {branch} = props; + const dispatch = useAppDispatch(); const currentBranch = useAppSelector(state => state.git.repo?.currentBranch); const selectedProjectRootFolder = useAppSelector(state => state.config.selectedProjectRootFolder); @@ -29,6 +31,10 @@ const BranchCell: React.FC = props => { promiseFromIpcRenderer('git.deleteLocalBranch', 'git.deleteLocalBranch.result', { localPath: selectedProjectRootFolder, branchName: branch.name, + }).then(result => { + if (result.error) { + showGitErrorModal(`Deleting ${branch.name} failed`, `git branch -d ${branch.name}`, dispatch); + } }); }, onCancel() {}, diff --git a/src/components/molecules/BranchSelect/BranchSelect.tsx b/src/components/molecules/BranchSelect/BranchSelect.tsx index f48459d242..23b12ef39c 100644 --- a/src/components/molecules/BranchSelect/BranchSelect.tsx +++ b/src/components/molecules/BranchSelect/BranchSelect.tsx @@ -1,11 +1,7 @@ import {useCallback, useState} from 'react'; -import {Modal} from 'antd'; - import {BranchesOutlined} from '@ant-design/icons'; -import {GIT_ERROR_MODAL_DESCRIPTION} from '@constants/constants'; - import {setCurrentBranch} from '@redux/git'; import {useAppDispatch, useAppSelector} from '@redux/hooks'; import {rootFolderSelector} from '@redux/selectors'; @@ -13,7 +9,7 @@ import {rootFolderSelector} from '@redux/selectors'; import {TableSelect} from '@atoms'; import {promiseFromIpcRenderer} from '@utils/promises'; -import {addDefaultCommandTerminal} from '@utils/terminal'; +import {showGitErrorModal} from '@utils/terminal'; import {GitBranch} from '@shared/models/git'; @@ -22,11 +18,8 @@ import BranchTable from './BranchTable'; function BranchSelect() { const dispatch = useAppDispatch(); - const bottomSelection = useAppSelector(state => state.ui.leftMenu.bottomSelection); const currentBranch = useAppSelector(state => state.git.repo?.currentBranch); - const defaultShell = useAppSelector(state => state.terminal.settings.defaultShell); const rootFolderPath = useAppSelector(rootFolderSelector); - const terminalsMap = useAppSelector(state => state.terminal.terminalsMap); const [visible, setVisible] = useState(false); @@ -46,36 +39,14 @@ function BranchSelect() { branchName, }).then(result => { if (result.error) { - Modal.warning({ - title: 'Checkout failed', - content:
{GIT_ERROR_MODAL_DESCRIPTION}
, - zIndex: 100000, - onCancel: () => { - addDefaultCommandTerminal( - terminalsMap, - `git checkout ${branchName}`, - defaultShell, - bottomSelection, - dispatch - ); - }, - onOk: () => { - addDefaultCommandTerminal( - terminalsMap, - `git checkout ${branchName}`, - defaultShell, - bottomSelection, - dispatch - ); - }, - }); + showGitErrorModal('Checkout failed', `git checkout -b ${branchName}`, dispatch); } else { dispatch(setCurrentBranch(branchName)); setVisible(false); } }); }, - [rootFolderPath, terminalsMap, bottomSelection, dispatch, defaultShell] + [rootFolderPath, dispatch] ); return ( diff --git a/src/components/molecules/BranchSelect/CreateBranchInput.tsx b/src/components/molecules/BranchSelect/CreateBranchInput.tsx index 4beb3746da..20464ad6d6 100644 --- a/src/components/molecules/BranchSelect/CreateBranchInput.tsx +++ b/src/components/molecules/BranchSelect/CreateBranchInput.tsx @@ -11,6 +11,7 @@ import {setCurrentBranch, setGitLoading, setRepo} from '@redux/git'; import {useAppDispatch, useAppSelector} from '@redux/hooks'; import {promiseFromIpcRenderer} from '@utils/promises'; +import {showGitErrorModal} from '@utils/terminal'; import * as S from './CreateBranchInput.styled'; @@ -22,7 +23,6 @@ const CreateBranchInput: React.FC = props => { const dispatch = useAppDispatch(); const {hideCreateBranchInputHandler} = props; const projectRootFolder = useAppSelector(state => state.config.selectedProjectRootFolder); - const gitRepoBranches = useAppSelector(state => state.git.repo?.branches || []); const selectedProjectRootFolder = useAppSelector(state => state.config.selectedProjectRootFolder); @@ -43,15 +43,22 @@ const CreateBranchInput: React.FC = props => { setLoading(true); - await promiseFromIpcRenderer('git.createLocalBranch', 'git.createLocalBranch.result', { + const result = await promiseFromIpcRenderer('git.createLocalBranch', 'git.createLocalBranch.result', { localPath: selectedProjectRootFolder, branchName, }); + if (result.error) { + showGitErrorModal(`Creating ${branchName} failed`, `git checkout -b ${branchName}`, dispatch); + setBranchName(''); + setLoading(false); + return; + } + setBranchName(''); setLoading(false); - await promiseFromIpcRenderer('git.getGitRepoInfo', 'git.getGitRepoInfo.result', projectRootFolder).then(result => { - dispatch(setRepo(result)); + await promiseFromIpcRenderer('git.getGitRepoInfo', 'git.getGitRepoInfo.result', projectRootFolder).then(repo => { + dispatch(setRepo(repo)); dispatch(setCurrentBranch(result.currentBranch)); dispatch(setGitLoading(false)); }); diff --git a/src/components/molecules/SectionRenderer/ItemRenderer/ItemRenderer.tsx b/src/components/molecules/SectionRenderer/ItemRenderer/ItemRenderer.tsx deleted file mode 100644 index ab1b2f8141..0000000000 --- a/src/components/molecules/SectionRenderer/ItemRenderer/ItemRenderer.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; - -import {isEqual} from 'lodash'; - -import {useAppDispatch, useAppSelector} from '@redux/hooks'; - -import {ScrollIntoView} from '@atoms'; -import {ScrollContainerRef} from '@atoms/ScrollIntoView'; - -import {ItemBlueprint, ItemCustomComponentProps} from '@shared/models/navigator'; - -import {useItemCustomization} from './useItemCustomization'; - -import * as S from './styled'; - -export type ItemRendererOptions = { - disablePrefix?: boolean; - disableSuffix?: boolean; - disableQuickAction?: boolean; - disableContextMenu?: boolean; -}; - -export type ItemRendererProps = { - itemId: string; - blueprint: ItemBlueprint; - level: number; - isLastItem: boolean; - isSectionCheckable: boolean; - sectionContainerElementId: string; - indentation: number; - options?: ItemRendererOptions; -}; - -const WrapperPlacehoder: React.FC = props => { - const {children} = props; - return
{children}
; -}; - -function ItemRenderer(props: ItemRendererProps) { - const {itemId, blueprint, level, isLastItem, isSectionCheckable, sectionContainerElementId, indentation, options} = - props; - - const instanceHandlerRef = useRef(blueprint.instanceHandler); - instanceHandlerRef.current = blueprint.instanceHandler; - - const dispatch = useAppDispatch(); - const checkedResourceIds = useAppSelector(state => state.main.checkedResourceIdentifiers); - const itemInstance = useAppSelector(state => state.navigator.itemInstanceMap[itemId], isEqual); - - const [isHovered, setIsHovered] = useState(false); - - const {Prefix, Suffix, QuickAction, ContextMenu, ContextMenuWrapper, NameDisplay, Information} = useItemCustomization( - blueprint.customization - ); - - const previouslyCheckedResourcesLength = useRef(checkedResourceIds.length); - const scrollContainer = useRef(null); - - const onClick = useCallback(() => { - if (instanceHandlerRef.current && instanceHandlerRef.current.onClick && !itemInstance.isDisabled) { - instanceHandlerRef.current.onClick(itemInstance, dispatch); - } - }, [itemInstance, dispatch]); - - const onCheck = useCallback(() => { - if (instanceHandlerRef.current && instanceHandlerRef.current.onCheck && !itemInstance.isDisabled) { - instanceHandlerRef.current.onCheck(itemInstance, dispatch); - } - }, [itemInstance, dispatch]); - - useEffect(() => { - if (!itemInstance.shouldScrollIntoView) { - return; - } - - // checking/unchecking a resource should not scroll - if (checkedResourceIds.length !== previouslyCheckedResourcesLength.current) { - previouslyCheckedResourcesLength.current = checkedResourceIds.length; - return; - } - - scrollContainer.current?.scrollIntoView(); - }, [checkedResourceIds.length, itemInstance.shouldScrollIntoView]); - - const Wrapper = useMemo( - () => (ContextMenuWrapper.Component ? ContextMenuWrapper.Component : WrapperPlacehoder), - [ContextMenuWrapper.Component] - ); - - return ( - - {Wrapper && ( - - setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - isSelected={itemInstance.isSelected} - isHighlighted={itemInstance.isHighlighted} - disableHoverStyle={Boolean(blueprint.customization?.disableHoverStyle)} - isHovered={isHovered} - level={level} - isLastItem={isLastItem} - onClick={onClick} - hasOnClick={Boolean(instanceHandlerRef.current?.onClick)} - $indentation={indentation} - $isSectionCheckable={isSectionCheckable} - $hasCustomNameDisplay={Boolean(NameDisplay.Component)} - $lastItemMarginBottom={blueprint.customization?.lastItemMarginBottom} - > - {itemInstance.isCheckable && - (blueprint.customization?.isCheckVisibleOnHover ? itemInstance.isChecked || isHovered : true) && ( - - onCheck()} - $level={level} - /> - - )} - - {Prefix.Component && !options?.disablePrefix && (Prefix.options?.isVisibleOnHover ? isHovered : true) && ( - - - - )} - - {NameDisplay.Component ? ( - - ) : ( - - {itemInstance.name} {itemInstance.isDirty ? '*' : ''} - - )} - - {Information.Component && (Information.options?.isVisibleOnHover ? isHovered : true) && ( - - - - )} - - {Suffix.Component && !options?.disableSuffix && (Suffix.options?.isVisibleOnHover ? isHovered : true) && ( - - - - )} - -
- {QuickAction.Component && - !options?.disableQuickAction && - (QuickAction.options?.isVisibleOnHover ? isHovered : true) && ( - - - - )} - - {ContextMenu.Component && - !options?.disableContextMenu && - (ContextMenu.options?.isVisibleOnHover ? isHovered : true) && ( - e.stopPropagation()}> - - - )} -
-
-
- )} -
- ); -} - -export default memo(ItemRenderer, isEqual); diff --git a/src/components/molecules/SectionRenderer/ItemRenderer/index.ts b/src/components/molecules/SectionRenderer/ItemRenderer/index.ts deleted file mode 100644 index a724a8025f..0000000000 --- a/src/components/molecules/SectionRenderer/ItemRenderer/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {default} from './ItemRenderer'; -export type {ItemRendererOptions} from './ItemRenderer'; diff --git a/src/components/molecules/SectionRenderer/ItemRenderer/styled.tsx b/src/components/molecules/SectionRenderer/ItemRenderer/styled.tsx deleted file mode 100644 index a0de997946..0000000000 --- a/src/components/molecules/SectionRenderer/ItemRenderer/styled.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import {Checkbox as RawCheckbox} from 'antd'; - -import styled from 'styled-components'; - -import {Colors} from '@shared/styles/colors'; - -type ItemContainerProps = { - isSelected: boolean; - isHighlighted: boolean; - disableHoverStyle: boolean; - isHovered: boolean; - level: number; - isLastItem: boolean; - hasOnClick: boolean; - $indentation: number; - $isSectionCheckable: boolean; - $hasCustomNameDisplay: boolean; - $lastItemMarginBottom?: number; - $isDisabled: boolean; -}; - -export const ItemContainer = styled.span` - display: flex; - align-items: center; - width: 100%; - user-select: none; - - > { - min-width: 0; - } - - ${props => { - const defaultIndentation = props.$isSectionCheckable ? 32 : 26; - return `padding-left: ${defaultIndentation + props.$indentation}px;`; - }} - padding-right: 8px; - margin-bottom: 2px; - ${props => props.hasOnClick && !props.$isDisabled && `cursor: pointer;`} - ${props => { - if (props.isLastItem) { - if (props.$lastItemMarginBottom !== undefined) { - return `margin-bottom: ${props.$lastItemMarginBottom}px;`; - } - return `margin-bottom: 12px;`; - } - }} - ${props => { - if (props.disableHoverStyle || props.$isDisabled) { - return; - } - if (!props.isSelected && props.isHighlighted) { - if (props.isHovered) { - return `background: ${Colors.highlightColorHover};`; - } - return `background: ${Colors.highlightColor};`; - } - if (props.isSelected) { - if (props.isHovered) { - return `background: ${Colors.selectionColorHover};`; - } - return `background: ${Colors.selectionColor};`; - } - if (props.isHovered) { - return `background: ${Colors.blackPearl};`; - } - }}; - ${props => !props.isHovered && 'padding-right: 46px;'} - ${props => props.$hasCustomNameDisplay && 'padding-right: 0px;'} -`; - -type ItemNameProps = { - isSelected: boolean; - isHighlighted: boolean; - isDirty: boolean; - isDisabled: boolean; - level: number; -}; - -export const ItemName = styled.div` - padding: 2px 0; - font-size: 12px; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - ${props => { - if (props.isSelected) { - return `font-weight: 700;`; - } - if (props.isHighlighted) { - return `font-weight: 500;`; - } - }}; - ${props => { - if (props.isDisabled) { - return `color: ${Colors.grey7};`; - } - if (!props.isSelected && props.isDirty) { - return `color: ${Colors.yellow7};`; - } - if (!props.isSelected && props.isHighlighted) { - return `color: ${Colors.cyan7};`; - } - if (props.isSelected) { - return `color: ${Colors.blackPure};`; - } - return `color: ${Colors.blue10};`; - }}; -`; - -export const PrefixContainer = styled.span` - min-width: 40px; -`; - -export const SuffixContainer = styled.span` - display: flex; - align-items: center; -`; - -export const InformationContainer = styled.span` - display: flex; - align-items: center; -`; - -export const QuickActionContainer = styled.span``; - -export const ContextMenuContainer = styled.span``; - -export const BlankSpace = styled.span` - flex-grow: 1; - height: 20px; -`; - -export const Checkbox = styled(RawCheckbox)<{$level: number}>` - margin-left: -24px; -`; diff --git a/src/components/molecules/SectionRenderer/ItemRenderer/useItemCustomization.ts b/src/components/molecules/SectionRenderer/ItemRenderer/useItemCustomization.ts deleted file mode 100644 index b6807b0bf6..0000000000 --- a/src/components/molecules/SectionRenderer/ItemRenderer/useItemCustomization.ts +++ /dev/null @@ -1,40 +0,0 @@ -import {useMemo} from 'react'; - -import {ItemCustomization} from '@shared/models/navigator'; - -export function useItemCustomization(customization: ItemCustomization = {}) { - const Prefix = useMemo( - () => ({Component: customization.prefix?.component, options: customization.prefix?.options}), - [customization.prefix] - ); - const Suffix = useMemo( - () => ({Component: customization.suffix?.component, options: customization.suffix?.options}), - [customization.suffix] - ); - const QuickAction = useMemo( - () => ({Component: customization.quickAction?.component, options: customization.quickAction?.options}), - [customization.quickAction] - ); - const ContextMenuWrapper = useMemo( - () => ({ - Component: customization.contextMenuWrapper?.component, - options: customization.contextMenuWrapper?.options, - }), - [customization.contextMenuWrapper] - ); - const ContextMenu = useMemo( - () => ({Component: customization.contextMenu?.component, options: customization.contextMenu?.options}), - [customization.contextMenu] - ); - const NameDisplay = useMemo( - () => ({Component: customization.nameDisplay?.component, options: customization.nameDisplay?.options}), - [customization.nameDisplay] - ); - - const Information = useMemo( - () => ({Component: customization.information?.component, options: customization.information?.options}), - [customization.information] - ); - - return {Prefix, Suffix, QuickAction, ContextMenu, ContextMenuWrapper, NameDisplay, Information}; -} diff --git a/src/components/molecules/SectionRenderer/SectionHeader.tsx b/src/components/molecules/SectionRenderer/SectionHeader.tsx deleted file mode 100644 index 08167edbf4..0000000000 --- a/src/components/molecules/SectionRenderer/SectionHeader.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import {useCallback, useMemo, useRef, useState} from 'react'; - -import {useAppDispatch} from '@redux/hooks'; - -import navSectionMap from '@src/navsections/sectionBlueprintMap'; - -import {SectionCustomComponent, SectionInstance} from '@shared/models/navigator'; - -import SectionHeaderDefaultNameCounter from './SectionHeaderDefaultNameCounter'; -import {useSectionCustomization} from './useSectionCustomization'; - -import * as S from './styled'; - -interface SectionHeaderProps { - name: string; - sectionInstance: SectionInstance; - isCollapsed: boolean; - isLastSection: boolean; - level: number; - indentation: number; - expandSection: () => void; - collapseSection: () => void; -} - -function SectionHeader(props: SectionHeaderProps) { - const {name, sectionInstance, isCollapsed, isLastSection, level, indentation, expandSection, collapseSection} = props; - const dispatch = useAppDispatch(); - const [isHovered, setIsHovered] = useState(false); - - const sectionBlueprintRef = useRef(navSectionMap.getById(sectionInstance.id)); - - const {NameDisplay, NamePrefix, NameSuffix, NameContext, NameCounter} = useSectionCustomization( - sectionBlueprintRef.current?.customization - ); - - const Counter: SectionCustomComponent = useMemo( - () => NameCounter.Component ?? SectionHeaderDefaultNameCounter, - [NameCounter] - ); - - const toggleCollapse = useCallback(() => { - if (isCollapsed) { - expandSection(); - } else { - collapseSection(); - } - }, [isCollapsed, expandSection, collapseSection]); - - const onCheck = useCallback(() => { - if (!sectionInstance.checkable || !sectionInstance.visibleDescendantItemIds) { - return; - } - if (sectionInstance.checkable.value === 'unchecked' || sectionInstance.checkable.value === 'partial') { - dispatch(sectionInstance.checkable.checkItemsAction); - return; - } - if (sectionInstance.checkable.value === 'checked') { - dispatch(sectionInstance.checkable.uncheckItemsAction); - } - }, [sectionInstance, dispatch]); - - return ( - 0 - )} - disableHoverStyle={Boolean(sectionBlueprintRef.current?.customization?.disableHoverStyle)} - isSelected={Boolean(sectionInstance.isSelected && isCollapsed)} - isHighlighted={Boolean(sectionInstance.isHighlighted && isCollapsed)} - isInitialized={Boolean(sectionInstance.isInitialized)} - isVisible={Boolean(sectionInstance.isVisible)} - isSectionCheckable={Boolean(sectionBlueprintRef.current?.builder?.makeCheckable)} - hasCustomNameDisplay={Boolean(NameDisplay.Component)} - isLastSection={isLastSection} - isCollapsed={isCollapsed} - onMouseEnter={() => setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - - {sectionInstance.checkable && - sectionInstance.isInitialized && - (sectionBlueprintRef.current?.customization?.isCheckVisibleOnHover - ? sectionInstance.checkable.value === 'partial' || - sectionInstance.checkable.value === 'checked' || - isHovered - : true) && ( - - onCheck()} - $level={level} - /> - - )} - {NameDisplay.Component ? ( - - ) : ( - <> - {NamePrefix.Component && ( - - )} - {name === 'K8s Resources' ? ( - - {name} - - ) : ( - - {name} - - )} - - - - - {NameSuffix.Component && (NameSuffix.options?.isVisibleOnHover ? isHovered : true) && ( - - )} - - )} - - - {!NameDisplay.Component && NameContext.Component && ( - - )} - - - ); -} - -export default SectionHeader; diff --git a/src/components/molecules/SectionRenderer/SectionHeaderDefaultNameCounter.tsx b/src/components/molecules/SectionRenderer/SectionHeaderDefaultNameCounter.tsx deleted file mode 100644 index 108e05756a..0000000000 --- a/src/components/molecules/SectionRenderer/SectionHeaderDefaultNameCounter.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React, {useMemo} from 'react'; - -import {useAppSelector} from '@redux/hooks'; - -import sectionBlueprintMap from '@src/navsections/sectionBlueprintMap'; - -import {SectionCustomComponentProps} from '@shared/models/navigator'; - -import * as S from './styled'; - -function SectionHeaderDefaultNameCounter({sectionInstance, onClick}: SectionCustomComponentProps) { - const {id, isSelected} = sectionInstance; - const sectionBlueprint = sectionBlueprintMap.getById(id); - const isCollapsed = useAppSelector(state => state.navigator.collapsedSectionIds.includes(id)); - - const resourceCount = useMemo(() => { - const counterDisplayMode = sectionBlueprint?.customization?.counterDisplayMode; - - if (!counterDisplayMode || counterDisplayMode === 'descendants') { - return sectionInstance?.visibleDescendantItemIds?.length || 0; - } - if (counterDisplayMode === 'items') { - return sectionInstance?.visibleItemIds.length; - } - if (counterDisplayMode === 'subsections') { - return sectionInstance?.visibleChildSectionIds?.length || 0; - } - return undefined; - }, [sectionInstance, sectionBlueprint]); - - if (resourceCount === undefined) { - return null; - } - - return ( - - {resourceCount} - - ); -} - -export default SectionHeaderDefaultNameCounter; diff --git a/src/components/molecules/SectionRenderer/SectionRenderer.tsx b/src/components/molecules/SectionRenderer/SectionRenderer.tsx deleted file mode 100644 index 25753e0e9a..0000000000 --- a/src/components/molecules/SectionRenderer/SectionRenderer.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; - -import {isEqual} from 'lodash'; - -import {useAppDispatch, useAppSelector} from '@redux/hooks'; -import {collapseSectionIds, expandSectionIds} from '@redux/reducers/navigator'; - -import navSectionMap from '@src/navsections/sectionBlueprintMap'; - -import {ItemBlueprint, ItemGroupInstance, SectionInstance} from '@shared/models/navigator'; - -import ItemRenderer, {ItemRendererOptions} from './ItemRenderer'; -import SectionHeader from './SectionHeader'; -import {useSectionCustomization} from './useSectionCustomization'; - -import * as S from './styled'; - -type SectionRendererProps = { - sectionId: string; - level: number; - isLastSection: boolean; - parentIndentation?: number; - itemRendererOptions?: ItemRendererOptions; -}; - -function SectionRenderer(props: SectionRendererProps) { - const {sectionId, level, isLastSection, itemRendererOptions, parentIndentation} = props; - - const sectionBlueprintRef = useRef(navSectionMap.getById(sectionId)); - const itemBlueprintRef = useRef(sectionBlueprintRef.current?.itemBlueprint); - - const sectionInstance: SectionInstance | undefined = useAppSelector( - state => state.navigator.sectionInstanceMap[sectionId], - isEqual - ); - const sectionInstanceRef = useRef(sectionInstance); - sectionInstanceRef.current = sectionInstance; - - const registeredSectionIds = useAppSelector(state => state.navigator.registeredSectionBlueprintIds); - - const visibleChildSectionIds = useMemo( - () => sectionInstance.visibleChildSectionIds?.filter(id => registeredSectionIds.includes(id)), - [sectionInstance.visibleChildSectionIds, registeredSectionIds] - ); - - const dispatch = useAppDispatch(); - - const {EmptyDisplay} = useSectionCustomization(sectionBlueprintRef.current?.customization); - - const collapsedSectionIds = useAppSelector(state => state.navigator.collapsedSectionIds); - - const sectionIndentation = useMemo(() => { - const indentation = sectionBlueprintRef.current?.customization?.indentation; - if (!parentIndentation && !indentation) { - return undefined; - } - if (parentIndentation && !indentation) { - return parentIndentation; - } - if (!parentIndentation && indentation) { - return indentation; - } - if (parentIndentation && indentation) { - return parentIndentation + indentation; - } - return undefined; - }, [parentIndentation]); - - const isCollapsedMode = useMemo(() => { - if (!sectionInstance?.id) { - return 'expanded'; - } - const visibleDescendantSectionIds = sectionInstance?.visibleDescendantSectionIds; - if (visibleDescendantSectionIds) { - if ( - collapsedSectionIds.includes(sectionInstance.id) && - visibleDescendantSectionIds.every(s => collapsedSectionIds.includes(s)) - ) { - return 'collapsed'; - } - if ( - !collapsedSectionIds.includes(sectionInstance.id) && - visibleDescendantSectionIds.every(s => !collapsedSectionIds.includes(s)) - ) { - return 'expanded'; - } - return 'mixed'; - } - if (collapsedSectionIds.includes(sectionInstance.id)) { - return 'collapsed'; - } - return 'expanded'; - }, [collapsedSectionIds, sectionInstance?.id, sectionInstance?.visibleDescendantSectionIds]); - - const isCollapsed = useMemo(() => { - return isCollapsedMode === 'collapsed'; - }, [isCollapsedMode]); - - const expandSection = useCallback(() => { - if (!sectionInstanceRef.current?.id) { - return; - } - if ( - !sectionInstanceRef.current?.visibleDescendantSectionIds || - sectionInstanceRef.current.visibleDescendantSectionIds.length === 0 - ) { - dispatch(expandSectionIds([sectionInstanceRef.current.id])); - } else { - dispatch( - expandSectionIds([sectionInstanceRef.current.id, ...sectionInstanceRef.current.visibleDescendantSectionIds]) - ); - } - }, [dispatch]); - - const collapseSection = useCallback(() => { - if (!sectionInstanceRef.current?.id) { - return; - } - if ( - !sectionInstanceRef.current?.visibleDescendantSectionIds || - sectionInstanceRef.current.visibleDescendantSectionIds.length === 0 - ) { - dispatch(collapseSectionIds([sectionInstanceRef.current?.id])); - } else { - dispatch( - collapseSectionIds([sectionInstanceRef.current?.id, ...sectionInstanceRef.current.visibleDescendantSectionIds]) - ); - } - }, [dispatch]); - - useEffect(() => { - if (sectionInstance?.shouldExpand) { - expandSection(); - } - }, [sectionInstance?.shouldExpand, expandSection]); - - const groupInstanceById: Record = useMemo(() => { - return sectionInstance?.groups - .map<[string, ItemGroupInstance]>(g => [g.id, g]) - .reduce>((acc, [k, v]) => { - acc[k] = v; - return acc; - }, {}); - }, [sectionInstance?.groups]); - - const lastVisibleChildSectionId = useMemo(() => { - if (!sectionInstance?.visibleChildSectionIds) { - return undefined; - } - return sectionInstance.visibleChildSectionIds - ? sectionInstance.visibleChildSectionIds[sectionInstance.visibleChildSectionIds.length - 1] - : undefined; - }, [sectionInstance?.visibleChildSectionIds]); - - const isLastVisibleItemId = useCallback((itemId: string) => { - if (!sectionInstanceRef.current?.visibleItemIds) { - return false; - } - const lastVisibleItemId = - sectionInstanceRef.current.visibleItemIds[sectionInstanceRef.current.visibleItemIds.length - 1]; - return itemId === lastVisibleItemId; - }, []); - - const isLastVisibleItemIdInGroup = useCallback( - (groupId: string, itemId: string) => { - const groupInstance = groupInstanceById[groupId]; - if (!groupInstance) { - return false; - } - const lastVisibleItemIdInGroup = groupInstance.visibleItemIds[groupInstance.visibleItemIds.length - 1]; - return itemId === lastVisibleItemIdInGroup; - }, - [groupInstanceById] - ); - - if (!sectionInstance?.isInitialized && sectionBlueprintRef.current?.customization?.beforeInitializationText) { - return ( - -

{sectionBlueprintRef.current?.customization.beforeInitializationText}

-
- ); - } - - if (!sectionInstance?.isVisible) { - return null; - } - - if (sectionInstance?.isLoading) { - return ; - } - - if (sectionInstance?.isEmpty) { - if (EmptyDisplay && EmptyDisplay.Component) { - return ( - - - - ); - } - return ( - -

{sectionInstance.name}

-

Section is empty.

-
- ); - } - - return ( - <> - - {sectionInstance && - sectionInstance.isVisible && - !isCollapsed && - itemBlueprintRef.current && - sectionInstance.groups.length === 0 && - sectionInstance.visibleItemIds.map(itemId => ( - } - level={level + 1} - isLastItem={isLastVisibleItemId(itemId)} - isSectionCheckable={Boolean(sectionInstance.checkable)} - sectionContainerElementId={sectionBlueprintRef.current?.containerElementId || ''} - options={itemRendererOptions} - indentation={sectionIndentation || 0} - /> - ))} - {sectionInstance?.isVisible && - !isCollapsed && - itemBlueprintRef.current && - groupInstanceById && - sectionInstance.visibleGroupIds.map(groupId => { - const group = groupInstanceById[groupId]; - return ( - - - - {group.name} - {group.visibleItemIds.length} - - - {group.visibleItemIds.length ? ( - group.visibleItemIds.map(itemId => ( - } - level={level + 2} - isLastItem={isLastVisibleItemIdInGroup(group.id, itemId)} - isSectionCheckable={Boolean(sectionInstance.checkable)} - sectionContainerElementId={sectionBlueprintRef.current?.containerElementId || ''} - options={itemRendererOptions} - indentation={sectionIndentation || 0} - /> - )) - ) : ( - - {sectionBlueprintRef.current?.customization?.emptyGroupText || 'No items in this group.'} - - )} - - ); - })} - {visibleChildSectionIds && - visibleChildSectionIds.map(childId => ( - - ))} - - ); -} - -export default React.memo(SectionRenderer, isEqual); diff --git a/src/components/molecules/SectionRenderer/index.ts b/src/components/molecules/SectionRenderer/index.ts deleted file mode 100644 index 2d0a0dcfa4..0000000000 --- a/src/components/molecules/SectionRenderer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default} from './SectionRenderer'; diff --git a/src/components/molecules/SectionRenderer/styled.tsx b/src/components/molecules/SectionRenderer/styled.tsx deleted file mode 100644 index 93e0f0ca2d..0000000000 --- a/src/components/molecules/SectionRenderer/styled.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import {Checkbox as RawCheckbox, Skeleton as RawSkeleton} from 'antd'; - -import styled from 'styled-components'; - -import {Colors, FontColors} from '@shared/styles/colors'; - -type NameContainerProps = { - $hasCustomNameDisplay: boolean; - $indentation: number; - isHovered?: boolean; - isCheckable?: boolean; -}; - -export const NameContainer = styled.span` - display: flex; - align-items: center; - width: 100%; - ${props => { - const defaultIndentation = props.isCheckable ? 24 : 0; - return `padding-left: ${defaultIndentation + props.$indentation}px;`; - }} - ${props => !props.isHovered && 'padding-right: 30px;'} - ${props => props.$hasCustomNameDisplay && 'padding: 0;'} -`; - -type SectionContainerProps = { - isSelected?: boolean; - isHighlighted?: boolean; - isHovered?: boolean; - isLastSection?: boolean; - isCollapsed?: boolean; - hasChildSections?: boolean; - isVisible?: boolean; - isInitialized?: boolean; - disableHoverStyle?: boolean; - isSectionCheckable?: boolean; - hasCustomNameDisplay?: boolean; -}; - -export const SectionContainer = styled.li` - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - width: 100%; - user-select: none; - ${props => - (!props.isSectionCheckable && !props.hasCustomNameDisplay) || !props.isInitialized ? 'padding-left: 16px;' : ''} - ${props => { - if (props.isVisible === false) { - return 'visibility: hidden; height: 0;'; - } - return 'visibility: visible;'; - }} - ${props => { - if (props.isLastSection && (props.isCollapsed || !props.isInitialized) && !props.hasChildSections) { - return `margin-bottom: 16px;`; - } - }} - ${props => { - if (props.disableHoverStyle) { - return; - } - if (!props.isSelected && props.isHighlighted) { - if (props.isHovered) { - return `background: ${Colors.highlightColorHover};`; - } - return `background: ${Colors.highlightColor};`; - } - if (props.isSelected) { - if (props.isHovered) { - return `background: ${Colors.selectionColorHover};`; - } - return `background: ${Colors.selectionColor};`; - } - if (props.isHovered) { - return `background: ${Colors.blackPearl};`; - } - }}; -`; - -type NameProps = { - $isSelected?: boolean; - $isHighlighted?: boolean; - $isCheckable?: boolean; - $level: number; - $nameColor?: string; - $nameSize?: number; - $nameWeight?: number; - $nameVerticalPadding?: number; - $nameHorizontalPadding?: number; -}; - -export const Name = styled.span` - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - ${props => - `padding: ${props.$nameVerticalPadding !== undefined ? props.$nameVerticalPadding : 0}px ${ - props.$nameHorizontalPadding !== undefined ? props.$nameHorizontalPadding : 5 - }px;`} - cursor: pointer; - ${props => { - if (props.$nameSize) { - return `font-size: ${props.$nameSize}px;`; - } - return `font-size: ${22 - 4 * props.$level}px;`; - }} - ${props => { - if (props.$isSelected) { - return `font-weight: 700;`; - } - if (props.$isHighlighted) { - return `font-weight: 500;`; - } - }} - ${props => { - if (props.$isSelected) { - return `color: ${Colors.blackPure};`; - } - return props.$nameColor ? `color: ${props.$nameColor};` : `color: ${Colors.whitePure};`; - }} - ${props => props.$nameWeight && `font-weight: ${props.$nameWeight};`} -`; - -export const EmptyGroupText = styled.p` - margin-left: 26px; - margin-bottom: 12px; - font-size: 12px; -`; - -export const Collapsible = styled.span` - margin-left: auto; - margin-right: 15px; -`; - -export const Skeleton = styled(RawSkeleton)` - margin: 20px; - width: 90%; -`; - -export const Counter = styled.span<{selected: boolean}>` - margin-left: 8px; - font-size: 14px; - cursor: pointer; - ${props => (props.selected ? `color: ${Colors.blackPure};` : `color: ${FontColors.grey};`)} -`; - -export const EmptyDisplayContainer = styled.div<{level: number}>` - margin-left: 16px; -`; - -export const BeforeInitializationContainer = styled.div<{level: number}>` - padding-top: 16px; - margin-left: 16px; -`; - -export const BlankSpace = styled.span<{level?: number}>` - flex-grow: 1; - height: 32px; - cursor: pointer; - ${props => props.level && `height: ${32 - props.level * 8}px;`} -`; - -export const Checkbox = styled(RawCheckbox)<{$level: number}>` - margin-left: -16px; -`; - -export const CheckboxPlaceholder = styled.span<{$level: number}>` - width: 24px; -`; - -export const NameDisplayContainer = styled.span` - margin-left: 26px; -`; diff --git a/src/components/molecules/SectionRenderer/useSectionCustomization.ts b/src/components/molecules/SectionRenderer/useSectionCustomization.ts deleted file mode 100644 index 12f58be564..0000000000 --- a/src/components/molecules/SectionRenderer/useSectionCustomization.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {useMemo} from 'react'; - -import {SectionCustomization} from '@shared/models/navigator'; - -export function useSectionCustomization(customization: SectionCustomization = {}) { - const NameDisplay = useMemo(() => { - return {Component: customization.nameDisplay?.component}; - }, [customization.nameDisplay]); - const NamePrefix = useMemo( - () => ({ - Component: customization.namePrefix?.component, - }), - [customization.namePrefix] - ); - const NameSuffix = useMemo( - () => ({ - Component: customization.nameSuffix?.component, - options: customization.nameSuffix?.options, - }), - [customization.nameSuffix] - ); - const EmptyDisplay = useMemo( - () => ({Component: customization.emptyDisplay?.component}), - [customization.emptyDisplay] - ); - const NameContext = useMemo(() => ({Component: customization.nameContext?.component}), [customization.nameContext]); - - const NameCounter = useMemo(() => ({Component: customization.nameCounter?.component}), [customization.nameCounter]); - - return {NameDisplay, EmptyDisplay, NamePrefix, NameSuffix, NameContext, NameCounter}; -} diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts index 0afd8cb8da..06590dd25e 100644 --- a/src/components/molecules/index.ts +++ b/src/components/molecules/index.ts @@ -19,6 +19,5 @@ export {default as ResourceGraphTab} from './ResourceGraphTab'; export {default as ResourceNamespaceFilter} from './ResourceNamespaceFilter'; export {default as ResourceRefsIconPopover} from './ResourceRefsIconPopover'; export {default as SaveResourcesToFileFolderModal} from './SaveResourcesToFileFolderModal'; -export {default as SectionRenderer} from './SectionRenderer'; export {default as TemplateFormRenderer} from './TemplateFormRenderer'; export {default as WelcomePopupContent} from './WelcomePopupContent'; diff --git a/src/components/organisms/ExplorerPane/AccordionPanel/AccordionPanel.styled.tsx b/src/components/organisms/ExplorerPane/AccordionPanel/AccordionPanel.styled.tsx deleted file mode 100644 index 9ca5e82860..0000000000 --- a/src/components/organisms/ExplorerPane/AccordionPanel/AccordionPanel.styled.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import {Collapse} from 'antd'; - -import styled from 'styled-components'; - -export const Panel = styled(Collapse.Panel)<{$contentHeight: number; $panelKey: string}>` - &.ant-collapse-item-active { - height: 100%; - } - - .ant-collapse-content-box { - padding: 0 !important; - overflow-y: ${props => (props.$panelKey !== 'helm' ? 'hidden' : 'auto')}; - max-height: ${props => props.$contentHeight}px; - height: ${props => props.$contentHeight}px; - } -`; diff --git a/src/components/organisms/ExplorerPane/AccordionPanel/AccordionPanel.tsx b/src/components/organisms/ExplorerPane/AccordionPanel/AccordionPanel.tsx index 40f088e9b1..04618ee668 100644 --- a/src/components/organisms/ExplorerPane/AccordionPanel/AccordionPanel.tsx +++ b/src/components/organisms/ExplorerPane/AccordionPanel/AccordionPanel.tsx @@ -1,14 +1,9 @@ -import {useState} from 'react'; -import {useEffectOnce} from 'react-use'; +import {Collapse, CollapsePanelProps} from 'antd'; -import {CollapsePanelProps} from 'antd'; - -import {useResizeObserverRef} from 'rooks'; +import styled from 'styled-components'; import {usePaneHeight} from '@hooks/usePaneHeight'; -import * as S from './AccordionPanel.styled'; - export const PANEL_HEADER_HEIGHT = 72; export const getPanelId = (panelKey?: string) => `accordion-panel-${panelKey}`; @@ -22,29 +17,25 @@ export type InjectedPanelProps = { const AccordionPanel: React.FC = props => { const id = getPanelId(props.panelKey); - const [contentHeight, setContentHeight] = useState(1); - - const height = usePaneHeight() - PANEL_HEADER_HEIGHT - 130; - - const [containerRef] = useResizeObserverRef(el => { - if (!el) return; - const [{contentRect}] = el; - setContentHeight(contentRect ? contentRect.height - PANEL_HEADER_HEIGHT : 1); - }); - - useEffectOnce(() => { - containerRef(document.querySelector(`#${id}`)); - }); - - return ( - - ); + + const height = usePaneHeight() - PANEL_HEADER_HEIGHT - 130 - (props.panelKey === 'files' ? 35 : 25); + + return ; }; export default AccordionPanel; + +// Styled Components + +const Panel = styled(Collapse.Panel)<{$contentHeight: number}>` + &.ant-collapse-item-active { + height: 100%; + } + + .ant-collapse-content-box { + padding: 0 !important; + overflow-y: hidden; + max-height: ${props => props.$contentHeight}px; + height: ${props => props.$contentHeight}px; + } +`; diff --git a/src/components/organisms/ExplorerPane/ExplorerPane.tsx b/src/components/organisms/ExplorerPane/ExplorerPane.tsx index 590515b724..f3787da6fc 100644 --- a/src/components/organisms/ExplorerPane/ExplorerPane.tsx +++ b/src/components/organisms/ExplorerPane/ExplorerPane.tsx @@ -1,5 +1,3 @@ -import {useMeasure} from 'react-use'; - import {Collapse as RawCollapse} from 'antd'; import styled from 'styled-components'; @@ -14,16 +12,15 @@ import FilePane from './FilePane'; import HelmPane from './HelmPane'; import ImagesPane from './ImagesPane'; import KustomizePane from './KustomizePane'; +import PreviewConfigurationPane from './PreviewConfigurationPane'; const ExplorerPane: React.FC = () => { const dispatch = useAppDispatch(); const explorerSelectedSection = useAppSelector(state => state.ui.explorerSelectedSection); const isInClusterMode = useAppSelector(isInClusterModeSelector); - const [containerRef, {width: containerWidth}] = useMeasure(); - return ( - + { > - + + diff --git a/src/components/organisms/ExplorerPane/FilePane/FilePane.styled.tsx b/src/components/organisms/ExplorerPane/FilePane/FilePane.styled.tsx index 6dff071237..b47e1b9ebd 100644 --- a/src/components/organisms/ExplorerPane/FilePane/FilePane.styled.tsx +++ b/src/components/organisms/ExplorerPane/FilePane/FilePane.styled.tsx @@ -7,8 +7,7 @@ import {Colors, FontColors} from '@shared/styles/colors'; export const FileTreeContainer = styled.div` width: 100%; height: 100%; - padding: 15px 0px; - padding-top: 0px; + padding: 0px 0px 15px 0px; & .ant-tree { font-family: 'Inter', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', diff --git a/src/components/organisms/ExplorerPane/HelmPane/HelmChartRenderer/HelmChartCollapse.tsx b/src/components/organisms/ExplorerPane/HelmPane/HelmChartRenderer/HelmChartCollapse.tsx new file mode 100644 index 0000000000..a5cfb4dcaf --- /dev/null +++ b/src/components/organisms/ExplorerPane/HelmPane/HelmChartRenderer/HelmChartCollapse.tsx @@ -0,0 +1,43 @@ +import {useCallback} from 'react'; + +import {MinusSquareOutlined, PlusSquareOutlined} from '@ant-design/icons'; + +import {useAppDispatch, useAppSelector} from '@redux/hooks'; +import {collapseHelmChart, toggleHelmChart} from '@redux/reducers/ui'; + +import {Colors} from '@shared/styles/colors'; + +type IProps = { + id: string; + isSelected: boolean; +}; + +const HelmChartCollapse: React.FC = props => { + const {id, isSelected} = props; + + const dispatch = useAppDispatch(); + const isSectionCollapsed = useAppSelector(state => state.ui.collapsedHelmCharts.includes(id)); + + const onClickHandler = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + + if (isSectionCollapsed) { + dispatch(toggleHelmChart(id)); + } else { + dispatch(collapseHelmChart(id)); + } + }, + [dispatch, id, isSectionCollapsed] + ); + + if (isSectionCollapsed) { + return ( + + ); + } + + return ; +}; + +export default HelmChartCollapse; diff --git a/src/components/organisms/ExplorerPane/HelmPane/HelmChartRenderer/HelmChartRenderer.styled.tsx b/src/components/organisms/ExplorerPane/HelmPane/HelmChartRenderer/HelmChartRenderer.styled.tsx new file mode 100644 index 0000000000..cf8afe3886 --- /dev/null +++ b/src/components/organisms/ExplorerPane/HelmPane/HelmChartRenderer/HelmChartRenderer.styled.tsx @@ -0,0 +1,92 @@ +import styled from 'styled-components'; + +import {Colors} from '@shared/styles/colors'; + +type ItemContainerProps = { + isDisabled: boolean; + isSelected: boolean; + isHovered: boolean; +}; + +type ItemNameProps = { + isDisabled: boolean; + isSelected: boolean; +}; + +export const ContextMenuContainer = styled.span``; + +export const ItemContainer = styled.span` + display: flex; + align-items: center; + width: 100%; + user-select: none; + > { + min-width: 0; + } + padding-left: 20px; + padding-right: 8px; + margin-bottom: 2px; + cursor: pointer; + + ${props => { + if (props.isDisabled) { + return; + } + + if (props.isSelected) { + if (props.isHovered) { + return `background: ${Colors.selectionColorHover};`; + } + return `background: ${Colors.selectionColor};`; + } + + if (props.isHovered) { + return `background: ${Colors.blackPearl};`; + } + }}; + ${props => !props.isHovered && 'padding-right: 46px;'} +`; + +export const ItemName = styled.div` + padding: 2px 0; + font-size: 14px; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + ${props => { + if (props.isSelected) { + return `font-weight: 700;`; + } + + return 'font-weight: 600'; + }}; + + ${props => { + if (props.isDisabled) { + return `color: ${Colors.grey7};`; + } + if (props.isSelected) { + return `color: ${Colors.blackPure};`; + } + return `color: ${Colors.grey9};`; + }}; +`; + +export const PrefixContainer = styled.span` + display: flex; + align-items: center; + gap: 8px; + margin-right: 7px; +`; + +export const SuffixContainer = styled.span<{isSelected: boolean}>` + min-width: 0; + color: ${({isSelected}) => (isSelected ? Colors.grey4 : Colors.grey6)}; + margin-left: 8px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 12px; +`; diff --git a/src/components/organisms/ExplorerPane/HelmPane/HelmChartRenderer/HelmChartRenderer.tsx b/src/components/organisms/ExplorerPane/HelmPane/HelmChartRenderer/HelmChartRenderer.tsx new file mode 100644 index 0000000000..714d320067 --- /dev/null +++ b/src/components/organisms/ExplorerPane/HelmPane/HelmChartRenderer/HelmChartRenderer.tsx @@ -0,0 +1,85 @@ +import {useState} from 'react'; + +import {Tooltip} from 'antd'; + +import {dirname} from 'path'; + +import {TOOLTIP_DELAY} from '@constants/constants'; + +import {isInClusterModeSelector} from '@redux/appConfig'; +import {useAppDispatch, useAppSelector} from '@redux/hooks'; +import {selectFile} from '@redux/reducers/main'; + +import {Icon} from '@monokle/components'; +import {Colors} from '@shared/styles/colors'; + +import HelmContextMenu from '../HelmContextMenu'; +import HelmChartCollapse from './HelmChartCollapse'; +import * as S from './HelmChartRenderer.styled'; + +type IProps = { + id: string; +}; + +const HelmChartRenderer: React.FC = props => { + const {id} = props; + + const dispatch = useAppDispatch(); + const fileOrFolderContainedIn = useAppSelector(state => state.main.resourceFilter.fileOrFolderContainedIn || ''); + const helmChart = useAppSelector(state => state.main.helmChartMap[id]); + const isDisabled = useAppSelector(state => + Boolean( + (state.main.preview?.type === 'helm' && + state.main.preview.valuesFileId && + state.main.preview.valuesFileId !== helmChart.id) || + isInClusterModeSelector(state) || + !helmChart.filePath.startsWith(fileOrFolderContainedIn) + ) + ); + const isSelected = useAppSelector( + state => state.main.selection?.type === 'file' && state.main.selection.filePath === helmChart.filePath + ); + + const [isHovered, setIsHovered] = useState(false); + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={() => dispatch(selectFile({filePath: helmChart.filePath}))} + > + + + + + + + + {helmChart.name} + + + + + {dirname(helmChart.filePath)} + + + {isHovered && ( +
{ + e.stopPropagation(); + }} + > + + + +
+ )} +
+ ); +}; + +export default HelmChartRenderer; diff --git a/src/components/organisms/ExplorerPane/HelmPane/HelmChartRenderer/index.ts b/src/components/organisms/ExplorerPane/HelmPane/HelmChartRenderer/index.ts new file mode 100644 index 0000000000..7516b3f858 --- /dev/null +++ b/src/components/organisms/ExplorerPane/HelmPane/HelmChartRenderer/index.ts @@ -0,0 +1 @@ +export {default} from './HelmChartRenderer'; diff --git a/src/navsections/HelmChartSectionBlueprint/HelmChartContextMenu.tsx b/src/components/organisms/ExplorerPane/HelmPane/HelmContextMenu.tsx similarity index 70% rename from src/navsections/HelmChartSectionBlueprint/HelmChartContextMenu.tsx rename to src/components/organisms/ExplorerPane/HelmPane/HelmContextMenu.tsx index 6f529c3c4b..4f4b1fbd17 100644 --- a/src/navsections/HelmChartSectionBlueprint/HelmChartContextMenu.tsx +++ b/src/components/organisms/ExplorerPane/HelmPane/HelmContextMenu.tsx @@ -1,4 +1,4 @@ -import {useCallback, useMemo} from 'react'; +import {useMemo} from 'react'; import {Modal} from 'antd'; @@ -12,26 +12,22 @@ import {useAppDispatch, useAppSelector} from '@redux/hooks'; import {selectFile} from '@redux/reducers/main'; import {setLeftMenuSelection} from '@redux/reducers/ui'; import {isInPreviewModeSelectorNew} from '@redux/selectors'; -import {setRootFolder} from '@redux/thunks/setRootFolder'; import {ContextMenu, Dots} from '@atoms'; -import {useCreate, useDuplicate, useFilterByFileOrFolder, useProcessing, useRename} from '@hooks/fileTreeHooks'; +import {useDuplicate, useRename} from '@hooks/fileTreeHooks'; import {deleteFileEntry, dispatchDeleteAlert} from '@utils/files'; import {ROOT_FILE_ENTRY} from '@shared/constants/fileEntry'; import {HelmValuesFile} from '@shared/models/helm'; -import {ItemCustomComponentProps} from '@shared/models/navigator'; import {Colors} from '@shared/styles/colors'; import {showItemInFolder} from '@shared/utils/shell'; -const StyledActionsMenuIconContainer = styled.span<{isSelected: boolean}>` - cursor: pointer; - padding: 8px; - display: flex; - align-items: center; -`; +type IProps = { + id: string; + isSelected: boolean; +}; // TODO: temporary solution for renaming value file const DEFAULT_HELM_VALUE: HelmValuesFile = { @@ -42,34 +38,26 @@ const DEFAULT_HELM_VALUE: HelmValuesFile = { values: [], }; -const HelmChartContextMenu: React.FC = props => { - const {itemInstance} = props; +const HelmContextMenu: React.FC = props => { + const {id, isSelected} = props; const dispatch = useAppDispatch(); - const rootFolderPath = useAppSelector(state => state.main.fileMap[ROOT_FILE_ENTRY].filePath); + const fileOrFolderContainedInFilter = useAppSelector(state => state.main.resourceFilter.fileOrFolderContainedIn); const helmChartMap = useAppSelector(state => state.main.helmChartMap); const helmTemplatesMap = useAppSelector(state => state.main.helmTemplatesMap); const helmValuesMap = useAppSelector(state => state.main.helmValuesMap); - const isInPreviewMode = useAppSelector(isInPreviewModeSelectorNew); const isInClusterMode = useAppSelector(isInClusterModeSelector); + const isInPreviewMode = useAppSelector(isInPreviewModeSelectorNew); const osPlatform = useAppSelector(state => state.config.osPlatform); + const rootFolderPath = useAppSelector(state => state.main.fileMap[ROOT_FILE_ENTRY].filePath); - const {onCreateResource} = useCreate(); const {onDuplicate} = useDuplicate(); - const {onFilterByFileOrFolder} = useFilterByFileOrFolder(); const {onRename} = useRename(); - const refreshFolder = useCallback(() => setRootFolder(rootFolderPath), [rootFolderPath]); - const {onExcludeFromProcessing} = useProcessing(refreshFolder); - const helmItem = useMemo( - () => - helmValuesMap[itemInstance.id] || - helmChartMap[itemInstance.id] || - helmTemplatesMap[itemInstance.id] || - DEFAULT_HELM_VALUE, - [helmChartMap, helmTemplatesMap, helmValuesMap, itemInstance.id] + () => helmValuesMap[id] || helmChartMap[id] || helmTemplatesMap[id] || DEFAULT_HELM_VALUE, + [helmChartMap, helmTemplatesMap, helmValuesMap, id] ); const fileEntry = useAppSelector(state => state.main.fileMap[helmItem.filePath]); @@ -83,18 +71,8 @@ const HelmChartContextMenu: React.FC = props => { () => (osPlatform === 'win32' ? path.win32.dirname(absolutePath) : path.dirname(absolutePath)), [absolutePath, osPlatform] ); - - const isRoot = useMemo(() => helmItem.filePath === ROOT_FILE_ENTRY, [helmItem.filePath]); const platformFileManagerName = useMemo(() => (osPlatform === 'darwin' ? 'Finder' : 'Explorer'), [osPlatform]); - const target = useMemo( - () => (isRoot ? ROOT_FILE_ENTRY : helmItem.filePath.replace(path.sep, '')), - [helmItem.filePath, isRoot] - ); - const isFiltered = useMemo( - () => !helmItem.filePath.startsWith(fileOrFolderContainedInFilter || ''), - [fileOrFolderContainedInFilter, helmItem.filePath] - ); const menuItems = useMemo( () => [ { @@ -115,9 +93,7 @@ const HelmChartContextMenu: React.FC = props => { key: 'create_resource', label: 'Add Resource', disabled: true, - onClick: () => { - onCreateResource({targetFile: target}); - }, + onClick: () => {}, }, {key: 'divider-2', type: 'divider'}, { @@ -127,21 +103,13 @@ const HelmChartContextMenu: React.FC = props => { ? 'Remove from filter' : 'Filter on this file', disabled: true, - onClick: () => { - if (isRoot || (fileOrFolderContainedInFilter && helmItem.filePath === fileOrFolderContainedInFilter)) { - onFilterByFileOrFolder(undefined); - } else { - onFilterByFileOrFolder(helmItem.filePath); - } - }, + onClick: () => {}, }, { key: 'add_to_files_exclude', label: 'Add to Files: Exclude', disabled: true, - onClick: () => { - onExcludeFromProcessing(helmItem.filePath); - }, + onClick: () => {}, }, {key: 'divider-3', type: 'divider'}, { @@ -207,29 +175,29 @@ const HelmChartContextMenu: React.FC = props => { helmItem, isInPreviewMode, isInClusterMode, - isRoot, - onCreateResource, onDuplicate, - onExcludeFromProcessing, - onFilterByFileOrFolder, onRename, platformFileManagerName, - target, fileEntry, ] ); - if (isFiltered) { - return null; - } - return ( - - - + + + ); }; -export default HelmChartContextMenu; +export default HelmContextMenu; + +// Styled Components + +const ActionsMenuIconContainer = styled.span<{isSelected: boolean}>` + cursor: pointer; + padding: 8px; + display: flex; + align-items: center; +`; diff --git a/src/components/organisms/ExplorerPane/HelmPane/HelmList.tsx b/src/components/organisms/ExplorerPane/HelmPane/HelmList.tsx new file mode 100644 index 0000000000..2497b7da30 --- /dev/null +++ b/src/components/organisms/ExplorerPane/HelmPane/HelmList.tsx @@ -0,0 +1,101 @@ +import {useRef} from 'react'; + +import {size} from 'lodash'; +import styled from 'styled-components'; + +import {useAppSelector} from '@redux/hooks'; +import {helmChartListSelector} from '@redux/selectors/helmSelectors'; + +import {Colors} from '@shared/styles/colors'; +import {elementScroll, useVirtualizer} from '@tanstack/react-virtual'; + +import HelmChartRenderer from './HelmChartRenderer'; +import HelmValueRenderer from './HelmValueRenderer'; +import {useScroll} from './useScroll'; + +const ROW_HEIGHT = 26; + +const HelmList: React.FC = () => { + const list = useAppSelector(helmChartListSelector); + const ref = useRef(null); + + const rowVirtualizer = useVirtualizer({ + count: list.length, + estimateSize: () => ROW_HEIGHT, + getScrollElement: () => ref.current, + scrollToFn: elementScroll, + }); + + useScroll({ + list, + scrollTo: index => + rowVirtualizer.scrollToIndex(index, { + align: 'center', + behavior: 'smooth', + }), + }); + + if (!size(list)) { + return No Helm Charts found in the current project.; + } + + return ( + +
+ {rowVirtualizer.getVirtualItems().map(virtualItem => { + const node = list[virtualItem.index]; + + if (!node) { + return null; + } + + return ( + + {node.type === 'helm-chart' ? ( + + ) : node.type === 'helm-value' ? ( + + ) : null} + + ); + })} +
+
+ ); +}; + +export default HelmList; + +// Styled Components + +const EmptyText = styled.div` + padding: 16px; + color: ${Colors.grey8}; +`; + +const ListContainer = styled.ul` + height: 100%; + overflow-y: auto; + padding: 0px; + margin-top: 15px; +`; + +const VirtualItem = styled.div` + position: absolute; + top: 0; + left: 0; + width: 100%; + overflow: hidden; +`; diff --git a/src/components/organisms/ExplorerPane/HelmPane/HelmPane.tsx b/src/components/organisms/ExplorerPane/HelmPane/HelmPane.tsx index 8a254fec43..345d7ff350 100644 --- a/src/components/organisms/ExplorerPane/HelmPane/HelmPane.tsx +++ b/src/components/organisms/ExplorerPane/HelmPane/HelmPane.tsx @@ -1,4 +1,4 @@ -import {memo} from 'react'; +import {memo, useMemo} from 'react'; import {CollapsePanelProps} from 'antd'; @@ -7,24 +7,24 @@ import {size} from 'lodash'; import {isInClusterModeSelector} from '@redux/appConfig'; import {useAppSelector} from '@redux/hooks'; -import {SectionRenderer} from '@molecules'; - -import {SectionBlueprintList} from '@atoms'; - -import RootHelmChartsSectionBlueprint from '@src/navsections/HelmChartSectionBlueprint'; - import {TitleBar, TitleBarCount} from '@monokle/components'; import {InjectedPanelProps} from '@shared/models/explorer'; import AccordionPanel from '../AccordionPanel'; import {AccordionTitleBarContainer} from '../AccordionPanel/AccordionTitleBarContainer'; +import HelmList from './HelmList'; const HelmPane: React.FC = props => { - const {isActive, panelKey, width} = props; + const {isActive, panelKey} = props; const helmChartMap = useAppSelector(state => state.main.helmChartMap); const isInClusterMode = useAppSelector(isInClusterModeSelector); + const count = useMemo( + () => size(Object.values(helmChartMap).filter(chart => !chart.name.includes('Unnamed Chart:'))), + [helmChartMap] + ); + return ( = props => { header={ } + actions={} /> } showArrow={false} key={panelKey as CollapsePanelProps['key']} > - - - + ); }; diff --git a/src/navsections/HelmChartSectionBlueprint/HelmChartQuickAction.tsx b/src/components/organisms/ExplorerPane/HelmPane/HelmValueRenderer/HelmValueQuickAction.tsx similarity index 72% rename from src/navsections/HelmChartSectionBlueprint/HelmChartQuickAction.tsx rename to src/components/organisms/ExplorerPane/HelmPane/HelmValueRenderer/HelmValueQuickAction.tsx index 0c12e36847..17b3792c60 100644 --- a/src/navsections/HelmChartSectionBlueprint/HelmChartQuickAction.tsx +++ b/src/components/organisms/ExplorerPane/HelmPane/HelmValueRenderer/HelmValueQuickAction.tsx @@ -1,6 +1,7 @@ -import React, {useCallback} from 'react'; +import {useCallback} from 'react'; import {useHotkeys} from 'react-hotkeys-hook'; +import styled from 'styled-components'; import invariant from 'tiny-invariant'; import {ExitHelmPreviewTooltip, HelmPreviewTooltip, ReloadHelmPreviewTooltip} from '@constants/tooltips'; @@ -15,16 +16,18 @@ import { } from '@redux/selectors'; import {restartPreview, startPreview, stopPreview} from '@redux/services/preview'; -import {QuickActionCompare, QuickActionPreview} from '@components/molecules'; +import {QuickActionCompare, QuickActionPreview} from '@molecules'; import {hotkeys} from '@shared/constants/hotkeys'; import {ResourceSet} from '@shared/models/compare'; -import {ItemCustomComponentProps} from '@shared/models/navigator'; import {RootState} from '@shared/models/rootState'; import {isDefined} from '@shared/utils/filter'; import {defineHotkey} from '@shared/utils/hotkey'; -import * as S from './HelmChartQuickAction.styled'; +type IProps = { + id: string; + isSelected: boolean; +}; const selectQuickActionData = (state: RootState, itemId: string) => { const previewedHelmValuesFile = previewedValuesFileSelector(state); @@ -56,32 +59,31 @@ const selectQuickActionData = (state: RootState, itemId: string) => { return {isFiltered, thisValuesFile, isAnyPreviewing, isThisPreviewing, isThisSelected, previewingResourceSet}; }; -const QuickAction = (props: ItemCustomComponentProps) => { - const {itemInstance} = props; +const HelmValueQuickAction: React.FC = props => { + const {id, isSelected} = props; + const dispatch = useAppDispatch(); const {isFiltered, thisValuesFile, isAnyPreviewing, isThisPreviewing, isThisSelected, previewingResourceSet} = - useAppSelector(state => selectQuickActionData(state, itemInstance.id)); - - const helmValuesFile = useAppSelector(state => state.main.helmValuesMap[itemInstance.id]); + useAppSelector(state => selectQuickActionData(state, id)); const selectAndPreviewHelmValuesFile = useCallback(() => { if (!isThisSelected) { - dispatch(selectHelmValuesFile({valuesFileId: itemInstance.id})); + dispatch(selectHelmValuesFile({valuesFileId: id})); } if (!isThisPreviewing) { - startPreview({type: 'helm', valuesFileId: itemInstance.id, chartId: helmValuesFile.helmChartId}, dispatch); + startPreview({type: 'helm', valuesFileId: id, chartId: thisValuesFile.helmChartId}, dispatch); } else { stopPreview(dispatch); } - }, [isThisSelected, isThisPreviewing, itemInstance.id, helmValuesFile.helmChartId, dispatch]); + }, [isThisSelected, isThisPreviewing, dispatch, id, thisValuesFile.helmChartId]); const reloadPreview = useCallback(() => { if (!isThisSelected) { - dispatch(selectHelmValuesFile({valuesFileId: itemInstance.id})); + dispatch(selectHelmValuesFile({valuesFileId: id})); } - restartPreview({type: 'helm', valuesFileId: itemInstance.id, chartId: helmValuesFile.helmChartId}, dispatch); - }, [isThisSelected, itemInstance.id, helmValuesFile.helmChartId, dispatch]); + restartPreview({type: 'helm', valuesFileId: id, chartId: thisValuesFile.helmChartId}, dispatch); + }, [isThisSelected, id, thisValuesFile.helmChartId, dispatch]); useHotkeys(defineHotkey(hotkeys.RELOAD_PREVIEW.key), () => { reloadPreview(); @@ -92,10 +94,10 @@ const QuickAction = (props: ItemCustomComponentProps) => { } return ( - + {isAnyPreviewing && !isThisPreviewing && ( { )} { selectAndPreview={selectAndPreviewHelmValuesFile} reloadPreview={reloadPreview} /> - + ); }; -export default QuickAction; +export default HelmValueQuickAction; + +// Styled Components + +const Container = styled.div` + display: flex; + align-items: center; +`; diff --git a/src/components/organisms/ExplorerPane/HelmPane/HelmValueRenderer/HelmValueRenderer.styled.tsx b/src/components/organisms/ExplorerPane/HelmPane/HelmValueRenderer/HelmValueRenderer.styled.tsx new file mode 100644 index 0000000000..71ca5f9710 --- /dev/null +++ b/src/components/organisms/ExplorerPane/HelmPane/HelmValueRenderer/HelmValueRenderer.styled.tsx @@ -0,0 +1,75 @@ +import styled from 'styled-components'; + +import {Colors} from '@shared/styles/colors'; + +type ItemContainerProps = { + isDisabled: boolean; + isSelected: boolean; + isHovered: boolean; +}; + +type ItemNameProps = { + isDisabled: boolean; + isSelected: boolean; +}; + +export const ItemContainer = styled.span` + display: flex; + align-items: center; + width: 100%; + user-select: none; + > { + min-width: 0; + } + padding-left: 45px; + padding-right: 8px; + margin-bottom: 2px; + cursor: pointer; + + ${props => { + if (props.isDisabled) { + return; + } + + if (props.isSelected) { + if (props.isHovered) { + return `background: ${Colors.selectionColorHover};`; + } + return `background: ${Colors.selectionColor};`; + } + if (props.isHovered) { + return `background: ${Colors.blackPearl};`; + } + }}; + ${props => !props.isHovered && 'padding-right: 46px;'} +`; + +export const ItemName = styled.div` + padding: 2px 0; + font-size: 12px; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + ${props => { + if (props.isSelected) { + return `font-weight: 700;`; + } + }}; + ${props => { + if (props.isDisabled) { + return `color: ${Colors.grey7};`; + } + + if (props.isSelected) { + return `color: ${Colors.blackPure};`; + } + + return `color: ${Colors.blue10};`; + }}; +`; + +export const QuickActionContainer = styled.span``; + +export const ContextMenuContainer = styled.span``; diff --git a/src/components/organisms/ExplorerPane/HelmPane/HelmValueRenderer/HelmValueRenderer.tsx b/src/components/organisms/ExplorerPane/HelmPane/HelmValueRenderer/HelmValueRenderer.tsx new file mode 100644 index 0000000000..bf2c77111f --- /dev/null +++ b/src/components/organisms/ExplorerPane/HelmPane/HelmValueRenderer/HelmValueRenderer.tsx @@ -0,0 +1,71 @@ +import {useState} from 'react'; + +import {isInClusterModeSelector} from '@redux/appConfig'; +import {useAppDispatch, useAppSelector} from '@redux/hooks'; +import {selectHelmValuesFile} from '@redux/reducers/main'; + +import HelmContextMenu from '../HelmContextMenu'; +import HelmValueQuickAction from './HelmValueQuickAction'; +import * as S from './HelmValueRenderer.styled'; + +type IProps = { + id: string; +}; + +const HelmValueRenderer: React.FC = props => { + const {id} = props; + + const dispatch = useAppDispatch(); + const fileOrFolderContainedIn = useAppSelector(state => state.main.resourceFilter.fileOrFolderContainedIn || ''); + const helmValue = useAppSelector(state => state.main.helmValuesMap[id]); + const isDisabled = useAppSelector(state => + Boolean( + (state.main.preview?.type === 'helm' && + state.main.preview.valuesFileId && + state.main.preview.valuesFileId !== helmValue.id) || + isInClusterModeSelector(state) || + !helmValue.filePath.startsWith(fileOrFolderContainedIn) + ) + ); + const isSelected = useAppSelector( + state => state.main.selection?.type === 'helm.values.file' && state.main.selection.valuesFileId === helmValue.id + ); + + const [isHovered, setIsHovered] = useState(false); + + if (!helmValue) return null; + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={() => dispatch(selectHelmValuesFile({valuesFileId: id}))} + > + + {helmValue.name} + + + {isHovered && ( +
{ + e.stopPropagation(); + }} + > + + + + + + + +
+ )} +
+ ); +}; + +export default HelmValueRenderer; diff --git a/src/components/organisms/ExplorerPane/HelmPane/HelmValueRenderer/index.ts b/src/components/organisms/ExplorerPane/HelmPane/HelmValueRenderer/index.ts new file mode 100644 index 0000000000..dce24d2340 --- /dev/null +++ b/src/components/organisms/ExplorerPane/HelmPane/HelmValueRenderer/index.ts @@ -0,0 +1 @@ +export {default} from './HelmValueRenderer'; diff --git a/src/components/organisms/ExplorerPane/HelmPane/useScroll.ts b/src/components/organisms/ExplorerPane/HelmPane/useScroll.ts new file mode 100644 index 0000000000..025a2f0b17 --- /dev/null +++ b/src/components/organisms/ExplorerPane/HelmPane/useScroll.ts @@ -0,0 +1,56 @@ +import {useLayoutEffect, useMemo, useRef} from 'react'; +import {usePrevious} from 'react-use'; + +import fastDeepEqual from 'fast-deep-equal'; + +import {useRefSelector, useSelectorWithRef} from '@utils/hooks'; + +import {HelmListNode} from '@shared/models/helm'; + +type ScrollType = { + list: HelmListNode[]; + scrollTo: (index: number) => void; +}; + +export function useScroll({scrollTo, list}: ScrollType) { + const [selection, selectionRef] = useSelectorWithRef(state => state.main.selection); + const previousSelection = usePrevious(selection); + const changed = useMemo(() => !fastDeepEqual(selection, previousSelection), [selection, previousSelection]); + const highlightsRef = useRefSelector(state => state.main.highlights); + const listRef = useRef(list); + listRef.current = list; + const scrollToRef = useRef(scrollTo); + scrollToRef.current = scrollTo; + + useLayoutEffect(() => { + if (!selectionRef.current || !changed) { + return; + } + + let helmChartFilePathToScrollTo: string | undefined; + let helmValueIdToScrollTo: string | undefined; + + // helm chart + if (selectionRef.current.type === 'file') { + helmChartFilePathToScrollTo = selectionRef.current.filePath; + } + + // helm value + if (selectionRef.current.type === 'helm.values.file') { + helmValueIdToScrollTo = selectionRef.current.valuesFileId; + } + + let index: number = -1; + + if (helmChartFilePathToScrollTo) { + index = listRef.current.findIndex( + item => item.type === 'helm-chart' && item.filePath === helmChartFilePathToScrollTo + ); + } else if (helmValueIdToScrollTo) { + index = listRef.current.findIndex(item => item.type === 'helm-value' && item.id === helmValueIdToScrollTo); + } + + if (index === -1) return; + scrollToRef.current(index); + }, [changed, highlightsRef, selectionRef]); +} diff --git a/src/components/organisms/ExplorerPane/ImagesPane/ImagesList.tsx b/src/components/organisms/ExplorerPane/ImagesPane/ImagesList.tsx index 3a0f8ddb9a..c747e74c11 100644 --- a/src/components/organisms/ExplorerPane/ImagesPane/ImagesList.tsx +++ b/src/components/organisms/ExplorerPane/ImagesPane/ImagesList.tsx @@ -83,7 +83,8 @@ const EmptyText = styled.div` const ListContainer = styled.ul` height: 100%; overflow-y: auto; - padding: 0 0 12px; + padding: 0px; + margin-top: 15px; `; const VirtualItem = styled.div` diff --git a/src/components/organisms/ExplorerPane/KustomizePane/KustomizeList.tsx b/src/components/organisms/ExplorerPane/KustomizePane/KustomizeList.tsx index b71903e119..39c602cb03 100644 --- a/src/components/organisms/ExplorerPane/KustomizePane/KustomizeList.tsx +++ b/src/components/organisms/ExplorerPane/KustomizePane/KustomizeList.tsx @@ -108,7 +108,8 @@ const EmptyText = styled.div` const ListContainer = styled.ul` height: 100%; overflow-y: auto; - padding: 10px 0 0px; + padding: 0px; + margin-top: 15px; `; const VirtualItem = styled.div` diff --git a/src/components/organisms/ExplorerPane/KustomizePane/KustomizeRenderer/KustomizeContextMenu.tsx b/src/components/organisms/ExplorerPane/KustomizePane/KustomizeRenderer/KustomizeContextMenu.tsx index 40089f25fe..b377b4c966 100644 --- a/src/components/organisms/ExplorerPane/KustomizePane/KustomizeRenderer/KustomizeContextMenu.tsx +++ b/src/components/organisms/ExplorerPane/KustomizePane/KustomizeRenderer/KustomizeContextMenu.tsx @@ -4,7 +4,7 @@ import {Modal} from 'antd'; import {ExclamationCircleOutlined} from '@ant-design/icons'; -import {basename, dirname, sep} from 'path'; +import {basename, dirname} from 'path'; import styled from 'styled-components'; import {useAppDispatch, useAppSelector} from '@redux/hooks'; @@ -18,7 +18,7 @@ import {setRootFolder} from '@redux/thunks/setRootFolder'; import {ContextMenu, Dots} from '@atoms'; -import {useCreate, useDuplicate, useFilterByFileOrFolder, useProcessing, useRename} from '@hooks/fileTreeHooks'; +import {useDuplicate, useProcessing, useRename} from '@hooks/fileTreeHooks'; import {deleteFileEntry, dispatchDeleteAlert} from '@utils/files'; import {useRefSelector} from '@utils/hooks'; @@ -54,21 +54,16 @@ const KustomizeContextMenu: React.FC = props => { ); const osPlatform = useAppSelector(state => state.config.osPlatform); - const {onCreateResource} = useCreate(); const {onDuplicate} = useDuplicate(); - const {onFilterByFileOrFolder} = useFilterByFileOrFolder(); + const {onRename} = useRename(); const absolutePath = useMemo( () => (resource?.origin.filePath ? getAbsoluteFilePath(resource?.origin.filePath, fileMapRef.current) : undefined), [resource?.origin.filePath, fileMapRef] ); - const isRoot = useMemo(() => resource?.origin.filePath === ROOT_FILE_ENTRY, [resource?.origin.filePath]); + const platformFileManagerName = useMemo(() => (osPlatform === 'darwin' ? 'Finder' : 'Explorer'), [osPlatform]); - const targetFile = useMemo( - () => (isRoot ? ROOT_FILE_ENTRY : resource?.origin.filePath.replace(sep, '')), - [isRoot, resource?.origin.filePath] - ); const isPassingFilter = useMemo( () => (resource ? isResourcePassingFilter(resource, filters) : false), [filters, resource] @@ -102,9 +97,7 @@ const KustomizeContextMenu: React.FC = props => { key: 'create_resource', label: 'Add Resource', disabled: true, - onClick: () => { - onCreateResource({targetFile}); - }, + onClick: () => {}, }, {key: 'divider-2', type: 'divider'}, { @@ -114,16 +107,7 @@ const KustomizeContextMenu: React.FC = props => { ? 'Remove from filter' : 'Filter on this file', disabled: true, - onClick: () => { - if ( - isRoot || - (fileOrFolderContainedInFilter && resourceRef.current?.origin.filePath === fileOrFolderContainedInFilter) - ) { - onFilterByFileOrFolder(undefined); - } else { - onFilterByFileOrFolder(resourceRef.current?.origin.filePath); - } - }, + onClick: () => {}, }, { key: 'add_to_files_exclude', @@ -206,15 +190,11 @@ const KustomizeContextMenu: React.FC = props => { [ fileEntry, isInPreviewMode, - isRoot, absolutePath, fileOrFolderContainedInFilter, platformFileManagerName, - targetFile, resourceRef, dispatch, - onCreateResource, - onFilterByFileOrFolder, onDuplicate, onRename, onExcludeFromProcessing, diff --git a/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationAdd.tsx b/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationAdd.tsx new file mode 100644 index 0000000000..b2514efb98 --- /dev/null +++ b/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationAdd.tsx @@ -0,0 +1,39 @@ +import {Button, Tooltip} from 'antd'; + +import {PlusOutlined} from '@ant-design/icons'; + +import styled from 'styled-components'; + +import {TOOLTIP_DELAY} from '@constants/constants'; +import {NewPreviewConfigurationTooltip} from '@constants/tooltips'; + +import {useAppDispatch} from '@redux/hooks'; +import {openPreviewConfigurationEditor} from '@redux/reducers/main'; + +const PreviewConfigurationAdd: React.FC = () => { + const dispatch = useAppDispatch(); + + return ( + + } + onClick={() => { + dispatch(openPreviewConfigurationEditor({})); + }} + > + Create preview configuration + + + ); +}; + +export default PreviewConfigurationAdd; + +// Styled Components + +const AddButton = styled(Button)` + border-radius: 4px; + font-size: 13px; +`; diff --git a/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationList.tsx b/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationList.tsx new file mode 100644 index 0000000000..f03bfa92de --- /dev/null +++ b/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationList.tsx @@ -0,0 +1,86 @@ +import {useRef} from 'react'; + +import {size} from 'lodash'; +import styled from 'styled-components'; + +import {useAppSelector} from '@redux/hooks'; +import {previewConfigurationListSelector} from '@redux/selectors/helmSelectors'; + +import {Colors} from '@shared/styles/colors'; +import {elementScroll, useVirtualizer} from '@tanstack/react-virtual'; + +import PreviewConfigurationRenderer from './PreviewConfigurationRenderer'; + +const ROW_HEIGHT = 26; + +const PreviewConfigurationList: React.FC = () => { + const list = useAppSelector(previewConfigurationListSelector); + const ref = useRef(null); + + const rowVirtualizer = useVirtualizer({ + count: list.length, + estimateSize: () => ROW_HEIGHT, + getScrollElement: () => ref.current, + scrollToFn: elementScroll, + }); + + if (!size(list)) { + return No Preview Configurations found in the current project.; + } + + return ( + +
+ {rowVirtualizer.getVirtualItems().map(virtualItem => { + const node = list[virtualItem.index]; + + if (!node) { + return null; + } + + return ( + + {node.type === 'preview-configuration' ? : null} + + ); + })} +
+
+ ); +}; + +export default PreviewConfigurationList; + +// Styled Components + +const EmptyText = styled.div` + padding: 0px 14px 0px 16px; + color: ${Colors.grey8}; +`; + +const ListContainer = styled.ul` + height: 100%; + overflow-y: auto; + padding: 0px; + margin-top: 15px; +`; + +const VirtualItem = styled.div` + position: absolute; + top: 0; + left: 0; + width: 100%; + overflow: hidden; +`; diff --git a/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationPane.tsx b/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationPane.tsx new file mode 100644 index 0000000000..12a312a94b --- /dev/null +++ b/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationPane.tsx @@ -0,0 +1,67 @@ +import {memo, useMemo} from 'react'; + +import {CollapsePanelProps} from 'antd'; + +import {size} from 'lodash'; +import styled from 'styled-components'; + +import {isInClusterModeSelector} from '@redux/appConfig'; +import {useAppSelector} from '@redux/hooks'; + +import {TitleBar, TitleBarCount} from '@monokle/components'; +import {InjectedPanelProps} from '@shared/models/explorer'; +import {isDefined} from '@shared/utils/filter'; + +import AccordionPanel from '../AccordionPanel'; +import {AccordionTitleBarContainer} from '../AccordionPanel/AccordionTitleBarContainer'; +import PreviewConfigurationAdd from './PreviewConfigurationAdd'; +import PreviewConfigurationList from './PreviewConfigurationList'; + +const PreviewConfigurationPane: React.FC = props => { + const {isActive, panelKey} = props; + + const isInClusterMode = useAppSelector(isInClusterModeSelector); + const previewConfigurationMap = useAppSelector(state => state.config.projectConfig?.helm?.previewConfigurationMap); + + const count = useMemo( + () => + isDefined(previewConfigurationMap) + ? size(Object.values(previewConfigurationMap).filter(previewConfiguration => isDefined(previewConfiguration))) + : 0, + [previewConfigurationMap] + ); + + return ( + + } + /> + + } + showArrow={false} + key={panelKey as CollapsePanelProps['key']} + > + + + + + + + ); +}; + +export default memo(PreviewConfigurationPane); + +// Styled Components + +const PreviewConfigurationTopContainer = styled.div` + width: 100%; + padding: 16px 14px 16px 16px; +`; diff --git a/src/navsections/HelmChartSectionBlueprint/PreviewConfigurationQuickAction.tsx b/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationRenderer/PreviewConfigurationQuickAction.tsx similarity index 74% rename from src/navsections/HelmChartSectionBlueprint/PreviewConfigurationQuickAction.tsx rename to src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationRenderer/PreviewConfigurationQuickAction.tsx index b845dfb54b..272e8ca374 100644 --- a/src/navsections/HelmChartSectionBlueprint/PreviewConfigurationQuickAction.tsx +++ b/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationRenderer/PreviewConfigurationQuickAction.tsx @@ -13,53 +13,49 @@ import {openPreviewConfigurationEditor} from '@redux/reducers/main'; import {startPreview} from '@redux/services/preview'; import {deletePreviewConfiguration} from '@redux/thunks/previewConfiguration'; -import {ItemCustomComponentProps} from '@shared/models/navigator'; import {Colors} from '@shared/styles/colors'; -const StyledButton = styled.span<{isItemSelected: boolean}>` - margin-right: 15px; - font-size: 12px; - font-weight: 500; - cursor: pointer; - color: ${props => (props.isItemSelected ? Colors.blackPure : Colors.blue6)}; -`; - -const PreviewConfigurationQuickAction: React.FC = props => { - const {itemInstance} = props; - const dispatch = useAppDispatch(); +type IProps = { + id: string; + isSelected: boolean; +}; - const previewConfiguration = useAppSelector( - state => state.config.projectConfig?.helm?.previewConfigurationMap?.[itemInstance.id] - ); +const PreviewConfigurationQuickAction: React.FC = props => { + const {id, isSelected} = props; + const dispatch = useAppDispatch(); + const previewConfiguration = useAppSelector(state => state.config.projectConfig?.helm?.previewConfigurationMap?.[id]); const helmChart = useAppSelector(state => previewConfiguration ? Object.values(state.main.helmChartMap).find(h => h.filePath === previewConfiguration.helmChartFilePath) : undefined ); - const onClickRun = useCallback(() => { + const onClickDelete = useCallback(() => { if (!previewConfiguration) { return; } - startPreview({type: 'helm-config', configId: previewConfiguration.id}, dispatch); - }, [dispatch, previewConfiguration]); + + dispatch(deletePreviewConfiguration(previewConfiguration.id)); + }, [previewConfiguration, dispatch]); const onClickEdit = useCallback(() => { if (!previewConfiguration || !helmChart) { return; } + dispatch( openPreviewConfigurationEditor({helmChartId: helmChart.id, previewConfigurationId: previewConfiguration.id}) ); }, [previewConfiguration, helmChart, dispatch]); - const onClickDelete = useCallback(() => { + const onClickRun = useCallback(() => { if (!previewConfiguration) { return; } - dispatch(deletePreviewConfiguration(previewConfiguration.id)); - }, [previewConfiguration, dispatch]); + + startPreview({type: 'helm-config', configId: previewConfiguration.id}, dispatch); + }, [dispatch, previewConfiguration]); if (!previewConfiguration || !helmChart) { return null; @@ -67,19 +63,31 @@ const PreviewConfigurationQuickAction: React.FC = prop return ( <> - onClickRun()}> + + + + onClickDelete()}> - + ); }; export default PreviewConfigurationQuickAction; + +// Styled Components + +const Button = styled.span<{isItemSelected: boolean}>` + margin-right: 15px; + font-size: 12px; + font-weight: 500; + cursor: pointer; + color: ${props => (props.isItemSelected ? Colors.blackPure : Colors.blue6)}; +`; diff --git a/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationRenderer/PreviewConfigurationRenderer.styled.tsx b/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationRenderer/PreviewConfigurationRenderer.styled.tsx new file mode 100644 index 0000000000..c6e1ba95b7 --- /dev/null +++ b/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationRenderer/PreviewConfigurationRenderer.styled.tsx @@ -0,0 +1,80 @@ +import styled from 'styled-components'; + +import {Colors} from '@shared/styles/colors'; + +type ItemContainerProps = { + isDisabled: boolean; + isSelected: boolean; + isHovered: boolean; +}; + +type ItemNameProps = { + isDisabled: boolean; + isSelected: boolean; +}; + +export const ItemContainer = styled.span` + display: flex; + align-items: center; + width: 100%; + user-select: none; + > { + min-width: 0; + } + padding-left: 20px; + padding-right: 8px; + margin-bottom: 2px; + cursor: pointer; + + ${props => { + if (props.isDisabled) { + return; + } + + if (props.isSelected) { + if (props.isHovered) { + return `background: ${Colors.selectionColorHover};`; + } + return `background: ${Colors.selectionColor};`; + } + + if (props.isHovered) { + return `background: ${Colors.blackPearl};`; + } + }}; + ${props => !props.isHovered && 'padding-right: 46px;'} +`; + +export const ItemName = styled.div` + padding: 2px 0; + font-size: 12px; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + ${props => { + if (props.isSelected) { + return `font-weight: 700;`; + } + }}; + ${props => { + if (props.isDisabled) { + return `color: ${Colors.grey7};`; + } + + if (props.isSelected) { + return `color: ${Colors.blackPure};`; + } + + return `color: ${Colors.blue10};`; + }}; +`; + +export const PrefixContainer = styled.span` + display: flex; + align-items: center; + margin-right: 7px; +`; + +export const QuickActionContainer = styled.span``; diff --git a/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationRenderer/PreviewConfigurationRenderer.tsx b/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationRenderer/PreviewConfigurationRenderer.tsx new file mode 100644 index 0000000000..b6311c235b --- /dev/null +++ b/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationRenderer/PreviewConfigurationRenderer.tsx @@ -0,0 +1,69 @@ +import {useState} from 'react'; + +import {EyeOutlined} from '@ant-design/icons'; + +import {isInClusterModeSelector} from '@redux/appConfig'; +import {useAppDispatch, useAppSelector} from '@redux/hooks'; +import {selectPreviewConfiguration} from '@redux/reducers/main'; + +import {Colors} from '@shared/styles/colors'; + +import PreviewConfigurationQuickAction from './PreviewConfigurationQuickAction'; +import * as S from './PreviewConfigurationRenderer.styled'; + +type IProps = { + id: string; +}; + +const PreviewConfigurationRenderer: React.FC = props => { + const {id} = props; + + const dispatch = useAppDispatch(); + const previewConfiguration = useAppSelector(state => state.config.projectConfig?.helm?.previewConfigurationMap?.[id]); + const isDisabled = useAppSelector(isInClusterModeSelector); + const isSelected = useAppSelector( + state => + state.main.selection?.type === 'preview.configuration' && + state.main.selection.previewConfigurationId === previewConfiguration?.id + ); + + const [isHovered, setIsHovered] = useState(false); + + if (!previewConfiguration) { + return null; + } + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={() => dispatch(selectPreviewConfiguration({previewConfigurationId: id}))} + > + + + + + + {previewConfiguration.name} + + + {isHovered && ( +
{ + e.stopPropagation(); + }} + > + + + +
+ )} +
+ ); +}; + +export default PreviewConfigurationRenderer; diff --git a/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationRenderer/index.ts b/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationRenderer/index.ts new file mode 100644 index 0000000000..881806e46b --- /dev/null +++ b/src/components/organisms/ExplorerPane/PreviewConfigurationPane/PreviewConfigurationRenderer/index.ts @@ -0,0 +1 @@ +export {default} from './PreviewConfigurationRenderer'; diff --git a/src/components/organisms/ExplorerPane/PreviewConfigurationPane/index.ts b/src/components/organisms/ExplorerPane/PreviewConfigurationPane/index.ts new file mode 100644 index 0000000000..1b1d4b30e4 --- /dev/null +++ b/src/components/organisms/ExplorerPane/PreviewConfigurationPane/index.ts @@ -0,0 +1 @@ +export {default} from './PreviewConfigurationPane'; diff --git a/src/components/organisms/GitPane/BottomActions.tsx b/src/components/organisms/GitPane/BottomActions.tsx index 6448b22895..3c4622d532 100644 --- a/src/components/organisms/GitPane/BottomActions.tsx +++ b/src/components/organisms/GitPane/BottomActions.tsx @@ -1,10 +1,10 @@ import {useCallback, useMemo, useState} from 'react'; -import {Modal, Tooltip} from 'antd'; +import {Tooltip} from 'antd'; import {ArrowDownOutlined, ArrowUpOutlined, DownOutlined} from '@ant-design/icons'; -import {GIT_ERROR_MODAL_DESCRIPTION, TOOLTIP_DELAY} from '@constants/constants'; +import {TOOLTIP_DELAY} from '@constants/constants'; import {GitCommitDisabledTooltip, GitCommitEnabledTooltip} from '@constants/tooltips'; import {addGitBranch, setGitLoading} from '@redux/git'; @@ -12,7 +12,7 @@ import {useAppDispatch, useAppSelector} from '@redux/hooks'; import {setAlert} from '@redux/reducers/alert'; import {promiseFromIpcRenderer} from '@utils/promises'; -import {addDefaultCommandTerminal} from '@utils/terminal'; +import {showGitErrorModal} from '@utils/terminal'; import {AlertEnum} from '@shared/models/alert'; @@ -21,14 +21,11 @@ import CommitModal from './CommitModal'; const BottomActions: React.FC = () => { const dispatch = useAppDispatch(); - const bottomSelection = useAppSelector(state => state.ui.leftMenu.bottomSelection); const changedFiles = useAppSelector(state => state.git.changedFiles); const currentBranch = useAppSelector(state => state.git.repo?.currentBranch); - const defaultShell = useAppSelector(state => state.terminal.settings.defaultShell); const gitLoading = useAppSelector(state => state.git.loading); const gitRepo = useAppSelector(state => state.git.repo); const selectedProjectRootFolder = useAppSelector(state => state.config.selectedProjectRootFolder); - const terminalsMap = useAppSelector(state => state.terminal.terminalsMap); const [showCommitModal, setShowCommitModal] = useState(false); @@ -59,35 +56,23 @@ const BottomActions: React.FC = () => { return; } - const result = await promiseFromIpcRenderer('git.pushChanges', 'git.pushChanges.result', { - localPath: selectedProjectRootFolder, - branchName: currentBranch || 'main', - }); + let result: any; - if (result.error) { - Modal.warning({ - title: `${type === 'pull' ? 'Pull' : 'Push'} failed`, - content:
{GIT_ERROR_MODAL_DESCRIPTION}
, - zIndex: 100000, - onCancel: () => { - addDefaultCommandTerminal( - terminalsMap, - `git ${type} origin ${gitRepo.currentBranch}`, - defaultShell, - bottomSelection, - dispatch - ); - }, - onOk: () => { - addDefaultCommandTerminal( - terminalsMap, - `git ${type} origin ${gitRepo.currentBranch}`, - defaultShell, - bottomSelection, - dispatch - ); - }, + if (type === 'pull') { + result = await promiseFromIpcRenderer('git.pullChanges', 'git.pushChanges.result', selectedProjectRootFolder); + } else if (type === 'push') { + result = await promiseFromIpcRenderer('git.pushChanges', 'git.pushChanges.result', { + localPath: selectedProjectRootFolder, + branchName: currentBranch || 'main', }); + } + + if (result.error) { + showGitErrorModal( + `${type === 'pull' ? 'Pull' : 'Push'} failed`, + `git ${type} origin ${gitRepo.currentBranch}`, + dispatch + ); } else { dispatch( setAlert({ @@ -100,7 +85,7 @@ const BottomActions: React.FC = () => { return result.error; }, - [bottomSelection, currentBranch, defaultShell, dispatch, gitRepo, selectedProjectRootFolder, terminalsMap] + [currentBranch, dispatch, gitRepo, selectedProjectRootFolder] ); const publishHandler = useCallback(async () => { @@ -110,10 +95,17 @@ const BottomActions: React.FC = () => { dispatch(setGitLoading(true)); - await promiseFromIpcRenderer('git.publishLocalBranch', 'git.publishLocalBranch.result', { + const result = await promiseFromIpcRenderer('git.publishLocalBranch', 'git.publishLocalBranch.result', { localPath: selectedProjectRootFolder, branchName: currentBranch || 'main', }); + + if (result.error) { + showGitErrorModal('Publishing local branch failed', `git push -u origin ${currentBranch || 'main'}`, dispatch); + setGitLoading(false); + return; + } + dispatch(addGitBranch(`origin/${currentBranch}`)); dispatch(setAlert({title: 'Branch published successfully', message: '', type: AlertEnum.Success})); diff --git a/src/components/organisms/GitPane/CommitModal.tsx b/src/components/organisms/GitPane/CommitModal.tsx index 8bcd206fb6..db55f2fa61 100644 --- a/src/components/organisms/GitPane/CommitModal.tsx +++ b/src/components/organisms/GitPane/CommitModal.tsx @@ -8,6 +8,7 @@ import {useAppDispatch, useAppSelector} from '@redux/hooks'; import {setAlert} from '@redux/reducers/alert'; import {promiseFromIpcRenderer} from '@utils/promises'; +import {showGitErrorModal} from '@utils/terminal'; import {AlertEnum} from '@shared/models/alert'; @@ -39,12 +40,16 @@ const CommitModal: React.FC = props => { setLoading(true); form.validateFields().then(async values => { - await promiseFromIpcRenderer('git.commitChanges', 'git.commitChanges.result', { + const result = await promiseFromIpcRenderer('git.commitChanges', 'git.commitChanges.result', { localPath: selectedProjectRootFolder, message: values.message, }); - dispatch(setAlert({title: 'Committed successfully', message: '', type: AlertEnum.Success})); + if (result.error) { + showGitErrorModal('Commit failed', `git commit -m "${values.message}"`, dispatch); + } else { + dispatch(setAlert({title: 'Committed successfully', message: '', type: AlertEnum.Success})); + } form.resetFields(); }); diff --git a/src/components/organisms/GitPane/FileList.tsx b/src/components/organisms/GitPane/FileList.tsx index 44f4d95b57..41f30121e3 100644 --- a/src/components/organisms/GitPane/FileList.tsx +++ b/src/components/organisms/GitPane/FileList.tsx @@ -18,6 +18,7 @@ import {Dots} from '@components/atoms'; import {createFileWithContent} from '@utils/files'; import {promiseFromIpcRenderer} from '@utils/promises'; +import {showGitErrorModal} from '@utils/terminal'; import {AlertEnum} from '@shared/models/alert'; import {GitChangedFile} from '@shared/models/git'; @@ -71,11 +72,21 @@ const FileList: React.FC = props => { promiseFromIpcRenderer('git.stageChangedFiles', 'git.stageChangedFiles.result', { localPath: selectedProjectRootFolder, filePaths: [item.fullGitPath], + }).then(result => { + if (result.error) { + showGitErrorModal('Stage changes failed!', `git add ${[item.fullGitPath].join(' ')}`, dispatch); + setGitLoading(false); + } }); } else { promiseFromIpcRenderer('git.unstageFiles', 'git.unstageFiles.result', { localPath: selectedProjectRootFolder, filePaths: [item.fullGitPath], + }).then(result => { + if (result.error) { + showGitErrorModal('Unstage changes failed!', `git reset ${[item.fullGitPath].join(' ')}`, dispatch); + setGitLoading(false); + } }); } }, diff --git a/src/components/organisms/GitPane/RemoteInput.tsx b/src/components/organisms/GitPane/RemoteInput.tsx index 893fa13789..5a411dbf6a 100644 --- a/src/components/organisms/GitPane/RemoteInput.tsx +++ b/src/components/organisms/GitPane/RemoteInput.tsx @@ -5,6 +5,8 @@ import {useForm} from 'antd/es/form/Form'; import {CheckOutlined} from '@ant-design/icons'; +import log from 'loglevel'; + import {updateRemoteRepo} from '@redux/git'; import {useAppDispatch, useAppSelector} from '@redux/hooks'; @@ -24,11 +26,16 @@ const RemoteInput: React.FC = () => { setLoading(true); form.validateFields().then(async values => { - await promiseFromIpcRenderer('git.setRemote', 'git.setRemote.result', { + const result = await promiseFromIpcRenderer('git.setRemote', 'git.setRemote.result', { localPath: selectedProjectRootFolder, remoteURL: values.remoteURL, }); + if (result.error) { + log.error('Error setting remote', result.error); + return; + } + dispatch(updateRemoteRepo({exists: true, authRequired: false})); form.resetFields(); diff --git a/src/components/organisms/PageHeader/PageHeader.tsx b/src/components/organisms/PageHeader/PageHeader.tsx index aaf9891734..1dd47b01bb 100644 --- a/src/components/organisms/PageHeader/PageHeader.tsx +++ b/src/components/organisms/PageHeader/PageHeader.tsx @@ -32,6 +32,7 @@ import BranchSelect from '@components/molecules/BranchSelect'; import {useHelpMenuItems} from '@hooks/menuItemsHooks'; import {promiseFromIpcRenderer} from '@utils/promises'; +import {showGitErrorModal} from '@utils/terminal'; import MonokleKubeshopLogo from '@assets/NewMonokleLogoDark.svg'; @@ -120,13 +121,19 @@ const PageHeader = () => { trackEvent('git/initialize'); setIsInitializingGitRepo(true); - await promiseFromIpcRenderer('git.initGitRepo', 'git.initGitRepo.result', projectRootFolder); + const result = await promiseFromIpcRenderer('git.initGitRepo', 'git.initGitRepo.result', projectRootFolder); + + if (result.error) { + showGitErrorModal('Failed to initialize git repo'); + setIsInitializingGitRepo(false); + return; + } monitorGitFolder(projectRootFolder, store); - promiseFromIpcRenderer('git.getGitRepoInfo', 'git.getGitRepoInfo.result', projectRootFolder).then(result => { - dispatch(setRepo(result)); - dispatch(setCurrentBranch(result.currentBranch)); + promiseFromIpcRenderer('git.getGitRepoInfo', 'git.getGitRepoInfo.result', projectRootFolder).then(repo => { + dispatch(setRepo(repo)); + dispatch(setCurrentBranch(repo.currentBranch)); setIsInitializingGitRepo(false); dispatch(updateProjectsGitRepo([{path: projectRootFolder, isGitRepo: true}])); }); diff --git a/src/components/organisms/PreviewConfigurationEditor/HelmChartSelect.tsx b/src/components/organisms/PreviewConfigurationEditor/HelmChartSelect.tsx new file mode 100644 index 0000000000..e1a9404029 --- /dev/null +++ b/src/components/organisms/PreviewConfigurationEditor/HelmChartSelect.tsx @@ -0,0 +1,51 @@ +import {Select, Tooltip} from 'antd'; + +import {orderBy} from 'lodash'; +import styled from 'styled-components'; + +import {TOOLTIP_DELAY} from '@constants/constants'; + +import {useAppDispatch, useAppSelector} from '@redux/hooks'; +import {setPreviewConfigurationEditorHelmChartId} from '@redux/reducers/main'; + +import {Colors} from '@shared/styles/colors'; + +const HelmChartSelect: React.FC = () => { + const dispatch = useAppDispatch(); + const helmChartMap = useAppSelector(state => state.main.helmChartMap); + const editorHelmChartId = useAppSelector(state => state.main.prevConfEditor.helmChartId); + const previewConfigurationId = useAppSelector(state => state.main.prevConfEditor.previewConfigurationId); + + return ( + <> + Helm Chart + + + + ); +}; + +export default HelmChartSelect; + +// Styled Components + +const FilePath = styled.span` + color: ${Colors.grey7}; + margin-left: 6px; +`; diff --git a/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx b/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx index 8332791390..e18a903eed 100644 --- a/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx +++ b/src/components/organisms/PreviewConfigurationEditor/PreviewConfigurationEditor.tsx @@ -15,9 +15,12 @@ import {startPreview} from '@redux/services/preview'; import {KeyValueInput} from '@atoms'; +import {useRefSelector} from '@utils/hooks'; + import {HelmPreviewConfiguration, PreviewConfigValuesFileItem} from '@shared/models/config'; import {HelmValuesFile} from '@shared/models/helm'; +import HelmChartSelect from './HelmChartSelect'; import ValuesFilesList from './ValuesFilesList'; import * as S from './styled'; @@ -38,13 +41,17 @@ const PreviewConfigurationEditor = () => { } return previewConfigurationMap[previewConfigurationId]; }); + const helmChart = useAppSelector(state => { const helmChartId = state.main.prevConfEditor.helmChartId; + if (!helmChartId) { return undefined; } + return state.main.helmChartMap[helmChartId]; }); + const projectConfigRef = useRefSelector(state => state.config.projectConfig); const [name, setName] = useState(() => previewConfiguration?.name || ''); const [showNameError, setShowNameError] = useState(false); @@ -139,10 +146,12 @@ const PreviewConfigurationEditor = () => { if (!helmChart) { return; } + if (!name.trim().length) { setShowNameError(true); return; } + const input: HelmPreviewConfiguration = { id: previewConfiguration ? previewConfiguration.id : uuidv4(), name, @@ -151,12 +160,14 @@ const PreviewConfigurationEditor = () => { options: helmOptions, valuesFileItemMap, }; + const updatedPreviewConfigurationMap = JSON.parse(JSON.stringify(previewConfigurationMap)); updatedPreviewConfigurationMap[input.id] = input; dispatch( updateProjectConfig({ config: { + ...projectConfigRef.current, helm: { previewConfigurationMap: updatedPreviewConfigurationMap, }, @@ -182,50 +193,52 @@ const PreviewConfigurationEditor = () => { ] ); - if (!helmChart) { - return

Something went wrong, could not find the helm chart.

; - } - return (
- - Name your configuration: - setName(e.target.value)} placeholder="Enter the configuration name" /> - {showNameError && You must enter a name for this Preview Configuration.} - - - Select which values files to use: - Drag and drop to specify order - setValuesFileItemMap(itemMap)} /> - - - Select which helm command to use for this Preview: - - - - - - - - - - + + + {helmChart && ( + <> + + Name your configuration: + setName(e.target.value)} placeholder="Enter the configuration name" /> + {showNameError && You must enter a name for this Preview Configuration.} + + + Select which values files to use: + Drag and drop to specify order + setValuesFileItemMap(itemMap)} /> + + + Select which helm command to use for this Preview: + + + + + + + + + + + + )}
); }; diff --git a/src/navsections/HelmChartSectionBlueprint/CollapseSectionPrefix.tsx b/src/navsections/HelmChartSectionBlueprint/CollapseSectionPrefix.tsx deleted file mode 100644 index 9a1c9b5ffc..0000000000 --- a/src/navsections/HelmChartSectionBlueprint/CollapseSectionPrefix.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import {useMemo} from 'react'; - -import {MinusSquareOutlined, PlusSquareOutlined} from '@ant-design/icons'; - -import {useAppSelector} from '@redux/hooks'; - -import {SectionCustomComponentProps} from '@shared/models/navigator'; -import {Colors} from '@shared/styles/colors'; - -const CollapseSectionPrefix = (props: SectionCustomComponentProps) => { - const {sectionInstance, onClick} = props; - - const isSectionCollapsed = useAppSelector(state => state.navigator.collapsedSectionIds.includes(sectionInstance.id)); - - const iconProps = useMemo(() => { - return { - style: { - color: sectionInstance.isSelected && isSectionCollapsed ? Colors.blackPure : Colors.grey9, - marginLeft: 4, - marginRight: 8, - }, - fontSize: 10, - cursor: 'pointer', - onClick, - }; - }, [sectionInstance.isSelected, isSectionCollapsed, onClick]); - - if (isSectionCollapsed) { - return ; - } - - return ; -}; - -export default CollapseSectionPrefix; diff --git a/src/navsections/HelmChartSectionBlueprint/HelmChartQuickAction.styled.tsx b/src/navsections/HelmChartSectionBlueprint/HelmChartQuickAction.styled.tsx deleted file mode 100644 index ce629bbe11..0000000000 --- a/src/navsections/HelmChartSectionBlueprint/HelmChartQuickAction.styled.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import styled from 'styled-components'; - -export const QuickActionsContainer = styled.div` - display: flex; - align-items: center; -`; diff --git a/src/navsections/HelmChartSectionBlueprint/HelmChartSectionBlueprint.ts b/src/navsections/HelmChartSectionBlueprint/HelmChartSectionBlueprint.ts deleted file mode 100644 index 0620e7885a..0000000000 --- a/src/navsections/HelmChartSectionBlueprint/HelmChartSectionBlueprint.ts +++ /dev/null @@ -1,389 +0,0 @@ -import {HELM_CHART_SECTION_NAME} from '@constants/constants'; - -import {isInClusterModeSelector} from '@redux/appConfig'; -import {selectFile, selectHelmValuesFile, selectPreviewConfiguration} from '@redux/reducers/main'; - -import {ROOT_FILE_ENTRY} from '@shared/constants/fileEntry'; -import {FileMapType, HelmTemplatesMapType, HelmValuesMapType} from '@shared/models/appState'; -import {HelmPreviewConfiguration} from '@shared/models/config'; -import {HelmChart, HelmTemplate, HelmValuesFile} from '@shared/models/helm'; -import {SectionBlueprint} from '@shared/models/navigator'; -import {Colors} from '@shared/styles/colors'; -import {isDefined} from '@shared/utils/filter'; - -import CollapseSectionPrefix from './CollapseSectionPrefix'; -import HelmChartContextMenu from './HelmChartContextMenu'; -import HelmChartQuickAction from './HelmChartQuickAction'; -import ItemPrefix from './ItemPrefix'; -import PreviewConfigurationNameSuffix from './PreviewConfigurationNameSuffix'; -import PreviewConfigurationQuickAction from './PreviewConfigurationQuickAction'; - -type TemplatesScopeType = { - fileMap: FileMapType; - fileOrFolderContainedIn: string; - helmTemplatesMap: HelmTemplatesMapType; - isFolderOpen: boolean; - selectedFile: string | undefined; - [currentHelmChart: string]: HelmChart | unknown; -}; - -export type ValuesFilesScopeType = { - helmValuesMap: HelmValuesMapType; - previewValuesFileId: string | undefined; - fileOrFolderContainedIn: string; - isInClusterMode: boolean; - isFolderOpen: boolean; - selectedHelmValuesId: string | undefined; - [currentHelmChart: string]: HelmChart | unknown; -}; - -type HelmChartScopeType = { - fileOrFolderContainedIn: string; - selectedFile: string | undefined; - previewValuesFileId: string | undefined; - isInClusterMode: boolean; - [currentHelmChart: string]: HelmChart | unknown; -}; - -type PreviewConfigurationScopeType = { - previewConfigurationMap: Record | undefined; - selectedPreviewConfigurationId: string | undefined; - [currentHelmChart: string]: HelmChart | unknown; -}; - -export function makeHelmChartSectionBlueprint(helmChart: HelmChart) { - const previewConfigurationsSectionBlueprint: SectionBlueprint< - HelmPreviewConfiguration, - PreviewConfigurationScopeType - > = { - name: 'Preview Configurations', - id: `${helmChart.id}-configurations`, - containerElementId: 'helm-section-container', - rootSectionId: HELM_CHART_SECTION_NAME, - getScope: state => { - return { - previewConfigurationMap: state.config.projectConfig?.helm?.previewConfigurationMap, - selectedPreviewConfigurationId: - state.main.selection?.type === 'preview.configuration' - ? state.main.selection.previewConfigurationId - : undefined, - [helmChart.id]: state.main.helmChartMap[helmChart.id], - }; - }, - builder: { - getRawItems: scope => { - const currentHelmChart = scope[helmChart.id] as HelmChart | undefined; - if (!currentHelmChart) { - return []; - } - return scope.previewConfigurationMap - ? Object.values(scope.previewConfigurationMap).filter((pc): pc is HelmPreviewConfiguration => - Boolean(pc && pc.helmChartFilePath === currentHelmChart.filePath) - ) - : []; - }, - isInitialized: () => true, - isVisible: () => true, - }, - itemBlueprint: { - getInstanceId: rawItem => rawItem.id, - getName: rawItem => rawItem.name, - builder: { - getMeta: () => { - return { - itemPrefixStyle: { - paddingLeft: 10, - }, - itemPrefixIcon: 'preview', - }; - }, - - isSelected: (item, scope) => { - return item.id === scope.selectedPreviewConfigurationId; - }, - }, - instanceHandler: { - onClick: (itemInstance, dispatch) => { - dispatch(selectPreviewConfiguration({previewConfigurationId: itemInstance.id})); - }, - }, - customization: { - quickAction: { - component: PreviewConfigurationQuickAction, - options: {isVisibleOnHover: true}, - }, - prefix: { - component: ItemPrefix, - }, - lastItemMarginBottom: 0, - }, - }, - customization: { - counterDisplayMode: 'items', - indentation: 10, - nameWeight: 400, - nameSize: 14, - nameColor: Colors.grey9, - nameHorizontalPadding: 0, - namePrefix: { - component: CollapseSectionPrefix, - }, - nameSuffix: { - component: PreviewConfigurationNameSuffix, - options: { - isVisibleOnHover: true, - }, - }, - }, - }; - - const templateFilesSectionBlueprint: SectionBlueprint = { - name: 'Templates', - id: `${helmChart.id}-templates`, - containerElementId: 'helm-section-container', - rootSectionId: HELM_CHART_SECTION_NAME, - getScope: state => { - return { - fileMap: state.main.fileMap, - fileOrFolderContainedIn: state.main.resourceFilter.fileOrFolderContainedIn || '', - helmTemplatesMap: state.main.helmTemplatesMap, - isFolderOpen: Boolean(state.main.fileMap[ROOT_FILE_ENTRY]), - selectedFile: state.main.selection?.type === 'file' ? state.main.selection.filePath : undefined, - [helmChart.id]: state.main.helmChartMap[helmChart.id], - }; - }, - builder: { - getRawItems: scope => { - const currentHelmChart = scope[helmChart.id] as HelmChart | undefined; - if (!currentHelmChart) { - return []; - } - return currentHelmChart.templateIds.map(id => scope.helmTemplatesMap[id]).filter(isDefined); - }, - isInitialized: scope => { - return scope.isFolderOpen; - }, - isEmpty: (scope, rawItems) => { - return scope.isFolderOpen && rawItems.length === 0; - }, - }, - customization: { - counterDisplayMode: 'items', - indentation: 10, - nameWeight: 400, - nameSize: 14, - nameColor: Colors.grey9, - nameHorizontalPadding: 0, - namePrefix: { - component: CollapseSectionPrefix, - }, - }, - itemBlueprint: { - getName: rawItem => rawItem.name, - getInstanceId: rawItem => rawItem.id, - builder: { - isDisabled: (rawItem, scope) => !rawItem.filePath.startsWith(scope.fileOrFolderContainedIn), - isSelected: (rawItem, scope) => { - return rawItem.filePath === scope.selectedFile; - }, - getMeta: template => { - return { - filePath: template.filePath, - itemPrefixStyle: { - paddingLeft: 10, - }, - itemPrefixIcon: 'helm', - }; - }, - }, - instanceHandler: { - onClick: (itemInstance, dispatch) => { - const filePath: string | undefined = itemInstance.meta?.filePath; - - if (!filePath) { - return; - } - - dispatch(selectFile({filePath})); - }, - }, - customization: { - contextMenu: {component: HelmChartContextMenu, options: {isVisibleOnHover: true}}, - prefix: { - component: ItemPrefix, - }, - lastItemMarginBottom: 0, - }, - }, - }; - - const valuesFilesSectionBlueprint: SectionBlueprint = { - name: 'Values Files', - id: `${helmChart.id}-values`, - containerElementId: 'helm-section-container', - rootSectionId: HELM_CHART_SECTION_NAME, - getScope: state => { - return { - helmValuesMap: state.main.helmValuesMap, - isInClusterMode: isInClusterModeSelector(state), - fileOrFolderContainedIn: state.main.resourceFilter.fileOrFolderContainedIn || '', - previewValuesFileId: state.main.preview?.type === 'helm' ? state.main.preview.valuesFileId : undefined, - isFolderOpen: Boolean(state.main.fileMap[ROOT_FILE_ENTRY]), - selectedHelmValuesId: - state.main.selection?.type === 'helm.values.file' ? state.main.selection.valuesFileId : undefined, - [helmChart.id]: state.main.helmChartMap[helmChart.id], - }; - }, - builder: { - getRawItems: scope => { - const currentHelmChart = scope[helmChart.id] as HelmChart | undefined; - if (!currentHelmChart) { - return []; - } - return currentHelmChart.valueFileIds - .map(id => scope.helmValuesMap[id]) - .filter((v): v is HelmValuesFile => v !== undefined); - }, - isInitialized: scope => { - return scope.isFolderOpen; - }, - isEmpty: (scope, rawItems) => { - return scope.isFolderOpen && rawItems.length === 0; - }, - }, - customization: { - counterDisplayMode: 'items', - indentation: 10, - nameWeight: 400, - nameSize: 14, - nameColor: Colors.grey9, - nameHorizontalPadding: 0, - namePrefix: { - component: CollapseSectionPrefix, - }, - }, - itemBlueprint: { - getName: rawItem => rawItem.name, - getInstanceId: rawItem => rawItem.id, - builder: { - isSelected: (rawItem, scope) => { - return rawItem.id === scope.selectedHelmValuesId; - }, - isDisabled: (rawItem, scope) => - Boolean( - (scope.previewValuesFileId && scope.previewValuesFileId !== rawItem.id) || - scope.isInClusterMode || - !rawItem.filePath.startsWith(scope.fileOrFolderContainedIn) - ), - getMeta: () => { - return { - itemPrefixStyle: { - paddingLeft: 10, - }, - itemPrefixIcon: 'helm', - }; - }, - }, - instanceHandler: { - onClick: (itemInstance, dispatch) => { - dispatch(selectHelmValuesFile({valuesFileId: itemInstance.id})); - }, - }, - customization: { - contextMenu: {component: HelmChartContextMenu, options: {isVisibleOnHover: true}}, - quickAction: { - component: HelmChartQuickAction, - options: {isVisibleOnHover: true}, - }, - prefix: { - component: ItemPrefix, - }, - lastItemMarginBottom: 0, - }, - }, - }; - - const helmChartSectionBlueprint: SectionBlueprint = { - id: helmChart.id, - name: helmChart.name, - containerElementId: 'helm-sections-container', - rootSectionId: HELM_CHART_SECTION_NAME, - childSectionIds: [ - valuesFilesSectionBlueprint.id, - templateFilesSectionBlueprint.id, - previewConfigurationsSectionBlueprint.id, - ], - getScope: state => { - return { - isInClusterMode: isInClusterModeSelector(state), - fileOrFolderContainedIn: state.main.resourceFilter.fileOrFolderContainedIn || '', - previewValuesFileId: state.main.preview?.type === 'helm' ? state.main.preview.valuesFileId : undefined, - selectedFile: state.main.selection?.type === 'file' ? state.main.selection.filePath : undefined, - [helmChart.id]: state.main.helmChartMap[helmChart.id], - }; - }, - builder: { - transformName: (_, scope) => { - const currentHelmChart = scope[helmChart.id] as HelmChart | undefined; - if (!currentHelmChart) { - return 'Unnamed'; - } - return currentHelmChart.name; - }, - getRawItems: scope => { - const currentHelmChart = scope[helmChart.id] as HelmChart | undefined; - if (!currentHelmChart) { - return []; - } - return [currentHelmChart]; - }, - }, - itemBlueprint: { - getName: () => 'Chart.yaml', - getInstanceId: chart => chart.id, - builder: { - getMeta: chart => ({ - filePath: chart.filePath, - itemPrefixIcon: 'helm', - }), - isSelected: (chart, scope) => { - return scope.selectedFile === chart.filePath; - }, - isDisabled: (rawItem, scope) => - Boolean( - (scope.previewValuesFileId && scope.previewValuesFileId !== rawItem.id) || - scope.isInClusterMode || - !rawItem.filePath.startsWith(scope.fileOrFolderContainedIn) - ), - }, - instanceHandler: { - onClick: (instance, dispatch) => { - const filePath: string | undefined = instance.meta?.filePath; - if (!filePath) { - return; - } - dispatch(selectFile({filePath})); - }, - }, - customization: { - contextMenu: {component: HelmChartContextMenu, options: {isVisibleOnHover: true}}, - prefix: {component: ItemPrefix}, - lastItemMarginBottom: 0, - }, - }, - customization: { - counterDisplayMode: 'none', - indentation: 0, - nameWeight: 600, - nameSize: 14, - nameColor: Colors.grey9, - }, - }; - - return { - helmChartSectionBlueprint, - valuesFilesSectionBlueprint, - previewConfigurationsSectionBlueprint, - templateFilesSectionBlueprint, - }; -} diff --git a/src/navsections/HelmChartSectionBlueprint/ItemPrefix.tsx b/src/navsections/HelmChartSectionBlueprint/ItemPrefix.tsx deleted file mode 100644 index e78fc5e20e..0000000000 --- a/src/navsections/HelmChartSectionBlueprint/ItemPrefix.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import {useMemo} from 'react'; - -import {EyeOutlined, FileOutlined} from '@ant-design/icons'; - -import {Icon} from '@monokle/components'; -import {ItemCustomComponentProps} from '@shared/models/navigator'; -import {Colors} from '@shared/styles/colors'; - -const ItemPrefix = (props: ItemCustomComponentProps) => { - const {itemInstance} = props; - - const {itemPrefixStyle, itemPrefixIcon} = itemInstance.meta; - - const style = useMemo(() => { - const baseStyle = { - marginLeft: 4, - marginRight: 8, - color: itemInstance.isSelected ? Colors.blackPure : Colors.whitePure, - }; - if (itemPrefixStyle) { - return { - ...baseStyle, - ...itemPrefixStyle, - }; - } - return baseStyle; - }, [itemPrefixStyle, itemInstance.isSelected]); - - if (itemPrefixIcon === 'file') { - return ; - } - - if (itemPrefixIcon === 'helm') { - return ; - } - - return ; -}; - -export default ItemPrefix; diff --git a/src/navsections/HelmChartSectionBlueprint/PreviewConfigurationNameSuffix.tsx b/src/navsections/HelmChartSectionBlueprint/PreviewConfigurationNameSuffix.tsx deleted file mode 100644 index d04e050abc..0000000000 --- a/src/navsections/HelmChartSectionBlueprint/PreviewConfigurationNameSuffix.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; - -import {Button, Tooltip} from 'antd'; - -import {PlusOutlined} from '@ant-design/icons'; - -import styled from 'styled-components'; - -import {TOOLTIP_DELAY} from '@constants/constants'; -import {NewPreviewConfigurationTooltip} from '@constants/tooltips'; - -import {useAppDispatch, useAppSelector} from '@redux/hooks'; -import {openPreviewConfigurationEditor} from '@redux/reducers/main'; - -import {SectionCustomComponentProps} from '@shared/models/navigator'; -import {Colors} from '@shared/styles/colors'; - -const SuffixContainer = styled.span` - display: inline-block; -`; - -const ButtonContainer = styled.span` - display: flex; - align-items: center; - padding: 0 4px; - margin-right: 2px; - & .ant-btn-sm { - height: 20px; - width: 20px; - } -`; - -const PreviewConfigurationNameSuffix: React.FC = props => { - const {sectionInstance} = props; - const isSectionCollapsed = useAppSelector(state => state.navigator.collapsedSectionIds.includes(sectionInstance.id)); - - const dispatch = useAppDispatch(); - - const onClick = () => { - dispatch(openPreviewConfigurationEditor({helmChartId: sectionInstance.id.replace('-configurations', '')})); - }; - - return ( - - - -