Merge remote-tracking branch 'origin/main'

This commit is contained in:
Kim 2023-07-20 12:44:32 +02:00
commit 66da08b407
41 changed files with 1165 additions and 4508 deletions

File diff suppressed because it is too large Load Diff

View File

@ -3,43 +3,30 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@aws-sdk/client-s3": "^3.316.0",
"@emotion/styled": "^11.10.5",
"@minoru/react-dnd-treeview": "^3.4.1",
"@mui/icons-material": "^5.11.0",
"@mui/lab": "^5.0.0-alpha.120",
"@mui/material": "^5.11.7",
"@mui/x-date-pickers": "^6.5.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.11",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/react-router-dom": "^5.3.3",
"axios": "^1.3.1",
"chart.js": "^4.2.1",
"css-loader": "^6.7.3",
"dayjs": "^1.11.7",
"formik": "^2.2.9",
"linq-to-typescript": "^11.0.0",
"package.json": "^2.0.1",
"plotly.js": "^2.20.0",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dnd-scrolling": "^1.3.3",
"react-dom": "^18.2.0",
"react-intl": "^6.2.10",
"react-plotly.js": "^2.6.0",
"react-router-dom": "^6.8.0",
"react-scripts": "5.0.1",
"reactflow": "^11.5.6",
"rxjs": "^7.8.0",
"sass": "^1.58.3",
"sass-loader": "^13.2.0",
"simplytyped": "^3.3.0",
"style-loader": "^3.3.1",
"testcafe": "^2.4.0",
@ -83,7 +70,7 @@
"devDependencies": {
"@formatjs/cli": "^6.0.3",
"@types/react-plotly.js": "^2.6.0",
"@types/react-window": "^1.8.5",
"depcheck": "^1.4.3",
"eslint-plugin-formatjs": "^4.10.1",
"prettier": "^2.8.8"
}

View File

