[WIP] add users route, change routing structure, change styling of tabs

This commit is contained in:
Sina Blattmann 2023-03-29 15:30:06 +02:00
parent 2a664ac213
commit 52eff388a5
30 changed files with 799 additions and 231 deletions

View File

@ -1,7 +1,7 @@
import useToken from "./hooks/useToken"; import useToken from "./hooks/useToken";
import Login from "./Login"; import Login from "./Login";
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; 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 routes from "./routes.json";
import { IntlProvider } from "react-intl"; import { IntlProvider } from "react-intl";
import { useState } from "react"; import { useState } from "react";
@ -10,21 +10,23 @@ import de from "./lang/de.json";
import Installations from "./components/Installations/Installations"; import Installations from "./components/Installations/Installations";
import LanguageSelect from "./components/Layout/LanguageSelect"; import LanguageSelect from "./components/Layout/LanguageSelect";
import LogoutButton from "./components/Layout/LogoutButton"; 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 App = () => {
const { token, setToken, removeToken } = useToken(); const { token, setToken, removeToken } = useToken();
const [language, setLanguage] = useState("en"); const [language, setLanguage] = useState("EN");
const getTranslations = () => { const getTranslations = () => {
if (language === "de") { if (language === "DE") {
return de; return de;
} }
return en; return en;
}; };
if (!token) { if (!token) {
return <Login setToken={setToken} />; return <Login setToken={setToken} setLanguage={setLanguage} />;
} }
return ( return (
@ -32,23 +34,36 @@ const App = () => {
<IntlProvider <IntlProvider
messages={getTranslations()} messages={getTranslations()}
locale={language} locale={language}
defaultLocale="en" defaultLocale="EN"
> >
<Grid container justifyContent="flex-end" sx={{ px: 1, pt: 1 }}> <Container maxWidth="xl" sx={{ margin: 2 }}>
<LanguageSelect language={language} setLanguage={setLanguage} /> <Grid container spacing={2}>
<LogoutButton removeToken={removeToken} /> <Grid item xs={3}>
</Grid> <NavigationButtons />
<Routes> </Grid>
<Route <Grid
path="*" item
element={<Navigate to={routes.installations} replace />} xs={9}
/> container
<Route justifyContent="flex-end"
path={routes.installations + "*"} sx={{ px: 1, pt: 1 }}
element={<Installations />} >
/> <LanguageSelect language={language} setLanguage={setLanguage} />
<Route path={routes.groups + "*"} element={<Groups />} /> <LogoutButton removeToken={removeToken} />
</Routes> </Grid>
</Grid>
<Routes>
<Route
path="*"
element={<Navigate to={routes.installations} replace />}
/>
<Route
path={routes.installations + "*"}
element={<InstallationPage />}
/>
<Route path={routes.users + "*"} element={<Users />} />
</Routes>
</Container>
</IntlProvider> </IntlProvider>
</BrowserRouter> </BrowserRouter>
); );

View File

@ -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 [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -31,6 +36,7 @@ const Login = ({ setToken }: { setToken: (value: string) => void }) => {
.then(() => { .then(() => {
setToken(data.token); setToken(data.token);
setLoading(false); setLoading(false);
setLanguage(data.user.language);
}) })
.catch((err) => { .catch((err) => {
setError(err); setError(err);

View File

@ -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<void>;
fetchUsersWithDirectAccessForResource: () => Promise<void>;
}
export const UserContext = createContext<UserContextProviderProps>({
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<I_User[]>([]);
const [inheritedAccessUsers, setInheritedAccessUsers] = useState<
UserWithInheritedAccess[]
>([]);
const [availableUsers, setAvailableUsers] = useState<I_User[]>([]);
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 (
<UserContext.Provider
value={{
directAccessUsers,
setDirectAccessUsers,
inheritedAccessUsers,
setInheritedAccessUsers,
availableUsers,
setAvailableUsers,
getAvailableUsers: getAvailableUsersForResource,
fetchUsersWithInheritedAccessForResource,
fetchUsersWithDirectAccessForResource,
}}
>
{children}
</UserContext.Provider>
);
};
export default UserContextProvider;

View File

@ -1,23 +1,24 @@
import { Grid } from "@mui/material"; import { Grid } from "@mui/material";
import AvailableUserList from "./AvailableUserList"; import UserContextProvider from "../../Context/UserContextProvider";
import InnovenergyList from "./UserList"; import AvailableUserDialog from "./AvailableUserDialog";
import InnovenergyList from "./InnovenergyList";
import UsersWithDirectAccess from "./UsersWithDirectAccess"; import UsersWithDirectAccess from "./UsersWithDirectAccess";
import UsersWithInheritedAccess from "./UsersWithInheritedAccess"; import UsersWithInheritedAccess from "./UsersWithInheritedAccess";
const Users = () => { const AccessManagement = () => {
return ( return (
<> <UserContextProvider>
<AvailableUserList />
<Grid container sx={{ mt: 1 }}> <Grid container sx={{ mt: 1 }}>
<Grid item xs={6}> <Grid item xs={6}>
<AvailableUserDialog />
<InnovenergyList> <InnovenergyList>
<UsersWithDirectAccess /> <UsersWithDirectAccess />
<UsersWithInheritedAccess /> <UsersWithInheritedAccess />
</InnovenergyList> </InnovenergyList>
</Grid> </Grid>
</Grid> </Grid>
</> </UserContextProvider>
); );
}; };
export default Users; export default AccessManagement;

View File

@ -1,23 +1,32 @@
import { Autocomplete, Button, Grid, TextField } from "@mui/material"; import {
import { useContext, useEffect, useState } from "react"; 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 { FormattedMessage } from "react-intl";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import axiosConfig from "../../../config/axiosConfig"; import axiosConfig from "../../../config/axiosConfig";
import { User } from "../../../util/user.util"; import { I_User } from "../../../util/user.util";
import { GroupContext } from "../../Context/GroupContextProvider"; import { GroupContext } from "../../Context/GroupContextProvider";
import { UserContext } from "../../Context/UserContextProvider";
const AvailableUserList = () => { const AvailableUserDialog = () => {
const [users, setUsers] = useState<User[]>([]); const [selectedUsers, setSelectedUsers] = useState<I_User[]>([]);
const [selectedUsers, setSelectedUsers] = useState<User[]>([]); const [open, setOpen] = useState(false);
const { currentType } = useContext(GroupContext); const { currentType } = useContext(GroupContext);
const { id } = useParams(); const { id } = useParams();
const {
getAvailableUsers,
fetchUsersWithDirectAccessForResource,
fetchUsersWithInheritedAccessForResource,
} = useContext(UserContext);
useEffect(() => {
axiosConfig.get("/GetAllChildUsers").then((res) => {
setUsers(res.data);
});
}, []);
const handleGrant = () => { const handleGrant = () => {
selectedUsers.forEach((user) => { selectedUsers.forEach((user) => {
const bodyProp = currentType === "Folder" ? "folderId" : "installationId"; const bodyProp = currentType === "Folder" ? "folderId" : "installationId";
@ -29,20 +38,32 @@ const AvailableUserList = () => {
: "/GrantUserAccessToInstallation", : "/GrantUserAccessToInstallation",
{ userId: user.id, [bodyProp]: elementId } { userId: user.id, [bodyProp]: elementId }
) )
.then((res) => { .then(() => {
console.log(res); fetchUsersWithDirectAccessForResource();
fetchUsersWithInheritedAccessForResource();
}); });
}); });
}; };
if (users) {
return ( return (
<> <>
<Grid container sx={{ pt: 1 }}> <Dialog
onClose={() => setOpen(false)}
aria-labelledby="customized-dialog-title"
open={open}
sx={{
".MuiDialogContent-root": { overflowX: "hidden" },
maxHeight: 500,
}}
scroll="paper"
>
<DialogTitle>Create new folder</DialogTitle>
<DialogContent>
<Autocomplete <Autocomplete
sx={{ width: "500px" }} sx={{ width: "500px" }}
multiple multiple
id="tags-standard" id="tags-standard"
options={users} options={getAvailableUsers()}
getOptionLabel={(option) => option.name} getOptionLabel={(option) => option.name}
renderInput={(params) => ( renderInput={(params) => (
<TextField <TextField
@ -54,6 +75,8 @@ const AvailableUserList = () => {
)} )}
onChange={(event, values) => setSelectedUsers(values)} onChange={(event, values) => setSelectedUsers(values)}
/> />
</DialogContent>
<DialogActions>
<Button <Button
variant="outlined" variant="outlined"
type="submit" type="submit"
@ -62,11 +85,13 @@ const AvailableUserList = () => {
> >
<FormattedMessage id="grantAccess" defaultMessage="Grant access" /> <FormattedMessage id="grantAccess" defaultMessage="Grant access" />
</Button> </Button>
</Grid> </DialogActions>
</> </Dialog>
); <Button variant="outlined" type="submit" onClick={() => setOpen(true)}>
} <FormattedMessage id="grantAccess" defaultMessage="Grant access" />
return null; </Button>
</>
);
}; };
export default AvailableUserList; export default AvailableUserDialog;

View File

@ -6,31 +6,23 @@ import {
Avatar, Avatar,
ListItemAvatar, ListItemAvatar,
} from "@mui/material"; } from "@mui/material";
import { Fragment, useCallback, useContext, useEffect, useState } from "react"; import { Fragment, useContext, useEffect } from "react";
import axiosConfig from "../../../config/axiosConfig"; import axiosConfig from "../../../config/axiosConfig";
import { User } from "../../../util/user.util";
import PersonRemoveIcon from "@mui/icons-material/PersonRemove"; 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 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 UsersWithDirectAccess = () => {
const [users, setUsers] = useState<User[]>(); const { fetchUsersWithDirectAccessForResource, directAccessUsers } =
useContext(UserContext);
const { currentType } = useContext(GroupContext); const { currentType } = useContext(GroupContext);
const { id } = useParams(); const { id } = useParams();
const fetchUsers = useCallback(async () => {
axiosConfig
.get("/GetUsersWithDirectAccessTo" + currentType, { params: { id } })
.then((res) => {
setUsers(res.data);
});
}, [currentType, id]);
useEffect(() => { useEffect(() => {
fetchUsers(); fetchUsersWithDirectAccessForResource();
}, [fetchUsers]); }, [fetchUsersWithDirectAccessForResource]);
const handleIconClick = (userId: number) => { const handleIconClick = (userId: number) => {
const folderId = id ? parseInt(id) : ""; const folderId = id ? parseInt(id) : "";
@ -40,14 +32,14 @@ const UsersWithDirectAccess = () => {
folderId: folderId, folderId: folderId,
}) })
.then((res) => { .then((res) => {
fetchUsers(); fetchUsersWithDirectAccessForResource();
}); });
}; };
if (users && users.length) { if (directAccessUsers && directAccessUsers.length) {
return ( return (
<> <>
{users.map((user) => { {directAccessUsers.map((user) => {
return ( return (
<Fragment key={user.id}> <Fragment key={user.id}>
<ListItem <ListItem

View File

@ -5,39 +5,28 @@ import {
ListItemAvatar, ListItemAvatar,
Avatar, Avatar,
} from "@mui/material"; } from "@mui/material";
import { Fragment, useCallback, useContext, useEffect, useState } from "react"; import { Fragment, useContext, useEffect } from "react";
import { Link, useParams } from "react-router-dom"; import { Link } from "react-router-dom";
import axiosConfig from "../../../config/axiosConfig";
import { import {
filterDuplicateUsers, filterDuplicateUsers,
UserWithInheritedAccess, UserWithInheritedAccess,
} from "../../../util/user.util"; } from "../../../util/user.util";
import { GroupContext } from "../../Context/GroupContextProvider";
import routes from "../../../routes.json"; import routes from "../../../routes.json";
import PersonIcon from "@mui/icons-material/Person"; import PersonIcon from "@mui/icons-material/Person";
import { UserContext } from "../../Context/UserContextProvider";
const UsersWithInheritedAccess = () => { const UsersWithInheritedAccess = () => {
const [users, setUsers] = useState<UserWithInheritedAccess[]>(); const { fetchUsersWithInheritedAccessForResource, inheritedAccessUsers } =
useContext(UserContext);
const { currentType } = useContext(GroupContext);
const { id } = useParams();
const fetchUsers = useCallback(async () => {
axiosConfig
.get("/GetUsersWithInheritedAccessTo" + currentType, { params: { id } })
.then((res) => {
setUsers(res.data);
});
}, [currentType, id]);
useEffect(() => { useEffect(() => {
fetchUsers(); fetchUsersWithInheritedAccessForResource();
}, [fetchUsers]); }, [fetchUsersWithInheritedAccessForResource]);
if (users && users.length) { if (inheritedAccessUsers && inheritedAccessUsers.length) {
return ( return (
<> <>
{filterDuplicateUsers(users).map( {filterDuplicateUsers(inheritedAccessUsers).map(
({ user, folderName }: UserWithInheritedAccess) => { ({ user, folderName }: UserWithInheritedAccess) => {
return ( return (
<Fragment key={user.id}> <Fragment key={user.id}>
@ -52,7 +41,11 @@ const UsersWithInheritedAccess = () => {
secondary={ secondary={
<> <>
Inherited access from{" "} Inherited access from{" "}
<Link to={routes.groups + routes.users + user.parentId}> <Link
to={
routes.groups + routes.manageAccess + user.parentId
}
>
{folderName} {folderName}
</Link> </Link>
</> </>

View File

@ -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<I_Folder>) => {
return axiosConfig.post("/CreateFolder", {
...childData,
parentId: data.id,
});
};
return (
<>
<Button variant="outlined" sx={{ ml: 2 }} onClick={() => setOpen(true)}>
<FormattedMessage id="addNewChild" defaultMessage="Add new child" />
</Button>
<Dialog
onClose={() => setOpen(false)}
aria-labelledby="customized-dialog-title"
open={open}
sx={{
".MuiDialogContent-root": { overflowX: "hidden" },
maxHeight: 500,
}}
scroll="paper"
>
<DialogTitle>Create new folder</DialogTitle>
<DialogContent>
<FolderForm values={props.values} handleSubmit={handleSubmit} />
</DialogContent>
</Dialog>
</>
);
};
export default AddNewDialog;

View File

@ -3,12 +3,14 @@ import { AxiosError } from "axios";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import axiosConfig from "../../config/axiosConfig"; 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 FolderForm from "./FolderForm";
import MoveDialog from "./Tree/MoveDialog";
const Folder = () => { const Folder = () => {
const { id } = useParams(); const { id } = useParams();
const [values, setValues] = useState<I_Installation>(); const [values, setValues] = useState<I_Folder>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<AxiosError>(); const [error, setError] = useState<AxiosError>();
@ -26,11 +28,22 @@ const Folder = () => {
}); });
}, [id]); }, [id]);
const handleSubmit = (data: I_Folder, formikValues: Partial<I_Folder>) => {
return axiosConfig.put("/UpdateFolder", { ...data, ...formikValues });
};
if (values && values.id && values.id.toString() === id) { if (values && values.id && values.id.toString() === id) {
const moveDialog = <MoveDialog values={values} />;
const addNewDialog = <AddNewDialog values={values} />;
return ( return (
<> <>
<Box sx={{ py: 3 }}> <Box sx={{ py: 3 }}>
<FolderForm values={values} id={id} /> <FolderForm
values={values}
handleSubmit={handleSubmit}
additionalButtons={[moveDialog, addNewDialog]}
/>
</Box> </Box>
</> </>
); );

View File

@ -1,25 +1,24 @@
import { Button, CircularProgress, Grid } from "@mui/material"; import { Button, CircularProgress, Grid } from "@mui/material";
import { AxiosResponse } from "axios";
import { useFormik } from "formik"; import { useFormik } from "formik";
import { useContext, useState } from "react"; import { ReactNode, useContext, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import axiosConfig from "../../config/axiosConfig";
import { I_Folder } from "../../util/types"; import { I_Folder } from "../../util/types";
import { GroupContext } from "../Context/GroupContextProvider"; import { GroupContext } from "../Context/GroupContextProvider";
import InnovenergySnackbar from "../InnovenergySnackbar"; import InnovenergySnackbar from "../InnovenergySnackbar";
import InnovenergyTextfield from "../Layout/InnovenergyTextfield"; import InnovenergyTextfield from "../Layout/InnovenergyTextfield";
import MoveDialog from "./Tree/MoveDialog";
interface I_CustomerFormProps { interface I_CustomerFormProps {
values: I_Folder; values: I_Folder;
id: string; handleSubmit: (
data: I_Folder,
formikValues: Partial<I_Folder>
) => Promise<AxiosResponse>;
additionalButtons?: ReactNode[];
} }
const updateFolder = (data: I_Folder) => {
return axiosConfig.put("/UpdateFolder", data);
};
const FolderForm = (props: I_CustomerFormProps) => { const FolderForm = (props: I_CustomerFormProps) => {
const { values, id } = props; const { values, additionalButtons, handleSubmit } = props;
const intl = useIntl(); const intl = useIntl();
const { fetchData } = useContext(GroupContext); const { fetchData } = useContext(GroupContext);
@ -34,14 +33,8 @@ const FolderForm = (props: I_CustomerFormProps) => {
}, },
onSubmit: (formikValues) => { onSubmit: (formikValues) => {
setLoading(true); setLoading(true);
const idAsNumber = parseInt(id, 10); handleSubmit(values, formikValues)
updateFolder({ .then(() => {
...values,
...formikValues,
id: idAsNumber,
})
.then((res) => {
// TODO figure out why this isnt refreshing tree
fetchData(); fetchData();
setSnackbarOpen(true); setSnackbarOpen(true);
setLoading(false); setLoading(false);
@ -79,12 +72,9 @@ const FolderForm = (props: I_CustomerFormProps) => {
/> />
<Grid container justifyContent="flex-end" sx={{ pt: 1 }}> <Grid container justifyContent="flex-end" sx={{ pt: 1 }}>
{loading && <CircularProgress />} {loading && <CircularProgress />}
<MoveDialog values={values} /> {additionalButtons && additionalButtons.map((button) => button)}
<Button variant="outlined" type="submit" sx={{ ml: 2 }}> <Button variant="outlined" type="submit" sx={{ ml: 2 }}>
<FormattedMessage <FormattedMessage id="submit" defaultMessage="Submit" />
id="applyChanges"
defaultMessage="Apply changes"
/>
</Button> </Button>
</Grid> </Grid>
<InnovenergySnackbar <InnovenergySnackbar

View File

@ -1,34 +1,33 @@
import * as React from "react";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import routes from "../../routes.json"; import routes from "../../routes.json";
import useRouteMatch from "../../hooks/useRouteMatch"; import useRouteMatch from "../../hooks/useRouteMatch";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { GroupContext } from "../Context/GroupContextProvider"; import { GroupContext } from "../Context/GroupContextProvider";
import { useContext } from "react";
import { AntTabs, StyledTab } from "../../util/installation.util";
const GroupTabs = () => { const GroupTabs = () => {
const routeMatch = useRouteMatch([ const routeMatch = useRouteMatch([
routes.groups + routes.folder + ":id", routes.groups + routes.folder + ":id",
routes.groups + routes.users + ":id", routes.groups + routes.manageAccess + ":id",
routes.groups + routes.installation + ":id", routes.groups + routes.installation + ":id",
]); ]);
const id = routeMatch?.params?.id; const id = routeMatch?.params?.id;
const intl = useIntl(); const intl = useIntl();
const { currentType } = React.useContext(GroupContext); const { currentType } = useContext(GroupContext);
if (id) { if (id) {
return ( return (
<Box sx={{ width: "100%" }}> <Box sx={{ width: "100%" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}> <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs <AntTabs
value={routeMatch?.pattern?.path} value={routeMatch?.pattern?.path}
aria-label="basic tabs example" aria-label="basic tabs example"
> >
{currentType === "Folder" ? ( {currentType === "Folder" ? (
<Tab <StyledTab
label={intl.formatMessage({ label={intl.formatMessage({
id: "folder", id: "folder",
defaultMessage: "Folder", defaultMessage: "Folder",
@ -38,7 +37,7 @@ const GroupTabs = () => {
to={routes.folder + id} to={routes.folder + id}
/> />
) : ( ) : (
<Tab <StyledTab
label={intl.formatMessage({ label={intl.formatMessage({
id: "installation", id: "installation",
defaultMessage: "Installation", defaultMessage: "Installation",
@ -49,16 +48,16 @@ const GroupTabs = () => {
/> />
)} )}
<Tab <StyledTab
label={intl.formatMessage({ label={intl.formatMessage({
id: "users", id: "manageAccess",
defaultMessage: "Users", defaultMessage: "Manage access",
})} })}
value={routes.groups + routes.users + ":id"} value={routes.groups + routes.manageAccess + ":id"}
component={Link} component={Link}
to={routes.users + id} to={routes.manageAccess + id}
/> />
</Tabs> </AntTabs>
</Box> </Box>
</Box> </Box>
); );

View File

@ -2,36 +2,43 @@ import { Grid } from "@mui/material";
import { Container } from "@mui/system"; import { Container } from "@mui/system";
import { Routes, Route } from "react-router"; import { Routes, Route } from "react-router";
import routes from "../../routes.json"; import routes from "../../routes.json";
import Installation from "../Installations/Installation";
import Folder from "./Folder"; import Folder from "./Folder";
import GroupTabs from "./GroupTabs"; import GroupTabs from "./GroupTabs";
import GroupContextProvider from "../Context/GroupContextProvider"; import GroupContextProvider from "../Context/GroupContextProvider";
import GroupTree from "./Tree/GroupTree"; import GroupTree from "./Tree/GroupTree";
import NavigationButtons from "../Layout/NavigationButtons"; 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 = () => { const Groups = () => {
return ( return (
<GroupContextProvider> <GroupContextProvider>
<Container maxWidth="xl"> <Grid container spacing={2}>
<Grid container spacing={2}> <Grid item xs={3}>
<Grid item xs={3}> <GroupTree />
<NavigationButtons />
<GroupTree />
</Grid>
<Grid item xs={9}>
<GroupTabs />
<Routes>
<Route path={routes.folder + ":id"} element={<Folder />} index />
<Route path={routes.users + ":id"} element={<Users />} />
<Route
path={routes.installation + ":id"}
element={<Installation />}
/>
</Routes>
</Grid>
</Grid> </Grid>
</Container> <Grid item xs={9}>
<GroupTabs />
<Routes>
<Route path={routes.folder + ":id"} element={<Folder />} index />
<Route
path={routes.manageAccess + ":id"}
element={<AccessManagement />}
/>
<Route
path={routes.installation + ":id"}
element={
<Detail<I_Installation>
route="/GetInstallationById?id="
formComponent={InstallationForm}
/>
}
/>
</Routes>
</Grid>
</Grid>
</GroupContextProvider> </GroupContextProvider>
); );
}; };

View File

@ -4,13 +4,14 @@ import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import axiosConfig from "../../config/axiosConfig"; import axiosConfig from "../../config/axiosConfig";
import { I_Installation } from "../../util/types"; import { I_Installation } from "../../util/types";
import MoveDialog from "../Groups/Tree/MoveDialog";
import InnovenergyTextfield from "../Layout/InnovenergyTextfield"; import InnovenergyTextfield from "../Layout/InnovenergyTextfield";
interface I_CustomerFormProps { interface I_InstallationFormProps {
values: I_Installation; values: I_Installation;
id: string | undefined; id: string | undefined;
} }
const CustomerForm = (props: I_CustomerFormProps) => { const InstallationForm = (props: I_InstallationFormProps) => {
const { values, id } = props; const { values, id } = props;
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -93,6 +94,7 @@ const CustomerForm = (props: I_CustomerFormProps) => {
handleChange={formik.handleChange} handleChange={formik.handleChange}
/> />
<Grid container justifyContent="flex-end" sx={{ pt: 1 }}> <Grid container justifyContent="flex-end" sx={{ pt: 1 }}>
<MoveDialog values={values} />
<Button variant="outlined" type="submit"> <Button variant="outlined" type="submit">
<FormattedMessage id="applyChanges" defaultMessage="Apply changes" /> <FormattedMessage id="applyChanges" defaultMessage="Apply changes" />
</Button> </Button>
@ -117,4 +119,4 @@ const CustomerForm = (props: I_CustomerFormProps) => {
); );
}; };
export default CustomerForm; export default InstallationForm;

View File

@ -43,7 +43,6 @@ const InstallationList = (props: InstallationListProps) => {
const routeMatch = useRouteMatch([ const routeMatch = useRouteMatch([
routes.installations + routes.installation + ":id", routes.installations + routes.installation + ":id",
routes.installations + routes.alarms + ":id", routes.installations + routes.alarms + ":id",
routes.installations + routes.users + ":id",
routes.installations + routes.log + ":id", routes.installations + routes.log + ":id",
]); ]);

View File

@ -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 (
<>
<Grid container spacing={2}>
<Grid item xs={3}>
<ModeButtons />
</Grid>
</Grid>
<Routes>
<Route path={routes.tree + "*"} element={<Groups />} />
<Route path={routes.list + "*"} element={<Installations />} />
</Routes>
</>
);
};
export default InstallationPage;

View File

@ -2,17 +2,18 @@ import * as React from "react";
import Tabs from "@mui/material/Tabs"; import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab"; import Tab from "@mui/material/Tab";
import Box from "@mui/material/Box"; 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 routes from "../../routes.json";
import useRouteMatch from "../../hooks/useRouteMatch"; import useRouteMatch from "../../hooks/useRouteMatch";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { styled } from "@mui/material";
import { AntTabs, StyledTab } from "../../util/installation.util";
const InstallationTabs = () => { const InstallationTabs = () => {
const routeMatch = useRouteMatch([ const routeMatch = useRouteMatch([
routes.installations + routes.installation + ":id", routes.installations + routes.list + routes.installation + ":id",
routes.installations + routes.alarms + ":id", routes.installations + routes.list + routes.alarms + ":id",
routes.installations + routes.users + ":id", routes.installations + routes.list + routes.log + ":id",
routes.installations + routes.log + ":id",
]); ]);
const id = routeMatch?.params?.id; const id = routeMatch?.params?.id;
@ -22,47 +23,40 @@ const InstallationTabs = () => {
return ( return (
<Box sx={{ width: "100%" }}> <Box sx={{ width: "100%" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}> <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs <AntTabs
value={routeMatch?.pattern?.path ?? routes.installation + ":id"} value={routeMatch?.pattern?.path ?? routes.installation + ":id"}
aria-label="basic tabs example" aria-label="basic tabs example"
> >
<Tab <StyledTab
label={intl.formatMessage({ label={intl.formatMessage({
id: "installation", id: "installation",
defaultMessage: "Installation", defaultMessage: "Installation",
})} })}
value={routes.installations + routes.installation + ":id"} value={
routes.installations + routes.list + routes.installation + ":id"
}
component={Link} component={Link}
to={routes.installation + id} to={routes.installation + id}
/> />
<Tab <StyledTab
label={intl.formatMessage({ label={intl.formatMessage({
id: "alarms", id: "alarms",
defaultMessage: "Alarms", defaultMessage: "Alarms",
})} })}
value={routes.installations + routes.alarms + ":id"} value={routes.installations + routes.list + routes.alarms + ":id"}
component={Link} component={Link}
to={routes.alarms + id} to={routes.alarms + id}
/> />
<Tab <StyledTab
label={intl.formatMessage({
id: "users",
defaultMessage: "Users",
})}
value={routes.installations + routes.users + ":id"}
component={Link}
to={routes.users + id}
/>
<Tab
label={intl.formatMessage({ label={intl.formatMessage({
id: "log", id: "log",
defaultMessage: "Log", defaultMessage: "Log",
})} })}
value={routes.installations + routes.log + ":id"} value={routes.installations + routes.list + routes.log + ":id"}
component={Link} component={Link}
to={routes.log + id} to={routes.log + id}
/> />
</Tabs> </AntTabs>
</Box> </Box>
</Box> </Box>
); );

View File

@ -3,38 +3,40 @@ import { Container } from "@mui/system";
import { Routes, Route } from "react-router"; import { Routes, Route } from "react-router";
import NavigationButtons from "../Layout/NavigationButtons"; import NavigationButtons from "../Layout/NavigationButtons";
import Sidebar from "../Layout/Sidebar"; import Sidebar from "../Layout/Sidebar";
import BasicTable from "../Layout/Table";
import Alarms from "./Alarms"; import Alarms from "./Alarms";
import Installation from "./Installation";
import InstallationTabs from "./InstallationTabs"; import InstallationTabs from "./InstallationTabs";
import Log from "./Log"; import Log from "./Log";
import routes from "../../routes.json"; import routes from "../../routes.json";
import InstallationContextProvider from "../Context/InstallationContextProvider"; import InstallationContextProvider from "../Context/InstallationContextProvider";
import Detail from "../Layout/Detail";
import { I_Installation } from "../../util/types";
import InstallationForm from "./InstallationForm";
const Installations = () => { const Installations = () => {
return ( return (
<InstallationContextProvider> <InstallationContextProvider>
<Container maxWidth="xl"> <Grid container spacing={2}>
<Grid container spacing={4}> <Grid item xs={3}>
<Grid item xs={3}> <Sidebar />
<NavigationButtons />
<Sidebar />
</Grid>
<Grid item xs={9}>
<InstallationTabs />
<Routes>
<Route
path={routes.installation + ":id"}
element={<Installation />}
index
/>
<Route path={routes.alarms + ":id"} element={<Alarms />} />
<Route path={routes.users + ":id"} element={<BasicTable />} />
<Route path={routes.log + ":id"} element={<Log />} />
</Routes>
</Grid>
</Grid> </Grid>
</Container> <Grid item xs={9}>
<InstallationTabs />
<Routes>
<Route
path={routes.installation + ":id"}
element={
<Detail<I_Installation>
route="/GetInstallationById?id="
formComponent={InstallationForm}
/>
}
index
/>
<Route path={routes.alarms + ":id"} element={<Alarms />} />
<Route path={routes.log + ":id"} element={<Log />} />
</Routes>
</Grid>
</Grid>
</InstallationContextProvider> </InstallationContextProvider>
); );
}; };

View File

@ -1,22 +1,27 @@
import { Alert, Box, CircularProgress } from "@mui/material"; import { Box, CircularProgress, Alert } from "@mui/material";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { useEffect, useState } from "react"; import { useState, useEffect, FC } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import CustomerForm from "./CustomerForm";
import axiosConfig from "../../config/axiosConfig"; import axiosConfig from "../../config/axiosConfig";
import { I_Installation } from "../../util/types"; export interface I_FormProps<T> {
import LocationForm from "../Groups/LocationForm"; values: T;
id: string;
const Installation = () => { }
interface I_DetailProps<T> {
formComponent: FC<I_FormProps<T>>;
route: string;
}
const Detail = <T extends { id: number }>(props: I_DetailProps<T>) => {
const { id } = useParams(); const { id } = useParams();
const [values, setValues] = useState<I_Installation>(); const { formComponent: FormComponent, route } = props;
const [values, setValues] = useState<T>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<AxiosError>(); const [error, setError] = useState<AxiosError>();
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
axiosConfig axiosConfig
.get("/GetInstallationById?id=" + id) .get(route + id)
.then((res) => { .then((res) => {
setValues(res.data); setValues(res.data);
setLoading(false); setLoading(false);
@ -25,15 +30,10 @@ const Installation = () => {
setError(err); setError(err);
setLoading(false); setLoading(false);
}); });
}, [id]); }, [id, route]);
if (values && values.id && values.id.toString() === id) { if (values && values.id && values.id.toString() === id) {
return ( return <FormComponent values={values} id={id} />;
<Box sx={{ py: 3 }}>
<CustomerForm values={values} id={id} />
<LocationForm parentId={values.parentId} />
</Box>
);
} else if (loading) { } else if (loading) {
return ( return (
<Box <Box
@ -52,4 +52,4 @@ const Installation = () => {
return null; return null;
}; };
export default Installation; export default Detail;

View File

@ -14,10 +14,10 @@ const LanguageSelect = (props: LanguageSelectProps) => {
label="Age" label="Age"
onChange={(e) => props.setLanguage(e.target.value)} onChange={(e) => props.setLanguage(e.target.value)}
> >
<MenuItem value="en"> <MenuItem value="EN">
<FormattedMessage id="english" defaultMessage="English" /> <FormattedMessage id="english" defaultMessage="English" />
</MenuItem> </MenuItem>
<MenuItem value="de"> <MenuItem value="DE">
<FormattedMessage id="german" defaultMessage="German" /> <FormattedMessage id="german" defaultMessage="German" />
</MenuItem> </MenuItem>
</Select> </Select>

View File

@ -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 (
<>
<AntTabs
value={routeMatch?.pattern?.path ?? routes.installation + ":id"}
aria-label="basic tabs example"
>
<StyledTab
icon={<ListIcon />}
value={routes.installations + routes.list + "*"}
component={Link}
to={routes.list}
/>
<StyledTab
icon={<AccountTreeIcon />}
value={routes.installations + routes.tree + "*"}
component={Link}
to={routes.tree}
/>
</AntTabs>
{/* <ToggleButtonGroup
color="primary"
value={routeMatch?.pattern?.path}
exclusive
sx={{ mb: 1 }}
size="small"
>
<ToggleButton
value={routes.installations + routes.list + "*"}
component={Link}
to={routes.list}
>
<ListIcon />
</ToggleButton>
<ToggleButton
value={routes.installations + routes.tree + "*"}
component={Link}
to={routes.tree}
>
<AccountTreeIcon />
</ToggleButton>
</ToggleButtonGroup> */}
</>
);
};
export default ModeButtons;

View File

@ -7,7 +7,7 @@ import routes from "../../routes.json";
const NavigationButtons = () => { const NavigationButtons = () => {
const routeMatch = useRouteMatch([ const routeMatch = useRouteMatch([
routes.installations + "*", routes.installations + "*",
routes.groups + "*", routes.users + "*",
]); ]);
return ( return (
@ -16,23 +16,21 @@ const NavigationButtons = () => {
value={routeMatch?.pattern?.path} value={routeMatch?.pattern?.path}
exclusive exclusive
sx={{ mb: 1 }} sx={{ mb: 1 }}
fullWidth
> >
<ToggleButton <ToggleButton
value={routes.installations + "*"} value={routes.installations + "*"}
component={Link} component={Link}
to={routes.installations} to={routes.installations}
> >
<FormattedMessage <FormattedMessage id="installations" defaultMessage="Installations" />
id="allInstallations"
defaultMessage="All installations"
/>
</ToggleButton> </ToggleButton>
<ToggleButton <ToggleButton
value={routes.groups + "*"} value={routes.users + "*"}
component={Link} component={Link}
to={routes.groups} to={routes.users}
> >
<FormattedMessage id="groups" defaultMessage="Groups" /> <FormattedMessage id="users" defaultMessage="Users" />
</ToggleButton> </ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
); );

View File

@ -20,6 +20,7 @@ const Sidebar = () => {
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
<InstallationList searchQuery={searchQuery} /> <InstallationList searchQuery={searchQuery} />
</> </>
); );

View File

@ -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 (
<form onSubmit={formik.handleSubmit}>
<InnovenergyTextfield
id="name-textfield"
label={intl.formatMessage({
id: "email",
defaultMessage: "Email",
})}
name="email"
value={formik.values.email}
handleChange={formik.handleChange}
/>
<InnovenergyTextfield
id="region-textfield"
label={intl.formatMessage({
id: "Information",
defaultMessage: "Information",
})}
name="information"
value={formik.values.information}
handleChange={formik.handleChange}
/>
<Grid container justifyContent="flex-end" sx={{ pt: 1 }}>
<Button variant="outlined" type="submit">
<FormattedMessage id="applyChanges" defaultMessage="Apply changes" />
</Button>
</Grid>
<Snackbar
open={open}
anchorOrigin={{
vertical: "top",
horizontal: "center",
}}
autoHideDuration={6000}
onClose={handleClose}
>
<Alert onClose={handleClose} severity="success" sx={{ width: "100%" }}>
<FormattedMessage
id="updatedSuccessfully"
defaultMessage="Updated successfully"
/>
</Alert>
</Snackbar>
</form>
);
};
export default UserForm;

View File

@ -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 (
<List
sx={{
width: "100%",
bgcolor: "background.paper",
position: "relative",
overflow: "auto",
maxHeight: 400,
py: 0,
mt: 1,
}}
component="nav"
aria-labelledby="nested-list-subheader"
>
{availableUsers.map((user) => {
return (
<Fragment key={user.id}>
<Link
to={getPathWithoutId(routeMatch?.pattern?.path) + user.id}
style={{ textDecoration: "none", color: "black" }}
>
<ListItemButton
selected={user.id === Number(routeMatch?.params.id)}
>
<ListItemText primary={user.name} />
</ListItemButton>
</Link>
<Divider />
</Fragment>
);
})}
</List>
);
}
return null;
};
export default UserList;

View File

@ -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 (
<Box sx={{ width: "100%" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={routeMatch?.pattern?.path ?? routes.user + ":id"}
aria-label="basic tabs example"
>
<Tab
label={intl.formatMessage({
id: "user",
defaultMessage: "User",
})}
value={routes.users + routes.user + ":id"}
component={Link}
to={routes.user + id}
/>
</Tabs>
</Box>
</Box>
);
}
return null;
};
export default UserTabs;

View File

@ -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 (
<UserContextProvider>
<Grid container spacing={2}>
<Grid item xs={3}>
<UserList />
</Grid>
<Grid item xs={9}>
<UserTabs />
<Routes>
<Route
path={routes.user + ":id"}
element={
<Detail<I_User>
route="/GetUserById?id="
formComponent={UserForm}
/>
}
index
/>
</Routes>
</Grid>
</Grid>
</UserContextProvider>
);
};
export default Users;

View File

@ -1,10 +1,14 @@
{ {
"installation": "installation/", "installation": "installation/",
"alarms": "alarms/", "alarms": "alarms/",
"users": "users/", "users": "/users/",
"log": "log/", "log": "log/",
"installations": "/installations/", "installations": "/installations/",
"groups": "/groups/", "groups": "/groups/",
"group": "group/", "group": "group/",
"folder": "folder/" "folder": "folder/",
"manageAccess": "manageAccess/",
"user": "user/",
"tree": "tree/",
"list": "list/"
} }

View File

@ -0,0 +1,46 @@
import { styled, Tab, Tabs } from "@mui/material";
export const StyledTab = styled((props: any) => (
<Tab disableRipple {...props} />
))(({ 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",
},
});

View File

@ -1,4 +1,4 @@
export interface User { export interface I_User {
email: string; email: string;
hasWriteAccess: boolean; hasWriteAccess: boolean;
id: number; id: number;
@ -14,7 +14,7 @@ export interface User {
export interface UserWithInheritedAccess { export interface UserWithInheritedAccess {
folderId: number; folderId: number;
folderName: string; folderName: string;
user: User; user: I_User;
} }
export const filterDuplicateUsers = (data: any[]) => { export const filterDuplicateUsers = (data: any[]) => {