diff --git a/services/frontend-service/src/images/Delete_Gray.svg b/services/frontend-service/src/images/Delete_Gray.svg new file mode 100644 index 000000000..a82c92a70 --- /dev/null +++ b/services/frontend-service/src/images/Delete_Gray.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/services/frontend-service/src/images/index.tsx b/services/frontend-service/src/images/index.tsx index fa694d296..ebe039f47 100644 --- a/services/frontend-service/src/images/index.tsx +++ b/services/frontend-service/src/images/index.tsx @@ -20,6 +20,7 @@ import { ReactComponent as Environments } from './Environments.svg'; import { ReactComponent as Locks } from './Locks.svg'; import { ReactComponent as LocksWhite } from './Locks_White.svg'; import { ReactComponent as Delete } from './Delete.svg'; +import { ReactComponent as DeleteGray } from './Delete_Gray.svg'; import { ReactComponent as DeleteWhite } from './Delete_White.svg'; import { ReactComponent as HistoryWhite } from './History_White.svg'; import { ReactComponent as ShowBar } from './ShowBar.svg'; @@ -37,6 +38,7 @@ export { LocksWhite, Environments, Delete, + DeleteGray, ShowBar, HideBar, History, diff --git a/services/frontend-service/src/ui/components/SideBar/SideBar.scss b/services/frontend-service/src/ui/components/SideBar/SideBar.scss index af1a1a05f..c93802351 100644 --- a/services/frontend-service/src/ui/components/SideBar/SideBar.scss +++ b/services/frontend-service/src/ui/components/SideBar/SideBar.scss @@ -12,7 +12,7 @@ width: 100%; } - .mdc-drawer-sidebar-header__button { + .mdc-drawer-sidebar-header__button{ position: relative; height: auto; color: white; @@ -21,13 +21,63 @@ } } +.mdc-drawer-sidebar-list{ + margin: 35px 40px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 20px; +} + +.mdc-drawer-sidebar-list-item{ + border: 2px solid #F2F5F7; + border-radius: 10px; + background-color: white; + padding: 20px; // 105px 20px 20px; + width: 100%; + position: relative; + display: flex; + flex-direction: row; +} + + +.mdc-drawer-sidebar-list-item-text{ + flex: 3; + display: flex; + flex-direction: column; + gap: 6px; + color: var(--mdc-theme-on-surface); + font-size: 14px; + line-height: 24px; + align-items: flex-start; +} + +.mdc-drawer-sidebar-list-item-text-name{ + font-weight: bold; +} + +.mdc-drawer-sidebar-list-item-delete-icon{ + flex: 2; + position: absolute; + right: 11px; + top: 11px; + color: #4A4A4A +} + +.mdc-drawer-sidebar-list-item-delete-icon:hover{ + color: #000000; +} + .mdc-drawer-sidebar-content{ + overflow-y: scroll; position: relative; - text-align: center; width: 100%; - background-color: white; + background-color: #FAFBFC; + height: 80vh; } + + .mdc-drawer-sidebar .mdc-sidebar-sidebar-footer .mdc-drawer-sidebar-apply-button{ position: relative; background-color: var(--mdc-theme-primary); @@ -42,6 +92,11 @@ font-weight: bold; } +.mdc-drawer__drawer{ + display: flex; + flex-direction: column; +} + .mdc-drawer-sidebar-container{ height: 100%; transition: $sidebar-transition-duration-large-screen; diff --git a/services/frontend-service/src/ui/components/SideBar/SideBar.test.tsx b/services/frontend-service/src/ui/components/SideBar/SideBar.test.tsx index 59e616e4d..6edab1b33 100644 --- a/services/frontend-service/src/ui/components/SideBar/SideBar.test.tsx +++ b/services/frontend-service/src/ui/components/SideBar/SideBar.test.tsx @@ -18,7 +18,7 @@ import { act, render, renderHook } from '@testing-library/react'; import { TopAppBar } from '../TopAppBar/TopAppBar'; import { MemoryRouter } from 'react-router-dom'; import { BatchAction } from '../../../api/api'; -import { addAction, updateActions, useActions } from '../../utils/store'; +import { addAction, deleteAction, useActions, updateActions } from '../../utils/store'; describe('Show and Hide Sidebar', () => { interface dataT { @@ -67,10 +67,142 @@ describe('Show and Hide Sidebar', () => { }); }); +describe('Sidebar shows list of actions', () => { + interface dataT { + name: string; + actions: BatchAction[]; + expectedNumOfActions: number; + } + + const data: dataT[] = [ + { + name: '2 results', + actions: [ + { action: { $case: 'undeploy', undeploy: { application: 'nmww' } } }, + { action: { $case: 'prepareUndeploy', prepareUndeploy: { application: 'nmww' } } }, + ], + expectedNumOfActions: 2, + }, + { + name: '3 results', + actions: [ + { action: { $case: 'undeploy', undeploy: { application: 'nmww' } } }, + { action: { $case: 'prepareUndeploy', prepareUndeploy: { application: 'nmww' } } }, + { action: { $case: 'undeploy', undeploy: { application: 'auth-service' } } }, + ], + expectedNumOfActions: 3, + }, + { + name: '0 results', + actions: [], + expectedNumOfActions: 0, + }, + ]; + + const getNode = (overrides?: {}): JSX.Element | any => { + // given + const defaultProps: any = { + children: null, + }; + return ( + + {' '} + + ); + }; + const getWrapper = (overrides?: {}) => render(getNode(overrides)); + + describe.each(data)('', (testcase) => { + it(testcase.name, () => { + // given + updateActions(testcase.actions); + // when + const { container } = getWrapper({}); + const result = container.querySelector('.mdc-show-button')! as HTMLElement; + act(() => { + result.click(); + }); + // then + expect(container.getElementsByClassName('mdc-drawer-sidebar-list')[0].children).toHaveLength( + testcase.expectedNumOfActions + ); + }); + }); +}); + +describe('Sidebar test deletebutton', () => { + interface dataT { + name: string; + actions: BatchAction[]; + expectedNumOfActions: number; + } + + const data: dataT[] = [ + { + name: '2 results', + actions: [ + { action: { $case: 'undeploy', undeploy: { application: 'nmww' } } }, + { action: { $case: 'prepareUndeploy', prepareUndeploy: { application: 'nmww' } } }, + ], + expectedNumOfActions: 1, + }, + { + name: '3 results', + actions: [ + { action: { $case: 'undeploy', undeploy: { application: 'nmww' } } }, + { action: { $case: 'prepareUndeploy', prepareUndeploy: { application: 'nmww' } } }, + { action: { $case: 'undeploy', undeploy: { application: 'auth-service' } } }, + ], + expectedNumOfActions: 2, + }, + { + name: '0 results', + actions: [], + expectedNumOfActions: 0, + }, + ]; + + const getNode = (overrides?: {}): JSX.Element | any => { + // given + const defaultProps: any = { + children: null, + }; + return ( + + {' '} + + ); + }; + const getWrapper = (overrides?: {}) => render(getNode(overrides)); + + describe.each(data)('', (testcase) => { + it(testcase.name, () => { + // given + updateActions(testcase.actions); + // when + const { container } = getWrapper({}); + const result = container.querySelector('.mdc-show-button')! as HTMLElement; + act(() => { + result.click(); + }); + const svg = container.getElementsByClassName('mdc-drawer-sidebar-list-item-delete-icon')[0]; + if (svg) { + const button = svg.parentElement; + if (button) button.click(); + } + // then + expect(container.getElementsByClassName('mdc-drawer-sidebar-list')[0].children).toHaveLength( + testcase.expectedNumOfActions + ); + }); + }); +}); + describe('Action Store functionality', () => { interface dataT { name: string; actions: BatchAction[]; + deleteActions?: BatchAction[]; expectedActions: BatchAction[]; } @@ -112,9 +244,10 @@ describe('Action Store functionality', () => { describe.each(dataGetSet)('Test getting actions from the store and setting the store from an array', (testcase) => { it(testcase.name, () => { // given - renderHook(() => updateActions(testcase.actions)); + updateActions(testcase.actions); // when const actions = renderHook(() => useActions()).result.current; + // then expect(actions).toStrictEqual(testcase.expectedActions); }); }); @@ -157,12 +290,71 @@ describe('Action Store functionality', () => { describe.each(dataAdding)('Test adding actions to the store', (testcase) => { it(testcase.name, () => { // given - renderHook(() => updateActions([])); + updateActions([]); testcase.actions.forEach((action) => { - renderHook(() => addAction(action)); + addAction(action); }); // when const actions = renderHook(() => useActions()).result.current; + // then + expect(actions).toStrictEqual(testcase.expectedActions); + }); + }); + + const dataDeleting: dataT[] = [ + { + name: 'delete 1 action - 0 remain', + actions: [{ action: { $case: 'undeploy', undeploy: { application: 'nmww' } } }], + deleteActions: [{ action: { $case: 'undeploy', undeploy: { application: 'nmww' } } }], + expectedActions: [], + }, + { + name: 'delete 1 action (different action type, same app) - 1 remains', + actions: [ + { action: { $case: 'undeploy', undeploy: { application: 'nmww' } } }, + { action: { $case: 'prepareUndeploy', prepareUndeploy: { application: 'nmww' } } }, + ], + deleteActions: [{ action: { $case: 'prepareUndeploy', prepareUndeploy: { application: 'nmww' } } }], + expectedActions: [{ action: { $case: 'undeploy', undeploy: { application: 'nmww' } } }], + }, + { + name: 'delete 1 action (same action type, different app) - 1 remains', + actions: [ + { action: { $case: 'undeploy', undeploy: { application: 'nmww' } } }, + { action: { $case: 'undeploy', undeploy: { application: 'auth-service' } } }, + ], + deleteActions: [{ action: { $case: 'undeploy', undeploy: { application: 'nmww' } } }], + expectedActions: [{ action: { $case: 'undeploy', undeploy: { application: 'auth-service' } } }], + }, + { + name: 'delete 1 action from empty array - 0 remain', + actions: [], + deleteActions: [{ action: { $case: 'undeploy', undeploy: { application: 'auth-service' } } }], + expectedActions: [], + }, + { + name: 'delete 2 actions - 1 remain', + actions: [ + { action: { $case: 'undeploy', undeploy: { application: 'nmww' } } }, + { action: { $case: 'undeploy', undeploy: { application: 'auth-service' } } }, + { action: { $case: 'prepareUndeploy', prepareUndeploy: { application: 'nmww' } } }, + ], + deleteActions: [ + { action: { $case: 'undeploy', undeploy: { application: 'nmww' } } }, + { action: { $case: 'prepareUndeploy', prepareUndeploy: { application: 'nmww' } } }, + ], + expectedActions: [{ action: { $case: 'undeploy', undeploy: { application: 'auth-service' } } }], + }, + ]; + + describe.each(dataDeleting)('Test deleting actions', (testcase) => { + it(testcase.name, () => { + // given + updateActions(testcase.actions); + // when + testcase.deleteActions?.map((action) => deleteAction(action)); + const actions = renderHook(() => useActions()).result.current; + // then expect(actions).toStrictEqual(testcase.expectedActions); }); }); diff --git a/services/frontend-service/src/ui/components/SideBar/SideBar.tsx b/services/frontend-service/src/ui/components/SideBar/SideBar.tsx index 3cd1f977c..7e4997b69 100644 --- a/services/frontend-service/src/ui/components/SideBar/SideBar.tsx +++ b/services/frontend-service/src/ui/components/SideBar/SideBar.tsx @@ -15,11 +15,167 @@ along with kuberpult. If not, see . Copyright 2021 freiheit.com*/ import { Button } from '../button'; -import { HideBarWhite } from '../../../images'; +import { DeleteGray, HideBarWhite } from '../../../images'; +import { BatchAction } from '../../../api/api'; +import { deleteAction, useActions } from '../../utils/store'; +import { useCallback } from 'react'; + +export enum ActionTypes { + Deploy, + PrepareUndeploy, + Undeploy, + DeleteQueue, + CreateEnvironmentLock, + DeleteEnvironmentLock, + CreateApplicationLock, + DeleteApplicationLock, + UNKNOWN, +} + +type ActionDetails = { + type: ActionTypes; + name: string; + summary: string; + dialogTitle: string; + description?: string; + + // action details optional + environment?: string; + application?: string; + lockId?: string; + lockMessage?: string; + version?: number; +}; + +const getActionDetails = ({ action }: BatchAction): ActionDetails => { + switch (action?.$case) { + case 'createEnvironmentLock': + return { + type: ActionTypes.CreateEnvironmentLock, + name: 'Create Env Lock', + dialogTitle: 'Are you sure you want to add this environment lock?', + summary: 'Create new environment lock on ' + action.createEnvironmentLock.environment, + environment: action.createEnvironmentLock.environment, + }; + case 'deleteEnvironmentLock': + return { + type: ActionTypes.DeleteEnvironmentLock, + name: 'Delete Env Lock', + dialogTitle: 'Are you sure you want to delete this environment lock?', + summary: 'Delete environment lock + on ' + action.deleteEnvironmentLock.environment, + environment: action.deleteEnvironmentLock.environment, + lockId: action.deleteEnvironmentLock.lockId, + }; + case 'createEnvironmentApplicationLock': + return { + type: ActionTypes.CreateApplicationLock, + name: 'Create App Lock', + dialogTitle: 'Are you sure you want to add this application lock?', + summary: + 'Lock "' + + action.createEnvironmentApplicationLock.application + + '" on ' + + action.createEnvironmentApplicationLock.environment, + environment: action.createEnvironmentApplicationLock.environment, + application: action.createEnvironmentApplicationLock.application, + }; + case 'deleteEnvironmentApplicationLock': + return { + type: ActionTypes.DeleteApplicationLock, + name: 'Delete App Lock', + dialogTitle: 'Are you sure you want to delete this application lock?', + summary: + 'Unlock "' + + action.deleteEnvironmentApplicationLock.application + + '" on ' + + action.deleteEnvironmentApplicationLock.environment, + environment: action.deleteEnvironmentApplicationLock.environment, + application: action.deleteEnvironmentApplicationLock.application, + lockId: action.deleteEnvironmentApplicationLock.lockId, + }; + case 'deploy': + return { + type: ActionTypes.Deploy, + name: 'Deploy', + dialogTitle: 'Please be aware:', + summary: + 'Deploy version ' + + action.deploy.version + + ' of "' + + action.deploy.application + + '" to ' + + action.deploy.environment, + environment: action.deploy.environment, + application: action.deploy.application, + version: action.deploy.version, + }; + case 'prepareUndeploy': + return { + type: ActionTypes.PrepareUndeploy, + name: 'Prepare Undeploy', + dialogTitle: 'Are you sure you want to start undeploy?', + description: + 'The new version will go through the same cycle as any other versions' + + ' (e.g. development->staging->production). ' + + 'The behavior is similar to any other version that is created normally.', + summary: 'Prepare undeploy version for Application ' + action.prepareUndeploy.application, + application: action.prepareUndeploy.application, + }; + case 'undeploy': + return { + type: ActionTypes.Undeploy, + name: 'Undeploy', + dialogTitle: 'Are you sure you want to undeploy this application?', + description: 'This application will be deleted permanently', + summary: 'Undeploy and delete Application "' + action.undeploy.application + '"', + application: action.undeploy.application, + }; + default: + return { + type: ActionTypes.UNKNOWN, + name: 'invalid', + dialogTitle: 'invalid', + summary: 'invalid', + }; + } +}; + +type SideBarListItemProps = { + children: BatchAction; +}; + +export const SideBarListItem: React.FC<{ children: BatchAction }> = ({ children: action }: SideBarListItemProps) => { + const actionDetails = getActionDetails(action); + const handleDelete = useCallback(() => deleteAction(action), [action]); + return ( + <> +
+
{actionDetails.name}
+
{actionDetails.summary}
+
+
+ +
+ + ); +}; + +export const SideBarList = () => { + const actions = useActions(); + + return ( + <> + {actions.map((action, key) => ( +
+ {action} +
+ ))} + + ); +}; export const SideBar: React.FC<{ className: string; toggleSidebar: () => void }> = (props) => { const { className, toggleSidebar } = props; - return (