@ -2,7 +2,7 @@ import useToken from "./hooks/useToken";
import Login from "./Login";
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import { Container, Grid, colors, Box } from "@mui/material";
import { Container, Grid, Box } from "@mui/material";
import routes from "./routes.json";
import { IntlProvider, MessageFormatElement } from "react-intl";
import { useContext, useState } from "react";
@ -17,20 +17,21 @@ import InstallationPage from "./components/Installations/InstallationPage";
import { UserContext } from "./components/Context/UserContextProvider";
import ResetPassword from "./ResetPassword";
import innovenergyLogo from "./resources/innoveng_logo_on_orange.png";
import { colors } from "./index";
const App = () => {
const { token, setToken, removeToken } = useToken();
const [language, setLanguage] = useState("EN");
const [language, setLanguage] = useState("en");
const { getCurrentUser } = useContext(UserContext);
const getTranslations = () => {
switch (language) {
case "DE":
case "en":
return de;
case "EN":
case "de":
return en;
case "FR":
case "fr":
return fr;
}
};
@ -41,12 +42,13 @@ const App = () => {
if (token && getCurrentUser()?.mustResetPassword) {
return <ResetPassword />;
}
console.log("lang", language);
return (
<BrowserRouter>
<IntlProvider
messages={getTranslations()}
locale={language}
defaultLocale="EN"
defaultLocale="en"
>
<Container maxWidth="xl" sx={{ pt: 2 }}>
<Grid container>
@ -73,7 +75,7 @@ const App = () => {
</Grid>
</Container>
<Box
bgcolor="#eaecf1"
bgcolor={colors.greyLight}
pt={3}
borderTop="2px solid"
borderColor=" #a8b0be"

View File

@ -1,10 +1,17 @@
import React, { useContext, useState } from "react";
import { Alert, CircularProgress, Grid, useTheme } from "@mui/material";
import {
Alert,
CircularProgress,
Grid,
Typography,
useTheme,
} from "@mui/material";
import Container from "@mui/material/Container";
import { axiosConfigWithoutToken } from "./config/axiosConfig";
import InnovenergyTextfield from "./components/Layout/InnovenergyTextfield";
import InnovenergyButton from "./components/Layout/InnovenergyButton";
import { UserContext } from "./components/Context/UserContextProvider";
import innovenergyLogo from "./resources/innoveng_logo_on_orange.png";
const loginUser = async (username: string, password: string) => {
return axiosConfigWithoutToken.post("/Login", null, {
@ -59,6 +66,14 @@ const Login = ({ setToken, setLanguage }: I_LoginProps) => {
};
return (
<>
<Container maxWidth="xl" sx={{ pt: 2 }}>
<Grid container>
<Grid item xs={3} container justifyContent="flex-start" mb={2}>
<img src={innovenergyLogo} alt="innovenergy logo" height="100" />
</Grid>
</Grid>
</Container>
<Container maxWidth="sm" sx={{ p: 2, alignContent: "center" }}>
<InnovenergyTextfield
id="username-textfield"
@ -75,14 +90,23 @@ const Login = ({ setToken, setLanguage }: I_LoginProps) => {
value={password}
handleChange={(e) => setPassword(e.target.value)}
/>
{error && <Alert severity="error">Incorrect username or password</Alert>}
{error && (
<Alert severity="error">Incorrect username or password</Alert>
)}
<Grid container justifyContent="flex-end" sx={{ pt: 1 }}>
<InnovenergyButton onClick={handleSubmit} sx={{ my: 1 }}>
<InnovenergyButton
onClick={handleSubmit}
sx={{ my: 1 }}
type="submit"
>
Login
</InnovenergyButton>
</Grid>
{loading && <CircularProgress sx={{color: theme.palette.secondary.main}} />}
{loading && (
<CircularProgress sx={{ color: theme.palette.secondary.main }} />
)}
</Container>
</>
);
};

View File

@ -27,19 +27,16 @@ const S3CredentialsContextProvider = ({
}) => {
const [s3Credentials, setS3Credentials] = useState<S3Credentials>();
console.log("creds", s3Credentials);
const saveS3Credentials = (credentials: S3Credentials, id: string) => {
//const s3Bucket = id + "-3e5b3069-214a-43ee-8d85-57d72000c19d";
/* const s3Bucket = "saliomameiringen";
setS3Credentials({ s3Bucket, ...credentials });*/
setS3Credentials({
const s3Bucket = id + "-3e5b3069-214a-43ee-8d85-57d72000c19d";
setS3Credentials({ s3Bucket, ...credentials });
/* setS3Credentials({
s3Region: "sos-ch-dk-2",
s3Provider: "exo.io",
s3Key: "EXO15c0bf710e158e9b83270f0a",
s3Secret: "Dd5jYSiZtt_Zt5Ba5mDmaiLCdASUaKLfduSKY-SU-lg",
s3Bucket: "saliomameiringen",
});
});*/
};
const fetchData = (timestamp: UnixTime): Promise<FetchResult<DataRecord>> => {

View File

@ -5,6 +5,7 @@ import {
DialogTitle,
TextField,
colors,
Alert,
} from "@mui/material";
import DialogActions from "@mui/material/DialogActions";
import { useContext, useState } from "react";
@ -19,6 +20,7 @@ import InnovenergyButton from "../../Layout/InnovenergyButton";
const AvailableUserDialog = () => {
const [selectedUsers, setSelectedUsers] = useState<I_User[]>([]);
const [open, setOpen] = useState(false);
const [errors, setErrors] = useState<string[]>([]);
const { currentType } = useContext(GroupContext);
const { id } = useParams();
@ -42,7 +44,8 @@ const AvailableUserDialog = () => {
.then(() => {
fetchUsersWithDirectAccessForResource();
fetchUsersWithInheritedAccessForResource();
});
})
.catch(() => setErrors((errors) => [...errors, user.name]));
});
};
@ -63,6 +66,17 @@ const AvailableUserDialog = () => {
<FormattedMessage id="manageAccess" defaultMessage="Manage access" />
</DialogTitle>
<DialogContent id="available-user-dialog-content">
{errors.length > 0 && (
<Alert sx={{ my: 1 }} severity="error">
<FormattedMessage
id="grantAccessError"
defaultMessage="Couldn't grant access to the following users: "
/>
{errors.map((error, index) =>
errors.length === index + 1 ? error : error + ", "
)}
</Alert>
)}
<Autocomplete
sx={{ width: "500px", pt: 1 }}
multiple

View File

@ -1,4 +1,4 @@
import { List } from "@mui/material";
import { List, useTheme } from "@mui/material";
import { ReactNode } from "react";
interface InnovenergyListProps {
@ -7,10 +7,11 @@ interface InnovenergyListProps {
}
const InnovenergyList = (props: InnovenergyListProps) => {
const theme = useTheme();
return (
<List
sx={{
bgcolor: "white",
bgcolor: theme.palette.primary.dark,
position: "relative",
overflow: "auto",
maxHeight: 500,

View File

@ -44,7 +44,7 @@ const UsersWithDirectAccess = () => {
});
};
if (directAccessUsers && directAccessUsers.length) {
if (directAccessUsers && directAccessUsers.length > 0) {
return (
<>
{error && (
@ -90,7 +90,14 @@ const UsersWithDirectAccess = () => {
</>
);
}
return null;
return (
<Alert severity="info" sx={{ mx: 2 }}>
<FormattedMessage
id="noDirectAccessUsers"
defaultMessage="No direct access users were found"
/>
</Alert>
);
};
export default UsersWithDirectAccess;

View File

@ -4,6 +4,7 @@ import {
Divider,
ListItemAvatar,
Avatar,
Alert,
} from "@mui/material";
import { Fragment, useContext, useEffect } from "react";
import { Link } from "react-router-dom";
@ -24,7 +25,7 @@ const UsersWithInheritedAccess = () => {
fetchUsersWithInheritedAccessForResource();
}, [fetchUsersWithInheritedAccessForResource]);
if (inheritedAccessUsers && inheritedAccessUsers.length) {
if (inheritedAccessUsers && inheritedAccessUsers.length > 0) {
return (
<>
{filterDuplicateUsers(inheritedAccessUsers).map(
@ -73,7 +74,14 @@ const UsersWithInheritedAccess = () => {
</>
);
}
return null;
return (
<Alert severity="info" sx={{ m: 2 }}>
<FormattedMessage
id="noInheritedAccessUsers"
defaultMessage="No inherited access users were found"
/>
</Alert>
);
};
export default UsersWithInheritedAccess;

View File

@ -1,43 +1,31 @@
import { Dialog, DialogContent, DialogTitle, IconButton } from "@mui/material";
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import axiosConfig from "../../config/axiosConfig";
import { I_Folder } from "../../util/types";
import FolderForm from "./FolderForm";
import CloseIcon from "@mui/icons-material/Close";
import { ReactNode, useState } from "react";
import InnovenergyButton from "../Layout/InnovenergyButton";
interface AddNewDialogProps {
values: I_Folder;
form: ReactNode;
message: ReactNode;
id: number;
}
const AddNewDialog = (props: AddNewDialogProps) => {
const [open, setOpen] = useState(false);
const { form, id, message } = props;
const intl = useIntl();
const handleSubmit = (data: I_Folder, childData: Partial<I_Folder>) => {
return axiosConfig
.post("/CreateFolder", {
...childData,
parentId: data.id,
})
.then((res) => {
setOpen(false);
return res;
});
};
return (
<>
<InnovenergyButton
id={"add-new-child-dialog-button-" + props.values.id}
id={"add-new-child-dialog-button-" + id}
sx={{ ml: 2 }}
onClick={() => setOpen(true)}
>
<FormattedMessage id="addNewChild" defaultMessage="Add new child" />
{message}
</InnovenergyButton>
<Dialog
id={"add-new-child-dialog-" + props.values.id}
id={"add-new-child-dialog-" + id}
onClose={() => setOpen(false)}
aria-labelledby="customized-dialog-title"
open={open}
@ -50,12 +38,9 @@ const AddNewDialog = (props: AddNewDialogProps) => {
maxWidth="sm"
>
<DialogTitle>
<FormattedMessage
id="createNewFolder"
defaultMessage="Create new folder"
/>
<FormattedMessage id="createNew" defaultMessage="Create new" />
<IconButton
id={"add-new-child-dialog-icon-button-" + props.values.id}
id={"add-new-child-dialog-icon-button-" + id}
edge="start"
color="inherit"
onClick={() => setOpen(false)}
@ -69,14 +54,10 @@ const AddNewDialog = (props: AddNewDialogProps) => {
top: 8,
}}
>
<CloseIcon
id={"add-new-child-dialog-close-icon-" + props.values.id}
/>
<CloseIcon id={"add-new-child-dialog-close-icon-" + id} />
</IconButton>
</DialogTitle>
<DialogContent>
<FolderForm values={props.values} handleSubmit={handleSubmit} />
</DialogContent>
<DialogContent>{form}</DialogContent>
</Dialog>
</>
);

View File

@ -0,0 +1,83 @@
import { Dialog, DialogContent, DialogTitle, IconButton } from "@mui/material";
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import axiosConfig from "../../config/axiosConfig";
import { I_Folder, I_Installation } from "../../util/types";
import FolderForm from "./FolderForm";
import CloseIcon from "@mui/icons-material/Close";
import InnovenergyButton from "../Layout/InnovenergyButton";
import InstallationForm from "../Installations/InstallationForm";
import AddNewDialog from "./AddNew";
interface AddNewDialogProps {
values: I_Folder | I_Installation;
isFolder: boolean;
}
const AddNewButtons = (props: AddNewDialogProps) => {
const [open, setOpen] = useState(false);
const handleFolderSubmit = (data: I_Folder, childData: Partial<I_Folder>) => {
return axiosConfig
.post("/CreateFolder", {
...childData,
parentId: data.id,
})
.then((res) => {
setOpen(false);
return res;
});
};
const handleInstallationSubmit = (
data: I_Installation,
childData: Partial<I_Installation>
) => {
return axiosConfig
.post("/CreateInstallation", {
...childData,
parentId: data.id,
})
.then((res) => {
setOpen(false);
return res;
});
};
const folderForm = (
<FolderForm
values={props.values as I_Folder}
handleSubmit={handleFolderSubmit}
/>
);
const installationForm = (
<InstallationForm
values={props.values as I_Installation}
handleSubmit={handleInstallationSubmit}
/>
);
return (
<>
<AddNewDialog
form={installationForm}
message={
<FormattedMessage
id="addNewInstallation"
defaultMessage="Add new installation"
/>
}
id={props.values.id}
/>
<AddNewDialog
form={folderForm}
message={
<FormattedMessage id="addNewFolder" defaultMessage="Add new folder" />
}
id={props.values.id}
/>
</>
);
};
export default AddNewButtons;

View File

@ -4,7 +4,7 @@ import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import axiosConfig from "../../config/axiosConfig";
import { I_Folder } from "../../util/types";
import AddNewDialog from "./AddNewDialog";
import AddNewButtons from "./AddNewButtons";
import FolderForm from "./FolderForm";
import MoveDialog from "./Tree/MoveDialog";
import { colors } from "index";
@ -36,7 +36,7 @@ const Folder = () => {
if (values && values.id && values.id.toString() === id) {
const moveDialog = <MoveDialog values={values} />;
const addNewDialog = <AddNewDialog values={values} />;
const addNewDialog = <AddNewButtons values={values} isFolder />;
return (
<>

View File

@ -7,7 +7,7 @@ import { I_Folder } from "../../util/types";
import { GroupContext } from "../Context/GroupContextProvider";
import InnovenergySnackbar from "../InnovenergySnackbar";
import InnovenergyButton from "../Layout/InnovenergyButton";
import InnovenergyTextfield, {
import {
I_InnovenergyTextfieldProps,
IePropertyGrid,
} from "../Layout/InnovenergyTextfield";

View File

@ -11,7 +11,7 @@ import { instanceOfFolder } from "../../../util/group.util";
import { Grid, CircularProgress, useTheme } from "@mui/material";
import { useIntl } from "react-intl";
import { colors } from "../../..";
import { color } from "chart.js/helpers";
import InnovEnergyTreeItem from "../../Layout/InnovEnergyTreeItem";
const GroupTree = () => {
const { setCurrentType, fetchData, data, loading } = useContext(GroupContext);
@ -48,27 +48,15 @@ const GroupTree = () => {
}}
draggable={false}
>
<TreeItem
<InnovEnergyTreeItem
id={"group-tree-item-" + element.id}
key={element.type + element.id}
nodeId={element.type + element.id}
label={element.name}
onClick={() => setCurrentType(element.type)}
sx={{
".MuiTreeItem-content": { py: "10px" },
".Mui-selected": {
backgroundColor: theme.palette.secondary.main,
},
".Mui-selected:hover": {
backgroundColor: theme.palette.secondary.main,
},
".Mui-focused": { backgroundColor: theme.palette.secondary.main },
bgcolor: colors.greyDark,
borderRadius: 2,
}}
>
{getNodes(element)}
</TreeItem>
</InnovEnergyTreeItem>
</Link>
);
});
@ -94,6 +82,9 @@ const GroupTree = () => {
flexGrow: 1,
overflow: "auto",
overflowX: "hidden",
".Mui-selected": {
borderRadius: "4px",
},
}}
expanded={openNodes}
onNodeToggle={(e, ids) => {

View File

@ -1,6 +1,6 @@
import { DialogActions, Dialog, DialogTitle } from "@mui/material";
import { DialogActions, Dialog, DialogTitle, Alert } from "@mui/material";
import DialogContent from "@mui/material/DialogContent";
import { useContext, useState } from "react";
import React, { useContext, useState } from "react";
import { FormattedMessage } from "react-intl";
import { useParams } from "react-router-dom";
import axiosConfig from "../../../config/axiosConfig";
@ -12,15 +12,18 @@ import MoveTree from "./MoveTree";
interface MoveDialogProps {
values: I_Folder;
}
const MoveDialog = (props: MoveDialogProps) => {
const { values } = props;
const [open, setOpen] = useState(false);
const [selectedParentId, setSelectedParentId] = useState<number>(
values.parentId
);
const { fetchData, currentType } = useContext(GroupContext);
const { id } = useParams();
const [error, setError] = useState();
const handleMove = () => {
const route =
@ -33,9 +36,9 @@ const MoveDialog = (props: MoveDialogProps) => {
.then((res) => {
setOpen(false);
fetchData();
});
})
.catch((err) => setError(err));
};
return (
<div>
<InnovenergyButton
@ -59,7 +62,16 @@ const MoveDialog = (props: MoveDialogProps) => {
<DialogTitle>
<FormattedMessage id="moveTo" defaultMessage="Move to" />
</DialogTitle>
<DialogContent>
{error && (
<Alert severity={"error"}>
<FormattedMessage
id="moveInstallationError"
defaultMessage="Couldn't move element, an error occured"
/>
</Alert>
)}
<MoveTree
parentId={selectedParentId}
setSelectedParentId={setSelectedParentId}

View File

@ -2,11 +2,11 @@ import TreeView from "@mui/lab/TreeView";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import { ReactNode, useContext } from "react";
import { TreeItem } from "@mui/lab";
import { I_Folder, I_Installation } from "../../../util/types";
import { GroupContext } from "../../Context/GroupContextProvider";
import { instanceOfFolder } from "../../../util/group.util";
import { useIntl } from "react-intl";
import InnovEnergyTreeItem from "../../Layout/InnovEnergyTreeItem";
interface MoveTreeProps {
setSelectedParentId: (value: number) => void;
@ -29,14 +29,15 @@ const MoveTree = (props: MoveTreeProps) => {
.filter((element) => element.type === "Folder")
.map((element) => {
return (
<TreeItem
<InnovEnergyTreeItem
id={"move-tree-item-" + element.id}
key={"move-tree-item-" + element.id}
nodeId={element.id.toString()}
color="primary"
label={element.name}
>
{getNodes(element)}
</TreeItem>
</InnovEnergyTreeItem>
);
});
};

View File

@ -6,6 +6,7 @@ interface InnovenergySnackbarProps {
setOpen: (value: boolean) => void;
error?: any;
}
const InnovenergySnackbar = (props: InnovenergySnackbarProps) => {
const { open, setOpen, error } = props;

View File

@ -3,10 +3,11 @@ import { AxiosError } from "axios";
import { useContext, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import axiosConfig from "../../config/axiosConfig";
import { I_Installation } from "../../util/types";
import { I_Folder, I_Installation } from "../../util/types";
import InstallationForm from "./InstallationForm";
import { colors } from "../..";
import { S3CredentialsContext } from "../Context/S3CredentialsContextProvider";
import MoveDialog from "../Groups/Tree/MoveDialog";
interface I_InstallationProps {
loading?: boolean;
@ -16,11 +17,16 @@ interface I_InstallationProps {
}
const Installation = (props: I_InstallationProps) => {
const { hasMoveButton, values, loading, error } = props;
const { values, loading, error } = props;
const { id } = useParams();
const theme = useTheme();
const handleSubmit = (data: I_Folder, formikValues: Partial<I_Folder>) => {
return axiosConfig.put("/UpdateInstallation", { ...data, ...formikValues });
};
if (values && values.id && values.id.toString() === id) {
const moveDialog = <MoveDialog values={values} />;
return (
<Box
sx={{
@ -39,9 +45,15 @@ const Installation = (props: I_InstallationProps) => {
>
<InstallationForm
values={values}
id={id}
hasMoveButton={hasMoveButton}
handleSubmit={handleSubmit}
additionalButtons={props.hasMoveButton ? [moveDialog] : []}
/>
<Box>
{values.s3WriteKey && <div>{`Write key: ${values.s3WriteKey}`}</div>}
{values.s3WriteSecret && (
<div>{`Write secret: ${values.s3WriteSecret}`}</div>
)}
</Box>
</Box>
);
} else if (loading) {

View File

@ -1,9 +1,9 @@
import { Alert, Grid, Snackbar } from "@mui/material";
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_Installation } from "../../util/types";
import { I_Folder, I_Installation } from "../../util/types";
import { InstallationsContext } from "../Context/InstallationsContextProvider";
import MoveDialog from "../Groups/Tree/MoveDialog";
import InnovenergyButton from "../Layout/InnovenergyButton";
@ -13,15 +13,19 @@ import InnovenergyTextfield, {
} from "../Layout/InnovenergyTextfield";
import { UserContext } from "../Context/UserContextProvider";
import * as Yup from "yup";
import { AxiosResponse } from "axios";
interface I_InstallationFormProps {
values: I_Installation;
id: string | undefined;
hasMoveButton?: boolean;
handleSubmit: (
data: I_Installation,
formikValues: Partial<I_Installation>
) => Promise<AxiosResponse>;
additionalButtons?: ReactNode[];
}
const InstallationForm = (props: I_InstallationFormProps) => {
const { values, id, hasMoveButton } = props;
const { values, additionalButtons, handleSubmit } = props;
const [open, setOpen] = useState(false);
const { fetchData } = useContext(InstallationsContext);
@ -53,22 +57,18 @@ const InstallationForm = (props: I_InstallationFormProps) => {
const formik = useFormik({
initialValues: {
name: values.name,
name: values.region ? values.name : "",
region: values.region,
location: values.location,
country: values.country,
orderNumbers: values.orderNumbers,
},
onSubmit: (formikValues) => {
axiosConfig
.put("/UpdateInstallation", {
...formikValues,
id,
})
handleSubmit(values, formikValues)
.then(() => {
setOpen(true);
fetchData();
});
})
.catch((err) => {});
},
validationSchema,
});
@ -152,7 +152,10 @@ const InstallationForm = (props: I_InstallationFormProps) => {
<form onSubmit={formik.handleSubmit}>
<IePropertyGrid rows={rows} />
<Grid container justifyContent="flex-end" sx={{ pt: 1 }}>
{hasMoveButton && !readOnly && <MoveDialog values={values} />}
{/*{hasMoveButton && !readOnly && <MoveDialog values={values} />}*/}
{!readOnly &&
additionalButtons &&
additionalButtons.map((button) => button)}
{!readOnly && (
<InnovenergyButton id="installation-form-submit-button" type="submit">
<FormattedMessage

View File

@ -15,6 +15,7 @@ import { Fragment, useContext, useEffect } from "react";
import { I_Installation } from "../../util/types";
import { InstallationsContext } from "../Context/InstallationsContextProvider";
import { colors } from "../..";
import { FormattedMessage } from "react-intl";
interface InstallationListProps {
searchQuery: string;
@ -65,7 +66,7 @@ const InstallationList = (props: InstallationListProps) => {
<CircularProgress sx={{ m: 6, color: theme.palette.secondary.main }} />
</Grid>
);
} else if (data && data.length) {
} else if (data && data.length > 0) {
return (
<List
sx={{
@ -90,7 +91,10 @@ const InstallationList = (props: InstallationListProps) => {
<Link
id={"installation-list-link-" + installation.id}
to={
getPathWithoutId(routeMatch?.pattern?.path) + installation.id
routes.installations +
routes.list +
routes.installation +
installation.id
}
style={{ textDecoration: "none", color: colors.black }}
>
@ -131,6 +135,12 @@ const InstallationList = (props: InstallationListProps) => {
{error.message}
</Alert>
);
} else if (data.length === 0) {
return (
<Alert severity="info" sx={{ mt: 1 }}>
<FormattedMessage id="noData" defaultMessage="No data found" />
</Alert>
);
}
return null;
};

View File

@ -11,22 +11,8 @@ import Installation from "./Installation";
import CheckboxTree from "./Log/CheckboxTree";
import LogContextProvider from "../Context/LogContextProvider";
import useRouteMatch from "../../hooks/useRouteMatch";
import { Background } from "reactflow";
import { red } from "@mui/material/colors";
import S3CredentialsContextProvider, {
S3CredentialsContext,
} from "../Context/S3CredentialsContextProvider";
import { UnixTime } from "../../dataCache/time";
import { FetchResult } from "../../dataCache/dataCache";
import { DataRecord } from "../../dataCache/data";
import { S3Access } from "../../dataCache/S3/S3Access";
import { parseCsv } from "../../util/graph.util";
import { useParams } from "react-router-dom";
import { useContext, useEffect, useState } from "react";
import axiosConfig from "../../config/axiosConfig";
import { I_Installation } from "../../util/types";
import { AxiosError } from "axios/index";
import useInstallation from "../../hooks/useInstallation";
import { useEffect } from "react";
const Installations = () => {
const routeMatch = useRouteMatch([

View File

@ -18,6 +18,7 @@ const LiveView = () => {
borderBottomLeftRadius: 4,
borderBottomRightRadius: 4,
borderColor: theme.palette.text.disabled,
overflowX: "auto",
}}
>
<TopologyView />

View File

@ -8,6 +8,7 @@ import useRouteMatch from "../../../hooks/useRouteMatch";
import routes from "../../../routes.json";
import React from "react";
import { colors } from "index";
import InnovEnergyTreeItem from "../../Layout/InnovEnergyTreeItem";
export interface ToggleElement {
[key: string]: boolean;
@ -54,7 +55,7 @@ const CheckboxTree = () => {
const checked = checkedToggles.find((toggle) => element.id === toggle);
const splitName = element.name.split("/");
return (
<TreeItem
<InnovEnergyTreeItem
id={"checkbox-tree-" + element.name}
key={element.name}
nodeId={element.name}
@ -74,16 +75,9 @@ const CheckboxTree = () => {
{splitName[splitName.length - 1]}
</>
}
sx={{
"& .MuiTreeItem-content": {
paddingY: "5px",
minHeight: "52px",
},
bgcolor: colors.greyDark,
}}
>
{getNodes(element)}
</TreeItem>
</InnovEnergyTreeItem>
);
});
};

View File

@ -28,8 +28,9 @@ const NUMBER_OF_NODES = 100;
const ScalarGraph = () => {
const timeRange = createTimes(
UnixTime.now() /* .fromTicks(1682085650) */
.rangeBefore(TimeSpan.fromDays(10)),
UnixTime.now()
/* .fromTicks(1682085650) */
.rangeBefore(TimeSpan.fromDays(1)),
NUMBER_OF_NODES
);
const [timeSeries, setTimeSeries] = useState<RecordSeries>([]);
@ -56,6 +57,8 @@ const ScalarGraph = () => {
setTimeSeries(timeSeries);
const toggleValues = timeSeries.find((timeStamp) => timeStamp.value);
console.log("toggles", timeSeries, toggleValues);
if (toggles === null && toggleValues && toggleValues.value) {
const treeElements = getTreeElements(toggleValues.value);
setToggles(treeElements);
@ -123,7 +126,6 @@ const ScalarGraph = () => {
const handleRelayout = useCallback(
(params: PlotRelayoutEvent) => {
console.log("relayout");
const xaxisRange0 = params["xaxis.range[0]"];
const xaxisRange1 = params["xaxis.range[1]"];
if (xaxisRange0 && xaxisRange1) {
@ -138,6 +140,7 @@ const ScalarGraph = () => {
const theme = useTheme();
const renderGraphs = () => {
console.log("toggles", toggles);
if (checkedToggles.length > 0) {
const coordinateTimeSeries = transformToGraphData(timeSeries);
const visibleGraphs = Object.keys(coordinateTimeSeries).filter((path) => {
@ -209,6 +212,16 @@ const ScalarGraph = () => {
</div>
);
}
} else if (toggles === null) {
return (
<Alert sx={{ mt: 2 }} severity="error">
<FormattedMessage
id="noGraphData"
defaultMessage="Couldn't find any graph data"
/>
</Alert>
);
}
return (
<Alert sx={{ mt: 2 }} severity="info">
<FormattedMessage
@ -217,12 +230,6 @@ const ScalarGraph = () => {
/>
</Alert>
);
}
return (
<Alert sx={{ mt: 2 }} severity="error">
<FormattedMessage id="error" defaultMessage="There was an error" />
</Alert>
);
};
return <>{renderGraphs()}</>;

View File

@ -24,13 +24,16 @@ export const BOX_SIZE = 75;
const TopologyBox = (props: TopologyBoxProps) => {
const { titleColor, boxColor } = getBoxColor(props.title);
return (
<div style={{ height: BOX_SIZE, width: BOX_SIZE }}>
<Box
sx={{
visibility: props.title ? "visible" : "hidden",
height: BOX_SIZE + "px",
width: BOX_SIZE + "px",
maxHeight: BOX_SIZE + "px",
borderRadius: "4px",
color: "white",
overflow: "hidden",
}}
>
<p
@ -43,6 +46,9 @@ const TopologyBox = (props: TopologyBoxProps) => {
borderTopRightRadius: "4px",
display: "flex",
justifyContent: "center",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
}}
>
{props.title}
@ -53,13 +59,12 @@ const TopologyBox = (props: TopologyBoxProps) => {
borderBottomLeftRadius: "4px",
borderBottomRightRadius: "4px",
padding: "5px",
height: "51px",
height: BOX_SIZE - 34,
}}
>
{props.data && (
<>
{props.data.values.map((boxData, index) => {
console.log("boxData", boxData);
return (
<p
key={props.title + "-" + index}
@ -80,6 +85,7 @@ const TopologyBox = (props: TopologyBoxProps) => {
)}
</div>
</Box>
</div>
);
};

View File

@ -9,6 +9,7 @@ type TopologyColumnProps = {
centerConnection?: TopologyFlowProps;
bottomBox?: TopologyBoxProps;
bottomConnection?: TopologyFlowProps;
isLastColumn?: boolean;
};
const TopologyColumn = (props: TopologyColumnProps) => {
@ -37,6 +38,7 @@ const TopologyColumn = (props: TopologyColumnProps) => {
/>
<TopologyBox {...props.bottomBox} />
</div>
{!props.isLastColumn && (
<div>
<TopologyFlow
{...props.centerConnection}
@ -44,6 +46,7 @@ const TopologyColumn = (props: TopologyColumnProps) => {
hidden={!props.centerBox}
/>
</div>
)}
</Box>
);
};

View File

@ -9,7 +9,9 @@ export type TopologyFlowProps = {
hidden?: boolean;
};
const TopologyFlow = (props: TopologyFlowProps) => {
const length = Math.abs((props.amount ?? 1) * BOX_SIZE);
const length = Math.abs(
((props.amount && props.amount < 0.1 ? 0.1 : props.amount) ?? 0) * BOX_SIZE
);
const values = props.data?.values;
return (
<div
@ -45,7 +47,8 @@ const TopologyFlow = (props: TopologyFlowProps) => {
{values
?.filter((boxData) => (boxData.value as number) !== 0)
.map(
(boxData) => `${Math.round(boxData.value as number)} ${boxData.unit}`
(boxData) =>
`${Math.round(boxData.value as number)} ${boxData.unit}`
)}
</p>
<div

View File

@ -15,6 +15,7 @@ import { S3CredentialsContext } from "../../Context/S3CredentialsContextProvider
const TopologyView = () => {
const [values, setValues] = useState<TopologyValues | null>(null);
const { fetchData } = useContext(S3CredentialsContext);
const theme = useTheme();
useEffect(() => {
const interval = setInterval(() => {
@ -37,8 +38,7 @@ const TopologyView = () => {
}, 2000);
return () => clearInterval(interval);
}, []);
const theme = useTheme();
}, [fetchData]);
if (values) {
const highestConnectionValue = getHighestConnectionValue(values);
@ -47,19 +47,13 @@ const TopologyView = () => {
sx={{
display: "flex",
flexDirection: "row",
paddingTop: 3,
paddingBottom: 3,
padding: 3,
bgcolor: "white",
px: 3 / 8,
border: 2,
borderTop: 0,
borderBottomLeftRadius: 4,
borderBottomRightRadius: 4,
borderColor: theme.palette.text.disabled,
borderTopColor: "white",
marginTop: 0.05,
fontFamily: `"Ubuntu", sans-serif`,
fontSize: "12px",
justifyContent: "center",
width: "fit-content",
}}
>
<div>
@ -217,6 +211,7 @@ const TopologyView = () => {
title: "Battery",
data: values.battery,
}}
isLastColumn
/>
</div>
</Box>

View File

@ -1,57 +0,0 @@
import { useState, useCallback } from "react";
import ReactFlow, {
addEdge,
FitViewOptions,
applyNodeChanges,
applyEdgeChanges,
Node,
Edge,
NodeChange,
EdgeChange,
Connection,
} from "reactflow";
const initialNodes: Node[] = [
{ id: "1", data: { label: "Node 1" }, position: { x: 5, y: 5 } },
{ id: "2", data: { label: "Node 2" }, position: { x: 5, y: 100 } },
];
const initialEdges: Edge[] = [{ id: "e1-2", source: "1", target: "2" }];
const fitViewOptions: FitViewOptions = {
padding: 0.2,
};
function Flow() {
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onNodesChange = useCallback(
(changes: NodeChange[]) =>
setNodes((nds) => applyNodeChanges(changes, nds)),
[setNodes]
);
const onEdgesChange = useCallback(
(changes: EdgeChange[]) =>
setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges]
);
const onConnect = useCallback(
(connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
[setEdges]
);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
fitViewOptions={fitViewOptions}
/>
);
}
export default Flow;

View File

@ -0,0 +1,40 @@
import { TreeItem, TreeItemProps } from "@mui/lab";
import { colors } from "../../index";
import { useTheme } from "@mui/material";
const InnovEnergyTreeItem = (props: TreeItemProps) => {
const theme = useTheme();
return (
<TreeItem
id={props.id}
nodeId={props.nodeId}
label={props.label}
onClick={() => props.onClick}
sx={{
".MuiTreeItem-content": {
py: "10px",
borderRadius: "4px",
width: "inherit",
},
".Mui-selected": {
backgroundColor: theme.palette.secondary.main + "!important",
borderRadius: "4px",
},
".MuiTreeItem-content:hover": {
borderRadius: "4px",
backgroundColor: theme.palette.secondary.light + "!important",
},
".MuiTreeItem-content.Mui-selected:hover": {
borderRadius: "4px",
backgroundColor: theme.palette.secondary.main + "!important",
},
borderRadius: "4px",
bgcolor: colors.greyDark,
}}
>
{props.children}
</TreeItem>
);
};
export default InnovEnergyTreeItem;

View File

@ -1,12 +1,11 @@
import { Button, SxProps, Theme, colors, useTheme } from "@mui/material";
import { DAY_MARGIN } from "@mui/x-date-pickers/internals";
import { ReactNode } from "react";
import { Background } from "reactflow";
interface I_InnovenergyButtonProps {
children?: ReactNode;
type?: "button" | "submit" | "reset" | undefined;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
sx?: SxProps<Theme> | undefined;
id?: string;
}
@ -19,13 +18,14 @@ const InnovenergyButton = (props: I_InnovenergyButtonProps) => {
variant="contained"
type={props.type}
onClick={props.onClick}
sx={{...props.sx,
onKeyDown={props.onKeyDown}
sx={{
...props.sx,
textTransform: "none",
bgcolor: theme.palette.secondary.main,
":hover": {
bgcolor: theme.palette.secondary.dark,
}
},
}}
>
{props.children}
@ -33,6 +33,4 @@ const InnovenergyButton = (props: I_InnovenergyButtonProps) => {
);
};
export default InnovenergyButton;

View File

@ -1,5 +1,4 @@
import { styled, SxProps, Tab, Tabs, Theme, useTheme } from "@mui/material";
import { TabProps } from "@mui/material/Tab/Tab";
import { Tab, useTheme } from "@mui/material";
import { colors } from "index";
const InnovenergyTab = (props: any) => {
@ -21,43 +20,8 @@ const InnovenergyTab = (props: any) => {
borderTopRightRadius: "0.3rem",
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": {
backgroundColor: "#eaecf1",
color: colors.black,
borderColor: theme.palette.text.disabled,
marginTop: "1px",
bottom: -1,
...(props.sx ? props.sx["&.Mui-selected"] : {}),
},
}}
/>
);
};
const InnovenergyTabBorder = (props: any) => {
const theme = useTheme();
return (
<Tab
{...props}
disableRipple
sx={{
textTransform: "none",
...props.sx,
bottom: 0,
fontWeight: theme.typography.fontWeightRegular,
fontSize: theme.typography.pxToRem(14),
marginRight: theme.spacing(1),
background: "0 0",
border: "2px solid",
borderColor: "#c0c6d0",
bgcolor: theme.palette.primary.light,
borderTopLeftRadius: "0.3rem",
borderTopRightRadius: "0.3rem",
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": {
backgroundColor: theme.palette.primary.light,
color: colors.black,
borderColor: theme.palette.text.disabled,
marginTop: "1px",

View File

@ -24,7 +24,7 @@ const InnovenergyTabs = (props: AntTabsProps) => {
},
"&.Mui-selected": {
color: colors.black,
backgroundColor: "#eaecf1",
backgroundColor: theme.palette.primary.light,
borderColor: `#90A7c5 #90A7c5 #fff`,
},
"& .MuiTabs-indicator": {

View File

@ -15,13 +15,13 @@ const LanguageSelect = (props: LanguageSelectProps) => {
label="Age"
onChange={(e) => props.setLanguage(e.target.value)}
>
<MenuItem id="english-menu-item" value="EN">
<MenuItem id="english-menu-item" value="en">
<FormattedMessage id="english" defaultMessage="English" />
</MenuItem>
<MenuItem id="german-menu-item" value="DE">
<MenuItem id="german-menu-item" value="de">
<FormattedMessage id="german" defaultMessage="German" />
</MenuItem>
<MenuItem id="german-menu-item" value="FR">
<MenuItem id="german-menu-item" value="fr">
<FormattedMessage id="french" defaultMessage="French" />
</MenuItem>
</Select>

View File

@ -29,7 +29,6 @@ const NavigationTabs = () => {
sx={{
bgcolor: theme.palette.primary.main,
"&.Mui-selected": {
backgroundColor: "#eaecf1",
borderColor: theme.palette.text.disabled,
borderBottom: 0,
mb: -1 / 8,
@ -50,7 +49,6 @@ const NavigationTabs = () => {
sx={{
bgcolor: theme.palette.primary.main,
"&.Mui-selected": {
backgroundColor: "#eaecf1",
borderColor: theme.palette.text.disabled,
borderBottom: 0,
mb: -1 / 8,

View File

@ -1,7 +1,7 @@
import List from "@mui/material/List";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemText from "@mui/material/ListItemText";
import { Divider, useTheme } from "@mui/material";
import { Alert, Divider, useTheme } from "@mui/material";
import { Link } from "react-router-dom";
import useRouteMatch from "../../hooks/useRouteMatch";
import routes from "../../routes.json";
@ -9,6 +9,7 @@ import { Fragment, useContext } from "react";
import { UsersContext } from "../Context/UsersContextProvider";
import { I_User } from "../../util/user.util";
import { colors } from "../..";
import { FormattedMessage } from "react-intl";
const getPathWithoutId = (path?: string) => {
if (path) {
@ -38,7 +39,7 @@ const UserList = (props: UserListProps) => {
const routeMatch = useRouteMatch([routes.users + routes.user + ":id"]);
const theme = useTheme();
if (filteredData && filteredData.length) {
if (filteredData && filteredData.length > 0) {
return (
<List
sx={{
@ -87,6 +88,12 @@ const UserList = (props: UserListProps) => {
})}
</List>
);
} else if (filteredData?.length === 0) {
return (
<Alert severity="info" sx={{ mt: 1 }}>
<FormattedMessage id="noData" defaultMessage="No data found" />
</Alert>
);
}
return null;
};

View File

@ -2,44 +2,42 @@ import {sha1Hmac} from "./Sha1";
import { Utf8 } from "./Utf8";
import { toBase64 } from "./UInt8Utils";
export class S3Access
{
constructor
(
export class S3Access {
constructor(
readonly bucket: string,
readonly region: string,
readonly provider: string,
readonly key: string,
readonly secret: string
)
{}
) {}
get host() : string { return `${this.bucket}.${this.region}.${this.provider}` }
get url() : string { return `https://${this.host}` }
get host(): string {
return `${this.region}.${this.provider}`;
}
public get(s3Path : string): Promise<Response>
{
get url(): string {
return `https://${this.host}`;
}
public get(s3Path: string): Promise<Response> {
const method = "GET";
const auth = this.createAuthorizationHeader(method, s3Path, "");
const url = this.url + "/" + s3Path
const headers = {"Host": this.host, "Authorization": auth};
const url = this.url + "/" + this.bucket + "/" + s3Path;
const headers = { Host: this.host, Authorization: auth };
try
{
return fetch(url, {method: method, mode: "cors", headers: headers})
}
catch
{
return Promise.reject()
try {
return fetch(url, { method: method, mode: "cors", headers: headers });
} catch {
return Promise.reject();
}
}
private createAuthorizationHeader(method: string,
private createAuthorizationHeader(
method: string,
s3Path: string,
date: string)
{
return createAuthorizationHeader
(
date: string
) {
return createAuthorizationHeader(
method,
this.bucket,
s3Path,
@ -50,15 +48,16 @@ export class S3Access
}
}
function createAuthorizationHeader(method: string,
function createAuthorizationHeader(
method: string,
bucket: string,
s3Path: string,
date: string,
s3Key: string,
s3Secret: string,
contentType: string = "",
md5Hash: string = "")
{
md5Hash: string = ""
) {
// StringToSign = HTTP-Verb + "\n" +
// Content-MD5 + "\n" +
// Content-Type + "\n" +
@ -66,12 +65,14 @@ function createAuthorizationHeader(method: string,
// CanonicalizedAmzHeaders +
// CanonicalizedResource;
const payload = Utf8.encode(`${method}\n${md5Hash}\n${contentType}\n${date}\n/${bucket}/${s3Path}`)
const payload = Utf8.encode(
`${method}\n${md5Hash}\n${contentType}\n${date}\n/${bucket}/${s3Path}`
);
//console.log(`${method}\n${md5Hash}\n${contentType}\n${date}\n/${bucket}/${s3Path}`)
const secret = Utf8.encode(s3Secret)
const secret = Utf8.encode(s3Secret);
const signature = toBase64(sha1Hmac(payload, secret));
return `AWS ${s3Key}:${signature}`
return `AWS ${s3Key}:${signature}`;
}

View File

@ -19,7 +19,7 @@ export const colors = {
//change this color in index.css too
greyDark: "#d1d7de",
greyLight: "#e1e4e7",
greyLight: "#eaecf1",
orangeSelected: "#f7b34d",
orangeHover: "#fad399",

View File

@ -197,12 +197,6 @@ export const getAmount = (
highestConnectionValue: number,
values: BoxDataValue[]
) => {
console.log(
"getamount",
Math.abs(values[0].value as number) / highestConnectionValue,
Math.abs(values[0].value as number),
highestConnectionValue
);
return Math.abs(values[0].value as number) / highestConnectionValue;
};

View File

@ -1,142 +0,0 @@
import { styled, Tab, Tabs } from "@mui/material";
import { colors } from "index";
export const StyledTab = styled((props: any) => (
<Tab disableRipple {...props} />
))(({ theme }) => ({
bottom: -1,
fontWeight: theme.typography.fontWeightRegular,
fontSize: theme.typography.pxToRem(14),
marginRight: theme.spacing(1),
background: "0 0",
border: "1px solid transparent",
borderTopLeftRadius: "0.3rem",
borderTopRightRadius: "0.3rem",
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: colors.black,
backgroundColor: "#F2F4F8",
borderColor: "#90A7c5 #90A7c5 #F2F4F8",
marginTop: "1px",
bottom: -1,
},
}));
export const StyledTabBlue = styled((props: any) => (
<Tab disableRipple {...props} />
))(({ theme }) => ({
bottom: -1,
fontWeight: theme.typography.fontWeightRegular,
fontSize: theme.typography.pxToRem(14),
marginRight: theme.spacing(1),
background: "0 0",
border: "1px solid transparent",
borderBottom: "0px",
borderTopLeftRadius: "0.3rem",
borderTopRightRadius: "0.3rem",
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": {
bottom: -1,
color: colors.black,
backgroundColor: "#CCD6E4",
borderColor: "#90A7c5 #90A7c5 #CCD6E4",
marginTop: "1px",
},
}));
export const StyledTabBig = styled((props: any) => (
<Tab disableRipple {...props} />
))(({ theme }) => ({
bottom: -2,
fontWeight: theme.typography.fontWeightRegular,
fontSize: theme.typography.pxToRem(14),
marginRight: theme.spacing(1),
background: "0 0",
border: "2px solid transparent",
borderTopLeftRadius: "0.3rem",
borderTopRightRadius: "0.3rem",
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: colors.black,
backgroundColor: "#F2F4F8",
borderColor: "#90A7c5 #90A7c5 #F2F4F8",
marginTop: "1px",
bottom: -2,
},
}));
export const StyledTabWhite = styled((props: any) => (
<Tab disableRipple {...props} />
))(({ theme }) => ({
bottom: -1,
fontWeight: theme.typography.fontWeightRegular,
fontSize: theme.typography.pxToRem(14),
marginRight: theme.spacing(1),
background: "0 0",
border: "1px solid transparent",
borderTopLeftRadius: "0.3rem",
borderTopRightRadius: "0.3rem",
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: colors.black,
backgroundColor: "White",
borderColor: "#90A7c5 #90A7c5 White",
marginTop: "1px",
bottom: -1,
},
}));
export const AntTabsBig = styled(Tabs)({
borderBottom: "2px solid #90A7c5",
overflow: "visible!important",
"& div.MuiTabs-scroller": {
overflow: "visible!important",
},
"&.Mui-selected": {
color: colors.black,
backgroundColor: "red",
borderColor: `#90A7c5 #90A7c5 #fff`,
},
"& .MuiTabs-indicator": {
display: "flex",
justifyContent: "center",
backgroundColor: "transparent",
},
"&.MuiTabs-root": {
width: "100%",
},
});
export const AntTabs = styled(Tabs)({
borderBottom: "1px solid #90A7c5",
overflow: "visible!important",
"& div.MuiTabs-scroller": {
overflow: "visible!important",
},
"&.Mui-selected": {
color: colors.black,
backgroundColor: "red",
borderColor: `#90A7c5 #90A7c5 #fff`,
},
"& .MuiTabs-indicator": {
display: "flex",
justifyContent: "center",
backgroundColor: "transparent",
},
"&.MuiTabs-root": {
width: "100%",
},
});

View File

@ -23,6 +23,8 @@ export interface I_Installation extends S3Credentials {
name: string;
information: string;
parentId: number;
s3WriteKey: string;
s3WriteSecret: string;
}
export interface I_Folder {