From 52eff388a58cdb4c66e6fef8ec1ea1e504c8af41 Mon Sep 17 00:00:00 2001 From: Sina Blattmann Date: Wed, 29 Mar 2023 15:30:06 +0200 Subject: [PATCH] [WIP] add users route, change routing structure, change styling of tabs --- typescript/Frontend/src/App.tsx | 57 +++++---- typescript/Frontend/src/Login.tsx | 8 +- .../Context/UserContextProvider.tsx | 109 ++++++++++++++++++ .../AccessManagement.tsx} | 15 +-- .../AvailableUserDialog.tsx} | 73 ++++++++---- .../InnovenergyList.tsx} | 0 .../UsersWithDirectAccess.tsx | 30 ++--- .../UsersWithInheritedAccess.tsx | 35 +++--- .../src/components/Groups/AddNewDialog.tsx | 46 ++++++++ .../Frontend/src/components/Groups/Folder.tsx | 19 ++- .../src/components/Groups/FolderForm.tsx | 34 ++---- .../src/components/Groups/GroupTabs.tsx | 27 +++-- .../Frontend/src/components/Groups/Groups.tsx | 47 ++++---- ...{CustomerForm.tsx => InstallationForm.tsx} | 8 +- .../Installations/InstallationList.tsx | 1 - .../Installations/InstallationPage.tsx | 23 ++++ .../Installations/InstallationTabs.tsx | 38 +++--- .../Installations/Installations.tsx | 46 ++++---- .../Installation.tsx => Layout/Detail.tsx} | 34 +++--- .../src/components/Layout/LanguageSelect.tsx | 4 +- .../src/components/Layout/ModeButtons.tsx | 59 ++++++++++ .../components/Layout/NavigationButtons.tsx | 14 +-- .../src/components/Layout/Sidebar.tsx | 1 + .../src/components/Users/UserForm.tsx | 87 ++++++++++++++ .../src/components/Users/UserList.tsx | 77 +++++++++++++ .../src/components/Users/UserTabs.tsx | 41 +++++++ .../Frontend/src/components/Users/Users.tsx | 39 +++++++ typescript/Frontend/src/routes.json | 8 +- .../Frontend/src/util/installation.util.tsx | 46 ++++++++ typescript/Frontend/src/util/user.util.tsx | 4 +- 30 files changed, 799 insertions(+), 231 deletions(-) create mode 100644 typescript/Frontend/src/components/Context/UserContextProvider.tsx rename typescript/Frontend/src/components/Groups/{Users/Users.tsx => AccessManagement/AccessManagement.tsx} (56%) rename typescript/Frontend/src/components/Groups/{Users/AvailableUserList.tsx => AccessManagement/AvailableUserDialog.tsx} (50%) rename typescript/Frontend/src/components/Groups/{Users/UserList.tsx => AccessManagement/InnovenergyList.tsx} (100%) rename typescript/Frontend/src/components/Groups/{Users => AccessManagement}/UsersWithDirectAccess.tsx (75%) rename typescript/Frontend/src/components/Groups/{Users => AccessManagement}/UsersWithInheritedAccess.tsx (59%) create mode 100644 typescript/Frontend/src/components/Groups/AddNewDialog.tsx rename typescript/Frontend/src/components/Installations/{CustomerForm.tsx => InstallationForm.tsx} (93%) create mode 100644 typescript/Frontend/src/components/Installations/InstallationPage.tsx rename typescript/Frontend/src/components/{Installations/Installation.tsx => Layout/Detail.tsx} (61%) create mode 100644 typescript/Frontend/src/components/Layout/ModeButtons.tsx create mode 100644 typescript/Frontend/src/components/Users/UserForm.tsx create mode 100644 typescript/Frontend/src/components/Users/UserList.tsx create mode 100644 typescript/Frontend/src/components/Users/UserTabs.tsx create mode 100644 typescript/Frontend/src/components/Users/Users.tsx create mode 100644 typescript/Frontend/src/util/installation.util.tsx diff --git a/typescript/Frontend/src/App.tsx b/typescript/Frontend/src/App.tsx index 14e903ff0..fc1a9108c 100644 --- a/typescript/Frontend/src/App.tsx +++ b/typescript/Frontend/src/App.tsx @@ -1,7 +1,7 @@ import useToken from "./hooks/useToken"; import Login from "./Login"; import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; -import { Grid } from "@mui/material"; +import { Container, Grid } from "@mui/material"; import routes from "./routes.json"; import { IntlProvider } from "react-intl"; import { useState } from "react"; @@ -10,21 +10,23 @@ import de from "./lang/de.json"; import Installations from "./components/Installations/Installations"; import LanguageSelect from "./components/Layout/LanguageSelect"; import LogoutButton from "./components/Layout/LogoutButton"; -import Groups from "./components/Groups/Groups"; +import Users from "./components/Users/Users"; +import NavigationButtons from "./components/Layout/NavigationButtons"; +import InstallationPage from "./components/Installations/InstallationPage"; const App = () => { const { token, setToken, removeToken } = useToken(); - const [language, setLanguage] = useState("en"); + const [language, setLanguage] = useState("EN"); const getTranslations = () => { - if (language === "de") { + if (language === "DE") { return de; } return en; }; if (!token) { - return ; + return ; } return ( @@ -32,23 +34,36 @@ const App = () => { - - - - - - } - /> - } - /> - } /> - + + + + + + + + + + + + } + /> + } + /> + } /> + + ); diff --git a/typescript/Frontend/src/Login.tsx b/typescript/Frontend/src/Login.tsx index bd06de185..9bafc45d3 100644 --- a/typescript/Frontend/src/Login.tsx +++ b/typescript/Frontend/src/Login.tsx @@ -10,7 +10,12 @@ const loginUser = async (username: string, password: string) => { }); }; -const Login = ({ setToken }: { setToken: (value: string) => void }) => { +interface I_LoginProps { + setToken: (value: string) => void; + setLanguage: (value: string) => void; +} + +const Login = ({ setToken, setLanguage }: I_LoginProps) => { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [loading, setLoading] = useState(false); @@ -31,6 +36,7 @@ const Login = ({ setToken }: { setToken: (value: string) => void }) => { .then(() => { setToken(data.token); setLoading(false); + setLanguage(data.user.language); }) .catch((err) => { setError(err); diff --git a/typescript/Frontend/src/components/Context/UserContextProvider.tsx b/typescript/Frontend/src/components/Context/UserContextProvider.tsx new file mode 100644 index 000000000..05fd19712 --- /dev/null +++ b/typescript/Frontend/src/components/Context/UserContextProvider.tsx @@ -0,0 +1,109 @@ +import { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useState, +} from "react"; +import { useParams } from "react-router-dom"; +import axiosConfig from "../../config/axiosConfig"; +import { I_User, UserWithInheritedAccess } from "../../util/user.util"; +import { GroupContext } from "./GroupContextProvider"; + +interface UserContextProviderProps { + directAccessUsers: I_User[]; + setDirectAccessUsers: (value: I_User[]) => void; + inheritedAccessUsers: UserWithInheritedAccess[]; + setInheritedAccessUsers: (value: UserWithInheritedAccess[]) => void; + availableUsers: I_User[]; + setAvailableUsers: (value: I_User[]) => void; + getAvailableUsers: () => I_User[]; + fetchUsersWithInheritedAccessForResource: () => Promise; + fetchUsersWithDirectAccessForResource: () => Promise; +} + +export const UserContext = createContext({ + directAccessUsers: [], + setDirectAccessUsers: () => {}, + inheritedAccessUsers: [], + setInheritedAccessUsers: () => {}, + availableUsers: [], + setAvailableUsers: () => {}, + getAvailableUsers: () => { + return []; + }, + fetchUsersWithInheritedAccessForResource: () => { + return Promise.resolve(); + }, + fetchUsersWithDirectAccessForResource: () => { + return Promise.resolve(); + }, +}); + +const UserContextProvider = ({ children }: { children: ReactNode }) => { + const [directAccessUsers, setDirectAccessUsers] = useState([]); + const [inheritedAccessUsers, setInheritedAccessUsers] = useState< + UserWithInheritedAccess[] + >([]); + const [availableUsers, setAvailableUsers] = useState([]); + + const { currentType } = useContext(GroupContext); + const { id } = useParams(); + + const fetchUsers = useCallback( + async (route: string, setState: (value: any) => void) => { + axiosConfig.get(route + currentType, { params: { id } }).then((res) => { + setState(res.data); + }); + }, + [currentType, id] + ); + + const fetchUsersWithInheritedAccessForResource = useCallback(async () => { + fetchUsers("/GetUsersWithInheritedAccessTo", setInheritedAccessUsers); + }, [fetchUsers]); + + const fetchUsersWithDirectAccessForResource = useCallback(async () => { + fetchUsers("/GetUsersWithDirectAccessTo", setDirectAccessUsers); + }, [fetchUsers]); + + const getAvailableUsersForResource = () => { + const inheritedUsers = inheritedAccessUsers.map( + (inheritedAccessUser) => inheritedAccessUser.user + ); + const allUsersWithAccess = [...inheritedUsers, ...directAccessUsers]; + return availableUsers.filter( + (availableUser) => + !allUsersWithAccess.find( + (userWithAccess) => availableUser.id === userWithAccess.id + ) + ); + }; + + useEffect(() => { + axiosConfig.get("/GetAllChildUsers").then((res) => { + setAvailableUsers(res.data); + }); + }, []); + + return ( + + {children} + + ); +}; + +export default UserContextProvider; diff --git a/typescript/Frontend/src/components/Groups/Users/Users.tsx b/typescript/Frontend/src/components/Groups/AccessManagement/AccessManagement.tsx similarity index 56% rename from typescript/Frontend/src/components/Groups/Users/Users.tsx rename to typescript/Frontend/src/components/Groups/AccessManagement/AccessManagement.tsx index b1b3134be..c35e5d0a9 100644 --- a/typescript/Frontend/src/components/Groups/Users/Users.tsx +++ b/typescript/Frontend/src/components/Groups/AccessManagement/AccessManagement.tsx @@ -1,23 +1,24 @@ import { Grid } from "@mui/material"; -import AvailableUserList from "./AvailableUserList"; -import InnovenergyList from "./UserList"; +import UserContextProvider from "../../Context/UserContextProvider"; +import AvailableUserDialog from "./AvailableUserDialog"; +import InnovenergyList from "./InnovenergyList"; import UsersWithDirectAccess from "./UsersWithDirectAccess"; import UsersWithInheritedAccess from "./UsersWithInheritedAccess"; -const Users = () => { +const AccessManagement = () => { return ( - <> - + + - + ); }; -export default Users; +export default AccessManagement; diff --git a/typescript/Frontend/src/components/Groups/Users/AvailableUserList.tsx b/typescript/Frontend/src/components/Groups/AccessManagement/AvailableUserDialog.tsx similarity index 50% rename from typescript/Frontend/src/components/Groups/Users/AvailableUserList.tsx rename to typescript/Frontend/src/components/Groups/AccessManagement/AvailableUserDialog.tsx index d63b0db51..5d46d6bd1 100644 --- a/typescript/Frontend/src/components/Groups/Users/AvailableUserList.tsx +++ b/typescript/Frontend/src/components/Groups/AccessManagement/AvailableUserDialog.tsx @@ -1,23 +1,32 @@ -import { Autocomplete, Button, Grid, TextField } from "@mui/material"; -import { useContext, useEffect, useState } from "react"; +import { + Autocomplete, + Button, + Dialog, + DialogContent, + DialogTitle, + TextField, +} from "@mui/material"; +import DialogActions from "@mui/material/DialogActions"; +import { useContext, useState } from "react"; import { FormattedMessage } from "react-intl"; import { useParams } from "react-router-dom"; import axiosConfig from "../../../config/axiosConfig"; -import { User } from "../../../util/user.util"; +import { I_User } from "../../../util/user.util"; import { GroupContext } from "../../Context/GroupContextProvider"; +import { UserContext } from "../../Context/UserContextProvider"; -const AvailableUserList = () => { - const [users, setUsers] = useState([]); - const [selectedUsers, setSelectedUsers] = useState([]); +const AvailableUserDialog = () => { + const [selectedUsers, setSelectedUsers] = useState([]); + const [open, setOpen] = useState(false); const { currentType } = useContext(GroupContext); const { id } = useParams(); + const { + getAvailableUsers, + fetchUsersWithDirectAccessForResource, + fetchUsersWithInheritedAccessForResource, + } = useContext(UserContext); - useEffect(() => { - axiosConfig.get("/GetAllChildUsers").then((res) => { - setUsers(res.data); - }); - }, []); const handleGrant = () => { selectedUsers.forEach((user) => { const bodyProp = currentType === "Folder" ? "folderId" : "installationId"; @@ -29,20 +38,32 @@ const AvailableUserList = () => { : "/GrantUserAccessToInstallation", { userId: user.id, [bodyProp]: elementId } ) - .then((res) => { - console.log(res); + .then(() => { + fetchUsersWithDirectAccessForResource(); + fetchUsersWithInheritedAccessForResource(); }); }); }; - if (users) { - return ( - <> - + + return ( + <> + setOpen(false)} + aria-labelledby="customized-dialog-title" + open={open} + sx={{ + ".MuiDialogContent-root": { overflowX: "hidden" }, + maxHeight: 500, + }} + scroll="paper" + > + Create new folder + option.name} renderInput={(params) => ( { )} onChange={(event, values) => setSelectedUsers(values)} /> + + - - - ); - } - return null; + + + + + ); }; -export default AvailableUserList; +export default AvailableUserDialog; diff --git a/typescript/Frontend/src/components/Groups/Users/UserList.tsx b/typescript/Frontend/src/components/Groups/AccessManagement/InnovenergyList.tsx similarity index 100% rename from typescript/Frontend/src/components/Groups/Users/UserList.tsx rename to typescript/Frontend/src/components/Groups/AccessManagement/InnovenergyList.tsx diff --git a/typescript/Frontend/src/components/Groups/Users/UsersWithDirectAccess.tsx b/typescript/Frontend/src/components/Groups/AccessManagement/UsersWithDirectAccess.tsx similarity index 75% rename from typescript/Frontend/src/components/Groups/Users/UsersWithDirectAccess.tsx rename to typescript/Frontend/src/components/Groups/AccessManagement/UsersWithDirectAccess.tsx index 18587a27b..14383a4b8 100644 --- a/typescript/Frontend/src/components/Groups/Users/UsersWithDirectAccess.tsx +++ b/typescript/Frontend/src/components/Groups/AccessManagement/UsersWithDirectAccess.tsx @@ -6,31 +6,23 @@ import { Avatar, ListItemAvatar, } from "@mui/material"; -import { Fragment, useCallback, useContext, useEffect, useState } from "react"; +import { Fragment, useContext, useEffect } from "react"; import axiosConfig from "../../../config/axiosConfig"; -import { User } from "../../../util/user.util"; import PersonRemoveIcon from "@mui/icons-material/PersonRemove"; -import { GroupContext } from "../../Context/GroupContextProvider"; -import { useParams } from "react-router-dom"; import PersonIcon from "@mui/icons-material/Person"; +import { UserContext } from "../../Context/UserContextProvider"; +import { useParams } from "react-router-dom"; +import { GroupContext } from "../../Context/GroupContextProvider"; const UsersWithDirectAccess = () => { - const [users, setUsers] = useState(); - + const { fetchUsersWithDirectAccessForResource, directAccessUsers } = + useContext(UserContext); const { currentType } = useContext(GroupContext); const { id } = useParams(); - const fetchUsers = useCallback(async () => { - axiosConfig - .get("/GetUsersWithDirectAccessTo" + currentType, { params: { id } }) - .then((res) => { - setUsers(res.data); - }); - }, [currentType, id]); - useEffect(() => { - fetchUsers(); - }, [fetchUsers]); + fetchUsersWithDirectAccessForResource(); + }, [fetchUsersWithDirectAccessForResource]); const handleIconClick = (userId: number) => { const folderId = id ? parseInt(id) : ""; @@ -40,14 +32,14 @@ const UsersWithDirectAccess = () => { folderId: folderId, }) .then((res) => { - fetchUsers(); + fetchUsersWithDirectAccessForResource(); }); }; - if (users && users.length) { + if (directAccessUsers && directAccessUsers.length) { return ( <> - {users.map((user) => { + {directAccessUsers.map((user) => { return ( { - const [users, setUsers] = useState(); - - const { currentType } = useContext(GroupContext); - const { id } = useParams(); - - const fetchUsers = useCallback(async () => { - axiosConfig - .get("/GetUsersWithInheritedAccessTo" + currentType, { params: { id } }) - .then((res) => { - setUsers(res.data); - }); - }, [currentType, id]); + const { fetchUsersWithInheritedAccessForResource, inheritedAccessUsers } = + useContext(UserContext); useEffect(() => { - fetchUsers(); - }, [fetchUsers]); + fetchUsersWithInheritedAccessForResource(); + }, [fetchUsersWithInheritedAccessForResource]); - if (users && users.length) { + if (inheritedAccessUsers && inheritedAccessUsers.length) { return ( <> - {filterDuplicateUsers(users).map( + {filterDuplicateUsers(inheritedAccessUsers).map( ({ user, folderName }: UserWithInheritedAccess) => { return ( @@ -52,7 +41,11 @@ const UsersWithInheritedAccess = () => { secondary={ <> Inherited access from{" "} - + {folderName} diff --git a/typescript/Frontend/src/components/Groups/AddNewDialog.tsx b/typescript/Frontend/src/components/Groups/AddNewDialog.tsx new file mode 100644 index 000000000..6af3b2465 --- /dev/null +++ b/typescript/Frontend/src/components/Groups/AddNewDialog.tsx @@ -0,0 +1,46 @@ +import { Button, Dialog, DialogContent, DialogTitle } from "@mui/material"; +import { useState } from "react"; +import { FormattedMessage } from "react-intl"; +import axiosConfig from "../../config/axiosConfig"; +import { I_Folder } from "../../util/types"; +import FolderForm from "./FolderForm"; + +interface AddNewDialogProps { + values: I_Folder; +} + +const AddNewDialog = (props: AddNewDialogProps) => { + const [open, setOpen] = useState(false); + + const handleSubmit = (data: I_Folder, childData: Partial) => { + return axiosConfig.post("/CreateFolder", { + ...childData, + parentId: data.id, + }); + }; + + return ( + <> + + setOpen(false)} + aria-labelledby="customized-dialog-title" + open={open} + sx={{ + ".MuiDialogContent-root": { overflowX: "hidden" }, + maxHeight: 500, + }} + scroll="paper" + > + Create new folder + + + + + + ); +}; + +export default AddNewDialog; diff --git a/typescript/Frontend/src/components/Groups/Folder.tsx b/typescript/Frontend/src/components/Groups/Folder.tsx index 3215d7564..457eff010 100644 --- a/typescript/Frontend/src/components/Groups/Folder.tsx +++ b/typescript/Frontend/src/components/Groups/Folder.tsx @@ -3,12 +3,14 @@ import { AxiosError } from "axios"; import { useState, useEffect } from "react"; import { useParams } from "react-router-dom"; import axiosConfig from "../../config/axiosConfig"; -import { I_Installation } from "../../util/types"; +import { I_Folder } from "../../util/types"; +import AddNewDialog from "./AddNewDialog"; import FolderForm from "./FolderForm"; +import MoveDialog from "./Tree/MoveDialog"; const Folder = () => { const { id } = useParams(); - const [values, setValues] = useState(); + const [values, setValues] = useState(); const [loading, setLoading] = useState(false); const [error, setError] = useState(); @@ -26,11 +28,22 @@ const Folder = () => { }); }, [id]); + const handleSubmit = (data: I_Folder, formikValues: Partial) => { + return axiosConfig.put("/UpdateFolder", { ...data, ...formikValues }); + }; + if (values && values.id && values.id.toString() === id) { + const moveDialog = ; + const addNewDialog = ; + return ( <> - + ); diff --git a/typescript/Frontend/src/components/Groups/FolderForm.tsx b/typescript/Frontend/src/components/Groups/FolderForm.tsx index 26852bce8..6d612b425 100644 --- a/typescript/Frontend/src/components/Groups/FolderForm.tsx +++ b/typescript/Frontend/src/components/Groups/FolderForm.tsx @@ -1,25 +1,24 @@ import { Button, CircularProgress, Grid } from "@mui/material"; +import { AxiosResponse } from "axios"; import { useFormik } from "formik"; -import { useContext, useState } from "react"; +import { ReactNode, useContext, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import axiosConfig from "../../config/axiosConfig"; import { I_Folder } from "../../util/types"; import { GroupContext } from "../Context/GroupContextProvider"; import InnovenergySnackbar from "../InnovenergySnackbar"; import InnovenergyTextfield from "../Layout/InnovenergyTextfield"; -import MoveDialog from "./Tree/MoveDialog"; interface I_CustomerFormProps { values: I_Folder; - id: string; + handleSubmit: ( + data: I_Folder, + formikValues: Partial + ) => Promise; + additionalButtons?: ReactNode[]; } -const updateFolder = (data: I_Folder) => { - return axiosConfig.put("/UpdateFolder", data); -}; - const FolderForm = (props: I_CustomerFormProps) => { - const { values, id } = props; + const { values, additionalButtons, handleSubmit } = props; const intl = useIntl(); const { fetchData } = useContext(GroupContext); @@ -34,14 +33,8 @@ const FolderForm = (props: I_CustomerFormProps) => { }, onSubmit: (formikValues) => { setLoading(true); - const idAsNumber = parseInt(id, 10); - updateFolder({ - ...values, - ...formikValues, - id: idAsNumber, - }) - .then((res) => { - // TODO figure out why this isnt refreshing tree + handleSubmit(values, formikValues) + .then(() => { fetchData(); setSnackbarOpen(true); setLoading(false); @@ -79,12 +72,9 @@ const FolderForm = (props: I_CustomerFormProps) => { /> {loading && } - + {additionalButtons && additionalButtons.map((button) => button)} { const routeMatch = useRouteMatch([ routes.groups + routes.folder + ":id", - routes.groups + routes.users + ":id", + routes.groups + routes.manageAccess + ":id", routes.groups + routes.installation + ":id", ]); const id = routeMatch?.params?.id; const intl = useIntl(); - const { currentType } = React.useContext(GroupContext); + const { currentType } = useContext(GroupContext); if (id) { return ( - {currentType === "Folder" ? ( - { to={routes.folder + id} /> ) : ( - { /> )} - - + ); diff --git a/typescript/Frontend/src/components/Groups/Groups.tsx b/typescript/Frontend/src/components/Groups/Groups.tsx index e1a80f23a..b715275c2 100644 --- a/typescript/Frontend/src/components/Groups/Groups.tsx +++ b/typescript/Frontend/src/components/Groups/Groups.tsx @@ -2,36 +2,43 @@ import { Grid } from "@mui/material"; import { Container } from "@mui/system"; import { Routes, Route } from "react-router"; import routes from "../../routes.json"; -import Installation from "../Installations/Installation"; import Folder from "./Folder"; import GroupTabs from "./GroupTabs"; import GroupContextProvider from "../Context/GroupContextProvider"; import GroupTree from "./Tree/GroupTree"; import NavigationButtons from "../Layout/NavigationButtons"; -import Users from "./Users/Users"; +import AccessManagement from "./AccessManagement/AccessManagement"; +import { I_Installation } from "../../util/types"; +import InstallationForm from "../Installations/InstallationForm"; +import Detail from "../Layout/Detail"; const Groups = () => { return ( - - - - - - - - - - } index /> - } /> - } - /> - - + + + - + + + + } index /> + } + /> + + route="/GetInstallationById?id=" + formComponent={InstallationForm} + /> + } + /> + + + ); }; diff --git a/typescript/Frontend/src/components/Installations/CustomerForm.tsx b/typescript/Frontend/src/components/Installations/InstallationForm.tsx similarity index 93% rename from typescript/Frontend/src/components/Installations/CustomerForm.tsx rename to typescript/Frontend/src/components/Installations/InstallationForm.tsx index 4be46703b..638f9bdd7 100644 --- a/typescript/Frontend/src/components/Installations/CustomerForm.tsx +++ b/typescript/Frontend/src/components/Installations/InstallationForm.tsx @@ -4,13 +4,14 @@ import { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import axiosConfig from "../../config/axiosConfig"; import { I_Installation } from "../../util/types"; +import MoveDialog from "../Groups/Tree/MoveDialog"; import InnovenergyTextfield from "../Layout/InnovenergyTextfield"; -interface I_CustomerFormProps { +interface I_InstallationFormProps { values: I_Installation; id: string | undefined; } -const CustomerForm = (props: I_CustomerFormProps) => { +const InstallationForm = (props: I_InstallationFormProps) => { const { values, id } = props; const [open, setOpen] = useState(false); @@ -93,6 +94,7 @@ const CustomerForm = (props: I_CustomerFormProps) => { handleChange={formik.handleChange} /> + @@ -117,4 +119,4 @@ const CustomerForm = (props: I_CustomerFormProps) => { ); }; -export default CustomerForm; +export default InstallationForm; diff --git a/typescript/Frontend/src/components/Installations/InstallationList.tsx b/typescript/Frontend/src/components/Installations/InstallationList.tsx index 602f94fb6..d110f5cc2 100644 --- a/typescript/Frontend/src/components/Installations/InstallationList.tsx +++ b/typescript/Frontend/src/components/Installations/InstallationList.tsx @@ -43,7 +43,6 @@ const InstallationList = (props: InstallationListProps) => { const routeMatch = useRouteMatch([ routes.installations + routes.installation + ":id", routes.installations + routes.alarms + ":id", - routes.installations + routes.users + ":id", routes.installations + routes.log + ":id", ]); diff --git a/typescript/Frontend/src/components/Installations/InstallationPage.tsx b/typescript/Frontend/src/components/Installations/InstallationPage.tsx new file mode 100644 index 000000000..7650d1d67 --- /dev/null +++ b/typescript/Frontend/src/components/Installations/InstallationPage.tsx @@ -0,0 +1,23 @@ +import { Route, Routes } from "react-router-dom"; +import Groups from "../Groups/Groups"; +import ModeButtons from "../Layout/ModeButtons"; +import Installations from "./Installations"; +import routes from "../../routes.json"; +import { Grid } from "@mui/material"; + +const InstallationPage = () => { + return ( + <> + + + + + + + } /> + } /> + + + ); +}; +export default InstallationPage; diff --git a/typescript/Frontend/src/components/Installations/InstallationTabs.tsx b/typescript/Frontend/src/components/Installations/InstallationTabs.tsx index 1b56e574d..273173287 100644 --- a/typescript/Frontend/src/components/Installations/InstallationTabs.tsx +++ b/typescript/Frontend/src/components/Installations/InstallationTabs.tsx @@ -2,17 +2,18 @@ import * as React from "react"; import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; import Box from "@mui/material/Box"; -import { Link } from "react-router-dom"; +import { Link, Routes } from "react-router-dom"; import routes from "../../routes.json"; import useRouteMatch from "../../hooks/useRouteMatch"; import { useIntl } from "react-intl"; +import { styled } from "@mui/material"; +import { AntTabs, StyledTab } from "../../util/installation.util"; const InstallationTabs = () => { const routeMatch = useRouteMatch([ - routes.installations + routes.installation + ":id", - routes.installations + routes.alarms + ":id", - routes.installations + routes.users + ":id", - routes.installations + routes.log + ":id", + routes.installations + routes.list + routes.installation + ":id", + routes.installations + routes.list + routes.alarms + ":id", + routes.installations + routes.list + routes.log + ":id", ]); const id = routeMatch?.params?.id; @@ -22,47 +23,40 @@ const InstallationTabs = () => { return ( - - - - - - + ); diff --git a/typescript/Frontend/src/components/Installations/Installations.tsx b/typescript/Frontend/src/components/Installations/Installations.tsx index c78588d83..703c9abda 100644 --- a/typescript/Frontend/src/components/Installations/Installations.tsx +++ b/typescript/Frontend/src/components/Installations/Installations.tsx @@ -3,38 +3,40 @@ import { Container } from "@mui/system"; import { Routes, Route } from "react-router"; import NavigationButtons from "../Layout/NavigationButtons"; import Sidebar from "../Layout/Sidebar"; -import BasicTable from "../Layout/Table"; import Alarms from "./Alarms"; -import Installation from "./Installation"; import InstallationTabs from "./InstallationTabs"; import Log from "./Log"; import routes from "../../routes.json"; import InstallationContextProvider from "../Context/InstallationContextProvider"; +import Detail from "../Layout/Detail"; +import { I_Installation } from "../../util/types"; +import InstallationForm from "./InstallationForm"; const Installations = () => { return ( - - - - - - - - - - } - index - /> - } /> - } /> - } /> - - + + + - + + + + + route="/GetInstallationById?id=" + formComponent={InstallationForm} + /> + } + index + /> + } /> + } /> + + + ); }; diff --git a/typescript/Frontend/src/components/Installations/Installation.tsx b/typescript/Frontend/src/components/Layout/Detail.tsx similarity index 61% rename from typescript/Frontend/src/components/Installations/Installation.tsx rename to typescript/Frontend/src/components/Layout/Detail.tsx index 85432d2e3..33823cca1 100644 --- a/typescript/Frontend/src/components/Installations/Installation.tsx +++ b/typescript/Frontend/src/components/Layout/Detail.tsx @@ -1,22 +1,27 @@ -import { Alert, Box, CircularProgress } from "@mui/material"; +import { Box, CircularProgress, Alert } from "@mui/material"; import { AxiosError } from "axios"; -import { useEffect, useState } from "react"; +import { useState, useEffect, FC } from "react"; import { useParams } from "react-router-dom"; -import CustomerForm from "./CustomerForm"; import axiosConfig from "../../config/axiosConfig"; -import { I_Installation } from "../../util/types"; -import LocationForm from "../Groups/LocationForm"; - -const Installation = () => { +export interface I_FormProps { + values: T; + id: string; +} +interface I_DetailProps { + formComponent: FC>; + route: string; +} +const Detail = (props: I_DetailProps) => { const { id } = useParams(); - const [values, setValues] = useState(); + const { formComponent: FormComponent, route } = props; + const [values, setValues] = useState(); const [loading, setLoading] = useState(false); const [error, setError] = useState(); useEffect(() => { setLoading(true); axiosConfig - .get("/GetInstallationById?id=" + id) + .get(route + id) .then((res) => { setValues(res.data); setLoading(false); @@ -25,15 +30,10 @@ const Installation = () => { setError(err); setLoading(false); }); - }, [id]); + }, [id, route]); if (values && values.id && values.id.toString() === id) { - return ( - - - - - ); + return ; } else if (loading) { return ( { return null; }; -export default Installation; +export default Detail; diff --git a/typescript/Frontend/src/components/Layout/LanguageSelect.tsx b/typescript/Frontend/src/components/Layout/LanguageSelect.tsx index 6349befc0..ef466afb8 100644 --- a/typescript/Frontend/src/components/Layout/LanguageSelect.tsx +++ b/typescript/Frontend/src/components/Layout/LanguageSelect.tsx @@ -14,10 +14,10 @@ const LanguageSelect = (props: LanguageSelectProps) => { label="Age" onChange={(e) => props.setLanguage(e.target.value)} > - + - + diff --git a/typescript/Frontend/src/components/Layout/ModeButtons.tsx b/typescript/Frontend/src/components/Layout/ModeButtons.tsx new file mode 100644 index 000000000..71fe2b925 --- /dev/null +++ b/typescript/Frontend/src/components/Layout/ModeButtons.tsx @@ -0,0 +1,59 @@ +import { Link } from "react-router-dom"; +import useRouteMatch from "../../hooks/useRouteMatch"; +import routes from "../../routes.json"; +import ListIcon from "@mui/icons-material/List"; +import AccountTreeIcon from "@mui/icons-material/AccountTree"; +import { AntTabs, StyledTab } from "../../util/installation.util"; + +const ModeButtons = () => { + const routeMatch = useRouteMatch([ + routes.installations + routes.tree + "*", + routes.installations + routes.list + "*", + ]); + + return ( + <> + + } + value={routes.installations + routes.list + "*"} + component={Link} + to={routes.list} + /> + } + value={routes.installations + routes.tree + "*"} + component={Link} + to={routes.tree} + /> + + {/* + + + + + + + */} + + ); +}; + +export default ModeButtons; diff --git a/typescript/Frontend/src/components/Layout/NavigationButtons.tsx b/typescript/Frontend/src/components/Layout/NavigationButtons.tsx index 66c53a8e8..1c309705f 100644 --- a/typescript/Frontend/src/components/Layout/NavigationButtons.tsx +++ b/typescript/Frontend/src/components/Layout/NavigationButtons.tsx @@ -7,7 +7,7 @@ import routes from "../../routes.json"; const NavigationButtons = () => { const routeMatch = useRouteMatch([ routes.installations + "*", - routes.groups + "*", + routes.users + "*", ]); return ( @@ -16,23 +16,21 @@ const NavigationButtons = () => { value={routeMatch?.pattern?.path} exclusive sx={{ mb: 1 }} + fullWidth > - + - + ); diff --git a/typescript/Frontend/src/components/Layout/Sidebar.tsx b/typescript/Frontend/src/components/Layout/Sidebar.tsx index 35c666f54..5b19d2462 100644 --- a/typescript/Frontend/src/components/Layout/Sidebar.tsx +++ b/typescript/Frontend/src/components/Layout/Sidebar.tsx @@ -20,6 +20,7 @@ const Sidebar = () => { value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} /> + ); diff --git a/typescript/Frontend/src/components/Users/UserForm.tsx b/typescript/Frontend/src/components/Users/UserForm.tsx new file mode 100644 index 000000000..62255d090 --- /dev/null +++ b/typescript/Frontend/src/components/Users/UserForm.tsx @@ -0,0 +1,87 @@ +import { Alert, Button, Grid, Snackbar } from "@mui/material"; +import { useFormik } from "formik"; +import { useState } from "react"; +import { FormattedMessage, useIntl } from "react-intl"; +import axiosConfig from "../../config/axiosConfig"; +import { I_User } from "../../util/user.util"; +import InnovenergyTextfield from "../Layout/InnovenergyTextfield"; + +interface I_UserFormProps { + values: I_User; + id: string; +} +const UserForm = (props: I_UserFormProps) => { + const { values, id } = props; + const [open, setOpen] = useState(false); + + const intl = useIntl(); + + const formik = useFormik({ + initialValues: { + email: values.email, + information: values.information, + }, + onSubmit: (formikValues) => { + axiosConfig + .put("/UpdateUser", { + ...formikValues, + id: id, + }) + .then(() => { + setOpen(true); + }); + }, + }); + + const handleClose = () => { + setOpen(false); + }; + + return ( +
+ + + + + + + + + + + + ); +}; + +export default UserForm; diff --git a/typescript/Frontend/src/components/Users/UserList.tsx b/typescript/Frontend/src/components/Users/UserList.tsx new file mode 100644 index 000000000..094d6df9f --- /dev/null +++ b/typescript/Frontend/src/components/Users/UserList.tsx @@ -0,0 +1,77 @@ +import List from "@mui/material/List"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemText from "@mui/material/ListItemText"; +import { Divider } from "@mui/material"; +import { Link } from "react-router-dom"; +import useRouteMatch from "../../hooks/useRouteMatch"; +import routes from "../../routes.json"; +import { Fragment, useContext } from "react"; +import { I_Installation } from "../../util/types"; +import { UserContext } from "../Context/UserContextProvider"; + +const getPathWithoutId = (path?: string) => { + if (path) { + const splitString = path.split(":"); + return splitString[0]; + } + return routes.user; +}; + +const filterData = ( + searchQuery: string, + data: I_Installation[] | undefined +) => { + if (data) { + return data.filter( + (installation) => + installation.name.toLowerCase().includes(searchQuery.toLowerCase()) || + installation.location.toLowerCase().includes(searchQuery.toLowerCase()) + ); + } + return data; +}; + +const UserList = () => { + const { availableUsers } = useContext(UserContext); + + const routeMatch = useRouteMatch([routes.users + routes.user + ":id"]); + + if (availableUsers && availableUsers.length) { + return ( + + {availableUsers.map((user) => { + return ( + + + + + + + + + ); + })} + + ); + } + return null; +}; + +export default UserList; diff --git a/typescript/Frontend/src/components/Users/UserTabs.tsx b/typescript/Frontend/src/components/Users/UserTabs.tsx new file mode 100644 index 000000000..f9955f47e --- /dev/null +++ b/typescript/Frontend/src/components/Users/UserTabs.tsx @@ -0,0 +1,41 @@ +import * as React from "react"; +import Tabs from "@mui/material/Tabs"; +import Tab from "@mui/material/Tab"; +import Box from "@mui/material/Box"; +import { Link } from "react-router-dom"; +import routes from "../../routes.json"; +import useRouteMatch from "../../hooks/useRouteMatch"; +import { useIntl } from "react-intl"; + +const UserTabs = () => { + const routeMatch = useRouteMatch([routes.users + routes.user + ":id"]); + + const id = routeMatch?.params?.id; + const intl = useIntl(); + + if (id) { + return ( + + + + + + + + ); + } + return null; +}; + +export default UserTabs; diff --git a/typescript/Frontend/src/components/Users/Users.tsx b/typescript/Frontend/src/components/Users/Users.tsx new file mode 100644 index 000000000..0499b1974 --- /dev/null +++ b/typescript/Frontend/src/components/Users/Users.tsx @@ -0,0 +1,39 @@ +import { Grid } from "@mui/material"; +import { Container } from "@mui/system"; +import { Routes, Route } from "react-router"; +import routes from "../../routes.json"; +import { I_User } from "../../util/user.util"; +import UserContextProvider from "../Context/UserContextProvider"; +import Detail from "../Layout/Detail"; +import UserForm from "./UserForm"; +import UserList from "./UserList"; +import UserTabs from "./UserTabs"; + +const Users = () => { + return ( + + + + + + + + + + route="/GetUserById?id=" + formComponent={UserForm} + /> + } + index + /> + + + + + ); +}; + +export default Users; diff --git a/typescript/Frontend/src/routes.json b/typescript/Frontend/src/routes.json index 619e97e29..27947ff1b 100644 --- a/typescript/Frontend/src/routes.json +++ b/typescript/Frontend/src/routes.json @@ -1,10 +1,14 @@ { "installation": "installation/", "alarms": "alarms/", - "users": "users/", + "users": "/users/", "log": "log/", "installations": "/installations/", "groups": "/groups/", "group": "group/", - "folder": "folder/" + "folder": "folder/", + "manageAccess": "manageAccess/", + "user": "user/", + "tree": "tree/", + "list": "list/" } diff --git a/typescript/Frontend/src/util/installation.util.tsx b/typescript/Frontend/src/util/installation.util.tsx new file mode 100644 index 000000000..e63e20506 --- /dev/null +++ b/typescript/Frontend/src/util/installation.util.tsx @@ -0,0 +1,46 @@ +import { styled, Tab, Tabs } from "@mui/material"; + +export const StyledTab = styled((props: any) => ( + +))(({ theme }) => ({ + textTransform: "none", + fontWeight: theme.typography.fontWeightRegular, + fontSize: theme.typography.pxToRem(15), + marginRight: theme.spacing(1), + color: "#0d6efd", + background: "0 0", + border: "1px solid transparent", + borderTopLeftRadius: "0.25rem", + borderTopRightRadius: "0.25rem", + padding: ".5rem 1rem", + textDecoration: "none", + transition: `color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out`, + "&.Mui-selected": { + color: "#495057", + backgroundColor: "#fff", + borderColor: "#dee2e6 #dee2e6 #fff", + marginBottom: "-2px", + }, + "&.Mui-focusVisible": { + backgroundColor: "rgba(100, 95, 228, 0.32)", + }, +})); + +export const AntTabs = styled(Tabs)({ + borderBottom: "1px solid #dee2e6", + + overflow: "visible!important", + "& div.MuiTabs-scroller": { + overflow: "visible!important", + }, + "&.Mui-selected": { + color: "#495057", + backgroundColor: "red", + borderColor: `#dee2e6 #dee2e6 #fff`, + }, + "& .MuiTabs-indicator": { + display: "flex", + justifyContent: "center", + backgroundColor: "transparent", + }, +}); diff --git a/typescript/Frontend/src/util/user.util.tsx b/typescript/Frontend/src/util/user.util.tsx index 7f1e2bed8..e2e4ca1e9 100644 --- a/typescript/Frontend/src/util/user.util.tsx +++ b/typescript/Frontend/src/util/user.util.tsx @@ -1,4 +1,4 @@ -export interface User { +export interface I_User { email: string; hasWriteAccess: boolean; id: number; @@ -14,7 +14,7 @@ export interface User { export interface UserWithInheritedAccess { folderId: number; folderName: string; - user: User; + user: I_User; } export const filterDuplicateUsers = (data: any[]) => {