Merge remote-tracking branch 'origin/feature/ui/TRA-4192_workspace_management' into origin/ui/TRA-4204_user_managment

This commit is contained in:
Leon
2022-02-01 16:05:02 +02:00
9 changed files with 283 additions and 136 deletions

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect, useState } from "react";
import logo from '../assets/MizuEntLogo.svg';
import './Header.sass';
import userImg from '../assets/user-circle.svg';
@@ -12,6 +12,7 @@ import {useSetRecoilState} from "recoil";
import entPageAtom, {Page} from "../../recoil/entPage";
import {useNavigate} from "react-router-dom";
import {RouterRoutes} from "../../helpers/routes";
import { SettingsModal } from "../SettingsModal/SettingModal";
const api = Api.getInstance();
@@ -23,6 +24,19 @@ interface EntHeaderProps {
export const EntHeader: React.FC<EntHeaderProps> = ({isFirstLogin, setIsFirstLogin}) => {
const navigate = useNavigate();
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
useEffect(() => {
if(isFirstLogin) {
setIsSettingsModalOpen(true)
}
}, [isFirstLogin])
const onSettingsModalClose = () => {
setIsSettingsModalOpen(false);
setIsFirstLogin(false);
}
return <div className="header">
<div>
<div className="title">
@@ -33,9 +47,11 @@ export const EntHeader: React.FC<EntHeaderProps> = ({isFirstLogin, setIsFirstLog
<img className="headerIcon" alt="settings" src={settingImg} style={{marginRight: 25}} onClick={() => navigate(RouterRoutes.SETTINGS)}/>
<ProfileButton/>
</div>
<SettingsModal isOpen={isSettingsModalOpen} onClose={onSettingsModalClose} isFirstLogin={isFirstLogin}/>
</div>;
}
const ProfileButton = () => {
const setEntPage = useSetRecoilState(entPageAtom);

View File

@@ -49,6 +49,7 @@ export const AddUserModal: FC<AddUserModalProps> = ({isOpen, onCloseModal, userD
useEffect(() => {
(async () => {
try {
<<<<<<< HEAD
// const workspacesList = [
// {
// "id": "f54b18ec-aa15-4b2c-a4d5-8eda17e44c93",
@@ -63,6 +64,21 @@ export const AddUserModal: FC<AddUserModalProps> = ({isOpen, onCloseModal, userD
const list = await api.getWorkspaces()
const workspacesList = list.map((obj) => {return {key:obj.id, value:obj.name,isChecked:false}})
setWorkspaces(workspacesList)
=======
const workspacesList = [
{
"id": "f54b18ec-aa15-4b2c-a4d5-8eda17e44c93",
"name": "sock-shop"
},
{
"id": "c7ad9158-d840-46c0-b5ce-2487c013723f",
"name": "test"
}
].map((obj) => {return {key:obj.id, value:obj.name}})
//await api.getWorkspaces()
setWorkspaces(workspacesList)
>>>>>>> origin/feature/ui/TRA-4192_workspace_management
} catch (e) {
toast.error("Error finding workspaces")
}
@@ -256,8 +272,8 @@ export const AddUserModal: FC<AddUserModalProps> = ({isOpen, onCloseModal, userD
<input className={classes.textField + " search-workspace"} placeholder="Search" value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}/>
</div>
<SelectList valuesListInput={workspaces} tableName={''} multiSelect={false} searchValue={searchValue}
setValues= {workspaceChange} tabelClassName={''}>
<SelectList items={workspaces} tableName={''} multiSelect={false} searchValue={searchValue}
setCheckedValues={workspaceChange} tabelClassName={''} checkedValues={[]} >
</SelectList>
</div>

View File

@@ -4,8 +4,10 @@ import { useCommonStyles } from '../../../helpers/commonStyle';
import ConfirmationModal from '../../UI/Modals/ConfirmationModal';
import SelectList from '../../UI/SelectList';
import './AddWorkspaceModal.sass'
import { toast } from "react-toastify";
export type WorkspaceData = {
id:string;
name:string;
namespaces: string[];
}
@@ -13,34 +15,38 @@ export type WorkspaceData = {
interface AddWorkspaceModalProp {
isOpen : boolean,
onCloseModal: () => void,
workspaceData: WorkspaceData,
workspaceId: string,
onEdit: boolean
}
const api = Api.getInstance();
const AddWorkspaceModal: FC<AddWorkspaceModalProp> = ({isOpen,onCloseModal, workspaceData ={}, onEdit}) => {
const AddWorkspaceModal: FC<AddWorkspaceModalProp> = ({isOpen,onCloseModal, workspaceId, onEdit}) => {
const [workspaceDataModel, setUserData] = useState(workspaceData as WorkspaceData);
const [searchValue, setSearchValue] = useState("");
const classes = useCommonStyles()
const [namespaces, setNamespaces] = useState({});
const [workspaceName, setWorkspaceName] = useState("");
const [checkedNamespacesKeys, setCheckedNamespacesKeys] = useState([]);
const [namespaces, setNamespaces] = useState([]);
const classes = useCommonStyles();
const title = onEdit ? "Edit Workspace" : "Add Workspace";
useEffect(() => {
if(!isOpen) return;
(async () => {
try {
setSearchValue("");
const tapConfig = await api.getTapConfig();
const namespacesObj = {...tapConfig?.tappedNamespaces}
Object.keys(tapConfig?.tappedNamespaces ?? {}).forEach(namespace => {
namespacesObj[namespace] = true;
if(onEdit){
const workspace = await api.getSpecificWorkspace(workspaceId);
setWorkspaceName(workspace.name);
setCheckedNamespacesKeys(workspace.namespaces);
}
setSearchValue("");
const namespaces = await api.getNamespaces();
const namespacesMapped = namespaces.map(namespace => {
return {key: namespace, value: namespace}
})
setNamespaces(namespacesObj);
setNamespaces(tapConfig?.tappedNamespaces);
setNamespaces(namespacesMapped);
} catch (e) {
console.error(e);
} finally {
@@ -48,23 +54,70 @@ const AddWorkspaceModal: FC<AddWorkspaceModalProp> = ({isOpen,onCloseModal, work
})()
}, [isOpen])
const onConfirm = () => {}
const onWorkspaceNameChange = (event) => {
setWorkspaceName(event.target.value);
}
const isFormValid = () : boolean => {
return (workspaceName.length > 0) && (checkedNamespacesKeys.length > 0);
}
const onConfirm = async () => {
try{
const workspaceData = {
name: workspaceName,
namespaces: checkedNamespacesKeys
}
if(onEdit){
await api.editWorkspace(workspaceId, workspaceData);
toast.success("Workspace Succesesfully Updated");
}
else{
await api.createWorkspace(workspaceData);
toast.success("Workspace Succesesfully Created ");
}
resetForm();
onCloseModal();
} catch{
toast.error("Couldn't Creat The Worksapce");
}
}
const onClose = () => {
onCloseModal();
resetForm();
}
const resetForm = () => {
setWorkspaceName("");
setCheckedNamespacesKeys([]);
setNamespaces([]);
}
return (<>
<ConfirmationModal isOpen={isOpen} onClose={onCloseModal} onConfirm={onConfirm} title={title}>
<h3 className='headline'>DETAILS</h3>
<div>
<input type="text" value={workspaceData?.name ?? ""} className={classes.textField + " workspace__name"} placeholder={"Workspace Name"}
onChange={(e) => {}}></input>
</div>
<h3 className='headline'>TAP SETTINGS</h3>
<div className="namespacesSettingsContainer">
<div>
<input className={classes.textField + " searchNamespace"} placeholder="Search" value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}/>
</div>
<SelectList valuesListInput={namespaces} tableName={"Namespaces"} multiSelect={true} searchValue={searchValue} setValues={setUserData} tabelClassName={undefined}></SelectList>
<ConfirmationModal isOpen={isOpen} onClose={onClose} onConfirm={onConfirm} title={title}>
<h3 className='comfirmation-modal__sub-section-header'>DETAILS</h3>
<div className='comfirmation-modal__sub-section'>
<div>
<input type="text" value={workspaceName ?? ""} className={classes.textField + " workspace__name"} placeholder={"Workspace Name"}
onChange={onWorkspaceNameChange}></input>
</div>
</div>
<h3 className='comfirmation-modal__sub-section-header'>TAP SETTINGS</h3>
<div className="namespacesSettingsContainer">
<div>
<input className={classes.textField + " searchNamespace"} placeholder="Search" value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}/>
</div>
<SelectList items={namespaces}
tableName={"Namespaces"}
multiSelect={true}
checkedValues={checkedNamespacesKeys}
searchValue={searchValue}
setCheckedValues={setCheckedNamespacesKeys}
tabelClassName={undefined}>
</SelectList>
</div>
</ConfirmationModal>
</>);
};

View File

@@ -38,7 +38,7 @@ export const InstallPage: React.FC<InstallPageProps> = ({onFirstLogin}) => {
try {
setIsLoading(true);
await api.register(adminUsername, password);
await api.setupAdminUser(adminUsername, password);
if (!await api.isAuthenticationNeeded()) {
setEntPage(Page.Traffic);
onFirstLogin();

View File

@@ -19,7 +19,8 @@ const api = Api.getInstance();
export const SettingsModal: React.FC<SettingsModalProps> = ({isOpen, onClose, isFirstLogin}) => {
const classes = useCommonStyles();
const [namespaces, setNamespaces] = useState({});
const [namespaces, setNamespaces] = useState([]);
const [checkedNamespacesKeys, setCheckedNamespacesKeys] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [searchValue, setSearchValue] = useState("");
@@ -29,16 +30,21 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({isOpen, onClose, is
try {
setSearchValue("");
setIsLoading(true);
const tapConfig = await api.getTapConfig()
if(isFirstLogin) {
const namespacesObj = {...tapConfig?.tappedNamespaces}
Object.keys(tapConfig?.tappedNamespaces ?? {}).forEach(namespace => {
namespacesObj[namespace] = true;
})
setNamespaces(namespacesObj);
} else {
setNamespaces(tapConfig?.tappedNamespaces);
}
// const tapConfig = await api.getTapConfig()
const namespaces = await api.getNamespaces();
const namespacesMapped = namespaces.map(namespace => {
return {key: namespace, value: namespace}
})
setNamespaces(namespacesMapped);
// if(isFirstLogin) {
// const namespacesObj = {...tapConfig?.tappedNamespaces}
// Object.keys(tapConfig?.tappedNamespaces ?? {}).forEach(namespace => {
// namespacesObj[namespace] = true;
// })
// setNamespaces(namespacesObj);
// } else {
// setNamespaces(tapConfig?.tappedNamespaces);
// }
} catch (e) {
console.error(e);
} finally {
@@ -49,7 +55,11 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({isOpen, onClose, is
const updateTappingSettings = async () => {
try {
await api.setTapConfig(namespaces);
const defaultWorkspace = {
name: "default",
namespaces: checkedNamespacesKeys
}
await api.createWorkspace(defaultWorkspace);
onClose();
toast.success("Saved successfully");
} catch (e) {
@@ -78,7 +88,7 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({isOpen, onClose, is
<div style={{padding: 32, paddingBottom: 0}}>
<div className="settingsTitle">Tapping Settings</div>
<div className="settingsSubtitle" style={{marginTop: 20}}>
Please choose from below the namespaces for tapping, traffic for namespaces selected will be displayed
Please choose from below the namespaces for tapping, traffic for namespaces selected will be displayed as default workspace.
</div>
{isLoading ? <div style={{textAlign: "center", padding: 20}}>
<img alt="spinner" src={spinner} style={{height: 35}}/>
@@ -87,7 +97,7 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({isOpen, onClose, is
<div style={{margin: "10px 0"}}>
<input className={classes.textField + " searchNamespace"} placeholder="Search" value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}/></div>
<SelectList valuesListInput={namespaces} tableName={'Namespace'} multiSelect={true} searchValue={searchValue} setValues={setNamespaces} tabelClassName={'namespacesTable'}/>
<SelectList items={namespaces} tableName={'Namespace'} multiSelect={true} searchValue={searchValue} setCheckedValues={setCheckedNamespacesKeys} tabelClassName={'namespacesTable'} checkedValues={checkedNamespacesKeys}/>
</div>
</>}
</div>

View File

@@ -1,70 +1,64 @@
import { useMemo, useState } from "react";
import { useEffect, useMemo } from "react";
import Checkbox from "./Checkbox"
import Radio from "./Radio";
import './style/SelectList.sass';
export interface Props {
valuesListInput;
tableName:string;
multiSelect:boolean;
searchValue?:string;
setValues: (newValues)=> void;
tabelClassName
}
export type ValuesListInput = {
key: string;
value: string;
isChecked: boolean;
}[]
export interface Props {
items;
tableName:string;
checkedValues?:string[];
multiSelect:boolean;
searchValue?:string;
setCheckedValues: (newValues)=> void;
tabelClassName
}
const SelectList: React.FC<Props> = ({valuesListInput ,tableName,multiSelect=true,searchValue="",setValues,tabelClassName}) => {
const [valuesList, setValuesList] = useState(valuesListInput as ValuesListInput);
const SelectList: React.FC<Props> = ({items ,tableName,checkedValues=[],multiSelect=true,searchValue="",setCheckedValues,tabelClassName}) => {
const filteredValues = useMemo(() => {
return items.filter((listValue) => listValue?.value?.includes(searchValue));
},[items, searchValue])
const toggleValues = (checkedKey) => {
const toggleValue = (checkedKey) => {
if (!multiSelect){
unToggleAll(checkedKey);
}
else {
const newValues: ValuesListInput = [...valuesList];
newValues.map(item => item.key === checkedKey ? item.isChecked = !item.isChecked : item.isChecked);
setValuesList(newValues);
setValues(newValues);
unToggleAll();
}
const newCheckedValues = [...checkedValues];
let index = newCheckedValues.indexOf(checkedKey);
if(index > -1) newCheckedValues.splice(index,1);
else newCheckedValues.push(checkedKey);
setCheckedValues(newCheckedValues);
}
const unToggleAll = (checkedKey) => {
const list = valuesList.map((obj) => {
return {...obj, isChecked:checkedKey === obj.key}
})
setValuesList(list);
setValues(list);
const unToggleAll = () => {
setCheckedValues([]);
}
const toggleAll = () => {
const list = valuesList.map((obj) => {
return {...obj, isChecked: true}
})
setValuesList(list);
setValues(list);
const newCheckedValues = [...checkedValues];
if(newCheckedValues.length === items.length) setCheckedValues([]);
else {
items.forEach((obj) => {
if(!newCheckedValues.includes(obj.key))
newCheckedValues.push(obj.key);
})
setCheckedValues(newCheckedValues);
}
}
const tableHead = multiSelect ?
<tr style={{borderBottomWidth: "2px"}}>
<th style={{width: 50}}><Checkbox checked={valuesList.every(valueTap => valueTap.isChecked === false)}
const tableHead = multiSelect ? <tr style={{borderBottomWidth: "2px"}}>
<th style={{width: 50}}><Checkbox checked={items.length === checkedValues.length}
onToggle={toggleAll}/></th>
<th>{tableName}</th>
</tr> :
<tr style={{borderBottomWidth: "2px",display: !tableName ? "none" : "table"}}>
<tr style={{borderBottomWidth: "2px"}}>
<th>{tableName}</th>
</tr>
const filteredValues = useMemo(() => {
return valuesList.filter((listValue) => listValue?.value?.includes(searchValue));
},[valuesList, searchValue])
return <div className={tabelClassName + " select-list-table"}>
<table cellPadding={5} style={{borderCollapse: "collapse"}}>
<thead>
@@ -74,8 +68,8 @@ const SelectList: React.FC<Props> = ({valuesListInput ,tableName,multiSelect=tru
{filteredValues?.map(listValue => {
return <tr key={listValue.key}>
<td style={{width: 50}}>
{multiSelect && <Checkbox checked={valuesList.find(item => item.key === listValue.key)?.isChecked} onToggle={() => toggleValues(listValue.key)}/>}
{!multiSelect && <Radio checked={valuesList.find(item => item.key === listValue.key)?.isChecked} onToggle={() => toggleValues(listValue.key)}/>}
{multiSelect && <Checkbox checked={checkedValues.includes(listValue.key)} onToggle={() => toggleValue(listValue.key)}/>}
{!multiSelect && <Radio checked={checkedValues.includes(listValue.key)} onToggle={() => toggleValue(listValue.key)}/>}
</td>
<td>{listValue.value}</td>
</tr>

View File

@@ -4,15 +4,6 @@ import {ColsType, FilterableTableAction} from "../UI/FilterableTableAction"
import { useEffect, useState } from "react";
import { UserData,AddUserModal } from "../Modals/AddUserModal/AddUserModal";
import Api from '../../helpers/api';
import {Snackbar} from "@material-ui/core";
import MuiAlert from "@material-ui/lab/Alert";
import { Select } from "../UI/Select";
import { MenuItem } from "@material-ui/core";
import { settings } from "cluster";
import { SettingsModal } from "../SettingsModal/SettingModal";
import OasModal from "../Modals/OasModal/OasModal";
import { apiDefineProperty } from "mobx/dist/internal";
import { toast } from "react-toastify";
import ConfirmationModal from "../UI/Modals/ConfirmationModal";

View File

@@ -1,45 +1,64 @@
import "../UserSettings/UserSettings.sass"
import {ColsType, FilterableTableAction} from "../UI/FilterableTableAction"
// import Api from "../../helpers/api"
import Api from "../../helpers/api"
import { useEffect, useState } from "react";
import AddWorkspaceModal, { WorkspaceData } from "../Modals/AddWorkspaceModal/AddWorkspaceModal";
import { toast } from "react-toastify";
import ConfirmationModal from "../UI/Modals/ConfirmationModal";
interface Props {}
// const api = Api.getInstance();
const api = Api.getInstance();
export const WorkspaceSettings : React.FC<Props> = ({}) => {
const [workspacesRows, setWorkspacesRows] = useState([]);
const cols : ColsType[] = [{field : "name",header:"Name"}];
const [workspaceData,SetWorkspaceData] = useState({} as WorkspaceData);
const [isOpenModal,setIsOpen] = useState(false);
const [isEditMode,setIsEditMode] = useState(false);
const cols : ColsType[] = [{field : "id",header:"Id"},{field : "name",header:"Name"}];
const [isOpenDeleteModal, setIsOpenDeleteModal] = useState(false);
const buttonConfig = {onClick: () => {setIsOpen(true); setIsEditMode(false);SetWorkspaceData({} as WorkspaceData)}, text:"Add Workspace"}
useEffect(() => {
(async () => {
try {
const workspacesDemo = [{id:"1", name:"Worksapce1"}]
setWorkspacesRows(workspacesDemo)
const workspaces = await api.getWorkspaces();
setWorkspacesRows(workspaces)
} catch (e) {
console.error(e);
}
})();
},[])
},[isOpenModal])
const filterFuncFactory = (searchQuery: string) => {
return (row) => row.name.toLowerCase().includes(searchQuery.toLowerCase())
return (row) => {
return row.name.toLowerCase().includes(searchQuery.toLowerCase());
}
}
const searchConfig = { searchPlaceholder: "Search Workspace",filterRows: filterFuncFactory}
const searchConfig = { searchPlaceholder: "Search Workspace",filterRows: filterFuncFactory};
const onRowDelete = (row) => {
const filterFunc = filterFuncFactory(row.name)
const newWorkspaceList = workspacesRows.filter(filterFunc)
setWorkspacesRows(newWorkspaceList)
const onRowDelete = async (workspace) => {
setIsOpenDeleteModal(true);
SetWorkspaceData(workspace);
}
const onDeleteConfirmation = () => {
(async() => {
try{
const workspaceLeft = workspacesRows.filter(ws => ws.id != workspaceData.id);
setWorkspacesRows(workspaceLeft);
await api.deleteWorkspace(workspaceData.id);
setIsOpenDeleteModal(false);
SetWorkspaceData({} as WorkspaceData);
toast.success("Workspace Succesesfully Deleted ");
} catch {
toast.error("Workspace hasn't deleted");
}
})();
}
const onRowEdit = (row) => {
@@ -47,16 +66,18 @@ export const WorkspaceSettings : React.FC<Props> = ({}) => {
setIsEditMode(true);
SetWorkspaceData(row);
}
return (<>
<FilterableTableAction onRowEdit={onRowEdit} onRowDelete={onRowDelete} searchConfig={searchConfig}
buttonConfig={buttonConfig} rows={workspacesRows} cols={cols}>
</FilterableTableAction>
<AddWorkspaceModal isOpen={isOpenModal} workspaceData={workspaceData} onEdit={isEditMode} onCloseModal={() => { setIsOpen(false);} } >
<AddWorkspaceModal isOpen={isOpenModal} workspaceId={workspaceData.id} onEdit={isEditMode} onCloseModal={() => { setIsOpen(false);} } >
</AddWorkspaceModal>
<ConfirmationModal isOpen={isOpenDeleteModal} onClose={() => setIsOpenDeleteModal(false)}
onConfirm={onDeleteConfirmation} confirmButtonText="Delete Workspace" title="Delete Workspace"
confirmButtonColor="#DB2156">
<p>Are you sure you want to delete this workspace?</p>
</ConfirmationModal>
</>);
}

View File

@@ -83,6 +83,31 @@ export default class Api {
return response.data;
}
getSpecificWorkspace = async(workspaceId) =>{
const response = await this.client.get(`/workspace/${workspaceId}`);
return response.data;
}
createWorkspace = async(workspaceData) =>{
const response = await this.client.post(`/workspace`,workspaceData);
return response.data;
}
editWorkspace = async(workspaceId, workspaceData) =>{
const response = await this.client.put(`/workspace/${workspaceId}`,workspaceData);
return response.data;
}
deleteWorkspace = async(workspaceId) => {
const response = await this.client.delete(`/workspace/${workspaceId}`);
return response.data;
}
getNamespaces = async() =>{
const response = await this.client.get(`/config/namespaces`);
return response.data;
}
analyzeStatus = async () => {
const response = await this.client.get("/status/analyze");
return response.data;
@@ -145,7 +170,7 @@ export default class Api {
}
getTapConfig = async () => {
const response = await this.client.get("/config/tapConfig");
const response = await this.client.get("/config/tap");
return response.data;
}
@@ -171,27 +196,48 @@ export default class Api {
}
}
register = async (username, password) => {
// register = async (username, password) => {
// const form = new FormData();
// form.append('username', username);
// form.append('password', password);
const form = new FormData();
form.append('password', password);
try {
const response = await this.client.post(`/install/admin`, form);
this.persistToken(response.data.token);
return response;
} catch (e) {
if (e.response.status === 400) {
const error = {
'type': FormValidationErrorType,
'messages': e.response.data
};
throw error;
} else {
throw e;
}
// try {
// const response = await this.client.post(`/user/register`, form);
// this.persistToken(response.data.token);
// return response;
// } catch (e) {
// if (e.response.status === 400) {
// const error = {
// 'type': FormValidationErrorType,
// 'messages': e.response.data
// };
// throw error;
// } else {
// throw e;
// }
// }
// }
setupAdminUser = async (password) => {
const form = new FormData();
form.append('password', password);
try {
const response = await this.client.post(`/install/admin`, form);
this.persistToken(response.data.token);
return response;
} catch (e) {
if (e.response.status === 400) {
const error = {
'type': FormValidationErrorType,
'messages': e.response.data
};
throw error;
} else {
throw e;
}
}
}
login = async (username, password) => {