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 (