Merge branch 'origin/ui/TRA-4204_user_managment' of github.com:up9inc/mizu into feature/ui/TRA-4192_workspace_management

This commit is contained in:
Amit Fainholts
2022-01-25 18:43:02 +02:00
20 changed files with 340 additions and 66 deletions

View File

@@ -11,6 +11,7 @@ import LoadingOverlay from "./LoadingOverlay";
import SystemViewer from "./Pages/SystemViewer/SystemViewer"; import SystemViewer from "./Pages/SystemViewer/SystemViewer";
import Api from "../helpers/api"; import Api from "../helpers/api";
import {TrafficPage} from "./Pages/TrafficPage/TrafficPage"; import {TrafficPage} from "./Pages/TrafficPage/TrafficPage";
import SettingsPage from "./Pages/SettingsPage/SettingsPage";
const api = Api.getInstance(); const api = Api.getInstance();
@@ -69,8 +70,9 @@ const AppSwitchRoutes = () => {
return <Routes> return <Routes>
<Route path={"/"} element={<SystemViewer isFirstLogin={isFirstLogin} setIsFirstLogin={setIsFirstLogin}/>}> <Route path={"/"} element={<SystemViewer isFirstLogin={isFirstLogin} setIsFirstLogin={setIsFirstLogin}/>}>
<Route path={RouterRoutes.SETTINGS} element={<SettingsPage/>} /> {/*todo: set settings component*/}
<Route path={"/"} element={<TrafficPage/>} /> <Route path={"/"} element={<TrafficPage/>} />
<Route path={RouterRoutes.SETTINGS} element={<></>} /> {/*todo: set settings component*/}
</Route> </Route>
<Route path={RouterRoutes.LOGIN} element={<AuthPageBase><LoginPage/></AuthPageBase>}/> <Route path={RouterRoutes.LOGIN} element={<AuthPageBase><LoginPage/></AuthPageBase>}/>
<Route path={RouterRoutes.SETUP} element={<AuthPageBase><InstallPage onFirstLogin={() => setIsFirstLogin(true)}/></AuthPageBase>}/> <Route path={RouterRoutes.SETUP} element={<AuthPageBase><InstallPage onFirstLogin={() => setIsFirstLogin(true)}/></AuthPageBase>}/>

View File

@@ -1,4 +1,4 @@
import React, {useEffect, useState} from "react"; import React from "react";
import logo from '../assets/MizuEntLogo.svg'; import logo from '../assets/MizuEntLogo.svg';
import './Header.sass'; import './Header.sass';
import userImg from '../assets/user-circle.svg'; import userImg from '../assets/user-circle.svg';
@@ -6,13 +6,12 @@ import settingImg from '../assets/settings.svg';
import {Menu, MenuItem} from "@material-ui/core"; import {Menu, MenuItem} from "@material-ui/core";
import PopupState, {bindMenu, bindTrigger} from "material-ui-popup-state"; import PopupState, {bindMenu, bindTrigger} from "material-ui-popup-state";
import logoutIcon from '../assets/logout.png'; import logoutIcon from '../assets/logout.png';
import {SettingsModal} from "../SettingsModal/SettingModal";
import Api from "../../helpers/api"; import Api from "../../helpers/api";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {useSetRecoilState} from "recoil"; import {useSetRecoilState} from "recoil";
import entPageAtom, {Page} from "../../recoil/entPage"; import entPageAtom, {Page} from "../../recoil/entPage";
import AdminSettings from "../Pages/SettingsPage/SettingsPage";
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
import {RouterRoutes} from "../../helpers/routes";
const api = Api.getInstance(); const api = Api.getInstance();
@@ -23,18 +22,6 @@ interface EntHeaderProps {
export const EntHeader: React.FC<EntHeaderProps> = ({isFirstLogin, setIsFirstLogin}) => { export const EntHeader: React.FC<EntHeaderProps> = ({isFirstLogin, setIsFirstLogin}) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
useEffect(() => {
if(isFirstLogin) {
setIsSettingsModalOpen(true)
}
}, [isFirstLogin])
const onSettingsModalClose = () => {
setIsSettingsModalOpen(false);
setIsFirstLogin(false);
}
return <div className="header"> return <div className="header">
<div> <div>
@@ -43,11 +30,9 @@ export const EntHeader: React.FC<EntHeaderProps> = ({isFirstLogin, setIsFirstLog
</div> </div>
</div> </div>
<div style={{display: "flex", alignItems: "center"}}> <div style={{display: "flex", alignItems: "center"}}>
<img className="headerIcon" alt="settings" src={settingImg} style={{marginRight: 25}} onClick={() => setIsSettingsModalOpen(true)}/> <img className="headerIcon" alt="settings" src={settingImg} style={{marginRight: 25}} onClick={() => navigate(RouterRoutes.SETTINGS)}/>
<ProfileButton/> <ProfileButton/>
</div> </div>
{/* <SettingsModal isOpen={isSettingsModalOpen} onClose={onSettingsModalClose} isFirstLogin={isFirstLogin}/> */}
{/* <AdminSettings isOpen={isSettingsModalOpen} onClose={onSettingsModalClose}/> */}
</div>; </div>;
} }

View File

@@ -0,0 +1,11 @@
import React, { lazy, Suspense } from 'react';
const LazyAddUserModal = lazy(() => import('./AddUserModal'));
const AddUserModal = (props: JSX.IntrinsicAttributes & { children?: React.ReactNode;isOpen:boolean }) => (
<Suspense fallback={null}>
<LazyAddUserModal {...props} />
</Suspense>
);
export default AddUserModal;

View File

@@ -0,0 +1 @@
.AddUserModal {}

View File

@@ -0,0 +1,28 @@
import React, { FC, useEffect, useState } from 'react';
import ConfirmationModal from '../../UI/Modals/ConfirmationModal';
import './AddUserModal.less';
interface AddUserModalProps {
isOpen : boolean
}
const AddUserModal: FC<AddUserModalProps> = ({isOpen}) => {
const [isOpenModal,setIsOpen] = useState(isOpen)
useEffect(() => {
setIsOpen(isOpen)
},[isOpen])
const onClose = () => {}
const onConfirm = () => {}
return (<>
<ConfirmationModal isOpen={isOpenModal} onClose={onClose} onConfirm={onConfirm} title=''>
</ConfirmationModal>
</>);
};
export default AddUserModal;

View File

@@ -0,0 +1,28 @@
import React, { FC, useEffect, useState } from 'react';
import ConfirmationModal from '../../UI/Modals/ConfirmationModal';
// import './AddUserModal.sass';
interface AddUserModalProps {
isOpen : boolean
}
const AddUserModal: FC<AddUserModalProps> = ({isOpen}) => {
const [isOpenModal,setIsOpen] = useState(isOpen)
useEffect(() => {
setIsOpen(isOpen)
},[isOpen])
const onClose = () => {}
const onConfirm = () => {}
return (<>
<ConfirmationModal isOpen={isOpenModal} onClose={onClose} onConfirm={onConfirm} title=''>
</ConfirmationModal>
</>);
};
export default AddUserModal;

View File

@@ -1,4 +1,4 @@
@import '../../variables.module.scss' @import '../../../variables.module.scss'
.NotSelectedMessage .NotSelectedMessage
margin-left: 41% margin-left: 41%

View File

@@ -1,9 +1,9 @@
import { Box, Fade, FormControl, MenuItem, Modal } from "@material-ui/core"; import { Box, Fade, FormControl, MenuItem, Modal } from "@material-ui/core";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { RedocStandalone } from "redoc"; import { RedocStandalone } from "redoc";
import Api from "../../helpers/api"; import Api from "../../../helpers/api";
import { Select } from "../UI/Select"; import { Select } from "../../UI/Select";
import closeIcon from "../assets/closeIcon.svg"; import closeIcon from "../../assets/closeIcon.svg";
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import './OasModal.sass' import './OasModal.sass'

View File

@@ -1,5 +1,8 @@
import React from "react";
import { useState } from "react"; import { useState } from "react";
import Tabs from "../../UI/Tabs" import Tabs from "../../UI/Tabs"
import { UserSettings } from "../../UserSettings/UserSettings";
import { WorkspaceSettings } from "../../WorkspaceSettings/WorkspaceSettings";
const AdminSettings: React.FC<any> = ({color}) => { const AdminSettings: React.FC<any> = ({color}) => {
var TABS = [ var TABS = [
@@ -10,6 +13,12 @@ const AdminSettings: React.FC<any> = ({color}) => {
return ( return (
<div style={{padding:" 0 1rem"}}> <div style={{padding:" 0 1rem"}}>
<Tabs tabs={TABS} currentTab={currentTab} color={color} onChange={setCurrentTab} leftAligned/> <Tabs tabs={TABS} currentTab={currentTab} color={color} onChange={setCurrentTab} leftAligned/>
{currentTab === TABS[0].tab && <React.Fragment>
<UserSettings/>
</React.Fragment>}
{currentTab === TABS[1].tab && <React.Fragment>
<WorkspaceSettings/>
</React.Fragment>}
</div> </div>
) )
} }

View File

@@ -18,7 +18,7 @@ import entriesAtom from "../../../recoil/entries";
import focusedEntryIdAtom from "../../../recoil/focusedEntryId"; import focusedEntryIdAtom from "../../../recoil/focusedEntryId";
import websocketConnectionAtom, {WsConnectionStatus} from "../../../recoil/wsConnection"; import websocketConnectionAtom, {WsConnectionStatus} from "../../../recoil/wsConnection";
import queryAtom from "../../../recoil/query"; import queryAtom from "../../../recoil/query";
import OasModal from "../../OasModal/OasModal"; import OasModal from "../../Modals/OasModal/OasModal";
import {useCommonStyles} from "../../../helpers/commonStyle" import {useCommonStyles} from "../../../helpers/commonStyle"
import {TLSWarning} from "../../TLSWarning/TLSWarning"; import {TLSWarning} from "../../TLSWarning/TLSWarning";
import serviceMapModalOpenAtom from "../../../recoil/serviceMapModalOpen"; import serviceMapModalOpenAtom from "../../../recoil/serviceMapModalOpen";

View File

@@ -0,0 +1,63 @@
import { Button } from "@material-ui/core";
import React, { useEffect, useMemo, useState } from "react";
import { Table } from "./Table";
import {useCommonStyles} from "../../helpers/commonStyle";
import {ColsType} from "../UI/Table"
import './style/FilterableTableAction.sass';
export type {ColsType} from "../UI/Table"
type filterFuncFactory = (query:string) => (any) => boolean
export interface Props {
onRowEdit : (any) => void;
onRowDelete : (any) => void;
searchConfig : {searchPlaceholder : string;filterRows: filterFuncFactory};
buttonConfig : {onClick : () => void, text:string}
rows: any[];
cols: ColsType[];
}
export const FilterableTableAction: React.FC<Props> = ({onRowDelete,onRowEdit, searchConfig, buttonConfig, rows, cols}) => {
const classes = useCommonStyles()
const [tableRows,setRows] = useState(rows as any[])
const [inputSearch, setInputSearch] = useState("")
useEffect(() => {
setRows(rows);
},[rows])
// useEffect(()=> {
// if(inputSearch !== ""){
// const searchFunc = searchConfig.filterRows(inputSearch)
// const filtered = tableRows.filter(searchFunc)
// setRows(filtered)
// }
// else{
// setRows(allRows);
// }
// },[inputSearch])
const onChange = (e) => {
setInputSearch(e.target.value)
}
const filteredValues = useMemo(() => {
const searchFunc = searchConfig.filterRows(inputSearch)
return tableRows.filter(searchFunc)
},[tableRows, inputSearch])
return (<>
<div className="filterable-table">
<div className="actions-header">
<input type="text" className={classes.textField + " actions-header__search-box"} placeholder={searchConfig.searchPlaceholder} onChange={onChange}></input>
<Button style={{height: '100%'}} className={classes.button + " actions-header__action-button"} size={"small"} onClick={buttonConfig.onClick}>
{buttonConfig.text}
</Button>
</div>
<Table rows={filteredValues} cols={cols} onRowEdit={onRowEdit} onRowDelete={onRowDelete}></Table>
</div>
</>);
};

View File

@@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { makeStyles, withStyles, Modal, Backdrop, Fade, Box } from '@material-ui/core'; import { makeStyles, Modal, Backdrop, Fade, Box } from '@material-ui/core';
import {useCommonStyles} from "../../../helpers/commonStyle"; import {useCommonStyles} from "../../../helpers/commonStyle";
import { PropertiesTable } from 'redoc/typings/common-elements';
const useStyles = makeStyles({ const useStyles = makeStyles({
modal: { modal: {

View File

@@ -1,22 +1,25 @@
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import './style/Table.sass'; import './style/Table.sass';
import editImg from "../assets/edit.svg";
import deleteImg from "../assets/delete.svg"
import circleImg from "../assets/dotted-circle.svg"
import Delete from '@material-ui/icons/Delete'; import Delete from '@material-ui/icons/Delete';
import Edit from '@material-ui/icons/Edit'; import Edit from '@material-ui/icons/Edit';
export interface ColsType {
field:string,
cellClassName?: string,
header:string,
width?:number,
getCellClassName?:(field:string,value : any) => string
};
interface Props { interface TableProps {
rows : any[]; rows : any[];
cols : {field:string, cellClassName?: string,header:string, width?:number, cols : ColsType[]
getCellClassName?:(field:string,value : any) => string}[];
onRowEdit : (any) => void; onRowEdit : (any) => void;
onRowDelete : (any) => void; onRowDelete : (any) => void;
noDataMeesage : string; noDataMeesage? : string;
} }
export const Table: React.FC<Props> = ({rows, cols, onRowDelete, onRowEdit,noDataMeesage}) => { export const Table: React.FC<TableProps> = ({rows, cols, onRowDelete, onRowEdit, noDataMeesage = "No Data Found"}) => {
const [tableRows, updateTableRows] = useState(rows); const [tableRows, updateTableRows] = useState(rows);
@@ -31,11 +34,16 @@ export const Table: React.FC<Props> = ({rows, cols, onRowDelete, onRowEdit,noDat
const _onRowDelete = (row) => { const _onRowDelete = (row) => {
onRowDelete(row); onRowDelete(row);
} }
// const filteredValues = useMemo(() => {
// return tableRows.filter((listValue) => listValue.find(row));
// },[tableRows, searchValue])
return <table cellPadding={5} style={{borderCollapse: "collapse"}} className="mui-table"> return <table cellPadding={5} style={{borderCollapse: "collapse"}} className="mui-table">
<thead className="mui-table__thead"> <thead className="mui-table__thead">
<tr style={{borderBottomWidth: "2px"}} className="mui-table__tr"> <tr style={{borderBottomWidth: "2px"}} className="mui-table__tr">
{cols?.map((col)=> { {cols?.map((col)=> {
return <th className="mui-table__th">{col.header}</th> return <th key={col.header} className="mui-table__th">{col.header}</th>
})} })}
<th></th> <th></th>
</tr> </tr>
@@ -44,18 +52,19 @@ export const Table: React.FC<Props> = ({rows, cols, onRowDelete, onRowEdit,noDat
{ {
((tableRows == null) || (tableRows.length === 0)) ? ((tableRows == null) || (tableRows.length === 0)) ?
<tr className="mui-table__no-data"> <tr className="mui-table__no-data">
<img src={circleImg} alt="No data Found"></img> {/* <img src={circleImg} alt="No data Found"></img>
<div className="mui-table__no-data-message">{noDataMeesage}</div> <div className="mui-table__no-data-message">{noDataMeesage}</div> */}
</tr> </tr>
: :
tableRows?.map(rowData => { tableRows?.map((rowData,index) => {
return <tr key={rowData?.id} className="mui-table__tr"> return <tr key={rowData?.id ?? index} className="mui-table__tr">
{cols.map(col => { {cols.map((col,index) => {
return <td className={`${col.getCellClassName ? col.getCellClassName(col.field, rowData[col.field]) : ""} return <td key={`${rowData?.id} + ${index}`} className="mui-table__td">
${col?.cellClassName ?? ""} mui-table__td`}> <span key={Math.random().toString()} className={`${col.getCellClassName ? col.getCellClassName(col.field, rowData[col.field]) : ""}${col?.cellClassName ?? ""}`}>
{rowData[col.field]} {rowData[col.field]}
</span>
</td> </td>
})} })}
<td className="mui-table__td mui-table__row-actions"> <td className="mui-table__td mui-table__row-actions">

View File

@@ -0,0 +1,15 @@
@import '../../../variables.module'
.filterable-table
padding: 0 5%;
.actions-header
display: flex;
justify-content: space-between;
&__search-box
width : 313px;
&__action-button
height: 100%

View File

@@ -67,26 +67,4 @@
@mixin row-actions @mixin row-actions
display: flex display: flex
justify-content: flex-end justify-content: flex-end
@mixin status-base($bg_color, $color, $border)
box-sizing: border-box
border-radius: 4px
height : 20px
width : 63px
display: flex
align-content: center
justify-content: center
align-items: center
background: $bg_color
color: $color
border: $border
.status
&--active
@include status-base(rgba(111, 207, 151, 0.5), #247E4B, 1px solid #219653)
&--pending
@include status-base(rgba(242, 201, 76, 0.5), #8C7325, 1px solid #F2994A)

View File

@@ -0,0 +1,21 @@
@import '../../variables.module'
@mixin status-base($bg_color, $color, $border)
box-sizing: border-box
border-radius: 4px
height : 20px
width : 63px
display: flex
align-content: center
justify-content: center
align-items: center
background: $bg_color
color: $color
border: $border
.status
&--active
@include status-base(rgba(111, 207, 151, 0.5), #247E4B, 1px solid #219653)
&--pending
@include status-base(rgba(242, 201, 76, 0.5), #8C7325, 1px solid #F2994A)

View File

@@ -0,0 +1,61 @@
import "./UserSettings.sass"
import {ColsType, FilterableTableAction} from "../UI/FilterableTableAction"
// import Api from "../../helpers/api"
import { useEffect, useState } from "react";
import AddUserModal from "../Modals/AddUserModal/AddUserModal";
interface Props {
}
// const api = Api.getInstance();
export const UserSettings : React.FC<Props> = ({}) => {
const [usersRows, setUserRows] = useState([]);
const cols : ColsType[] = [{field : "userName",header:"User"},
{field : "role",header:"Role"},
{field : "status",header:"Status",getCellClassName : (field, val) =>{
return val === "Active" ? "status--active" : "status--pending"
}}]
const [isOpenModal,setIsOpen] = useState(false)
useEffect(() => {
(async () => {
try {
const users = [{userName:"asd",role:"Admin",status:"Active"}]//await api.getUsers()
setUserRows(users)
} catch (e) {
console.error(e);
}
})();
},[])
const filterFuncFactory = (searchQuery: string) => {
return (row) => {
return row.userName.toLowerCase().includes(searchQuery.toLowerCase())
}
}
const searchConfig = { searchPlaceholder: "Search User",filterRows: filterFuncFactory}
const onRowDelete = (row) => {
const filterFunc = filterFuncFactory(row.userName)
const newUserList = usersRows.filter(filterFunc)
setUserRows(newUserList)
}
const onRowEdit = (row) => {
// open Edit user Modal
}
const buttonConfig = {onClick: () => {setIsOpen(true)}, text:"Add User"}
return (<>
<FilterableTableAction onRowEdit={onRowEdit} onRowDelete={onRowDelete} searchConfig={searchConfig}
buttonConfig={buttonConfig} rows={usersRows} cols={cols}>
</FilterableTableAction>
<AddUserModal isOpen={isOpenModal}>
</AddUserModal>
</>);
}

View File

@@ -0,0 +1,51 @@
import "../UserSettings/UserSettings.sass"
import {ColsType, FilterableTableAction} from "../UI/FilterableTableAction"
// import Api from "../../helpers/api"
import { useEffect, useState } from "react";
interface Props {}
// const api = Api.getInstance();
export const WorkspaceSettings : React.FC<Props> = ({}) => {
const [workspacesRows, setWorkspaces] = useState([]);
const cols : ColsType[] = [{field : "id",header:"Id"},{field : "name",header:"Name"}]
useEffect(() => {
(async () => {
try {
const workspacesDemo = [{id:"1", name:"Worksapce1"}]
setWorkspaces(workspacesDemo)
} catch (e) {
console.error(e);
}
})();
},[])
const filterFuncFactory = (searchQuery: string) => {
return (row) => {
return row.name.toLowerCase().includes(searchQuery.toLowerCase())
}
}
const searchConfig = { searchPlaceholder: "Search Workspace",filterRows: filterFuncFactory}
const onRowDelete = (row) => {
const filterFunc = filterFuncFactory(row.name)
const newWorkspaceList = workspacesRows.filter(filterFunc)
setWorkspaces(newWorkspaceList)
}
const onRowEdit = (row) => {
}
const buttonConfig = {onClick: () => {}, text:"Add Workspace"}
return (<>
<FilterableTableAction onRowEdit={onRowEdit} onRowDelete={onRowDelete} searchConfig={searchConfig}
buttonConfig={buttonConfig} rows={workspacesRows} cols={cols}>
</FilterableTableAction>
</>);
}

View File

@@ -48,6 +48,16 @@ export default class Api {
return response.data; return response.data;
} }
getUsers = async(filter = "") =>{
const response = await this.client.get(`/user/listUsers?usernameFilter=${filter}`);
return response.data;
}
getWorkspaces = async() =>{
const response = await this.client.get(``);
return response.data;
}
analyzeStatus = async () => { analyzeStatus = async () => {
const response = await this.client.get("/status/analyze"); const response = await this.client.get("/status/analyze");
return response.data; return response.data;

View File

@@ -6,6 +6,9 @@ import 'react-toastify/dist/ReactToastify.css';
import {RecoilRoot} from "recoil"; import {RecoilRoot} from "recoil";
import AppChooser from "./AppChooser"; import AppChooser from "./AppChooser";
window["isOasEnabled"]=true;
window["isServiceMapEnabled"]=true;
ReactDOM.render( <> ReactDOM.render( <>
<RecoilRoot> <RecoilRoot>
<AppChooser/> <AppChooser/>