Merge pull request #496 from bluewave-labs/feat/autodeploy
Feat/autodeploy
No files matched your search
@@ -1,2 +1,4 @@
|
||||
.vite
|
||||
.vscode
|
||||
.vscode
|
||||
*.sh
|
||||
!uptime.sh
|
||||
@@ -25,7 +25,6 @@
|
||||
"dayjs": "1.11.11",
|
||||
"joi": "17.13.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"mjml-react": "^2.0.8",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "9.1.2",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
// import "./App.css";
|
||||
import NotFound from "./Pages/NotFound";
|
||||
import Login from "./Pages/Auth/Login";
|
||||
import Register from "./Pages/Auth/Register";
|
||||
import Register from "./Pages/Auth/Register/Register";
|
||||
import HomeLayout from "./Layouts/HomeLayout";
|
||||
import Account from "./Pages/Account";
|
||||
import Monitors from "./Pages/Monitors";
|
||||
@@ -18,10 +20,13 @@ import NewPasswordConfirmed from "./Pages/Auth/NewPasswordConfirmed";
|
||||
import ProtectedRoute from "./Components/ProtectedRoute";
|
||||
import Details from "./Pages/Monitors/Details";
|
||||
import Maintenance from "./Pages/Maintenance";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import withAdminCheck from "./HOC/withAdminCheck";
|
||||
import Configure from "./Pages/Monitors/Configure";
|
||||
import PageSpeed from "./Pages/PageSpeed";
|
||||
import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed";
|
||||
|
||||
function App() {
|
||||
const AdminCheckedRegister = withAdminCheck(Register);
|
||||
return (
|
||||
<>
|
||||
<Routes>
|
||||
@@ -43,6 +48,10 @@ function App() {
|
||||
path="/monitors/:monitorId/"
|
||||
element={<ProtectedRoute Component={Details} />}
|
||||
/>
|
||||
<Route
|
||||
path="/monitors/configure/:monitorId/"
|
||||
element={<ProtectedRoute Component={Configure} />}
|
||||
/>
|
||||
<Route
|
||||
path="incidents"
|
||||
element={<ProtectedRoute Component={Incidents} />}
|
||||
@@ -76,10 +85,20 @@ function App() {
|
||||
path="account/team"
|
||||
element={<ProtectedRoute Component={Account} open="team" />}
|
||||
/>
|
||||
<Route
|
||||
path="page-speed"
|
||||
element={<ProtectedRoute Component={PageSpeed} />}
|
||||
/>
|
||||
<Route
|
||||
path="page-speed/create"
|
||||
element={<ProtectedRoute Component={CreatePageSpeed} />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route exact path="/login" element={<Login />} />
|
||||
<Route exact path="/register" element={<Register />} />
|
||||
|
||||
<Route exact path="/register" element={<AdminCheckedRegister />} />
|
||||
<Route exact path="/register/:token" element={<Register />} />
|
||||
{/* <Route path="/toast" element={<ToastComponent />} /> */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||
|
||||
@@ -30,12 +30,12 @@ const Avatar = ({ src, small, sx }) => {
|
||||
|
||||
return (
|
||||
<MuiAvatar
|
||||
alt={`${user?.firstname} ${user?.lastname}`}
|
||||
alt={`${user?.firstName} ${user?.lastName}`}
|
||||
src={
|
||||
src ? src : user?.avatarImage ? image : "/static/images/avatar/2.jpg"
|
||||
}
|
||||
sx={{
|
||||
fontSize: small ? "12px" : "20px",
|
||||
fontSize: small ? "13px" : "20px",
|
||||
display: "inline-flex",
|
||||
"&::before": {
|
||||
content: `""`,
|
||||
@@ -51,7 +51,7 @@ const Avatar = ({ src, small, sx }) => {
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{user.firstname?.charAt(0)}{user.lastname?.charAt(0)}
|
||||
{user.firstName?.charAt(0)}{user.lastName?.charAt(0)}
|
||||
</MuiAvatar>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -77,7 +77,6 @@
|
||||
}
|
||||
|
||||
.MuiPaper-root + .MuiPagination-root {
|
||||
flex: 1;
|
||||
margin-top: 35px;
|
||||
border: solid 1px var(--env-var-color-16);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
|
||||
@@ -32,6 +32,8 @@ import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
|
||||
* @param {JSX.Element} props.data.rows[].data[].data - The content to display in the cell.
|
||||
* @param {function} props.data.rows.data.handleClick - Function to call when the row is clicked.
|
||||
* @param {boolean} [props.paginated=false] - Flag to enable pagination.
|
||||
* @param {boolean} [props.reversed=false] - Flag to enable reverse order.
|
||||
* @param {number} props.rowsPerPage- Number of rows per page (table).
|
||||
*
|
||||
* @example
|
||||
* const data = {
|
||||
@@ -66,11 +68,10 @@ import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
|
||||
* <BasicTable data={data} rows={rows} paginated={true} />
|
||||
*/
|
||||
|
||||
const BasicTable = ({ data, paginated }) => {
|
||||
const BasicTable = ({ data, paginated, reversed, rowsPerPage = 5 }) => {
|
||||
// Add headers to props validation
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const rowsPerPage = 5;
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
@@ -79,15 +80,46 @@ const BasicTable = ({ data, paginated }) => {
|
||||
let displayData = [];
|
||||
|
||||
if (data && data.rows) {
|
||||
let rows = reversed ? [...data.rows].reverse() : data.rows;
|
||||
displayData = paginated
|
||||
? data.rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
: data.rows;
|
||||
? rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
: rows;
|
||||
}
|
||||
|
||||
if (!data || !data.cols || !data.rows) {
|
||||
return <div>No data</div>;
|
||||
}
|
||||
|
||||
let paginationComponent = <></>;
|
||||
if (paginated === true && data.rows.length > rowsPerPage) {
|
||||
paginationComponent = (
|
||||
<Pagination
|
||||
count={Math.ceil(data.rows.length / rowsPerPage)}
|
||||
page={page + 1}
|
||||
onChange={(event, value) => handleChangePage(event, value - 1)}
|
||||
shape="rounded"
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
slots={{
|
||||
previous: ArrowBackRoundedIcon,
|
||||
next: ArrowForwardRoundedIcon,
|
||||
}}
|
||||
{...item}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& .MuiTouchRipple-root": {
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
@@ -116,32 +148,8 @@ const BasicTable = ({ data, paginated }) => {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{paginated === true && (
|
||||
<Pagination
|
||||
count={Math.ceil(data.rows.length / rowsPerPage)}
|
||||
page={page + 1}
|
||||
onChange={(event, value) => handleChangePage(event, value - 1)}
|
||||
shape="rounded"
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
slots={{
|
||||
previous: ArrowBackRoundedIcon,
|
||||
next: ArrowForwardRoundedIcon,
|
||||
}}
|
||||
{...item}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& .MuiTouchRipple-root": {
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{paginationComponent}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -149,6 +157,8 @@ const BasicTable = ({ data, paginated }) => {
|
||||
BasicTable.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
paginated: PropTypes.bool,
|
||||
reversed: PropTypes.bool,
|
||||
rowPerPage: PropTypes.number,
|
||||
};
|
||||
|
||||
export default BasicTable;
|
||||
@@ -58,15 +58,12 @@ const Button = ({ id, type, level, label, disabled, img, onClick, sx }) => {
|
||||
color={color}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
disableRipple
|
||||
sx={{
|
||||
textTransform: "none",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& .MuiTouchRipple-root": {
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -70,6 +70,7 @@ const ButtonSpinner = ({
|
||||
endIcon={position === "end" && img}
|
||||
onClick={onClick}
|
||||
loading={isLoading}
|
||||
disableRipple
|
||||
sx={{
|
||||
textTransform: "none",
|
||||
"&:focus": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
:root {
|
||||
--spacing-general-0: 10px 25px;
|
||||
--spacing-general-0: 12px 18px;
|
||||
--spacing-general-1: 20px;
|
||||
--color-border-0: #eaecf0;
|
||||
--border-radius-0: 4px;
|
||||
@@ -12,17 +12,19 @@
|
||||
}
|
||||
|
||||
.server-status-tile {
|
||||
min-width: 250px;
|
||||
min-width: 100px;
|
||||
width: calc(100% - 310px);
|
||||
padding: var(--spacing-general-1);
|
||||
padding: var(--spacing-general-0);
|
||||
border: 1px solid var(--color-border-0);
|
||||
border-radius: var(--border-radius-0);
|
||||
background-color: var(--env-var-color-8);
|
||||
}
|
||||
|
||||
.server-status-tile-title {
|
||||
font-size: var(--font-size-0);
|
||||
font-weight: bold;
|
||||
margin-bottom: 25px;
|
||||
margin-bottom: calc(var(--spacing-general-1) / 2);
|
||||
color: var(--env-var-color-1);
|
||||
}
|
||||
|
||||
.server-status-tile-value {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "./check.css";
|
||||
import PropTypes from "prop-types";
|
||||
import CheckGrey from "../../assets/icons/check.svg?react";
|
||||
import CheckOutlined from "../../assets/icons/check-outlined.svg?react";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
@@ -11,6 +12,7 @@ import { useTheme } from "@emotion/react";
|
||||
* @param {Object} props - The properties that define the `Check` component.
|
||||
* @param {string} props.text - The text to be displayed as the label next to the check icon.
|
||||
* @param {'info' | 'error' | 'success'} [props.variant='info'] - The variant of the check component, affecting its styling.
|
||||
* @param {boolean} [props.outlined] - Whether the check icon should be outlined or not.
|
||||
*
|
||||
* @example
|
||||
* // To use this component, import it and use it in your JSX like this:
|
||||
@@ -18,12 +20,12 @@ import { useTheme } from "@emotion/react";
|
||||
*
|
||||
* @returns {React.Element} The `Check` component with a check icon and a label, defined by the `text` prop.
|
||||
*/
|
||||
const Check = ({ text, variant = "info" }) => {
|
||||
const Check = ({ text, variant = "info", outlined = false }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.gap.small}
|
||||
gap={outlined ? theme.gap.medium : theme.gap.small}
|
||||
className={`check${
|
||||
variant === "error"
|
||||
? " check-error"
|
||||
@@ -33,7 +35,11 @@ const Check = ({ text, variant = "info" }) => {
|
||||
}`}
|
||||
alignItems="center"
|
||||
>
|
||||
<CheckGrey alt="form checks" />
|
||||
{outlined ? (
|
||||
<CheckOutlined alt="check" />
|
||||
) : (
|
||||
<CheckGrey alt="form checks" />
|
||||
)}
|
||||
<Typography component="span">{text}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
@@ -42,6 +48,7 @@ const Check = ({ text, variant = "info" }) => {
|
||||
Check.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
variant: PropTypes.oneOf(["info", "error", "success"]),
|
||||
outlined: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Check;
|
||||
@@ -1,36 +0,0 @@
|
||||
.checkbox-holder {
|
||||
height: var(--env-var-img-width-1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
width: var(--env-var-font-size-medium);
|
||||
height: var(--env-var-font-size-medium);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-checkbox {
|
||||
width: var(--env-var-img-width-1);
|
||||
height: var(--env-var-img-width-1);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
margin-left: 5px;
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-5);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-checkbox-label {
|
||||
margin-left: var(--env-var-font-size-large);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-5);
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from "react";
|
||||
import "./Checkbox.css";
|
||||
|
||||
const CheckBox = () => {
|
||||
return (
|
||||
<div className="checkbox-holder">
|
||||
<input className="checkbox" type="checkbox" id="checkbox" />
|
||||
<label className="checkbox-label">Remember for 30 days</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckBox;
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from "react";
|
||||
import "./Checkbox.css";
|
||||
import PropsTypes from "prop-types";
|
||||
|
||||
const CustomizableCheckbox = (props) => {
|
||||
return (
|
||||
<div className="checkbox-holder">
|
||||
<input
|
||||
checked={props.isChecked}
|
||||
onChange={props.handleChange}
|
||||
className="custom-checkbox"
|
||||
type="checkbox"
|
||||
id={props.id}
|
||||
/>
|
||||
<label className="custom-checkbox-label" htmlFor={props.id}>{props.title}</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CustomizableCheckbox.propsTypes = {
|
||||
title: PropsTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CustomizableCheckbox;
|
||||
@@ -1,15 +0,0 @@
|
||||
.config-box {
|
||||
display: flex;
|
||||
padding: var(--env-var-spacing-4) 50px;
|
||||
padding-bottom: 60px;
|
||||
border: 1px solid var(--env-var-color-16);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
justify-content: space-between;
|
||||
}
|
||||
.config-box>.MuiStack-root{
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
.config-box>.MuiStack-root:first-of-type{
|
||||
margin-right: var(--env-var-spacing-3);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import "./index.css";
|
||||
import React from "react";
|
||||
|
||||
const ConfigBox = ({ leftLayout, rightLayout }) => {
|
||||
return (
|
||||
<div className="config-box">
|
||||
{leftLayout}
|
||||
{rightLayout}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigBox;
|
||||
@@ -1,20 +0,0 @@
|
||||
.dashboard-menu-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dashboard-menu-text {
|
||||
margin-left: 8px;
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import "./index.css";
|
||||
import DashboardMenuButton from "../DashboardMenuButton";
|
||||
|
||||
import Monitors from "../../../assets/Images/Icon-monitor-gray.png";
|
||||
import Incidents from "../../../assets/Images/Icon-warning-gray.png";
|
||||
import SensorsIcon from "../../../assets/Images/Icon-signal-gray.png";
|
||||
import AllInclusiveIcon from "../../../assets/Images/Icon-link-gray.png";
|
||||
import MaintenanceIcon from "../../../assets/Images/Icon-maintenance-gray.png";
|
||||
import SettingsIcon from "../../../assets/Images/Icon-setting-gray.png";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import pathMap from "../../../Utils/PathMap";
|
||||
|
||||
/**
|
||||
* @component
|
||||
* DashboardMenu component displays a menu with icons and corresponding text.
|
||||
* Each menu item consists of an icon and a text label.
|
||||
*
|
||||
* @returns {JSX.Element} The JSX element representing the DashboardMenu component.
|
||||
*/
|
||||
|
||||
function DashboardMenu() {
|
||||
const [activeButton, setActiveButton] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
setActiveButton(location.pathname);
|
||||
}, [location]);
|
||||
|
||||
const handleClick = (title, path) => {
|
||||
setActiveButton(title);
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dashboard-menu-container">
|
||||
{DashboardMenuButton(
|
||||
Monitors,
|
||||
"Monitors",
|
||||
"/monitors",
|
||||
activeButton === "/monitors",
|
||||
() => handleClick(pathMap["/monitors"], "/monitors")
|
||||
)}
|
||||
{DashboardMenuButton(
|
||||
Incidents,
|
||||
"Incidents",
|
||||
"/incidents",
|
||||
activeButton === "/incidents",
|
||||
() => handleClick(pathMap["/incidents"], "/incidents")
|
||||
)}
|
||||
{DashboardMenuButton(
|
||||
SensorsIcon,
|
||||
"Status Pages",
|
||||
"/status",
|
||||
activeButton === "/status",
|
||||
() => handleClick(pathMap["/status"], "/status")
|
||||
)}
|
||||
{DashboardMenuButton(
|
||||
AllInclusiveIcon,
|
||||
"Integrations",
|
||||
"/integrations",
|
||||
activeButton === "/integrations",
|
||||
() => handleClick(pathMap["/integrations"], "/integrations")
|
||||
)}
|
||||
{DashboardMenuButton(
|
||||
MaintenanceIcon,
|
||||
"Maintenance",
|
||||
"/maintenance",
|
||||
activeButton === "/maintenance",
|
||||
() => handleClick(pathMap["/maintenance"], "/maintenance")
|
||||
)}
|
||||
{DashboardMenuButton(
|
||||
SettingsIcon,
|
||||
"Settings",
|
||||
"/settings",
|
||||
activeButton === "/settings",
|
||||
() => handleClick(pathMap["/settings"], "/settings")
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardMenu;
|
||||
@@ -1,23 +0,0 @@
|
||||
.icon-container {
|
||||
padding: 8px 15px;
|
||||
}
|
||||
|
||||
.icon-container:hover {
|
||||
background-color: var(--env-var-color-13);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-container.active {
|
||||
background-color: var(--env-var-color-13);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: var(--env-var-img-width-1);
|
||||
}
|
||||
|
||||
.dashboard-menu-text {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-12);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import "./index.css";
|
||||
import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
/**
|
||||
* Renders a menu button with an icon and title.
|
||||
*
|
||||
* @component
|
||||
* @param {React.ElementType} icon - The icon component to be rendered.
|
||||
* @param {string} title - The title of the menu button.
|
||||
* @returns {React.ReactElement} The rendered menu button component.
|
||||
*/
|
||||
|
||||
const DashboardMenuButton = (icon, title, to, isActive, onClick) => {
|
||||
const handleClick = () => {
|
||||
onClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`icon-container ${isActive ? "active" : ""}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<img className="icon" src={icon} alt="Monitors" />
|
||||
<div className="text-container">
|
||||
<span className="dashboard-menu-text">{title}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardMenuButton;
|
||||
@@ -1,30 +0,0 @@
|
||||
.dashboard-sidebar {
|
||||
height: calc(100vh - 64px);
|
||||
width: 250px;
|
||||
border: 1px solid var(--color-border-0);
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
top: 64px;
|
||||
position: sticky;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.support-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28px 35px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.support-text {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-12);
|
||||
}
|
||||
|
||||
.support-icon {
|
||||
width: var(--env-var-img-width-1);
|
||||
margin-right: 8px;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import DashboardMenu from "../DashboardMenu";
|
||||
import SvgIcon from "@mui/material/SvgIcon";
|
||||
import "./index.css";
|
||||
import SupportIcon from "../../../assets/Images/Icon-support-gray.png";
|
||||
|
||||
/**
|
||||
* @component
|
||||
* DashboardSidebar component serves as a sidebar containing the DashboardMenu and a Support icon at the bottom.
|
||||
*
|
||||
* @returns {JSX.Element} The JSX element representing the DashboardSidebar component.
|
||||
*/
|
||||
|
||||
function DashboardSidebar() {
|
||||
return (
|
||||
<div className="dashboard-sidebar">
|
||||
<div className="menu-container">
|
||||
<DashboardMenu />
|
||||
</div>
|
||||
<div className="support-container">
|
||||
<img className="support-icon" src={SupportIcon} alt="SupportIcon" />
|
||||
<span className="support-text">Support</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardSidebar;
|
||||
@@ -0,0 +1,28 @@
|
||||
[class*="fallback__"] {
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
margin-top: 100px;
|
||||
}
|
||||
[class*="fallback__"] h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 600;
|
||||
}
|
||||
[class*="fallback__"] .check span.MuiTypography-root,
|
||||
[class*="fallback__"] h1.MuiTypography-root {
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
[class*="fallback__"] button.MuiButtonBase-root,
|
||||
[class*="fallback__"] .check {
|
||||
width: max-content;
|
||||
}
|
||||
[class*="fallback__"] button.MuiButtonBase-root {
|
||||
height: 34px;
|
||||
}
|
||||
[class*="fallback__"] .check span.MuiTypography-root,
|
||||
[class*="fallback__"] button.MuiButtonBase-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
.fallback__status > .MuiStack-root {
|
||||
margin-left: var(--env-var-spacing-2);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import Skeleton from "../../assets/Images/create-placeholder.svg?react";
|
||||
import Button from "../Button";
|
||||
import Check from "../Check/Check";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Fallback component to display a fallback UI with a title, a list of checks, and a navigation button.
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string} props.title - The title to be displayed in the fallback UI.
|
||||
* @param {Array<string>} props.checks - An array of strings representing the checks to display.
|
||||
* @param {string} [props.link="/"] - The link to navigate to.
|
||||
*
|
||||
* @returns {JSX.Element} The rendered fallback UI.
|
||||
*/
|
||||
|
||||
const Fallback = ({ title, checks, link = "/" }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className={`fallback__${title.trim().split(" ")[0]}`}
|
||||
alignItems="center"
|
||||
gap={theme.gap.xl}
|
||||
>
|
||||
<Skeleton />
|
||||
<Stack gap={theme.gap.small} maxWidth={"275px"}>
|
||||
<Typography component="h1" marginY={theme.gap.medium}>
|
||||
A {title} is used to:
|
||||
</Typography>
|
||||
{checks.map((check, index) => (
|
||||
<Check
|
||||
text={check}
|
||||
key={`${title.trim().split(" ")[0]}-${index}`}
|
||||
outlined={true}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Button
|
||||
level="primary"
|
||||
label={`Let's create your ${title}`}
|
||||
sx={{ alignSelf: "center" }}
|
||||
onClick={() => navigate(link)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Fallback.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
checks: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
link: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Fallback;
|
||||
@@ -0,0 +1,14 @@
|
||||
.checkbox-wrapper {
|
||||
border-radius: var(--env-var-radius-1);
|
||||
}
|
||||
.checkbox-wrapper:not(:has(.Mui-disabled)):hover {
|
||||
background-color: var(--env-var-color-15);
|
||||
}
|
||||
.checkbox-wrapper span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-2);
|
||||
}
|
||||
.checkbox-wrapper .MuiButtonBase-root {
|
||||
margin-right: var(--env-var-spacing-1);
|
||||
padding: 0;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material";
|
||||
import CheckboxOutline from "../../../assets/icons/checkbox-outline.svg?react";
|
||||
import CheckboxFilled from "../../../assets/icons/checkbox-filled.svg?react";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* @param {Object} props
|
||||
* @param {string} props.id - The id attribute for the checkbox input.
|
||||
* @param {string} props.label - The label to display next to the checkbox.
|
||||
* @param {('small' | 'medium' | 'large')} - The size of the checkbox.
|
||||
* @param {boolean} props.isChecked - Whether the checkbox is checked or not.
|
||||
* @param {string} [props.value] - The value of the checkbox input.
|
||||
* @param {function} [props.onChange] - The function to call when the checkbox value changes.
|
||||
* @param {boolean} [props.isDisabled] - Whether the checkbox is disabled or not.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*
|
||||
* @example
|
||||
* <Checkbox
|
||||
* id="checkbox-id"
|
||||
* label="Ping monitoring"
|
||||
* isChecked={checks.type === "ping"}
|
||||
* value="ping"
|
||||
* onChange={handleChange}
|
||||
* />
|
||||
*/
|
||||
|
||||
const Checkbox = ({
|
||||
id,
|
||||
label,
|
||||
size = "medium",
|
||||
isChecked,
|
||||
value,
|
||||
onChange,
|
||||
isDisabled,
|
||||
}) => {
|
||||
const sizes = { small: "14px", medium: "16px", large: "18px" };
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
className="checkbox-wrapper"
|
||||
control={
|
||||
<MuiCheckbox
|
||||
checked={isDisabled ? false : isChecked}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
icon={<CheckboxOutline />}
|
||||
checkedIcon={<CheckboxFilled />}
|
||||
inputProps={{
|
||||
"aria-label": "controlled checkbox",
|
||||
id: id,
|
||||
}}
|
||||
sx={{
|
||||
"&:hover": { backgroundColor: "transparent" },
|
||||
"& svg": { width: sizes[size], height: sizes[size] },
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={label}
|
||||
disabled={isDisabled}
|
||||
sx={{
|
||||
p: "5px",
|
||||
m: "-5px",
|
||||
"& .MuiButtonBase-root": {
|
||||
width: "20px",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Checkbox.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
size: PropTypes.oneOf(["small", "medium", "large"]),
|
||||
isChecked: PropTypes.bool.isRequired,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
isDisabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Checkbox;
|
||||
@@ -19,8 +19,10 @@ import { useState } from "react";
|
||||
* @param {string} [props.type] - Type of input field (e.g., 'text', 'password').
|
||||
* @param {string} props.id - ID of the input field.
|
||||
* @param {string} [props.label] - Label for the input field.
|
||||
* @param {boolean} [props.https] - Indicates if it should display http or https.
|
||||
* @param {boolean} [props.isRequired] - Indicates if the field is required, will display a red asterisk.
|
||||
* @param {boolean} [props.isOptional] - Indicates if the field is optional, will display optional text.
|
||||
* @param {string} [props.optionalLabel] - Optional label for the input field.
|
||||
* @param {boolean} [props.hasCopy] - Indicates if the field supports copying.
|
||||
* @param {string} [props.autoComplete] - Autocomplete value for the input field.
|
||||
* @param {string} [props.placeholder] - Placeholder text for the input field.
|
||||
@@ -34,8 +36,10 @@ const Field = ({
|
||||
type = "text",
|
||||
id,
|
||||
label,
|
||||
https,
|
||||
isRequired,
|
||||
isOptional,
|
||||
optionalLabel,
|
||||
hasCopy,
|
||||
autoComplete,
|
||||
placeholder,
|
||||
@@ -61,7 +65,13 @@ const Field = ({
|
||||
<Typography component="h3">
|
||||
{label}
|
||||
{isRequired ? <span className="field-required">*</span> : ""}
|
||||
{isOptional ? <span className="field-optional">(optional)</span> : ""}
|
||||
{isOptional ? (
|
||||
<span className="field-optional">
|
||||
{optionalLabel || "(optional)"}
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
<TextField
|
||||
@@ -74,6 +84,17 @@ const Field = ({
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
sx={
|
||||
type === "url"
|
||||
? {
|
||||
"& .MuiInputBase-root": { padding: 0 },
|
||||
"& .MuiStack-root": {
|
||||
borderTopLeftRadius: `${theme.shape.borderRadius}px`,
|
||||
borderBottomLeftRadius: `${theme.shape.borderRadius}px`,
|
||||
},
|
||||
}
|
||||
: {}
|
||||
}
|
||||
InputProps={{
|
||||
startAdornment: type === "url" && (
|
||||
<Stack
|
||||
@@ -82,9 +103,13 @@ const Field = ({
|
||||
height="100%"
|
||||
sx={{
|
||||
borderRight: `solid 1px ${theme.palette.section.borderColor}`,
|
||||
backgroundColor: "#f9f9fa",
|
||||
pl: theme.gap.medium,
|
||||
}}
|
||||
>
|
||||
<Typography component="h5">https://</Typography>
|
||||
<Typography component="h5" sx={{ lineHeight: 1 }}>
|
||||
{https ? "https" : "http"}://
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
endAdornment:
|
||||
@@ -144,8 +169,10 @@ Field.propTypes = {
|
||||
type: PropTypes.oneOf(["text", "password", "url", "email", "description"]),
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
https: PropTypes.bool,
|
||||
isRequired: PropTypes.bool,
|
||||
isOptional: PropTypes.bool,
|
||||
optionalLabel: PropTypes.string,
|
||||
hasCopy: PropTypes.bool,
|
||||
autoComplete: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
.select-wrapper .select-component > .MuiSelect-select {
|
||||
padding: 0 10px;
|
||||
height: 34px !important;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
text-transform: capitalize;
|
||||
line-height: 1;
|
||||
}
|
||||
.select-wrapper .MuiInputBase-root:has(> .MuiSelect-select) {
|
||||
min-width: 150px;
|
||||
}
|
||||
.select-wrapper .MuiInputBase-root:has(> .MuiSelect-select),
|
||||
.MuiMenu-List .MuiMenuItem-root,
|
||||
.select-wrapper h3.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
.MuiInputBase-root:has(> .MuiSelect-select) fieldset {
|
||||
border-color: var(--env-var-color-29);
|
||||
}
|
||||
.MuiInputBase-root:not(.Mui-focused):has(> .MuiSelect-select):hover fieldset {
|
||||
border-color: var(--env-var-color-29);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import {
|
||||
MenuItem,
|
||||
Select as MuiSelect,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* @component
|
||||
* @param {object} props
|
||||
* @param {string} props.id - The ID attribute for the select element.
|
||||
* @param {string} props.placeholder - The label of the select element.
|
||||
* @param {string} props.placeholder - The placeholder text when no option is selected.
|
||||
* @param {boolean} props.isHidden - Whether the placeholder should be hidden.
|
||||
* @param {string} props.value - The currently selected value.
|
||||
* @param {object[]} props.items - The array of items to populate in the select dropdown.
|
||||
* @param {(string | number)} props.items._id - The unique identifier of each item.
|
||||
* @param {string} props.items.name - The display name of each item.
|
||||
* @param {function} props.onChange - The function to handle onChange event.
|
||||
* @param {object} props.sx - The custom styles object for MUI Select component.
|
||||
* @returns {JSX.Element}
|
||||
*
|
||||
* @example
|
||||
* const frequencies = [
|
||||
* { _id: 1, name: "1 minute" },
|
||||
* { _id: 2, name: "2 minutes" },
|
||||
* { _id: 3, name: "3 minutes" },
|
||||
* ];
|
||||
*
|
||||
* <Select
|
||||
* id="frequency-id"
|
||||
* label="Check frequency"
|
||||
* placeholder="Select frequency"
|
||||
* value={value}
|
||||
* onChange={handleChange}
|
||||
* items={frequencies}
|
||||
* />
|
||||
*/
|
||||
|
||||
const Select = ({
|
||||
id,
|
||||
label,
|
||||
placeholder,
|
||||
isHidden,
|
||||
value,
|
||||
items,
|
||||
onChange,
|
||||
sx,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const itemStyles = {
|
||||
fontSize: "var(--env-var-font-size-medium)",
|
||||
color: theme.palette.otherColors.bluishGray,
|
||||
textTransform: "capitalize",
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
margin: theme.gap.xs,
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={theme.gap.xs} className="select-wrapper">
|
||||
{label && <Typography component="h3">{label}</Typography>}
|
||||
<MuiSelect
|
||||
className="select-component"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
displayEmpty
|
||||
inputProps={{ id: id }}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
className: "select-dropdown",
|
||||
style: {
|
||||
marginTop: theme.gap.xs,
|
||||
},
|
||||
},
|
||||
MenuListProps: {
|
||||
style: { padding: 0 },
|
||||
},
|
||||
}}
|
||||
IconComponent={KeyboardArrowDownIcon}
|
||||
sx={{ ...sx }}
|
||||
>
|
||||
{placeholder && (
|
||||
<MenuItem
|
||||
className="select-placeholder"
|
||||
value="0"
|
||||
sx={{
|
||||
display: isHidden ? "none" : "flex",
|
||||
visibility: isHidden ? "none" : "visible",
|
||||
...itemStyles,
|
||||
}}
|
||||
>
|
||||
{placeholder}
|
||||
</MenuItem>
|
||||
)}
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
value={item._id}
|
||||
key={`${id}-${item._id}`}
|
||||
sx={{
|
||||
...itemStyles,
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MuiSelect>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Select.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
isHidden: PropTypes.bool,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
sx: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Select;
|
||||
@@ -1,71 +0,0 @@
|
||||
import { Box, Typography, useTheme } from '@mui/material';
|
||||
import Button from '../Button';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Integrations component
|
||||
* @param {Object} props - Props for the IntegrationsComponent.
|
||||
* @param {string} props.url - The URL for the integration image.
|
||||
* @param {string} props.header - The header for the integration.
|
||||
* @param {string} props.info - Information about the integration.
|
||||
* @param {Function} props.onClick - The onClick handler for the integration button.
|
||||
* @returns {JSX.Element} The JSX representation of the IntegrationsComponent.
|
||||
*/
|
||||
const IntegrationsComponent = ({ url, header, info, onClick }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
p={theme.spacing(2)}
|
||||
border={1}
|
||||
borderColor={theme.palette.divider}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={url}
|
||||
alt="Integration"
|
||||
width={100}
|
||||
height={100}
|
||||
/>
|
||||
|
||||
<Box
|
||||
flex={1}
|
||||
mx={theme.spacing(2)}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Typography variant="h6" component="div" style={{ fontSize: '16px' }}>
|
||||
{header}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
component="div"
|
||||
sx={{
|
||||
maxWidth: '300px',
|
||||
wordWrap: 'break-word',
|
||||
textAlign: 'left'
|
||||
}}
|
||||
style={{ fontSize: '13px' }}
|
||||
>
|
||||
{info}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Button label="Click Me" level="primary" onClick={onClick} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// PropTypes for IntegrationsComponent
|
||||
IntegrationsComponent.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
header: PropTypes.string.isRequired,
|
||||
info: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default IntegrationsComponent;
|
||||
@@ -117,34 +117,32 @@ ColoredLabel.propTypes = {
|
||||
/**
|
||||
* @component
|
||||
* @param {Object} props
|
||||
* @param {'Seen' | 'Waiting' | 'New' | 'Active'} props.status - The status for the label
|
||||
* @param {string} props.dot - The color of the dot
|
||||
* @param { 'up' | 'down' | 'cannot resolve'} props.status - The status for the label
|
||||
* @param {string} props.text - The text of the label
|
||||
* @returns {JSX.Element}
|
||||
* @example
|
||||
* // Render an active label
|
||||
* <StatusLabel status="Active" />
|
||||
* <StatusLabel status="up" text="Active" />
|
||||
*/
|
||||
|
||||
const StatusLabel = ({ status, dot, customStyles }) => {
|
||||
const StatusLabel = ({ status, text, customStyles }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const colorLookup = {
|
||||
Seen: theme.palette.labelGray.color,
|
||||
Waiting: theme.palette.labelRed.color,
|
||||
New: theme.palette.labelOrange.color,
|
||||
Active: theme.palette.labelGreen.color,
|
||||
Down: theme.palette.error.main, // Assuming theme.palette.error.main is red
|
||||
};
|
||||
|
||||
// Look up the color for the status, default to labelGray if not found
|
||||
const color = colorLookup[status] || theme.palette.labelGray.color;
|
||||
// Look up the color for the status
|
||||
const { borderColor, bgColor, dotColor } = theme.label[status];
|
||||
|
||||
return (
|
||||
<BaseLabel label={status} styles={customStyles}>
|
||||
<BaseLabel
|
||||
label={text}
|
||||
styles={{
|
||||
backgroundColor: bgColor,
|
||||
borderColor: borderColor,
|
||||
...customStyles,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width={7}
|
||||
height={7}
|
||||
bgcolor={dot || color}
|
||||
bgcolor={dotColor}
|
||||
borderRadius="50%"
|
||||
marginRight="5px"
|
||||
/>
|
||||
@@ -153,15 +151,8 @@ const StatusLabel = ({ status, dot, customStyles }) => {
|
||||
};
|
||||
|
||||
StatusLabel.propTypes = {
|
||||
status: PropTypes.oneOf([
|
||||
"Seen",
|
||||
"Waiting",
|
||||
"New",
|
||||
"Active",
|
||||
"Up",
|
||||
"Down",
|
||||
"Cannot resolve",
|
||||
]),
|
||||
status: PropTypes.oneOf(["up", "down", "cannot resolve"]),
|
||||
text: PropTypes.string,
|
||||
customStyles: PropTypes.object,
|
||||
};
|
||||
|
||||
|
||||
@@ -15,13 +15,21 @@ const Link = ({ level, label, url }) => {
|
||||
|
||||
const levelConfig = {
|
||||
primary: {},
|
||||
secondary: {},
|
||||
secondary: {
|
||||
color: theme.palette.otherColors.bluishGray,
|
||||
sx: {
|
||||
":hover": {
|
||||
color: theme.palette.otherColors.bluishGray,
|
||||
},
|
||||
},
|
||||
},
|
||||
tertiary: {
|
||||
color: theme.palette.tertiary.main,
|
||||
sx: {
|
||||
textDecoration: "underline",
|
||||
textDecorationStyle: "dashed",
|
||||
textDecorationColor: theme.palette.primary.main,
|
||||
textUnderlineOffset: "1px",
|
||||
":hover": {
|
||||
color: theme.palette.tertiary.main,
|
||||
textDecorationColor: theme.palette.primary.main,
|
||||
@@ -33,7 +41,13 @@ const Link = ({ level, label, url }) => {
|
||||
};
|
||||
const { sx, color } = levelConfig[level];
|
||||
return (
|
||||
<MuiLink href={url} sx={sx} color={color}>
|
||||
<MuiLink
|
||||
href={url}
|
||||
sx={{ width: "fit-content", ...sx }}
|
||||
color={color}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{label}
|
||||
</MuiLink>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
/* NavBar Component Styles*/
|
||||
.icon-button-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.MuiToolbar-root {
|
||||
min-height: var(--env-var-nav-bar-height) !important;
|
||||
}
|
||||
|
||||
.icon-button-toggle-title {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-5);
|
||||
margin-right: var(--env-var-spacing-2);
|
||||
}
|
||||
|
||||
.icon-button-toggle-pic {
|
||||
@@ -17,7 +14,6 @@
|
||||
}
|
||||
|
||||
#icon-button {
|
||||
color: transparent;
|
||||
-webkit-transition: none;
|
||||
transition: none;
|
||||
outline: none;
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import "./index.css";
|
||||
import { useState } from "react";
|
||||
import { cloneElement, useState } from "react";
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
import Box from "@mui/material/Box";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import Container from "@mui/material/Container";
|
||||
import Avatar from "../Avatar";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import ChevronDown from "../../assets/Images/Icon-chevron-down.png";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import { clearAuthState } from "../../Features/Auth/authSlice";
|
||||
import { clearMonitorState } from "../../Features/Monitors/monitorsSlice";
|
||||
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@@ -22,10 +20,8 @@ import LockSvg from "../../assets/icons/lock.svg?react";
|
||||
import UserSvg from "../../assets/icons/user.svg?react";
|
||||
import TeamSvg from "../../assets/icons/user-two.svg?react";
|
||||
import LogoutSvg from "../../assets/icons/logout.svg?react";
|
||||
import { Stack, useScrollTrigger } from "@mui/material";
|
||||
|
||||
import BWULogo from "../../assets/Images/bwl-logo.svg?react";
|
||||
|
||||
const settings = ["Profile", "Password", "Team", "Logout"];
|
||||
const icons = {
|
||||
Profile: <UserSvg />,
|
||||
Team: <TeamSvg />,
|
||||
@@ -33,6 +29,33 @@ const icons = {
|
||||
Logout: <LogoutSvg />,
|
||||
};
|
||||
|
||||
function AddBorderOnScroll(props) {
|
||||
const { children, window } = props;
|
||||
const trigger = useScrollTrigger({
|
||||
target: window ? window() : undefined,
|
||||
disableHysteresis: true,
|
||||
threshold: 0,
|
||||
});
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
className={trigger ? "scrolled" : ""}
|
||||
position="sticky"
|
||||
sx={{
|
||||
boxShadow: "none",
|
||||
transition: "all 0.3s ease",
|
||||
borderBottom: "1px solid transparent",
|
||||
"&.scrolled": {
|
||||
borderBottom: "1px solid #eaecf0",
|
||||
backgroundColor: "white",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* NavBar component
|
||||
*
|
||||
@@ -51,6 +74,12 @@ function NavBar() {
|
||||
const navigate = useNavigate();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
|
||||
// Initialize settings and update based on user role
|
||||
let settings = ["Profile", "Password", "Team", "Logout"];
|
||||
if (authState.user?.role && !authState.user.role.includes("admin")) {
|
||||
settings = ["Profile", "Password", "Logout"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles opening the user menu.
|
||||
*
|
||||
@@ -67,7 +96,7 @@ function NavBar() {
|
||||
const logout = () => {
|
||||
// Clear auth state
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearMonitorState());
|
||||
dispatch(clearUptimeMonitorState());
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
@@ -95,109 +124,62 @@ function NavBar() {
|
||||
};
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
position="sticky"
|
||||
sx={{
|
||||
width: "100%",
|
||||
backgroundColor: "white",
|
||||
boxShadow: "var(--env-var-shadow-1)",
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="xxl" sx={{ width: "100%" }}>
|
||||
<Toolbar disableGutters>
|
||||
<BWULogo id="bw-uptime-logo-dashboard" alt="BlueWave Uptime Logo" />
|
||||
|
||||
<Box sx={{ flexGrow: 1, display: { xs: "flex", md: "none" } }}>
|
||||
<IconButton
|
||||
size="large"
|
||||
aria-label="account of current user"
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
color="inherit"
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="h5"
|
||||
noWrap
|
||||
component="a"
|
||||
href="#"
|
||||
sx={{
|
||||
mr: 2,
|
||||
display: { xs: "flex", md: "none" },
|
||||
flexGrow: 1,
|
||||
fontFamily: "monospace",
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
letterSpacing: theme.spacing(0.3),
|
||||
color: "inherit",
|
||||
textDecoration: "none",
|
||||
}}
|
||||
<AddBorderOnScroll>
|
||||
<Toolbar disableGutters sx={{ alignSelf: "flex-end", paddingX: "25px" }}>
|
||||
<Tooltip title="Open settings">
|
||||
<IconButton
|
||||
id="icon-button"
|
||||
onClick={handleOpenUserMenu}
|
||||
sx={{ p: 0 }}
|
||||
>
|
||||
UPTIME GENIE
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
|
||||
<Box sx={{ flexGrow: 0 }}>
|
||||
<Tooltip title="Open settings">
|
||||
<IconButton
|
||||
id="icon-button"
|
||||
onClick={handleOpenUserMenu}
|
||||
sx={{ p: 0 }}
|
||||
<Stack direction="row" alignItems="center" gap="8px">
|
||||
<Avatar small={true} />
|
||||
<Box
|
||||
className="icon-button-toggle-title"
|
||||
sx={{ mr: "3px", lineHeight: 2 }}
|
||||
>
|
||||
<div className="icon-button-toggle">
|
||||
<Avatar small={true} sx={{ mr: "8px" }} />
|
||||
<div className="icon-button-toggle-title">
|
||||
{authState.user.firstname} {authState.user.lastname}
|
||||
</div>
|
||||
<img
|
||||
className="icon-button-toggle-pic"
|
||||
src={ChevronDown}
|
||||
alt="ChevronDown"
|
||||
/>
|
||||
</div>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Menu
|
||||
sx={{ mt: theme.spacing(5.5) }}
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorElUser}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
open={Boolean(anchorElUser)}
|
||||
onClose={handleCloseUserMenu}
|
||||
{authState.user?.firstName} {authState.user?.lastName}
|
||||
</Box>
|
||||
<KeyboardArrowDownIcon sx={{ mt: "2px" }} />
|
||||
</Stack>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Menu
|
||||
sx={{ mt: theme.spacing(5.5) }}
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorElUser}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
open={Boolean(anchorElUser)}
|
||||
onClose={handleCloseUserMenu}
|
||||
>
|
||||
{settings.map((setting) => (
|
||||
<MenuItem
|
||||
id="menu-item"
|
||||
key={setting}
|
||||
onClick={() => handleCloseUserMenu(setting)}
|
||||
sx={{ width: "150px" }}
|
||||
>
|
||||
{settings.map((setting) => (
|
||||
<MenuItem
|
||||
id="menu-item"
|
||||
key={setting}
|
||||
onClick={() => handleCloseUserMenu(setting)}
|
||||
sx={{ width: "150px" }}
|
||||
>
|
||||
{icons[setting]}
|
||||
<Typography
|
||||
fontSize="var(--env-var-font-size-medium)"
|
||||
textAlign="center"
|
||||
marginLeft="8px"
|
||||
>
|
||||
{setting}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</Container>
|
||||
</AppBar>
|
||||
{icons[setting]}
|
||||
<Typography
|
||||
fontSize="var(--env-var-font-size-medium)"
|
||||
textAlign="center"
|
||||
marginLeft="8px"
|
||||
>
|
||||
{setting}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Toolbar>
|
||||
</AddBorderOnScroll>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
.create-monitor-form .custom-radio-button.MuiFormControlLabel-root {
|
||||
.create-monitor-form .custom-radio-button.MuiFormControlLabel-root,
|
||||
.configure-monitor .custom-radio-button.MuiFormControlLabel-root {
|
||||
padding: 5px;
|
||||
margin: -5px;
|
||||
border-radius: var(--env-var-radius-1);
|
||||
}
|
||||
.create-monitor-form .custom-radio-button.MuiFormControlLabel-root:hover {
|
||||
background-color: var(--env-var-color-13);
|
||||
}
|
||||
.create-monitor-form .custom-radio-button.MuiFormControlLabel-root svg{
|
||||
stroke-width: 0;
|
||||
.create-monitor-form .custom-radio-button.MuiFormControlLabel-root:hover,
|
||||
.configure-monitor .custom-radio-button.MuiFormControlLabel-root:hover {
|
||||
background-color: var(--env-var-color-15);
|
||||
}
|
||||
|
||||
.custom-radio-button.MuiFormControlLabel-root .MuiButtonBase-root {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
aside .MuiStack-root {
|
||||
cursor: pointer;
|
||||
}
|
||||
aside > svg {
|
||||
margin: var(--env-var-spacing-2) var(--env-var-spacing-1);
|
||||
}
|
||||
aside .selected-path,
|
||||
aside .MuiStack-root:hover {
|
||||
background-color: var(--env-var-color-13);
|
||||
}
|
||||
aside .MuiStack-root svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
aside span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-5);
|
||||
line-height: 1;
|
||||
}
|
||||
aside .MuiStack-root:nth-last-child(2) {
|
||||
margin-top: auto;
|
||||
position: relative;
|
||||
}
|
||||
aside .MuiStack-root:nth-last-child(2):before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
border-top: solid 1px var(--env-var-color-6);
|
||||
}
|
||||
aside .MuiStack-root:last-child {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
import BWULogo from "../../assets/Images/bwl-logo.svg?react";
|
||||
import Support from "../../assets/icons/support.svg?react";
|
||||
import StatusPages from "../../assets/icons/status-pages.svg?react";
|
||||
import Maintenance from "../../assets/icons/maintenance.svg?react";
|
||||
import Monitors from "../../assets/icons/monitors.svg?react";
|
||||
import Incidents from "../../assets/icons/incidents.svg?react";
|
||||
import Integrations from "../../assets/icons/integrations.svg?react";
|
||||
import PageSpeed from "../../assets/icons/page-speed.svg?react";
|
||||
import Settings from "../../assets/icons/settings.svg?react";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* @component
|
||||
* Sidebar component serves as a sidebar containing a menu.
|
||||
*
|
||||
* @returns {JSX.Element} The JSX element representing the Sidebar component.
|
||||
*/
|
||||
|
||||
const menu = [
|
||||
{ name: "Monitors", path: "monitors", icon: <Monitors /> },
|
||||
{ name: "Incidents", path: "incidents", icon: <Incidents /> },
|
||||
{ name: "Status pages", path: "status", icon: <StatusPages /> },
|
||||
{ name: "Maintenance", path: "maintenance", icon: <Maintenance /> },
|
||||
{ name: "Page speed", path: "page-speed", icon: <PageSpeed /> },
|
||||
{ name: "Integrations", path: "integrations", icon: <Integrations /> },
|
||||
{ name: "Support", path: "support", icon: <Support /> },
|
||||
{ name: "Settings", path: "settings", icon: <Settings /> },
|
||||
];
|
||||
|
||||
function Sidebar() {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Stack component="aside" gap={theme.gap.xs}>
|
||||
<BWULogo alt="BlueWave Uptime Logo" />
|
||||
{menu.map((item) => (
|
||||
<Stack
|
||||
className={
|
||||
location.pathname.includes(item.path) ? "selected-path" : ""
|
||||
}
|
||||
key={item.path}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.gap.small}
|
||||
borderRadius={`${theme.shape.borderRadius}px`}
|
||||
onClick={() => navigate(`/${item.path}`)}
|
||||
sx={{ p: `${theme.gap.small} ${theme.gap.medium}` }}
|
||||
>
|
||||
{item.icon}
|
||||
<Typography component="span">{item.name}</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sidebar;
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import ImageIcon from "@mui/icons-material/Image";
|
||||
import ProgressUpload from "../../ProgressBars";
|
||||
import { formatBytes } from "../../../Utils/fileUtils";
|
||||
import { clearMonitorState } from "../../../Features/Monitors/monitorsSlice";
|
||||
import { clearUptimeMonitorState } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
|
||||
/**
|
||||
@@ -36,16 +36,16 @@ const ProfilePanel = () => {
|
||||
const { user, authToken, isLoading } = useSelector((state) => state.auth);
|
||||
|
||||
const idToName = {
|
||||
"edit-first-name": "firstname",
|
||||
"edit-last-name": "lastname",
|
||||
"edit-first-name": "firstName",
|
||||
"edit-last-name": "lastName",
|
||||
// Disabled for now, will revisit in the future
|
||||
// "edit-email": "email",
|
||||
};
|
||||
|
||||
// Local state for form data, errors, and file handling
|
||||
const [localData, setLocalData] = useState({
|
||||
firstname: user.firstname,
|
||||
lastname: user.lastname,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
// email: user.email, // Disabled for now
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
@@ -156,8 +156,8 @@ const ProfilePanel = () => {
|
||||
const handleSaveProfile = async (event) => {
|
||||
event.preventDefault();
|
||||
if (
|
||||
localData.firstname === user.firstname &&
|
||||
localData.lastname === user.lastname &&
|
||||
localData.firstName === user.firstName &&
|
||||
localData.lastName === user.lastName &&
|
||||
localData.deleteProfileImage === undefined &&
|
||||
localData.file === undefined
|
||||
) {
|
||||
@@ -194,7 +194,7 @@ const ProfilePanel = () => {
|
||||
const action = await dispatch(deleteUser(authToken));
|
||||
if (action.payload.success) {
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearMonitorState());
|
||||
dispatch(clearUptimeMonitorState());
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
@@ -223,7 +223,7 @@ const ProfilePanel = () => {
|
||||
</Stack>
|
||||
<Field
|
||||
id="edit-first-name"
|
||||
value={localData.firstname}
|
||||
value={localData.firstName}
|
||||
placeholder="Enter your first name"
|
||||
autoComplete="given-name"
|
||||
onChange={handleChange}
|
||||
@@ -238,7 +238,7 @@ const ProfilePanel = () => {
|
||||
id="edit-last-name"
|
||||
placeholder="Enter your last name"
|
||||
autoComplete="family-name"
|
||||
value={localData.lastname}
|
||||
value={localData.lastName}
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-last-name"]]}
|
||||
/>
|
||||
@@ -255,7 +255,8 @@ const ProfilePanel = () => {
|
||||
value={user.email}
|
||||
placeholder="Enter your email"
|
||||
autoComplete="email"
|
||||
// onChange={handleChange}
|
||||
// TODO - add onChange
|
||||
onChange={() => console.log("Disabled.")}
|
||||
// error={errors[idToName["edit-email"]]}
|
||||
disabled={true}
|
||||
/>
|
||||
|
||||
@@ -2,25 +2,26 @@ import { useTheme } from "@emotion/react";
|
||||
import TabPanel from "@mui/lab/TabPanel";
|
||||
import {
|
||||
Box,
|
||||
Checkbox,
|
||||
Container,
|
||||
ButtonGroup,
|
||||
Divider,
|
||||
MenuItem,
|
||||
IconButton,
|
||||
Modal,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import ButtonSpinner from "../../ButtonSpinner";
|
||||
import Button from "../../Button";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import EditSvg from "../../../assets/icons/edit.svg?react";
|
||||
import Field from "../../Inputs/Field";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
import axiosInstance from "../../../Utils/axiosConfig";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import BasicTable from "../../BasicTable";
|
||||
import Remove from "../../../assets/icons/trash-bin.svg?react";
|
||||
import Select from "../../Inputs/Select";
|
||||
|
||||
/**
|
||||
* TeamPanel component manages the organization and team members,
|
||||
@@ -30,227 +31,279 @@ import EditSvg from "../../../assets/icons/edit.svg?react";
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
|
||||
const teamColumns = [
|
||||
{
|
||||
id: "checkbox",
|
||||
label: "",
|
||||
sx: { minWidth: "20px", width: "40px" },
|
||||
},
|
||||
{
|
||||
id: "name",
|
||||
label: "NAME",
|
||||
sx: { fontSize: "12px" },
|
||||
},
|
||||
{ id: "email", label: "EMAIL", sx: { fontSize: "12px" } },
|
||||
{ id: "role", label: "ROLE", sx: { fontSize: "12px" } },
|
||||
];
|
||||
//for testing, will be removed later
|
||||
const teamConfig = [
|
||||
{
|
||||
id: 0,
|
||||
isChecked: false,
|
||||
name: "John Connor",
|
||||
email: "john@domain.com",
|
||||
type: "admin",
|
||||
role: "Administrator",
|
||||
createdAt: "10/4/2022",
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
isChecked: false,
|
||||
name: "Adam McFadden",
|
||||
email: "adam@domain.com",
|
||||
type: "member",
|
||||
role: "Member",
|
||||
createdAt: "10/4/2022",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
isChecked: false,
|
||||
name: "Cris Cross",
|
||||
email: "cris@domain.com",
|
||||
type: "member",
|
||||
role: "Member",
|
||||
createdAt: "10/4/2022",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
isChecked: false,
|
||||
name: "Prince",
|
||||
email: "prince@domain.com",
|
||||
type: "member",
|
||||
role: "Member",
|
||||
createdAt: "10/4/2022",
|
||||
},
|
||||
];
|
||||
const actionsConfig = [
|
||||
{
|
||||
value: "bulk",
|
||||
label: "Bulk actions",
|
||||
},
|
||||
];
|
||||
const roleConfig = [
|
||||
{
|
||||
value: "role",
|
||||
label: "Change role to",
|
||||
},
|
||||
];
|
||||
|
||||
const TeamPanel = () => {
|
||||
const theme = useTheme();
|
||||
//TODO - use redux loading state
|
||||
//!! - currently all loading buttons are tied to the same state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
//TODO - connect to redux
|
||||
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
//TODO
|
||||
const [orgStates, setOrgStates] = useState({
|
||||
name: "Bluewave Labs",
|
||||
isLoading: false,
|
||||
isEdit: false,
|
||||
newName: "",
|
||||
});
|
||||
const [toInvite, setToInvite] = useState({
|
||||
email: "",
|
||||
role: ["0"],
|
||||
});
|
||||
const [tableData, setTableData] = useState({});
|
||||
const [members, setMembers] = useState([]);
|
||||
const [filter, setFilter] = useState("all");
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTeam = async () => {
|
||||
try {
|
||||
const response = await axiosInstance.get("/auth/users", {
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
});
|
||||
|
||||
setMembers(response.data.data);
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message || "Error fetching team members.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
fetchTeam();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let team = members;
|
||||
if (filter !== "all")
|
||||
team = members.filter((member) => member.role.includes(filter));
|
||||
|
||||
const data = {
|
||||
cols: [
|
||||
{ id: 1, name: "NAME" },
|
||||
{ id: 2, name: "EMAIL" },
|
||||
{ id: 3, name: "ROLE" },
|
||||
{ id: 4, name: "ACTION" },
|
||||
],
|
||||
rows: team?.map((member, idx) => {
|
||||
return {
|
||||
id: member._id,
|
||||
data: [
|
||||
{
|
||||
id: idx,
|
||||
data: (
|
||||
<Stack>
|
||||
<Typography
|
||||
style={{ color: theme.palette.otherColors.blackish }}
|
||||
>
|
||||
{member.firstName + " " + member.lastName}
|
||||
</Typography>
|
||||
<Typography sx={{ opacity: 0.6 }}>
|
||||
Created {new Date(member.createdAt).toLocaleDateString()}
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{ id: idx + 1, data: member.email },
|
||||
{
|
||||
// TODO - Add select dropdown
|
||||
id: idx + 2,
|
||||
data: member.role.includes("admin") ? "Administrator" : "Member",
|
||||
},
|
||||
{
|
||||
// TODO - Add delete onClick
|
||||
id: idx + 3,
|
||||
data: (
|
||||
<IconButton
|
||||
aria-label="remove member"
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Remove />
|
||||
</IconButton>
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
setTableData(data);
|
||||
}, [members, filter]);
|
||||
|
||||
// RENAME ORGANIZATION
|
||||
const toggleEdit = () => {
|
||||
setOrgStates((prev) => ({ ...prev, isEdit: !prev.isEdit }));
|
||||
};
|
||||
const handleChange = (event) => {
|
||||
const { value } = event.target;
|
||||
setOrgStates((prev) => ({
|
||||
...prev,
|
||||
name: value,
|
||||
}));
|
||||
};
|
||||
const handleRename = () => {};
|
||||
|
||||
const [teamStates, setTeamStates] = useState({
|
||||
members: teamConfig,
|
||||
filter: "",
|
||||
});
|
||||
const handleCheckCell = (id) => {
|
||||
const updatedTeamStates = [...teamStates.members];
|
||||
updatedTeamStates[id] = {
|
||||
...updatedTeamStates[id],
|
||||
isChecked: !updatedTeamStates[id].isChecked,
|
||||
};
|
||||
setTeamStates((prev) => ({
|
||||
...prev,
|
||||
members: updatedTeamStates,
|
||||
}));
|
||||
};
|
||||
const handleFilter = (filter) => {
|
||||
setTeamStates((prev) => ({
|
||||
...prev,
|
||||
filter: filter,
|
||||
}));
|
||||
};
|
||||
//TODO - implement select action function
|
||||
const handleSelectActionType = () => {
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 2000);
|
||||
};
|
||||
//TODO - implement select role function
|
||||
const handleSelectRoleType = () => {
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 2000);
|
||||
};
|
||||
//TODO - implement save team function
|
||||
const handleSaveTeam = () => {
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 2000);
|
||||
};
|
||||
const handleSaveTeam = () => {};
|
||||
|
||||
// INVITE MEMBER
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const handleInviteMember = () => {};
|
||||
const handleMembersQuery = (type) => {
|
||||
let count = 0;
|
||||
teamStates.members.forEach((member) => {
|
||||
type === "" ? count++ : member.type === type ? count++ : "";
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value } = event.target;
|
||||
setToInvite((prev) => ({
|
||||
...prev,
|
||||
email: value,
|
||||
}));
|
||||
|
||||
const validation = credentials.validate(
|
||||
{ email: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
|
||||
if (validation.error) {
|
||||
updatedErrors.email = validation.error.details[0].message;
|
||||
} else {
|
||||
delete updatedErrors.email;
|
||||
}
|
||||
return updatedErrors;
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
const handleInviteMember = async () => {
|
||||
if (!toInvite.role.includes("user") || !toInvite.role.includes("admin"))
|
||||
setToInvite((prev) => ({ ...prev, role: ["user"] }));
|
||||
|
||||
const { error } = credentials.validate(
|
||||
{ email: toInvite.email },
|
||||
{
|
||||
abortEarly: false,
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
|
||||
} else
|
||||
try {
|
||||
await axiosInstance.post(
|
||||
"/auth/invite",
|
||||
{
|
||||
email: toInvite.email,
|
||||
role: toInvite.role,
|
||||
},
|
||||
{ headers: { Authorization: `Bearer ${authToken}` } }
|
||||
);
|
||||
|
||||
closeInviteModal();
|
||||
createToast({
|
||||
body: "Member invited. They will receive an email with details on how to create their account.",
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message || "Unknown error.",
|
||||
});
|
||||
}
|
||||
};
|
||||
const closeInviteModal = () => {
|
||||
setIsOpen(false);
|
||||
setToInvite({ email: "", role: ["0"] });
|
||||
setErrors({});
|
||||
};
|
||||
|
||||
return (
|
||||
<TabPanel value="team">
|
||||
<form className="edit-organization-form">
|
||||
<div className="edit-organization-form__wrapper">
|
||||
<Stack>
|
||||
<Typography component="h1">Organization name</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="row-stack"
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
sx={{ minHeight: "34px", maxHeight: "34px" }}
|
||||
>
|
||||
<TextField
|
||||
value={orgStates.name}
|
||||
onChange={handleChange}
|
||||
disabled={!orgStates.isEdit}
|
||||
sx={{
|
||||
color: theme.palette.otherColors.bluishGray,
|
||||
"& .Mui-disabled": {
|
||||
WebkitTextFillColor: "initial !important",
|
||||
},
|
||||
"& .Mui-disabled fieldset": {
|
||||
borderColor: "transparent !important",
|
||||
},
|
||||
}}
|
||||
inputProps={{
|
||||
sx: { textAlign: "end", padding: theme.gap.small },
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level={orgStates.isEdit ? "secondary" : "tertiary"}
|
||||
label={orgStates.isEdit ? "Save" : ""}
|
||||
img={!orgStates.isEdit ? <EditSvg /> : ""}
|
||||
onClick={() => toggleEdit()}
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
paddingX: theme.gap.small,
|
||||
ml: orgStates.isEdit ? theme.gap.small : 0,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
<Divider
|
||||
aria-hidden="true"
|
||||
className="short-divider"
|
||||
sx={{ marginY: theme.spacing(4) }}
|
||||
/>
|
||||
<Box sx={{ alignSelf: "flex-start" }}>
|
||||
<Typography component="h1">Organization name</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
className="row-stack"
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
sx={{ height: "34px" }}
|
||||
>
|
||||
<TextField
|
||||
value={orgStates.name}
|
||||
onChange={(event) =>
|
||||
setOrgStates((prev) => ({
|
||||
...prev,
|
||||
name: event.target.value,
|
||||
}))
|
||||
}
|
||||
disabled={!orgStates.isEdit}
|
||||
sx={{
|
||||
color: theme.palette.otherColors.bluishGray,
|
||||
"& .Mui-disabled": {
|
||||
WebkitTextFillColor: "initial !important",
|
||||
},
|
||||
"& .Mui-disabled fieldset": {
|
||||
borderColor: "transparent !important",
|
||||
},
|
||||
}}
|
||||
inputProps={{
|
||||
sx: { textAlign: "end", padding: theme.gap.small },
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level={orgStates.isEdit ? "secondary" : "tertiary"}
|
||||
label={orgStates.isEdit ? "Save" : ""}
|
||||
img={!orgStates.isEdit ? <EditSvg /> : ""}
|
||||
onClick={() => toggleEdit()}
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
paddingX: theme.gap.small,
|
||||
ml: orgStates.isEdit ? theme.gap.small : 0,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
<form className="edit-team-form" noValidate spellCheck="false">
|
||||
<div className="edit-team-form__wrapper">
|
||||
<Typography component="h1">Team members</Typography>
|
||||
</div>
|
||||
<div className="edit-team-form__wrapper compact">
|
||||
<Divider
|
||||
aria-hidden="true"
|
||||
className="short-divider"
|
||||
sx={{ marginY: theme.spacing(4) }}
|
||||
/>
|
||||
<form
|
||||
className="edit-team-form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.gap.large,
|
||||
}}
|
||||
>
|
||||
<Typography component="h1">Team members</Typography>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Stack
|
||||
direction="row"
|
||||
gap="20px"
|
||||
alignItems="center"
|
||||
alignItems="flex-end"
|
||||
gap={theme.gap.medium}
|
||||
sx={{ fontSize: "14px" }}
|
||||
>
|
||||
<Box onClick={() => handleFilter("")}>
|
||||
All
|
||||
<span className="members-query">
|
||||
<span>{handleMembersQuery("")}</span>
|
||||
</span>
|
||||
</Box>
|
||||
<Box onClick={() => handleFilter("admin")}>
|
||||
Administrator
|
||||
<span className="members-query">
|
||||
<span>{handleMembersQuery("admin")}</span>
|
||||
</span>
|
||||
</Box>
|
||||
<Box onClick={() => handleFilter("member")}>
|
||||
Member
|
||||
<span className="members-query">
|
||||
<span>{handleMembersQuery("member")}</span>
|
||||
</span>
|
||||
</Box>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="All"
|
||||
onClick={() => setFilter("all")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "all" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Administrator"
|
||||
onClick={() => setFilter("admin")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "admin" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Member"
|
||||
onClick={() => setFilter("user")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "user" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<Button
|
||||
level="primary"
|
||||
@@ -258,146 +311,17 @@ const TeamPanel = () => {
|
||||
sx={{ paddingX: "30px" }}
|
||||
onClick={() => setIsOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className="edit-team-form__wrapper compact">
|
||||
<Container
|
||||
disableGutters
|
||||
sx={{
|
||||
border: `1px solid ${theme.palette.section.borderColor}`,
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
borderBottom: "none",
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" gap="40px" p="20px">
|
||||
<Stack
|
||||
direction="row"
|
||||
gap="10px"
|
||||
alignItems="center"
|
||||
className="table-stack"
|
||||
>
|
||||
<Select
|
||||
id="select-actions"
|
||||
value="bulk"
|
||||
inputProps={{ id: "select-actions-input" }}
|
||||
>
|
||||
{actionsConfig.map((action) => (
|
||||
<MenuItem
|
||||
value={action.value}
|
||||
key={action.value}
|
||||
sx={{ fontSize: "13px" }}
|
||||
>
|
||||
{action.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<ButtonSpinner
|
||||
level="secondary"
|
||||
label="Apply"
|
||||
onClick={handleSelectActionType}
|
||||
isLoading={isLoading}
|
||||
sx={{
|
||||
bgcolor: "#fafafa",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row" gap="10px" alignItems="center">
|
||||
<Select
|
||||
id="select-role"
|
||||
value="role"
|
||||
inputProps={{ id: "select-role-input" }}
|
||||
>
|
||||
{roleConfig.map((role) => (
|
||||
<MenuItem
|
||||
value={role.value}
|
||||
key={role.value}
|
||||
sx={{ fontSize: "13px" }}
|
||||
>
|
||||
{role.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<ButtonSpinner
|
||||
level="secondary"
|
||||
label="Apply"
|
||||
onClick={handleSelectRoleType}
|
||||
isLoading={isLoading}
|
||||
sx={{
|
||||
bgcolor: "#fafafa",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Table
|
||||
sx={{
|
||||
borderTop: `1px solid ${theme.palette.section.borderColor}`,
|
||||
tableLayout: "fixed",
|
||||
}}
|
||||
>
|
||||
<TableHead>
|
||||
<TableRow
|
||||
sx={{
|
||||
bgcolor: "#fafafa",
|
||||
}}
|
||||
>
|
||||
{teamColumns.map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
sx={{
|
||||
...cell.sx,
|
||||
color: theme.palette.otherColors.slateGray,
|
||||
}}
|
||||
>
|
||||
{cell.label}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{teamStates.members.map((cell) =>
|
||||
teamStates.filter === "" ||
|
||||
teamStates.filter === cell.type ? (
|
||||
<TableRow key={cell.id}>
|
||||
<TableCell align="center">
|
||||
<Checkbox
|
||||
id={`${cell.id}-${cell.name}`}
|
||||
checked={cell.isChecked}
|
||||
onChange={() => handleCheckCell(cell.id)}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="column">
|
||||
<Box
|
||||
sx={{
|
||||
color: theme.palette.otherColors.blackish,
|
||||
verticalAlign: "top",
|
||||
}}
|
||||
>
|
||||
{cell.name}
|
||||
</Box>
|
||||
<Box>Created at {cell.createdAt}</Box>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>{cell.email}</TableCell>
|
||||
<TableCell>{cell.role}</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Container>
|
||||
</div>
|
||||
<Divider aria-hidden="true" width="0" />
|
||||
</Stack>
|
||||
<BasicTable data={tableData} paginated={false} reversed={true} />
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Box width="fit-content">
|
||||
<ButtonSpinner
|
||||
level="primary"
|
||||
label="Save"
|
||||
onClick={handleSaveTeam}
|
||||
isLoading={isLoading}
|
||||
isLoading={false}
|
||||
loadingText="Saving..."
|
||||
disabled={true}
|
||||
sx={{
|
||||
paddingX: "40px",
|
||||
}}
|
||||
@@ -409,7 +333,7 @@ const TeamPanel = () => {
|
||||
aria-labelledby="modal-invite-member"
|
||||
aria-describedby="invite-member-to-team"
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
onClose={closeInviteModal}
|
||||
disablePortal
|
||||
>
|
||||
<Stack
|
||||
@@ -433,32 +357,55 @@ const TeamPanel = () => {
|
||||
<Typography id="modal-invite-member" component="h1">
|
||||
Invite new team member
|
||||
</Typography>
|
||||
<Typography id="invite-member-to-team" component="p">
|
||||
<Typography
|
||||
id="invite-member-to-team"
|
||||
component="p"
|
||||
sx={{ mb: theme.gap.medium }}
|
||||
>
|
||||
When you add a new team member, they will get access to all
|
||||
monitors.
|
||||
</Typography>
|
||||
<TextField
|
||||
<Field
|
||||
type="email"
|
||||
id="input-team-member"
|
||||
spellCheck="false"
|
||||
// value={orgStates.newName}
|
||||
// onChange={(event) =>
|
||||
// setOrgStates((prev) => ({
|
||||
// ...prev,
|
||||
// newName: event.target.value,
|
||||
// }))
|
||||
// }
|
||||
></TextField>
|
||||
<Stack direction="row" gap="10px" mt="10px" justifyContent="flex-end">
|
||||
placeholder="Email"
|
||||
value={toInvite.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
/>
|
||||
<Select
|
||||
id="team-member-role"
|
||||
placeholder="Select role"
|
||||
isHidden={true}
|
||||
value={toInvite.role[0]}
|
||||
onChange={(event) =>
|
||||
setToInvite((prev) => ({
|
||||
...prev,
|
||||
role: [event.target.value],
|
||||
}))
|
||||
}
|
||||
items={[
|
||||
{ _id: "admin", name: "admin" },
|
||||
{ _id: "user", name: "user" },
|
||||
]}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.gap.small}
|
||||
mt={theme.gap.ml}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Cancel"
|
||||
onClick={() => setIsOpen(false)}
|
||||
onClick={closeInviteModal}
|
||||
/>
|
||||
<ButtonSpinner
|
||||
level="primary"
|
||||
label="Send invite"
|
||||
onClick={handleInviteMember}
|
||||
isLoading={isLoading}
|
||||
isLoading={false}
|
||||
disabled={Object.keys(errors).length !== 0}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -47,8 +47,8 @@ export const update = createAsyncThunk(
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
const fd = new FormData();
|
||||
form.firstname && fd.append("firstname", form.firstname);
|
||||
form.lastname && fd.append("lastname", form.lastname);
|
||||
form.firstName && fd.append("firstName", form.firstName);
|
||||
form.lastName && fd.append("lastName", form.lastName);
|
||||
form.password && fd.append("password", form.password);
|
||||
form.newPassword && fd.append("newPassword", form.newPassword);
|
||||
if (form.file && form.file !== "") {
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import axiosInstance from "../../Utils/axiosConfig";
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
monitors: [],
|
||||
success: null,
|
||||
msg: null,
|
||||
};
|
||||
|
||||
export const createPageSpeed = createAsyncThunk(
|
||||
"pageSpeedMonitors/createPageSpeed",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
|
||||
const res = await axiosInstance.post(`/monitors`, monitor, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getPageSpeedMonitors = createAsyncThunk(
|
||||
"pageSpeedMonitors/getPageSpeedMonitors",
|
||||
async (token, thunkApi) => {
|
||||
try {
|
||||
const res = await axiosInstance.get("/monitors");
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getPageSpeedByUserId = createAsyncThunk(
|
||||
"pageSpeedMonitors/getPageSpeedByUserId",
|
||||
async (token, thunkApi) => {
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/user/${user._id}?limit=25&type=pagespeed&sortOrder=desc`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const pageSpeedMonitorSlice = createSlice({
|
||||
name: "pageSpeedMonitor",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearMonitorState: (state) => {
|
||||
state.isLoading = false;
|
||||
state.monitors = [];
|
||||
state.success = null;
|
||||
state.msg = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// *****************************************************
|
||||
// All Monitors
|
||||
// *****************************************************
|
||||
.addCase(getPageSpeedMonitors.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getPageSpeedMonitors.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.monitors = action.payload.data;
|
||||
})
|
||||
.addCase(getPageSpeedMonitors.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Getting montiors failed";
|
||||
})
|
||||
// *****************************************************
|
||||
// Monitors by userId
|
||||
// *****************************************************
|
||||
|
||||
.addCase(getPageSpeedByUserId.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getPageSpeedByUserId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.msg;
|
||||
state.monitors = action.payload.data;
|
||||
})
|
||||
.addCase(getPageSpeedByUserId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Getting page speed monitors failed";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Create Monitor
|
||||
// *****************************************************
|
||||
.addCase(createPageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(createPageSpeed.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(createPageSpeed.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to create page speed monitor";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setMonitors, clearMonitorState } = pageSpeedMonitorSlice.actions;
|
||||
|
||||
export default pageSpeedMonitorSlice.reducer;
|
||||
@@ -8,7 +8,7 @@ const initialState = {
|
||||
msg: null,
|
||||
};
|
||||
|
||||
export const createMonitor = createAsyncThunk(
|
||||
export const createUptimeMonitor = createAsyncThunk(
|
||||
"monitors/createMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
@@ -30,7 +30,7 @@ export const createMonitor = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
export const getMonitors = createAsyncThunk(
|
||||
export const getUptimeMonitors = createAsyncThunk(
|
||||
"monitors/getMonitors",
|
||||
async (token, thunkApi) => {
|
||||
try {
|
||||
@@ -45,13 +45,13 @@ export const getMonitors = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
export const getMonitorsByUserId = createAsyncThunk(
|
||||
export const getUptimeMonitorsByUserId = createAsyncThunk(
|
||||
"montiors/getMonitorsByUserId",
|
||||
async (token, thunkApi) => {
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/user/${user._id}?limit=25`,
|
||||
`/monitors/user/${user._id}?limit=25&type=http&type=ping&sortOrder=desc`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
@@ -68,11 +68,11 @@ export const getMonitorsByUserId = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
const monitorsSlice = createSlice({
|
||||
name: "monitors",
|
||||
const uptimeMonitorsSlice = createSlice({
|
||||
name: "uptimeMonitors",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearMonitorState: (state) => {
|
||||
clearUptimeMonitorState: (state) => {
|
||||
state.isLoading = false;
|
||||
state.monitors = [];
|
||||
state.success = null;
|
||||
@@ -84,63 +84,64 @@ const monitorsSlice = createSlice({
|
||||
// *****************************************************
|
||||
// All Monitors
|
||||
// *****************************************************
|
||||
.addCase(getMonitors.pending, (state) => {
|
||||
.addCase(getUptimeMonitors.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getMonitors.fulfilled, (state, action) => {
|
||||
.addCase(getUptimeMonitors.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.monitors = action.payload.data;
|
||||
})
|
||||
.addCase(getMonitors.rejected, (state, action) => {
|
||||
.addCase(getUptimeMonitors.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Getting montiors failed";
|
||||
: "Getting uptime monitors failed";
|
||||
})
|
||||
// *****************************************************
|
||||
// Monitors by userId
|
||||
// *****************************************************
|
||||
|
||||
.addCase(getMonitorsByUserId.pending, (state) => {
|
||||
.addCase(getUptimeMonitorsByUserId.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getMonitorsByUserId.fulfilled, (state, action) => {
|
||||
.addCase(getUptimeMonitorsByUserId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.msg;
|
||||
state.monitors = action.payload.data;
|
||||
})
|
||||
.addCase(getMonitorsByUserId.rejected, (state, action) => {
|
||||
.addCase(getUptimeMonitorsByUserId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Getting montiors failed";
|
||||
: "Getting uptime monitors failed";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Create Monitor
|
||||
// *****************************************************
|
||||
.addCase(createMonitor.pending, (state) => {
|
||||
.addCase(createUptimeMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(createMonitor.fulfilled, (state, action) => {
|
||||
.addCase(createUptimeMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(createMonitor.rejected, (state, action) => {
|
||||
.addCase(createUptimeMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to create monitor";
|
||||
: "Failed to create uptime monitor";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setMonitors, clearMonitorState } = monitorsSlice.actions;
|
||||
export const { setUptimeMonitors, clearUptimeMonitorState } =
|
||||
uptimeMonitorsSlice.actions;
|
||||
|
||||
export default monitorsSlice.reducer;
|
||||
export default uptimeMonitorsSlice.reducer;
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import axiosInstance from "../Utils/axiosConfig";
|
||||
|
||||
const withAdminCheck = (WrappedComponent) => {
|
||||
const WithAdminCheck = (props) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
axiosInstance
|
||||
.get("/auth/users/admin")
|
||||
.then((response) => {
|
||||
if (response.data.data === true) {
|
||||
navigate("/login");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}, [navigate]);
|
||||
return <WrappedComponent {...props} isAdmin={true} />;
|
||||
};
|
||||
const wrappedComponentName =
|
||||
WrappedComponent.displayName || WrappedComponent.name || "Component";
|
||||
WithAdminCheck.displayName = `WithAdminCheck(${wrappedComponentName})`;
|
||||
|
||||
return WithAdminCheck;
|
||||
};
|
||||
|
||||
export default withAdminCheck;
|
||||
@@ -1,13 +1,58 @@
|
||||
.main-content {
|
||||
.home-layout * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.home-layout {
|
||||
display: grid;
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
justify-content: flex-start;
|
||||
grid-template-areas:
|
||||
"aside header"
|
||||
"aside main";
|
||||
grid-template-columns: var(--env-var-side-bar-width) auto;
|
||||
grid-template-rows: var(--env-var-nav-bar-height) auto;
|
||||
background-color: var(--env-var-color-30);
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.main-content>div:nth-child(2){
|
||||
position: relative;
|
||||
/* overflow-y: auto; */
|
||||
flex: 1;
|
||||
.home-layout aside {
|
||||
grid-area: aside;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: var(--env-var-side-bar-width);
|
||||
background-color: var(--env-var-color-8);
|
||||
border-right: 1px solid var(--color-border-0);
|
||||
padding: var(--env-var-spacing-1) var(--env-var-spacing-1-plus);
|
||||
}
|
||||
.home-layout header {
|
||||
grid-area: header;
|
||||
background-color: var(--env-var-color-30);
|
||||
}
|
||||
.home-layout > div {
|
||||
height: 100%;
|
||||
grid-area: main;
|
||||
padding: var(--env-var-spacing-2) calc(var(--env-var-spacing-4) * 2);
|
||||
padding-bottom: var(--env-var-spacing-4);
|
||||
max-width: 100%;
|
||||
}
|
||||
.home-layout > div:not(:has([class*="fallback"])) {
|
||||
max-width: 1500px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.home-layout {
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"main main";
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 64px auto;
|
||||
}
|
||||
|
||||
aside {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.home-layout > div {
|
||||
padding: var(--env-var-spacing-1);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import NavBar from "../../Components/NavBar";
|
||||
import Sidebar from "../../Components/Sidebar";
|
||||
import { Outlet } from "react-router";
|
||||
import DashboardSidebar from "../../Components/Dashboard/DashboardSidebar";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
const HomeLayout = () => {
|
||||
return (
|
||||
<div className="home-layout">
|
||||
<Box className="home-layout">
|
||||
<Sidebar />
|
||||
<NavBar />
|
||||
<div className="main-content">
|
||||
<DashboardSidebar />
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
<Outlet />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
margin: 50px 0;
|
||||
}
|
||||
.account .MuiDivider-root,
|
||||
.account .MuiOutlinedInput-root fieldset {
|
||||
border-color: var(--env-var-color-29);
|
||||
.account .MuiButtonGroup-root button {
|
||||
border-color: var(--env-var-color-16);
|
||||
}
|
||||
.account [class$="-form"] {
|
||||
width: inherit;
|
||||
@@ -41,9 +41,8 @@
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.edit-profile-form__wrapper>.MuiStack-root:not(.row-stack),
|
||||
.edit-password-form__wrapper>.MuiStack-root:not(.row-stack),
|
||||
.edit-organization-form__wrapper>.MuiStack-root:not(.row-stack) {
|
||||
.edit-profile-form__wrapper > .MuiStack-root:not(.row-stack),
|
||||
.edit-password-form__wrapper > .MuiStack-root:not(.row-stack) {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-right: 10px;
|
||||
@@ -68,56 +67,6 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.edit-team-form__wrapper td {
|
||||
padding: 10px 0;
|
||||
padding-top: 20px;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
.edit-team-form__wrapper th {
|
||||
padding: var(--env-var-spacing-1) 0;
|
||||
}
|
||||
.edit-team-form__wrapper th:first-of-type {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.edit-team-form__wrapper td {
|
||||
color: var(--env-var-color-25);
|
||||
}
|
||||
|
||||
#select-actions,
|
||||
#select-role {
|
||||
padding: 5px 0;
|
||||
padding-left: 15px;
|
||||
padding-right: 50px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
.MuiInputBase-root:has(#select-actions) fieldset,
|
||||
.MuiInputBase-root:has(#select-role) fieldset {
|
||||
border-color: #a1a7b1;
|
||||
margin: -1px;
|
||||
border-width: 1px;
|
||||
}
|
||||
.MuiInputBase-root:has(#select-actions):hover fieldset,
|
||||
.MuiInputBase-root:has(#select-role):hover fieldset {
|
||||
border-color: var(--env-var-color-2);
|
||||
}
|
||||
|
||||
.MuiStack-root:has(.MuiBox-root > .members-query) .MuiBox-root {
|
||||
cursor: pointer;
|
||||
}
|
||||
.members-query {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-left: 6px;
|
||||
padding: 2px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--env-var-color-3);
|
||||
background-color: #e0e9fd;
|
||||
min-width: 22px;
|
||||
min-height: 22px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* for testing, will remove later */
|
||||
@media only screen and (max-width: 1600px) {
|
||||
@@ -131,22 +80,10 @@
|
||||
.account .MuiStack-root:has(span.MuiTypography-root.input-error) {
|
||||
position: relative;
|
||||
}
|
||||
.account .text-field-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.account span.MuiTypography-root.input-error {
|
||||
.account:not(:has(#modal-invite-member)) span.MuiTypography-root.input-error {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
}
|
||||
.account .email-text-field,
|
||||
.account .password-text-field {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.edit-profile-form__wrapper .MuiFormControl-root,
|
||||
.edit-password-form__wrapper .MuiFormControl-root {
|
||||
@@ -155,4 +92,15 @@
|
||||
.edit-profile-form__wrapper .MuiStack-root:has(.MuiFormControl-root),
|
||||
.edit-password-form__wrapper .MuiStack-root:has(.MuiFormControl-root) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.edit-organization-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.edit-team-form .MuiTableCell-root {
|
||||
color: var(--env-var-color-25) !important;
|
||||
}
|
||||
.edit-team-form .MuiTableBody-root .MuiTableCell-root {
|
||||
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2) !important;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import ProfilePanel from "../../Components/TabPanels/Account/ProfilePanel";
|
||||
import PasswordPanel from "../../Components/TabPanels/Account/PasswordPanel";
|
||||
import TeamPanel from "../../Components/TabPanels/Account/TeamPanel";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
/**
|
||||
* Account component renders a settings page with tabs for Profile, Password, and Team settings.
|
||||
@@ -15,8 +16,6 @@ import { useNavigate } from "react-router";
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
|
||||
const tabList = ["Profile", "Password", "Team"];
|
||||
|
||||
const Account = ({ open = "profile" }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
@@ -25,20 +24,17 @@ const Account = ({ open = "profile" }) => {
|
||||
navigate(`/account/${newTab}`);
|
||||
};
|
||||
|
||||
let tabList = ["Profile", "Password", "Team"];
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
if (!user.role.includes("admin")) tabList = ["Profile", "Password"];
|
||||
|
||||
return (
|
||||
<Box
|
||||
//TODO - breakpoints for responsive design
|
||||
minWidth={theme.spacing(55)}
|
||||
maxWidth="1200px"
|
||||
py={theme.content.pY}
|
||||
px={theme.content.pX}
|
||||
className="account"
|
||||
>
|
||||
<Box className="account">
|
||||
<TabContext value={tab}>
|
||||
<Box
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: theme.palette.section.borderColor,
|
||||
borderColor: "var(--env-var-color-16)",
|
||||
"& .MuiTabs-root": { height: "fit-content", minHeight: "0" },
|
||||
}}
|
||||
>
|
||||
@@ -71,7 +67,7 @@ const Account = ({ open = "profile" }) => {
|
||||
</Box>
|
||||
<ProfilePanel />
|
||||
<PasswordPanel />
|
||||
<TeamPanel />
|
||||
{user.role.includes("admin") && <TeamPanel />}
|
||||
</TabContext>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { clearAuthState } from "../../Features/Auth/authSlice";
|
||||
import { clearMonitorState } from "../../Features/Monitors/monitorsSlice";
|
||||
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
|
||||
const NewPasswordConfirmed = () => {
|
||||
const theme = useTheme();
|
||||
@@ -17,7 +17,7 @@ const NewPasswordConfirmed = () => {
|
||||
|
||||
const handleNavigate = () => {
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearMonitorState());
|
||||
dispatch(clearUptimeMonitorState());
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
|
||||
@@ -1,62 +1,68 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import "./index.css";
|
||||
import background from "../../assets/Images/background_pattern_decorative.png";
|
||||
import Logomark from "../../assets/Images/bwl-logo-2.svg?react";
|
||||
import Check from "../../Components/Check/Check";
|
||||
import Button from "../../Components/Button";
|
||||
import { credentials } from "../../Validation/validation";
|
||||
import axiosInstance from "../../Utils/axiosConfig";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { register } from "../../Features/Auth/authSlice";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useDispatch } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const Register = () => {
|
||||
import "../index.css";
|
||||
import background from "../../../assets/Images/background_pattern_decorative.png";
|
||||
import Logomark from "../../../assets/Images/bwl-logo-2.svg?react";
|
||||
import Check from "../../../Components/Check/Check";
|
||||
import Button from "../../../Components/Button";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import { register } from "../../../Features/Auth/authSlice";
|
||||
import { useParams } from "react-router-dom";
|
||||
import axiosInstance from "../../../Utils/axiosConfig";
|
||||
|
||||
const Register = ({ isAdmin }) => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { token } = useParams();
|
||||
const theme = useTheme();
|
||||
|
||||
// TODO If possible, change the IDs of these fields to match the backend
|
||||
const idMap = {
|
||||
"register-firstname-input": "firstname",
|
||||
"register-lastname-input": "lastname",
|
||||
"register-firstname-input": "firstName",
|
||||
"register-lastname-input": "lastName",
|
||||
"register-email-input": "email",
|
||||
"register-password-input": "password",
|
||||
"register-confirm-input": "confirm",
|
||||
};
|
||||
|
||||
const [form, setForm] = useState({
|
||||
firstname: "",
|
||||
lastname: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirm: "",
|
||||
role: "",
|
||||
role: [],
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
axiosInstance
|
||||
.get("/auth/users/admin")
|
||||
.then((response) => {
|
||||
if (response.data.data === true) {
|
||||
navigate("/login");
|
||||
const fetchInvite = async () => {
|
||||
if (token !== undefined) {
|
||||
try {
|
||||
const res = await axiosInstance.post(`/auth/invite/verify`, {
|
||||
token,
|
||||
});
|
||||
const { role, email } = res.data.data;
|
||||
console.log(role);
|
||||
setForm({ ...form, email, role });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}, [form, navigate]);
|
||||
}
|
||||
};
|
||||
fetchInvite();
|
||||
}, [token]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const adminForm = { ...form, role: "admin" };
|
||||
const { error } = credentials.validate(adminForm, {
|
||||
const registerForm = { ...form, role: isAdmin ? ["admin"] : form.role };
|
||||
const { error } = credentials.validate(registerForm, {
|
||||
abortEarly: false,
|
||||
context: { password: form.password },
|
||||
});
|
||||
@@ -75,8 +81,8 @@ const Register = () => {
|
||||
: "Error validating data.",
|
||||
});
|
||||
} else {
|
||||
delete adminForm.confirm;
|
||||
const action = await dispatch(register(adminForm));
|
||||
delete registerForm.confirm;
|
||||
const action = await dispatch(register(registerForm));
|
||||
if (action.payload.success) {
|
||||
const token = action.payload.data;
|
||||
localStorage.setItem("token", token);
|
||||
@@ -132,7 +138,7 @@ const Register = () => {
|
||||
<Stack gap={theme.gap.small} alignItems="center">
|
||||
<Logomark alt="BlueWave Uptime Icon" />
|
||||
<Typography component="h1" sx={{ mt: theme.gap.xl }}>
|
||||
Create admin account
|
||||
Create{isAdmin ? " admin " : " "}account
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.gap.large} sx={{ mt: `calc(${theme.gap.ml}*2)` }}>
|
||||
@@ -142,9 +148,9 @@ const Register = () => {
|
||||
isRequired={true}
|
||||
placeholder="Daniel"
|
||||
autoComplete="given-name"
|
||||
value={form.firstname}
|
||||
value={form.firstName}
|
||||
onChange={handleChange}
|
||||
error={errors.firstname}
|
||||
error={errors.firstName}
|
||||
/>
|
||||
<Field
|
||||
id="register-lastname-input"
|
||||
@@ -152,9 +158,9 @@ const Register = () => {
|
||||
isRequired={true}
|
||||
placeholder="Cojocea"
|
||||
autoComplete="family-name"
|
||||
value={form.lastname}
|
||||
value={form.lastName}
|
||||
onChange={handleChange}
|
||||
error={errors.lastname}
|
||||
error={errors.lastName}
|
||||
/>
|
||||
<Field
|
||||
type="email"
|
||||
@@ -241,5 +247,7 @@ const Register = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Register.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
};
|
||||
export default Register;
|
||||
@@ -0,0 +1,17 @@
|
||||
.incidents h1.MuiTypography-root {
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
.incidents h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 600;
|
||||
}
|
||||
.incidents button.MuiButtonBase-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
height: 34px;
|
||||
border-color: var(--env-var-color-16);
|
||||
}
|
||||
|
||||
/* remove later */
|
||||
.incidents .MuiSelect-select {
|
||||
padding: 0 10px;
|
||||
}
|
||||
@@ -1,180 +1,140 @@
|
||||
import * as React from "react";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Autocomplete, { createFilterOptions } from "@mui/material/Autocomplete";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import TuneIcon from "@mui/icons-material/Tune";
|
||||
import { StatusLabel } from "../../Components/Label/";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Typography, ButtonGroup, Stack } from "@mui/material";
|
||||
|
||||
const filterOptions = createFilterOptions({
|
||||
matchFrom: "start",
|
||||
stringify: (option) => option.title,
|
||||
});
|
||||
import Button from "../../Components/Button";
|
||||
import axiosInstance from "../../Utils/axiosConfig";
|
||||
import BasicTable from "../../Components/BasicTable";
|
||||
import { StatusLabel } from "../../Components/Label";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const titles = [
|
||||
{ title: "Down" },
|
||||
{ title: "Cannot resolve" },
|
||||
{ title: "Clear / show all" },
|
||||
];
|
||||
import "./index.css";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
|
||||
/**
|
||||
* Filter component with autocomplete for status filtering.
|
||||
* @returns {JSX.Element} The JSX element representing the filter component.
|
||||
*/
|
||||
function Filter() {
|
||||
return (
|
||||
<Autocomplete
|
||||
id="filter-demo"
|
||||
options={titles}
|
||||
getOptionLabel={(option) => option.title}
|
||||
filterOptions={filterOptions}
|
||||
sx={{
|
||||
width: 170,
|
||||
"& .MuiAutocomplete-inputRoot": {
|
||||
height: "50px",
|
||||
},
|
||||
"& .MuiAutocomplete-listbox": {
|
||||
maxHeight: "200px",
|
||||
},
|
||||
}}
|
||||
disableClearable
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
size="small"
|
||||
label={
|
||||
<React.Fragment>
|
||||
<TuneIcon
|
||||
sx={{ marginRight: "0.5rem", position: "relative", top: "6px" }}
|
||||
/>
|
||||
Filter by status
|
||||
</React.Fragment>
|
||||
}
|
||||
InputProps={{ ...params.InputProps, endAdornment: null }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const sampleData = [
|
||||
{
|
||||
name: (
|
||||
<StatusLabel
|
||||
status="Down"
|
||||
customStyles={{
|
||||
backgroundColor: "#fff9f9",
|
||||
borderColor: "#ffcac6",
|
||||
color: "#344054",
|
||||
}}
|
||||
/>
|
||||
),
|
||||
date: "2024-03-14 21:41:09",
|
||||
message: "HTTP 350 - NOK",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<StatusLabel
|
||||
status="Down"
|
||||
customStyles={{
|
||||
backgroundColor: "#fff9f9",
|
||||
borderColor: "#ffcac6",
|
||||
color: "#344054",
|
||||
}}
|
||||
/>
|
||||
),
|
||||
date: "2024-03-14 21:41:09",
|
||||
message: "timeout of 48000ms exceeded",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<StatusLabel
|
||||
status="Cannot resolve"
|
||||
customStyles={{
|
||||
backgroundColor: "#f2f4f7",
|
||||
borderColor: "#d2d6de",
|
||||
color: "#344054",
|
||||
}}
|
||||
/>
|
||||
),
|
||||
date: "2024-03-14 21:41:09",
|
||||
message: "timeout of 48000ms exceeded",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<StatusLabel
|
||||
status="Cannot resolve"
|
||||
customStyles={{
|
||||
backgroundColor: "#f2f4f7",
|
||||
borderColor: "#d2d6de",
|
||||
color: "#344054",
|
||||
}}
|
||||
/>
|
||||
),
|
||||
date: "2024-03-14 21:41:09",
|
||||
message: "timeout of 48000ms exceeded",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<StatusLabel
|
||||
status="Down"
|
||||
customStyles={{
|
||||
backgroundColor: "#fff9f9",
|
||||
borderColor: "#ffcac6",
|
||||
color: "#344054",
|
||||
}}
|
||||
/>
|
||||
),
|
||||
date: "2024-03-14 21:41:09",
|
||||
message: "HTTP 350 - NOK",
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{ id: "name", header: "Status" },
|
||||
{ id: "date", header: "Date & Time" },
|
||||
{ id: "message", header: "Message" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Customized table component displaying incident history with a filter.
|
||||
* @returns {JSX.Element} The JSX element representing the customized table with a filter.
|
||||
*/
|
||||
export default function CustomizedTables() {
|
||||
const Incidents = () => {
|
||||
const theme = useTheme();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const [monitors, setMonitors] = useState({});
|
||||
const [selectedMonitor, setSelectedMonitor] = useState("0");
|
||||
|
||||
// TODO do something with these filters
|
||||
const [filter, setFilter] = useState("all");
|
||||
|
||||
let data = {
|
||||
cols: [
|
||||
{ id: 1, name: "Status" },
|
||||
{ id: 2, name: "Timestamp" },
|
||||
{ id: 3, name: "Monitor" },
|
||||
{ id: 4, name: "Message" },
|
||||
],
|
||||
rows: [],
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchIncidents = async () => {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/user/${authState.user._id}?status=false`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authState.authToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Reduce to a lookup object for 0(1) lookup
|
||||
if (res.data && res.data.data.length > 0) {
|
||||
const monitorLookup = res.data.data.reduce((acc, monitor) => {
|
||||
acc[monitor._id] = monitor;
|
||||
return acc;
|
||||
}, {});
|
||||
setMonitors(monitorLookup);
|
||||
}
|
||||
};
|
||||
|
||||
fetchIncidents();
|
||||
}, [authState]);
|
||||
|
||||
const handleSelect = (event) => {
|
||||
setSelectedMonitor(event.target.value);
|
||||
};
|
||||
let incidents = [];
|
||||
if (selectedMonitor === "0") {
|
||||
Object.values(monitors).forEach((monitor) => {
|
||||
incidents = incidents.concat(monitor.checks);
|
||||
});
|
||||
} else {
|
||||
const monitor = monitors[selectedMonitor];
|
||||
incidents = monitor.checks;
|
||||
}
|
||||
incidents = incidents
|
||||
.sort((a, b) => {
|
||||
return new Date(b.createdAt) - new Date(a.createdAt);
|
||||
})
|
||||
.filter((incident) => {
|
||||
if (filter === "all") return true;
|
||||
else if (filter === "resolve") return incident.statusCode === 5000;
|
||||
else if (filter === "down") return incident.status === false;
|
||||
})
|
||||
.map((incident, idx) => {
|
||||
return {
|
||||
id: idx,
|
||||
data: [
|
||||
{
|
||||
id: idx + 0,
|
||||
data: <StatusLabel status="down" text="Down" />,
|
||||
},
|
||||
{ id: idx + 1, data: new Date(incident.createdAt).toLocaleString() },
|
||||
{ id: idx + 2, data: incident.statusCode },
|
||||
{ id: idx + 3, data: monitors[incident.monitorId].name },
|
||||
],
|
||||
};
|
||||
});
|
||||
data.rows = incidents;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: theme.spacing(4),
|
||||
marginX: "auto",
|
||||
width: "80%",
|
||||
paddingX: theme.spacing(6),
|
||||
[theme.breakpoints.down("md")]: {
|
||||
width: "100%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="div"
|
||||
sx={{ fontWeight: 600, fontSize: 16 }}
|
||||
>
|
||||
Incident History
|
||||
</Typography>
|
||||
<Filter />
|
||||
</Box>
|
||||
{/* TODO Replace with BasicTable */}
|
||||
{/* <StatusTable data={sampleData} columns={columns} /> */}
|
||||
</Box>
|
||||
<Stack className="incidents" gap={theme.gap.large}>
|
||||
<Stack direction="row" alignItems="center" gap={theme.gap.medium}>
|
||||
<Typography component="h1">Incident history for: </Typography>
|
||||
<Select
|
||||
id="incidents-select-monitor"
|
||||
placeholder="All servers"
|
||||
value={selectedMonitor}
|
||||
onChange={handleSelect}
|
||||
items={Object.values(monitors)}
|
||||
/>
|
||||
<ButtonGroup sx={{ ml: "auto" }}>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="All"
|
||||
onClick={() => setFilter("all")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "all" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Down"
|
||||
onClick={() => setFilter("down")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "down" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Cannot Resolve"
|
||||
onClick={() => setFilter("resolve")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "resolve" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<BasicTable data={data} paginated={true} rowsPerPage={12} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Incidents;
|
||||
@@ -0,0 +1,20 @@
|
||||
.integrations h1.MuiTypography-root,
|
||||
.integrations p.MuiTypography-root {
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
.integrations h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 600;
|
||||
}
|
||||
.integrations p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.integrations button {
|
||||
height: var(--env-var-height-2);
|
||||
}
|
||||
|
||||
.integrations .MuiGrid-item > .MuiStack-root {
|
||||
border: solid 1px var(--env-var-color-6);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
background-color: var(--env-var-color-8);
|
||||
}
|
||||
@@ -1,60 +1,106 @@
|
||||
import { Box, Typography, useTheme } from '@mui/material';
|
||||
import IntegrationsComponent from '../../Components/Integrations';
|
||||
import PropTypes from "prop-types";
|
||||
import { Stack, Box, Typography, Grid } from "@mui/material";
|
||||
import Button from "../../Components/Button";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Integrations component
|
||||
* @param {Object} props - Props for the IntegrationsComponent.
|
||||
* @param {string} props.url - The URL for the integration image.
|
||||
* @param {string} props.header - The header for the integration.
|
||||
* @param {string} props.info - Information about the integration.
|
||||
* @param {Function} props.onClick - The onClick handler for the integration button.
|
||||
* @returns {JSX.Element} The JSX representation of the IntegrationsComponent.
|
||||
*/
|
||||
const IntegrationsComponent = ({ url, header, info, onClick }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Grid item lg={6} flexGrow={1}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.large}
|
||||
p={theme.gap.ml}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={url}
|
||||
alt="Integration"
|
||||
width={80}
|
||||
height={80}
|
||||
/>
|
||||
<Stack gap={theme.gap.xs} flex={1}>
|
||||
<Typography component="h1">{header}</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
maxWidth: "300px",
|
||||
wordWrap: "break-word",
|
||||
}}
|
||||
>
|
||||
{info}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Button
|
||||
label="Add"
|
||||
level="primary"
|
||||
onClick={onClick}
|
||||
sx={{ alignSelf: "center" }}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
// PropTypes for IntegrationsComponent
|
||||
IntegrationsComponent.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
header: PropTypes.string.isRequired,
|
||||
info: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Integrations Page Component
|
||||
* @returns {JSX.Element} The JSX representation of the Integrations page.
|
||||
*/
|
||||
|
||||
const Integrations = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
|
||||
const integrations = [
|
||||
{
|
||||
url: 'https://via.placeholder.com/100',
|
||||
header: 'Integration 1',
|
||||
info: 'Info about Integration 1',
|
||||
onClick: () => {}
|
||||
{
|
||||
url: "https://via.placeholder.com/80",
|
||||
header: "Slack",
|
||||
info: "Connect with Slack and see incidents in a channel",
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
url: "https://via.placeholder.com/80",
|
||||
header: "Discord",
|
||||
info: "Connect with Discord and view incidents directly in a channel",
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
url: "https://via.placeholder.com/80",
|
||||
header: "Zapier",
|
||||
info: "Send all incidents to Zapier, and then see them everywhere",
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
url: 'https://via.placeholder.com/100',
|
||||
header: 'Integration 2',
|
||||
info: 'Info about Integration 2',
|
||||
onClick: () => {}
|
||||
},
|
||||
{
|
||||
url: 'https://via.placeholder.com/100',
|
||||
header: 'Integration 2',
|
||||
info: 'Info about Integration 2',
|
||||
onClick: () => {}
|
||||
}
|
||||
// Add more integrations as needed
|
||||
];
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="flex-start"
|
||||
justifyContent="flex-start"
|
||||
height="100vh"
|
||||
p={theme.spacing(4)}
|
||||
mt={theme.spacing(8)}
|
||||
>
|
||||
<Typography variant="h4" component="h1" gutterBottom style={{ fontSize: '16px' }}>
|
||||
Integrations
|
||||
</Typography>
|
||||
<Typography variant="h6" component="h2" gutterBottom style={{ fontSize: '13px' }}>
|
||||
<Stack className="integrations" gap={theme.gap.xs}>
|
||||
<Typography component="h1">Integrations</Typography>
|
||||
<Typography mb={theme.gap.large}>
|
||||
Connect Uptime Genie to your favorite service
|
||||
</Typography>
|
||||
<Box
|
||||
display="flex"
|
||||
flexWrap="wrap"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Grid container spacing={theme.gap.large}>
|
||||
{integrations.map((integration, index) => (
|
||||
<IntegrationsComponent
|
||||
<IntegrationsComponent
|
||||
key={index}
|
||||
url={integration.url}
|
||||
header={integration.header}
|
||||
@@ -62,8 +108,8 @@ const Integrations = () => {
|
||||
onClick={integration.onClick}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,37 +1,20 @@
|
||||
import React from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Fallback from "../../Components/Fallback";
|
||||
import "./index.css";
|
||||
import WindowFrame from "./../../assets/Images/maintenance_window_frame.svg";
|
||||
import { Button } from "@mui/material";
|
||||
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
|
||||
|
||||
const Maintenance = () => {
|
||||
const maintenanceItems = [
|
||||
{ id: 1, text: "Mark your maintenance periods" },
|
||||
{ id: 2, text: "Eliminate any misunderstandings" },
|
||||
{ id: 3, text: "Stop sending alerts in maintenance windows" },
|
||||
];
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div className="maintenance-checklist-main">
|
||||
<img className="maintenance-image" src={WindowFrame} alt="WindowFrame" />
|
||||
<div className="maintenance-title">Create a maintenance window to</div>
|
||||
<div>
|
||||
{maintenanceItems.map((item) => (
|
||||
<div key={item.id} className="checklist-item">
|
||||
<CheckCircleOutlineIcon color="primary" />
|
||||
<div className="checklist-item-text">{item.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className="maintenance-checklist-button"
|
||||
sx={{ textTransform: "none" }}
|
||||
>
|
||||
Let's create your maintenance window
|
||||
</Button>
|
||||
<div className="maintenance">
|
||||
<Fallback
|
||||
title="maintenance window"
|
||||
checks={[
|
||||
"Mark your maintenance periods",
|
||||
"Eliminate any misunderstandings",
|
||||
"Stop sending alerts in maintenance windows",
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
.configure-monitor h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large-plus);
|
||||
color: var(--env-var-color-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
.configure-monitor h2.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 600;
|
||||
}
|
||||
.configure-monitor h2.MuiTypography-root,
|
||||
.configure-monitor .config-box p.MuiTypography-root,
|
||||
.configure-monitor .MuiSelect-select {
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
.configure-monitor p.MuiTypography-root,
|
||||
.configure-monitor .MuiSelect-select {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.configure-monitor h6.MuiTypography-root {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.configure-monitor h6.MuiTypography-root,
|
||||
.configure-monitor p.MuiTypography-root:has(span),
|
||||
.configure-monitor p.MuiTypography-root span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
.configure-monitor button.MuiButtonBase-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.configure-monitor button.MuiButtonBase-root {
|
||||
height: var(--env-var-height-2);
|
||||
}
|
||||
.configure-monitor button.MuiButtonBase-root:has(svg) {
|
||||
line-height: 1;
|
||||
}
|
||||
.configure-monitor .config-box {
|
||||
padding: var(--env-var-spacing-4) 50px;
|
||||
padding-bottom: 60px;
|
||||
border: 1px solid var(--env-var-color-16);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
}
|
||||
.configure-monitor .config-box .MuiBox-root,
|
||||
.configure-monitor .config-box .MuiStack-root {
|
||||
flex: 1;
|
||||
}
|
||||
.configure-monitor .MuiStack-root:has(span.MuiTypography-root.input-error) {
|
||||
position: relative;
|
||||
}
|
||||
.configure-monitor span.MuiTypography-root.input-error {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.MuiInputBase-root:has(#monitor-interval) {
|
||||
height: 34px;
|
||||
width: 100%;
|
||||
}
|
||||
#monitor-interval {
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.MuiInputBase-root:not(.Mui-focused):has(#monitor-interval):hover fieldset {
|
||||
border-color: var(--env-var-color-29);
|
||||
}
|
||||
|
||||
.configure-monitor-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--env-var-spacing-4);
|
||||
}
|
||||
|
||||
body:has(.configure-monitor) .select-dropdown .MuiMenuItem-root,
|
||||
.configure-monitor .select-wrapper .select-component > .MuiSelect-select {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEffect, useState } from "react";
|
||||
import Button from "../../../Components/Button";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import RadioButton from "../../../Components/RadioButton";
|
||||
import { Box, MenuItem, Stack, Typography } from "@mui/material";
|
||||
import WestRoundedIcon from "@mui/icons-material/WestRounded";
|
||||
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
|
||||
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
|
||||
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
|
||||
|
||||
import "./index.css";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import { formatDurationRounded } from "../../../Utils/timeUtils";
|
||||
|
||||
/**
|
||||
* Helper function to get duration since last check
|
||||
* @param {Array} checks Array of check objects.
|
||||
* @returns {number} Timestamp of the most recent check.
|
||||
*/
|
||||
const getLastChecked = (checks) => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0; // Handle case when no checks are available
|
||||
}
|
||||
return new Date() - new Date(checks[0].createdAt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure page displays monitor configurations and allows for editing actions.
|
||||
* @component
|
||||
*/
|
||||
const Configure = () => {
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const { monitors } = useSelector((state) => state.uptimeMonitors);
|
||||
const { monitorId } = useParams();
|
||||
|
||||
const idMap = {
|
||||
"monitor-url": "url",
|
||||
"monitor-name": "name",
|
||||
"monitor-checks-http": "type",
|
||||
"monitor-checks-ping": "type",
|
||||
};
|
||||
|
||||
const [config, setConfig] = useState();
|
||||
const [monitor, setMonitor] = useState();
|
||||
const [errors, setErrors] = useState({});
|
||||
useEffect(() => {
|
||||
const fetchMonitor = () => {
|
||||
const data = monitors.find((monitor) => monitor._id === monitorId);
|
||||
setConfig(data);
|
||||
setMonitor({
|
||||
name: data.name,
|
||||
url: data.url.replace(/^https?:\/\//, ""),
|
||||
type: data.type,
|
||||
interval: data.interval / MS_PER_MINUTE,
|
||||
});
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [monitorId, authToken]);
|
||||
|
||||
const handleChange = (event, name) => {
|
||||
const { value, id } = event.target;
|
||||
if (!name) name = idMap[id];
|
||||
setMonitor((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
const validation = monitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
|
||||
if (validation.error)
|
||||
updatedErrors[name] = validation.error.details[0].message;
|
||||
else delete updatedErrors[name];
|
||||
return updatedErrors;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
// TODO
|
||||
};
|
||||
|
||||
const frequencies = [
|
||||
{ _id: 1, name: "1 minute" },
|
||||
{ _id: 2, name: "2 minutes" },
|
||||
{ _id: 3, name: "3 minutes" },
|
||||
{ _id: 4, name: "4 minutes" },
|
||||
{ _id: 5, name: "5 minutes" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box className="configure-monitor">
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back"
|
||||
img={<WestRoundedIcon />}
|
||||
onClick={() => navigate(-1)}
|
||||
sx={{
|
||||
backgroundColor: "#f4f4f4",
|
||||
mb: theme.gap.medium,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
pr: theme.gap.small,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<form className="configure-monitor-form" noValidate spellCheck="false">
|
||||
<Stack direction="row" gap={theme.gap.small} mt={theme.gap.small}>
|
||||
{config?.status ? <GreenCheck /> : <RedCheck />}
|
||||
<Box>
|
||||
<Typography component="h1" sx={{ lineHeight: 1 }}>
|
||||
{config?.url.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Typography mt={theme.gap.small}>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: config?.status
|
||||
? "var(--env-var-color-17)"
|
||||
: "var(--env-var-color-24)",
|
||||
}}
|
||||
>
|
||||
Your site is {config?.status ? "up" : "down"}.
|
||||
</Typography>{" "}
|
||||
Checking every {formatDurationRounded(config?.interval)}. Last
|
||||
time checked{" "}
|
||||
{formatDurationRounded(getLastChecked(config?.checks))} ago.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.gap.medium}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Pause"
|
||||
img={<PauseCircleOutlineIcon />}
|
||||
sx={{
|
||||
backgroundColor: "#f4f4f4",
|
||||
pl: theme.gap.small,
|
||||
pr: theme.gap.medium,
|
||||
"& svg": {
|
||||
pr: theme.gap.xs,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="error"
|
||||
label="Remove"
|
||||
sx={{
|
||||
boxShadow: "none",
|
||||
px: theme.gap.ml,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="config-box"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.xxl}
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h2">General settings</Typography>
|
||||
<Typography component="p" sx={{ mt: theme.gap.small }}>
|
||||
Here you can select the URL of the host, together with the type of
|
||||
monitor.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.xl}>
|
||||
<Field
|
||||
type="url"
|
||||
id="monitor-url"
|
||||
label="URL to monitor"
|
||||
placeholder="google.com"
|
||||
value={monitor?.url || ""}
|
||||
onChange={handleChange}
|
||||
error={errors["url"]}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
label="Friendly name"
|
||||
isOptional={true}
|
||||
placeholder="Google"
|
||||
value={monitor?.name || ""}
|
||||
onChange={handleChange}
|
||||
error={errors["name"]}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="config-box"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.xxl}
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h2">Checks to perform</Typography>
|
||||
<Typography component="p" sx={{ mt: theme.gap.small }}>
|
||||
You can always add or remove checks after adding your site.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.xl}>
|
||||
<RadioButton
|
||||
id="monitor-checks-http"
|
||||
title="HTTP/website monitoring"
|
||||
desc="Use HTTP(s) to monitor your website or API endpoint."
|
||||
size="small"
|
||||
value="http"
|
||||
checked={monitor?.type === "http"}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<RadioButton
|
||||
id="monitor-checks-ping"
|
||||
title="Ping monitoring"
|
||||
desc="Check whether your server is available or not."
|
||||
size="small"
|
||||
value="ping"
|
||||
checked={monitor?.type === "ping"}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Box className="error-container">
|
||||
{errors["type"] ? (
|
||||
<Typography component="p" className="input-error">
|
||||
{errors["type"]}
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="config-box"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.xxl}
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h2">Advanced settings</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.xl}>
|
||||
<Select
|
||||
id="monitor-interval-configure"
|
||||
label="Check frequency"
|
||||
value={monitor?.interval || 1}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
items={frequencies}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Button level="primary" label="Save" sx={{ px: theme.gap.ml }} />
|
||||
</Stack>
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Configure;
|
||||
@@ -1,76 +1,72 @@
|
||||
.create-monitor-form h1.MuiTypography-root {
|
||||
.create-monitor h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large-plus);
|
||||
color: var(--env-var-color-1);
|
||||
}
|
||||
.create-monitor-form h1.MuiTypography-root,
|
||||
.create-monitor-form h2.MuiTypography-root {
|
||||
.create-monitor h1.MuiTypography-root,
|
||||
.create-monitor h2.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
}
|
||||
.create-monitor-form h2.MuiTypography-root {
|
||||
.create-monitor h2.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
}
|
||||
.create-monitor-form p.MuiTypography-root,
|
||||
.create-monitor-form button.MuiButtonBase-root,
|
||||
#monitor-frequencies {
|
||||
.create-monitor p.MuiTypography-root,
|
||||
.create-monitor button.MuiButtonBase-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.create-monitor-form h2.MuiTypography-root,
|
||||
.create-monitor-form h6.MuiTypography-root,
|
||||
.create-monitor-form p.MuiTypography-root,
|
||||
#monitor-frequencies {
|
||||
.create-monitor h2.MuiTypography-root,
|
||||
.create-monitor h6.MuiTypography-root,
|
||||
.create-monitor p.MuiTypography-root {
|
||||
color: var(--env-var-color-2);
|
||||
}
|
||||
.create-monitor-form h6.MuiTypography-root {
|
||||
.create-monitor h6.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.create-monitor-form .error-container {
|
||||
.create-monitor button.MuiButtonBase-root {
|
||||
height: 34px;
|
||||
}
|
||||
.create-monitor .error-container {
|
||||
position: relative;
|
||||
}
|
||||
.create-monitor-form .error-container p.MuiTypography-root.input-error {
|
||||
.create-monitor .error-container p.MuiTypography-root.input-error {
|
||||
color: var(--env-var-color-24);
|
||||
opacity: 0.8;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* for testing, will remove later */
|
||||
/* .config-box:has(.error-container):has(.input-error) {
|
||||
border-color: var(--env-var-color-24);
|
||||
} */
|
||||
|
||||
.create-monitor-form .MuiSelect-select.MuiSelect-outlined.MuiInputBase-input {
|
||||
padding: var(--env-var-spacing-1);
|
||||
}
|
||||
.create-monitor-form .MuiInputBase-root:has(#monitor-frequencies) {
|
||||
min-width: 300px;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
border: 1px solid var(--env-var-color-4);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.create-monitor-form .MuiInputBase-root:has(#monitor-frequencies) fieldset {
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
.create-monitor-form .MuiStack-root:has(span.MuiTypography-root.input-error) {
|
||||
.create-monitor .MuiStack-root:has(span.MuiTypography-root.input-error) {
|
||||
position: relative;
|
||||
}
|
||||
.create-monitor-form span.MuiTypography-root.input-error {
|
||||
.create-monitor span.MuiTypography-root.input-error {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.monitors-gaps-medium {
|
||||
height: var(--env-var-spacing-2);
|
||||
.create-monitor .config-box {
|
||||
padding: var(--env-var-spacing-4) 50px;
|
||||
padding-bottom: 60px;
|
||||
border: 1px solid var(--env-var-color-16);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
flex-direction: row;
|
||||
gap: var(--env-var-spacing-4);
|
||||
}
|
||||
.monitors-gaps-small-plus {
|
||||
height: var(--env-var-spacing-1-plus);
|
||||
.create-monitor .config-box .MuiBox-root,
|
||||
.create-monitor .config-box .MuiStack-root {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.monitors-create-button-holder {
|
||||
.create-monitor-form {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
flex-direction: column;
|
||||
gap: var(--env-var-spacing-4);
|
||||
}
|
||||
|
||||
.create-monitor .MuiStack-root .MuiButtonGroup-root button {
|
||||
font-size: var(--env-var-font-size-small);
|
||||
height: 28px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
body:has(.create-monitor) .select-dropdown .MuiMenuItem-root,
|
||||
.create-monitor .select-wrapper .select-component > .MuiSelect-select {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
import "./index.css";
|
||||
import ConfigBox from "../../../Components/ConfigBox";
|
||||
import React, { useState } from "react";
|
||||
import RadioButton from "../../../Components/RadioButton";
|
||||
import Button from "../../../Components/Button";
|
||||
import { Box, MenuItem, Select, Stack, Typography } from "@mui/material";
|
||||
import { Box, ButtonGroup, Stack, Typography } from "@mui/material";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { createMonitorValidation } from "../../../Validation/validation";
|
||||
import { createMonitor } from "../../../Features/Monitors/monitorsSlice";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import WestRoundedIcon from "@mui/icons-material/WestRounded";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
|
||||
const CreateMonitor = () => {
|
||||
const MS_PER_MINUTE = 60000;
|
||||
@@ -37,7 +38,7 @@ const CreateMonitor = () => {
|
||||
// });
|
||||
//Advanced Settings Form
|
||||
const [advancedSettings, setAdvancedSettings] = useState({
|
||||
frequency: 1,
|
||||
interval: 1,
|
||||
// retries: "",
|
||||
// codes: "",
|
||||
// redirects: "",
|
||||
@@ -57,7 +58,7 @@ const CreateMonitor = () => {
|
||||
[id]: checkbox ? true : value,
|
||||
}));
|
||||
|
||||
const validation = createMonitorValidation.validate(
|
||||
const validation = monitorValidation.validate(
|
||||
{ [id]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
@@ -84,15 +85,21 @@ const CreateMonitor = () => {
|
||||
event.preventDefault();
|
||||
//obj to submit
|
||||
let monitor = {
|
||||
...generalSettings,
|
||||
url:
|
||||
checks.type === "http"
|
||||
? "https://" + generalSettings.url
|
||||
//preprending protocol for url
|
||||
checks.type === "http" || checks.type === "https"
|
||||
? `${checks.type}://` + generalSettings.url
|
||||
: generalSettings.url,
|
||||
...checks,
|
||||
name:
|
||||
generalSettings.name === ""
|
||||
? generalSettings.url
|
||||
: generalSettings.name,
|
||||
//there is no separate monitor type for https since the operations for the two protocols are identical
|
||||
//however the URL does need the correct prepend hence https is still being tracked but overwritten when prepping the monitor obj
|
||||
type: checks.type === "https" ? "http" : checks.type,
|
||||
};
|
||||
|
||||
const { error } = createMonitorValidation.validate(monitor, {
|
||||
const { error } = monitorValidation.validate(monitor, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
@@ -107,11 +114,13 @@ const CreateMonitor = () => {
|
||||
...monitor,
|
||||
description: monitor.name,
|
||||
userId: user._id,
|
||||
// ...advancedSettings, // TODO frequency should be interval, then we can use spread
|
||||
interval: advancedSettings.frequency * MS_PER_MINUTE,
|
||||
// ...advancedSettings
|
||||
interval: advancedSettings.interval * MS_PER_MINUTE,
|
||||
};
|
||||
try {
|
||||
const action = await dispatch(createMonitor({ authToken, monitor }));
|
||||
const action = await dispatch(
|
||||
createUptimeMonitor({ authToken, monitor })
|
||||
);
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
navigate("/monitors");
|
||||
}
|
||||
@@ -123,20 +132,16 @@ const CreateMonitor = () => {
|
||||
|
||||
//select values
|
||||
// const ports = ["Port 1", "Port 2", "Port 3"];
|
||||
const frequencies = [1, 2, 3, 4, 5];
|
||||
const frequencies = [
|
||||
{ _id: 1, name: "1 minute" },
|
||||
{ _id: 2, name: "2 minutes" },
|
||||
{ _id: 3, name: "3 minutes" },
|
||||
{ _id: 4, name: "4 minutes" },
|
||||
{ _id: 5, name: "5 minutes" },
|
||||
];
|
||||
|
||||
return (
|
||||
<form
|
||||
className="create-monitor-form"
|
||||
onSubmit={handleCreateMonitor}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
style={{
|
||||
maxWidth: "1200px",
|
||||
flex: 1,
|
||||
padding: `${theme.content.pY} ${theme.content.pX}`,
|
||||
}}
|
||||
>
|
||||
<Box className="create-monitor">
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back"
|
||||
@@ -151,24 +156,31 @@ const CreateMonitor = () => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Typography component="h1">Create new monitor</Typography>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<ConfigBox
|
||||
leftLayout={
|
||||
<Stack gap={theme.gap.small}>
|
||||
<form
|
||||
className="create-monitor-form"
|
||||
onSubmit={handleCreateMonitor}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
>
|
||||
<Typography component="h1">Create new monitor</Typography>
|
||||
<Stack className="config-box">
|
||||
<Box>
|
||||
<Typography component="h2">General settings</Typography>
|
||||
<Typography component="p">
|
||||
<Typography component="p" mt={theme.gap.small}>
|
||||
Here you can select the URL of the host, together with the type of
|
||||
monitor.
|
||||
</Typography>
|
||||
</Stack>
|
||||
}
|
||||
rightLayout={
|
||||
</Box>
|
||||
<Stack gap={theme.gap.xl}>
|
||||
<Field
|
||||
type="url"
|
||||
type={
|
||||
checks.type === "http" || checks.type === "https"
|
||||
? "url"
|
||||
: "text"
|
||||
}
|
||||
id="monitor-url"
|
||||
label="URL to monitor"
|
||||
https={checks.type === "https"}
|
||||
placeholder="google.com"
|
||||
value={generalSettings.url}
|
||||
onChange={(event) =>
|
||||
@@ -189,30 +201,56 @@ const CreateMonitor = () => {
|
||||
error={errors["name"]}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<ConfigBox
|
||||
leftLayout={
|
||||
<Stack gap={theme.gap.small}>
|
||||
</Stack>
|
||||
<Stack className="config-box">
|
||||
<Box>
|
||||
<Typography component="h2">Checks to perform</Typography>
|
||||
<Typography component="p">
|
||||
<Typography component="p" mt={theme.gap.small}>
|
||||
You can always add or remove checks after adding your site.
|
||||
</Typography>
|
||||
</Stack>
|
||||
}
|
||||
rightLayout={
|
||||
</Box>
|
||||
<Stack gap={theme.gap.large}>
|
||||
<RadioButton
|
||||
id="monitor-checks-http"
|
||||
title="HTTP/website monitoring"
|
||||
desc="Use HTTP(s) to monitor your website or API endpoint."
|
||||
size="small"
|
||||
value="http"
|
||||
checked={checks.type === "http"}
|
||||
onChange={(event) => handleChange(event, "type", setChecks)}
|
||||
/>
|
||||
<Stack gap={theme.gap.medium}>
|
||||
<RadioButton
|
||||
id="monitor-checks-http"
|
||||
title="Website monitoring"
|
||||
desc="Use HTTP(s) to monitor your website or API endpoint."
|
||||
size="small"
|
||||
value="http"
|
||||
checked={checks.type === "http" || checks.type === "https"}
|
||||
onChange={(event) => handleChange(event, "type", setChecks)}
|
||||
/>
|
||||
{checks.type === "http" || checks.type === "https" ? (
|
||||
<ButtonGroup sx={{ ml: "32px" }}>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="HTTP"
|
||||
onClick={() =>
|
||||
setChecks((prev) => ({ ...prev, type: "http" }))
|
||||
}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
checks.type === "http" &&
|
||||
theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="HTTPS"
|
||||
onClick={() =>
|
||||
setChecks((prev) => ({ ...prev, type: "https" }))
|
||||
}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
checks.type === "https" &&
|
||||
theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
<RadioButton
|
||||
id="monitor-checks-ping"
|
||||
title="Ping monitoring"
|
||||
@@ -231,7 +269,6 @@ const CreateMonitor = () => {
|
||||
checked={checks.type === "port"}
|
||||
onChange={(event) => handleChange(event, "type", setChecks)}
|
||||
/>
|
||||
<div className="monitors-gaps-small-plus"></div>
|
||||
<div className="monitors-dropdown-holder">
|
||||
<Select
|
||||
id="monitor-ports"
|
||||
@@ -259,108 +296,51 @@ const CreateMonitor = () => {
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
{/* TODO */}
|
||||
{/* <div className="monitors-gaps-medium"></div>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<ConfigBox
|
||||
leftLayout={
|
||||
<div className="config-box-desc">
|
||||
<div className="config-box-desc-title">Incident notifications</div>
|
||||
<div className="config-box-desc-text">
|
||||
When there is an incident, notify users.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
rightLayout={
|
||||
<div className="incident-notif-config">
|
||||
<div className="incident-notif-config-title">
|
||||
When there is a new incident,
|
||||
</div>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<div className="incident-notif-config-checks">
|
||||
<CustomizableCheckBox
|
||||
id="monitor-notify-sms"
|
||||
title="Notify via SMS (to be implemented)"
|
||||
isChecked={notifications.viaSms}
|
||||
handleChange={() => handleCheck("viaSms", setNotifications)}
|
||||
/>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<CustomizableCheckBox
|
||||
id="monitor-notify-email"
|
||||
title="Notify via email (to current email address)"
|
||||
isChecked={notifications.viaEmail}
|
||||
handleChange={() => handleCheck("viaEmail", setNotifications)}
|
||||
/>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<CustomizableCheckBox
|
||||
id="monitor-notify-other"
|
||||
title="Notify via email (to another email address below)"
|
||||
isChecked={notifications.viaOther}
|
||||
handleChange={() => handleCheck("viaOther", setNotifications)}
|
||||
/>
|
||||
<div className="monitors-gaps-small-plus"></div>
|
||||
<div className="monitors-dropdown-holder">
|
||||
<FlexibileTextField
|
||||
id="monitor-notify-other-email"
|
||||
placeholder="notifications@gmail.com"
|
||||
type="email"
|
||||
value={notifications.email}
|
||||
onChange={(event) =>
|
||||
handleChange(event, "email", setNotifications)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/> */}
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<ConfigBox
|
||||
leftLayout={
|
||||
<Stack gap={theme.gap.small}>
|
||||
<Typography component="h2">Advanced settings</Typography>
|
||||
</Stack>
|
||||
}
|
||||
rightLayout={
|
||||
</Stack>
|
||||
|
||||
<Stack className="config-box">
|
||||
<Box>
|
||||
<Typography component="h2">Incident notifications</Typography>
|
||||
<Typography component="p" mt={theme.gap.small}>
|
||||
When there is an incident, notify users
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.large}>
|
||||
{/* TODO - refactor select component */}
|
||||
<Box>
|
||||
<Typography component="p" mb={theme.gap.small}>
|
||||
Check frequency
|
||||
</Typography>
|
||||
<Select
|
||||
id="monitor-frequencies"
|
||||
value={advancedSettings.frequency || 1}
|
||||
inputProps={{ id: "monitor-frequencies-select" }}
|
||||
onChange={(event) =>
|
||||
handleChange(event, "frequency", setAdvancedSettings)
|
||||
}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: {
|
||||
marginTop: "10px",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{frequencies.map((freq) => (
|
||||
<MenuItem
|
||||
key={`port-${freq}`}
|
||||
value={freq}
|
||||
sx={{
|
||||
fontSize: "13px",
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
margin: "5px",
|
||||
}}
|
||||
>
|
||||
{freq} {freq === 1 ? "minute" : "minutes"}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Box>
|
||||
<Typography component="p" mt={theme.gap.small}>
|
||||
When there is a new incident,
|
||||
</Typography>
|
||||
<Checkbox id="notify-sms" label="Notify via SMS (coming soon)" />
|
||||
<Checkbox
|
||||
id="notify-email-default"
|
||||
label={`Notify via email (to ${user.email})`}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email"
|
||||
label="Also notify via email to the following addresses:"
|
||||
/>
|
||||
<Field
|
||||
id="notify-email-list"
|
||||
type="text"
|
||||
label="You can separate multiple emails with a comma"
|
||||
placeholder="name@gmail.com"
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Stack className="config-box">
|
||||
<Box>
|
||||
<Typography component="h2">Advanced settings</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.large}>
|
||||
<Select
|
||||
id="monitor-interval"
|
||||
label="Check frequency"
|
||||
value={advancedSettings.interval || 1}
|
||||
onChange={(event) =>
|
||||
handleChange(event, "interval", setAdvancedSettings)
|
||||
}
|
||||
items={frequencies}
|
||||
/>
|
||||
{/* TODO */}
|
||||
{/* <FlexibileTextField
|
||||
id="monitor-settings-retries"
|
||||
@@ -371,7 +351,6 @@ const CreateMonitor = () => {
|
||||
handleChange(event, "retries", setAdvancedSettings)
|
||||
}
|
||||
/>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<FlexibileTextField
|
||||
id="monitor-settings-codes"
|
||||
title="Accepted status codes"
|
||||
@@ -381,7 +360,6 @@ const CreateMonitor = () => {
|
||||
handleChange(event, "codes", setAdvancedSettings)
|
||||
}
|
||||
/>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<FlexibileTextField
|
||||
id="monitor-settings-redirects"
|
||||
title="Maximum redirects"
|
||||
@@ -392,11 +370,9 @@ const CreateMonitor = () => {
|
||||
}
|
||||
/> */}
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
{/* TODO */}
|
||||
{/* <div className="monitors-gaps-medium"></div>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
</Stack>
|
||||
{/* TODO */}
|
||||
{/*
|
||||
<ConfigBox
|
||||
leftLayout={
|
||||
<div className="config-box-desc">
|
||||
@@ -411,21 +387,18 @@ const CreateMonitor = () => {
|
||||
isChecked={proxy.enabled}
|
||||
handleChange={() => handleCheck("enabled", setProxy)}
|
||||
/>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<FlexibileTextField
|
||||
id="monitor-proxy-protocol"
|
||||
title="Proxy protocol"
|
||||
value={proxy.protocol}
|
||||
onChange={(event) => handleChange(event, "protocol", setProxy)}
|
||||
/>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<FlexibileTextField
|
||||
id="monitor-proxy-address"
|
||||
title="Proxy address"
|
||||
value={proxy.address}
|
||||
onChange={(event) => handleChange(event, "address", setProxy)}
|
||||
/>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<FlexibileTextField
|
||||
id="monitor-proxy-port"
|
||||
title="Proxy port"
|
||||
@@ -435,19 +408,17 @@ const CreateMonitor = () => {
|
||||
</div>
|
||||
}
|
||||
/> */}
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<div className="monitors-gaps-small"></div>
|
||||
<div className="monitors-create-button-holder">
|
||||
<Button
|
||||
id="create-new-monitor-btn"
|
||||
level="primary"
|
||||
label="Create new monitor"
|
||||
sx={{ width: "210px", fontSize: "var(--env-var-font-size-medium)" }}
|
||||
onClick={handleCreateMonitor}
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Button
|
||||
id="create-new-monitor-btn"
|
||||
level="primary"
|
||||
label="Create new monitor"
|
||||
onClick={handleCreateMonitor}
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,53 +13,12 @@ import WestRoundedIcon from "@mui/icons-material/WestRounded";
|
||||
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
|
||||
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
|
||||
import SettingsIcon from "../../../assets/icons/settings.svg?react";
|
||||
import {
|
||||
formatDuration,
|
||||
formatDurationRounded,
|
||||
} from "../../../Utils/timeUtils";
|
||||
import "./index.css";
|
||||
|
||||
const formatDuration = (ms) => {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
let dateStr = "";
|
||||
|
||||
days && (dateStr += `${days}d `);
|
||||
hours && (dateStr += `${hours % 24}h `);
|
||||
minutes && (dateStr += `${minutes % 60}m `);
|
||||
seconds && (dateStr += `${seconds % 60}s `);
|
||||
|
||||
dateStr === "" && (dateStr = "0s");
|
||||
|
||||
return dateStr;
|
||||
};
|
||||
|
||||
const formatDurationRounded = (ms) => {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
let time = "";
|
||||
if (days > 0) {
|
||||
time += `${days} day${days !== 1 ? "s" : ""}`;
|
||||
return time;
|
||||
}
|
||||
if (hours > 0) {
|
||||
time += `${hours} hour${hours !== 1 ? "s" : ""}`;
|
||||
return time;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
time += `${minutes} minute${minutes !== 1 ? "s" : ""}`;
|
||||
return time;
|
||||
}
|
||||
if (seconds > 0) {
|
||||
time += `${seconds} second${seconds !== 1 ? "s" : ""}`;
|
||||
return time;
|
||||
}
|
||||
|
||||
return time;
|
||||
};
|
||||
|
||||
const StatBox = ({ title, value }) => {
|
||||
return (
|
||||
<Box className="stat-box">
|
||||
@@ -82,11 +41,14 @@ const DetailsPage = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitor = async () => {
|
||||
const res = await axiosInstance.get(`/monitors/${monitorId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/${monitorId}?sortOrder=asc`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
setMonitor(res.data.data);
|
||||
const data = {
|
||||
cols: [
|
||||
@@ -95,32 +57,14 @@ const DetailsPage = () => {
|
||||
{ id: 3, name: "Message" },
|
||||
],
|
||||
rows: res.data.data.checks.map((check, idx) => {
|
||||
const params = {
|
||||
status: check.status === true ? "Up" : "Down",
|
||||
backgroundColor:
|
||||
check.status === true
|
||||
? "var(--env-var-color-20)"
|
||||
: "var(--env-var-color-21)",
|
||||
statusDotColor:
|
||||
check.status === true
|
||||
? "var(--env-var-color-17)"
|
||||
: "var(--env-var-color-19)",
|
||||
};
|
||||
const status = check.status === true ? "up" : "down";
|
||||
|
||||
return {
|
||||
id: check._id,
|
||||
data: [
|
||||
{
|
||||
id: idx,
|
||||
data: (
|
||||
<StatusLabel
|
||||
status={params.status}
|
||||
dot={params.statusDotColor}
|
||||
customStyles={{
|
||||
backgroundColor: params.backgroundColor,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
data: <StatusLabel status={status} text={status} />,
|
||||
},
|
||||
{ id: idx + 1, data: new Date(check.createdAt).toLocaleString() },
|
||||
{ id: idx + 2, data: check.statusCode },
|
||||
@@ -149,17 +93,25 @@ const DetailsPage = () => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
const latestCheck = new Date(checks[0].createdAt);
|
||||
|
||||
const latestCheck = new Date(checks[checks.length - 1].createdAt);
|
||||
let latestDownCheck = 0;
|
||||
for (let i = 0; i < checks.length; i++) {
|
||||
|
||||
// Checks are ordered oldest -> newest
|
||||
// So we iterate backwards and find the first down check
|
||||
for (let i = checks.length - 1; i >= 0; i--) {
|
||||
if (checks[i].status === false) {
|
||||
latestDownCheck = new Date(checks[i].createdAt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no down check is found, uptime is from the first check to now
|
||||
if (latestDownCheck === 0) {
|
||||
return Date.now() - new Date(checks[checks.length - 1].createdAt);
|
||||
return Date.now() - new Date(checks[0].createdAt);
|
||||
}
|
||||
|
||||
// Otherwise the uptime is from the last check to the last down check
|
||||
return latestCheck - latestDownCheck;
|
||||
};
|
||||
|
||||
@@ -172,7 +124,8 @@ const DetailsPage = () => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0; // Handle case when no checks are available
|
||||
}
|
||||
return new Date() - new Date(checks[0].createdAt);
|
||||
// Data is sorted oldest -> newest, so last check is the most recent
|
||||
return new Date() - new Date(checks[checks.length - 1].createdAt);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -190,12 +143,7 @@ const DetailsPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="monitor-details"
|
||||
style={{
|
||||
padding: `${theme.content.pY} ${theme.content.pX}`,
|
||||
}}
|
||||
>
|
||||
<Box className="monitor-details">
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back to Monitors"
|
||||
@@ -237,6 +185,7 @@ const DetailsPage = () => {
|
||||
level="tertiary"
|
||||
label="Configure"
|
||||
img={<SettingsIcon />}
|
||||
onClick={() => navigate(`/monitors/configure/${monitorId}`)}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
@@ -297,10 +246,10 @@ const DetailsPage = () => {
|
||||
<Typography component="h2" mb={theme.gap.ml}>
|
||||
History
|
||||
</Typography>
|
||||
<BasicTable data={data} paginated={true} />
|
||||
<BasicTable data={data} paginated={true} reversed={true} />
|
||||
</Box>
|
||||
</Stack>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,205 +1,33 @@
|
||||
.monitors,
|
||||
.new-monitor {
|
||||
width: 70vw;
|
||||
}
|
||||
|
||||
.monitors {
|
||||
font-family: var(--env-var-font-familt-1);
|
||||
}
|
||||
|
||||
.monitors-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monitors-bar-title {
|
||||
.monitors h1.MuiTypography-root,
|
||||
.monitors h2.MuiTypography-root {
|
||||
color: var(--env-var-color-1);
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.monitors-stats {
|
||||
margin-top: var(--env-var-spacing-2);
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.monitors-gaps-medium {
|
||||
height: var(--env-var-spacing-2);
|
||||
}
|
||||
|
||||
.monitors-gaps-small-plus {
|
||||
height: var(--env-var-spacing-1-plus);
|
||||
}
|
||||
|
||||
.monitors-gaps-small {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.new-monitor-header {
|
||||
color: var(--env-var-color-1);
|
||||
font-size: var(--env-var-font-size-large-plus);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.service-check-list-title {
|
||||
color: var(--env-var-color-5);
|
||||
.monitors h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large-plus);
|
||||
}
|
||||
.monitors h2.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
}
|
||||
.monitors .MuiStack-root > button {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
.service-check-list-desc {
|
||||
color: var(--env-var-color-25);
|
||||
font-size: var(--env-var-font-size-small);
|
||||
}
|
||||
|
||||
.host-status-box-text {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
.monitors-dropdown-holder {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
#ports-list {
|
||||
width: 172.92px;
|
||||
height: 12px;
|
||||
font-size: var(--env-var-font-size-small);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.incident-notif-config-title,
|
||||
.adv-setting-title {
|
||||
color: var(--env-var-color-5);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
.adv-setting-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.config-box-inputs,
|
||||
.service-check-list,
|
||||
.incident-notif-config,
|
||||
.advanced-setting-config,
|
||||
.proxy-setting-config {
|
||||
min-width: 350px;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.monitors-create-button-holder {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
#create-new-monitor-btn {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
#ports-dropdown,
|
||||
#freq-dropdown {
|
||||
width: 226.8px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* TODO */
|
||||
.current-monitors {
|
||||
margin-top: var(--env-var-spacing-2);
|
||||
border: 1px solid #eaecf0;
|
||||
padding: 40px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.current-monitors-title-holder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.current-monitors-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.current-monitors-title {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
margin-right: var(--env-var-spacing-1);
|
||||
color: var(--env-var-color-1);
|
||||
height: var(--env-var-height-2);
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.current-monitors-counter {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2px;
|
||||
min-width: 22px;
|
||||
min-height: 22px;
|
||||
padding: 5px;
|
||||
min-width: 25px;
|
||||
min-height: 25px;
|
||||
background-color: var(--env-var-color-15);
|
||||
border: 1px solid var(--env-var-color-6);
|
||||
border-radius: 50%;
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-weight: 500;
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
|
||||
.monitors-v-gaping {
|
||||
height: var(--env-var-spacing-3);
|
||||
}
|
||||
|
||||
.current-monitors-table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.current-monitors-table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid var(--env-var-color-16);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
}
|
||||
|
||||
.current-monitors-table th,
|
||||
.current-monitors-table td {
|
||||
border: 1px solid var(--env-var-color-16);
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.theader-row {
|
||||
background-color: var(--env-var-color-13);
|
||||
color: var(--env-var-color-2);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.theader-row td {
|
||||
padding: var(--env-var-spacing-1) var(--env-var-spacing-2);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
.host-list-item-precentage {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
.tbody-row td {
|
||||
padding: var(--env-var-spacing-2) var(--env-var-img-width-2);
|
||||
}
|
||||
|
||||
.host-status-box {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
border: 1px solid var(--env-var-color-4);
|
||||
border-radius: var(--env-var-radius-2);
|
||||
align-items: center;
|
||||
padding: calc(var(--env-var-spacing-1) / 2);
|
||||
}
|
||||
|
||||
.host-actions {
|
||||
margin: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tbody-row-cell.actions-cell {
|
||||
padding-left: 30px;
|
||||
margin-left: 10px;
|
||||
line-height: 0.8;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import "./index.css";
|
||||
import { useEffect } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { getMonitorsByUserId } from "../../Features/Monitors/monitorsSlice";
|
||||
import { getUptimeMonitorsByUserId } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Button from "../../Components/Button";
|
||||
import ServerStatus from "../../Components/Charts/Servers/ServerStatus";
|
||||
@@ -11,6 +11,7 @@ import OpenInNewPage from "../../assets/icons/open-in-new-page.svg?react";
|
||||
import BasicTable from "../../Components/BasicTable";
|
||||
import { StatusLabel } from "../../Components/Label";
|
||||
import ResponseTimeChart from "../../Components/Charts/ResponseTimeChart";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
|
||||
/**
|
||||
* Host component.
|
||||
@@ -44,12 +45,12 @@ const Host = ({ params }) => {
|
||||
const Monitors = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const monitorState = useSelector((state) => state.monitors);
|
||||
const monitorState = useSelector((state) => state.uptimeMonitors);
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getMonitorsByUserId(authState.authToken));
|
||||
dispatch(getUptimeMonitorsByUserId(authState.authToken));
|
||||
}, []);
|
||||
|
||||
const up = monitorState.monitors.reduce((acc, cur) => {
|
||||
@@ -73,7 +74,8 @@ const Monitors = () => {
|
||||
),
|
||||
},
|
||||
{ id: 3, name: "Response Time" },
|
||||
{ id: 4, name: "Actions" },
|
||||
{ id: 4, name: "Type" },
|
||||
{ id: 5, name: "Actions" },
|
||||
],
|
||||
rows: [],
|
||||
};
|
||||
@@ -87,17 +89,12 @@ const Monitors = () => {
|
||||
monitor.status === true
|
||||
? "var(--env-var-color-17)"
|
||||
: "var(--env-var-color-19)",
|
||||
status: monitor.status === true ? "Up" : "Down",
|
||||
backgroundColor:
|
||||
monitor.status === true
|
||||
? "var(--env-var-color-20)"
|
||||
: "var(--env-var-color-21)",
|
||||
statusDotColor:
|
||||
monitor.status === true
|
||||
? "var(--env-var-color-17)"
|
||||
: "var(--env-var-color-19)",
|
||||
status: monitor.status === true ? "up" : "down",
|
||||
};
|
||||
|
||||
// Reverse checks so latest check is on the right
|
||||
const reversedChecks = monitor.checks.slice().reverse();
|
||||
|
||||
return {
|
||||
id: monitor._id,
|
||||
handleClick: () => navigate(`/monitors/${monitor._id}`),
|
||||
@@ -108,62 +105,60 @@ const Monitors = () => {
|
||||
data: (
|
||||
<StatusLabel
|
||||
status={params.status}
|
||||
dot={params.statusDotColor}
|
||||
customStyles={{
|
||||
backgroundColor: params.backgroundColor,
|
||||
}}
|
||||
text={params.status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ id: idx + 2, data: <ResponseTimeChart checks={monitor.checks} /> },
|
||||
{ id: idx + 3, data: "TODO" },
|
||||
{ id: idx + 2, data: <ResponseTimeChart checks={reversedChecks} /> },
|
||||
{ id: idx + 3, data: monitor.type },
|
||||
{ id: idx + 4, data: "TODO" },
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="monitors"
|
||||
style={{
|
||||
padding: `${theme.content.pY} ${theme.content.pX}`,
|
||||
}}
|
||||
>
|
||||
<div className="monitors-bar">
|
||||
<div className="monitors-bar-title">
|
||||
Hello, {authState.user.firstname}
|
||||
</div>
|
||||
<Stack className="monitors" gap={theme.gap.large}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography component="h1">
|
||||
Hello, {authState.user.firstName}
|
||||
</Typography>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Create new monitor"
|
||||
onClick={() => {
|
||||
navigate("/monitors/create");
|
||||
}}
|
||||
sx={{ padding: "6px 25px", fontSize: "13px" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="monitors-stats">
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={theme.gap.large}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<ServerStatus title="Up" value={up} state="up" />
|
||||
<ServerStatus title="Down" value={down} state="down" />
|
||||
<ServerStatus title="Paused" value={0} state="pause" />
|
||||
</div>
|
||||
|
||||
<div className="current-monitors">
|
||||
<div className="current-monitors-bar">
|
||||
<div className="current-monitors-title-holder">
|
||||
<div className="current-monitors-title">Current monitors</div>
|
||||
<div className="current-monitors-counter">
|
||||
{monitorState.monitors.length}
|
||||
</div>
|
||||
</div>
|
||||
<div className="current-monitors-search-bar">
|
||||
{/* TODO - add search bar */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="monitors-v-gaping" />
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={theme.gap.large}
|
||||
p={theme.gap.xl}
|
||||
sx={{
|
||||
border: `solid 1px ${theme.palette.otherColors.graishWhite}`,
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
backgroundColor: theme.palette.otherColors.white,
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Typography component="h2">Current monitors</Typography>
|
||||
<Box className="current-monitors-counter">
|
||||
{monitorState.monitors.length}
|
||||
</Box>
|
||||
{/* TODO - add search bar */}
|
||||
</Stack>
|
||||
<BasicTable data={data} paginated={true} />
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1 +1,15 @@
|
||||
/*Notfoundpage styles*/
|
||||
.not-found-page {
|
||||
height: 100vh;
|
||||
}
|
||||
.not-found-page button,
|
||||
.not-found-page p {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.not-found-page h1 {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 600;
|
||||
}
|
||||
.not-found-page h1,
|
||||
.not-found-page p {
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
@@ -1,10 +1,57 @@
|
||||
import React from 'react'
|
||||
import './index.css'
|
||||
import Button from "../../Components/Button";
|
||||
import "./index.css";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
const NotFound = () => {
|
||||
return (
|
||||
<div>NotFoundPage</div>
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Support for defaultProps will be removed from function components in a future major release
|
||||
* So That why we're using JavaScript default parameters instead.
|
||||
*/
|
||||
const DefaultValue = {
|
||||
title: "We cannot find this page",
|
||||
desc: "Either the URL doesn’t exist, or you don’t have access to it.",
|
||||
};
|
||||
|
||||
export default NotFound
|
||||
/**
|
||||
* Renders an error page component with a customizable title and description.
|
||||
*
|
||||
* @component
|
||||
* @example
|
||||
* // Usage:
|
||||
* <ErrorPage
|
||||
* title="Page Not Found"
|
||||
* desc="The requested page could not be found."
|
||||
* />
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string} [props.title="We cannot find this page"] - The title of the error page.
|
||||
* @param {string} [props.desc="Either the URL doesn’t exist, or you don’t have access to it."] - The description of the error page.
|
||||
* @returns {JSX.Element} The rendered error page component.
|
||||
*/
|
||||
const NotFound = ({ title = DefaultValue.title, desc = DefaultValue.desc }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Stack className="not-found-page" justifyContent="center">
|
||||
<Stack gap="20px" alignItems="center">
|
||||
<Typography component="h1">{title}</Typography>
|
||||
<Typography>{desc}</Typography>
|
||||
<Button
|
||||
label="Go to the main dashboard"
|
||||
level="primary"
|
||||
sx={{ mt: "24px" }}
|
||||
onClick={() => navigate("/")}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
NotFound.propTypes = {
|
||||
title: PropTypes.string,
|
||||
desc: PropTypes.string,
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
@@ -0,0 +1,63 @@
|
||||
.create-page-speed form {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
margin-top: var(--env-var-default-2);
|
||||
background-color: var(--env-var-color-8);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
box-shadow: var(--env-var-shadow-1);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
padding: var(--env-var-default-2);
|
||||
}
|
||||
.create-page-speed h1.MuiTypography-root,
|
||||
.create-page-speed h2.MuiTypography-root,
|
||||
.create-page-speed p.MuiTypography-root,
|
||||
.create-page-speed h2.MuiTypography-root > span.MuiTypography-root {
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
.create-page-speed h1.MuiTypography-root,
|
||||
.create-page-speed h2.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
}
|
||||
.create-page-speed h2.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium-plus);
|
||||
}
|
||||
.create-page-speed p.MuiTypography-root,
|
||||
.create-page-speed button,
|
||||
.create-page-speed h2.MuiTypography-root > span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.create-page-speed .MuiBox-root > .field + p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
.create-page-speed .MuiBox-root > .field + p.MuiTypography-root,
|
||||
.create-page-speed h2.MuiTypography-root > span.MuiTypography-root {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.create-page-speed .checkbox-wrapper {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
/* to be removed */
|
||||
.create-page-speed .section-disabled {
|
||||
opacity: 0.4;
|
||||
background-color: #f1f1f2;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.create-page-speed button:not(.MuiIconButton-root) {
|
||||
height: var(--env-var-height-2);
|
||||
}
|
||||
.create-page-speed .MuiIconButton-root {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: var(--env-var-spacing-1);
|
||||
}
|
||||
|
||||
body:has(.create-page-speed) .select-dropdown .MuiMenuItem-root,
|
||||
.create-page-speed .select-wrapper .select-component > .MuiSelect-select {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import { Box, IconButton, Stack, Typography } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router";
|
||||
import CloseRoundedIcon from "@mui/icons-material/CloseRounded";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Button from "../../../Components/Button";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { createPageSpeed } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import "./index.css";
|
||||
|
||||
const CreatePageSpeed = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
|
||||
const frequencies = [
|
||||
{ _id: 1, name: "1 minute" },
|
||||
{ _id: 2, name: "2 minutes" },
|
||||
{ _id: 3, name: "3 minutes" },
|
||||
{ _id: 4, name: "4 minutes" },
|
||||
{ _id: 5, name: "5 minutes" },
|
||||
];
|
||||
|
||||
const [form, setForm] = useState({
|
||||
name: "",
|
||||
url: "",
|
||||
interval: 1,
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const handleChange = (event, id) => {
|
||||
const { value } = event.target;
|
||||
setForm((prev) => ({ ...prev, [id]: value }));
|
||||
|
||||
const { error } = monitorValidation.validate(
|
||||
{ [id]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev };
|
||||
if (error) newErrors[id] = error.details[0].message;
|
||||
else delete newErrors[id];
|
||||
return newErrors;
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreate = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
let monitor = {
|
||||
url: "http://" + form.url,
|
||||
name: form.name === "" ? form.url : form.name,
|
||||
};
|
||||
|
||||
const { error } = monitorValidation.validate(form, { abortEarly: false });
|
||||
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({ body: "Error validating data." });
|
||||
} else {
|
||||
monitor = {
|
||||
...monitor,
|
||||
description: monitor.name,
|
||||
userId: user._id,
|
||||
interval: form.interval * MS_PER_MINUTE,
|
||||
type: "pagespeed",
|
||||
};
|
||||
try {
|
||||
const action = await dispatch(createPageSpeed({ authToken, monitor }));
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
navigate("/page-speed");
|
||||
}
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body:
|
||||
error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Unknown error.",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="create-page-speed">
|
||||
<form
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
onSubmit={handleCreate}
|
||||
style={{ display: "flex", flexDirection: "column", gap: theme.gap.xl }}
|
||||
>
|
||||
<IconButton
|
||||
aria-label="close modal"
|
||||
onClick={() => navigate("/page-speed")}
|
||||
sx={{
|
||||
p: "5px",
|
||||
opacity: 0.6,
|
||||
"&:focus": { outline: "none" },
|
||||
}}
|
||||
>
|
||||
<CloseRoundedIcon />
|
||||
</IconButton>
|
||||
<Stack gap={theme.gap.large}>
|
||||
<Typography component="h1">Create a page speed monitor</Typography>
|
||||
<Field
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
label="Monitor friendly name"
|
||||
placeholder="Example monitor"
|
||||
isOptional={true}
|
||||
value={form.name}
|
||||
onChange={(event) => handleChange(event, "name")}
|
||||
error={errors.name}
|
||||
/>
|
||||
<Field
|
||||
type="url"
|
||||
id="monitor-url"
|
||||
label="URL"
|
||||
placeholder="random.website.com"
|
||||
value={form.url}
|
||||
onChange={(event) => handleChange(event, "url")}
|
||||
error={errors.url}
|
||||
/>
|
||||
<Select
|
||||
id="monitor-frequency"
|
||||
label="Check frequency"
|
||||
items={frequencies}
|
||||
value={form.interval}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack gap={theme.gap.small}>
|
||||
<Typography component="h2">
|
||||
Incidents notifications{" "}
|
||||
<Typography component="span">(coming soon)</Typography>
|
||||
</Typography>
|
||||
<Box className="section-disabled">
|
||||
<Typography mb={theme.gap.small}>
|
||||
When there is a new incident,
|
||||
</Typography>
|
||||
<Checkbox
|
||||
id="notify-sms"
|
||||
label="Notify via SMS (coming soon)"
|
||||
isChecked={false}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email"
|
||||
label="Notify via email (to gorkem.cetin@bluewavelabs.ca)"
|
||||
isChecked={false}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-emails"
|
||||
label="Notify via email to following emails"
|
||||
isChecked={false}
|
||||
/>
|
||||
<Box mx={`calc(${theme.gap.ml} * 2)`}>
|
||||
<Field
|
||||
id="notify-emails-list"
|
||||
placeholder="notifications@gmail.com"
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
error=""
|
||||
/>
|
||||
<Typography mt={theme.gap.small}>
|
||||
You can separate multiple emails with a comma
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="flex-end" gap={theme.gap.small}>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Cancel"
|
||||
onClick={() => navigate("/page-speed")}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
level="primary"
|
||||
label="Create"
|
||||
onClick={handleCreate}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatePageSpeed;
|
||||
@@ -0,0 +1,33 @@
|
||||
.page-speed h1.MuiTypography-root,
|
||||
.page-speed span.MuiTypography-root,
|
||||
.page-speed p.MuiTypography-root,
|
||||
.page-speed .label {
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
.page-speed h2.MuiTypography-root {
|
||||
color: var(--env-var-color-3);
|
||||
line-height: 1.1;
|
||||
}
|
||||
.page-speed h1.MuiTypography-root,
|
||||
.page-speed h2.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 600;
|
||||
}
|
||||
.page-speed p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.page-speed p:has(> span.MuiTypography-root),
|
||||
.page-speed p.MuiTypography-root > span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
.page-speed .MuiGrid-item > .MuiStack-root {
|
||||
border: solid 1px var(--env-var-color-6);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
background-color: var(--env-var-color-8);
|
||||
}
|
||||
.page-speed .label {
|
||||
padding: 0 5px;
|
||||
height: 22px;
|
||||
font-size: var(--env-var-font-size-small);
|
||||
border-color: var(--env-var-color-29);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Box, Grid, Stack, Typography } from "@mui/material";
|
||||
import { useEffect } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { formatDate, formatDurationRounded } from "../../Utils/timeUtils";
|
||||
import { StatusLabel } from "../../Components/Label";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getPageSpeedByUserId } from "../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import PageSpeedIcon from "../../assets/icons/page-speed.svg?react";
|
||||
import Fallback from "../../Components/Fallback";
|
||||
import "./index.css";
|
||||
|
||||
const Card = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
/**
|
||||
* Helper function to get duration since last check or the last date checked
|
||||
* @param {Array} checks Array of check objects.
|
||||
* @param {boolean} duration Whether the function should return the duration since last checked or the date itself
|
||||
* @returns {number} Timestamp of the most recent check.
|
||||
*/
|
||||
const getLastChecked = (checks, duration = true) => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0; // Handle case when no checks are available
|
||||
}
|
||||
|
||||
// Data is sorted newest -> oldest, so newest check is the most recent
|
||||
if (!duration) {
|
||||
return new Date(checks[0].createdAt);
|
||||
}
|
||||
return new Date() - new Date(checks[0].createdAt);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid item lg={6} flexGrow={1}>
|
||||
<Stack direction="row" gap={theme.gap.medium} p={theme.gap.ml}>
|
||||
<PageSpeedIcon style={{ width: theme.gap.ml, height: theme.gap.ml }} />
|
||||
<Box flex={1}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography component="h2" mb={theme.gap.xs}>
|
||||
{data.name}
|
||||
</Typography>
|
||||
<StatusLabel
|
||||
status={data.status ? "up" : "cannot resolve"}
|
||||
text={data.status ? "Live (collecting data)" : "Inactive"}
|
||||
/>
|
||||
</Stack>
|
||||
<Typography>{data.url.replace(/^https?:\/\//, "")}</Typography>
|
||||
<Typography mt={theme.gap.large}>
|
||||
<Typography component="span" fontWeight={600}>
|
||||
Last checked:{" "}
|
||||
</Typography>
|
||||
{formatDate(getLastChecked(data.checks, false))}{" "}
|
||||
<Typography component="span" fontStyle="italic">
|
||||
({formatDurationRounded(getLastChecked(data.checks))} ago)
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const PageSpeed = () => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const { monitors } = useSelector((state) => state.pageSpeedMonitors);
|
||||
useEffect(() => {
|
||||
dispatch(getPageSpeedByUserId(authToken));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box className="page-speed">
|
||||
{monitors ? (
|
||||
<Stack gap={theme.gap.xs}>
|
||||
<Typography component="h1">All page speed monitors</Typography>
|
||||
<Typography mb={theme.gap.large}>
|
||||
Click on one of the monitors to get more site speed information.
|
||||
</Typography>
|
||||
<Grid container spacing={theme.gap.large}>
|
||||
{monitors?.map((monitor) => (
|
||||
<Card data={monitor} key={`monitor-${monitor._id}`} />
|
||||
))}
|
||||
</Grid>
|
||||
</Stack>
|
||||
) : (
|
||||
<Fallback
|
||||
title="page speed"
|
||||
checks={[
|
||||
"Report on the user experience of a page",
|
||||
"Help analyze webpage speed",
|
||||
"Give suggestions on how the page can be improved",
|
||||
]}
|
||||
link="/page-speed/create"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageSpeed;
|
||||
@@ -0,0 +1,42 @@
|
||||
.settings a.MuiTypography-root,
|
||||
.settings h2.MuiTypography-root,
|
||||
.settings span.MuiTypography-root,
|
||||
.settings button,
|
||||
.settings p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.settings h1.MuiTypography-root,
|
||||
.settings h2.MuiTypography-root,
|
||||
.settings p.MuiTypography-root {
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
.settings h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
}
|
||||
.settings h1.MuiTypography-root,
|
||||
.settings h2.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
}
|
||||
.settings button {
|
||||
height: 34px;
|
||||
}
|
||||
.settings span.MuiTypography-root {
|
||||
opacity: 0.6;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.settings .config-box {
|
||||
padding: var(--env-var-spacing-4) 50px;
|
||||
padding-bottom: 60px;
|
||||
border: 1px solid var(--env-var-color-16);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
}
|
||||
.settings .config-box .MuiBox-root,
|
||||
.settings .config-box .MuiStack-root {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--env-var-spacing-4);
|
||||
}
|
||||
@@ -1,7 +1,120 @@
|
||||
import React from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import Button from "../../Components/Button";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import Link from "../../Components/Link";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
const Settings = () => {
|
||||
return <div>Settings</div>;
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="settings"
|
||||
style={{
|
||||
paddingBottom: 0,
|
||||
}}
|
||||
>
|
||||
<form className="settings-form" noValidate spellCheck="false">
|
||||
<Stack
|
||||
className="config-box"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.xxl}
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">General Settings</Typography>
|
||||
<Typography sx={{ mt: theme.gap.small, mb: theme.gap.xs }}>
|
||||
<Typography component="span">Display timezone</Typography>- The
|
||||
timezone of the dashboard you publicly display.
|
||||
</Typography>
|
||||
<Typography>
|
||||
<Typography component="span">Server timezone</Typography>- The
|
||||
timezone of your server.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.xl}>
|
||||
<Select
|
||||
id="display-timezone"
|
||||
label="Display timezone"
|
||||
value="est"
|
||||
onChange={() => console.log("disabled")}
|
||||
items={[{ _id: "est", name: "America / Toronto" }]}
|
||||
/>
|
||||
<Select
|
||||
id="server-timezone"
|
||||
label="Server timezone"
|
||||
value="est"
|
||||
onChange={() => console.log("disabled")}
|
||||
items={[{ _id: "est", name: "America / Toronto" }]}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="config-box"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.xxl}
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">History and monitoring</Typography>
|
||||
<Typography sx={{ mt: theme.gap.small }}>
|
||||
Define here for how long you want to keep the data. You can also
|
||||
remove all past data.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.xl}>
|
||||
<Field
|
||||
type="text"
|
||||
id="history-monitoring"
|
||||
label="The days you want to keep monitoring history."
|
||||
isOptional={true}
|
||||
optionalLabel="0 for infinite"
|
||||
placeholder="90"
|
||||
value=""
|
||||
onChange={() => console.log("Disabled")}
|
||||
/>
|
||||
<Box>
|
||||
<Typography>Clear all stats. This is irreversible.</Typography>
|
||||
<Button
|
||||
level="error"
|
||||
label="Clear all stats"
|
||||
sx={{ mt: theme.gap.small }}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="config-box"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.xxl}
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">About</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography component="h2">Uptime Genie v1.0.0</Typography>
|
||||
<Typography
|
||||
sx={{ mt: theme.gap.xs, mb: theme.gap.medium, opacity: 0.6 }}
|
||||
>
|
||||
Developed by Bluewave Labs.
|
||||
</Typography>
|
||||
<Link
|
||||
level="secondary"
|
||||
url="https://github.com/bluewave-labs"
|
||||
label="https://github.com/bluewave-labs"
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Button level="primary" label="Save" />
|
||||
</Stack>
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -1,7 +1,21 @@
|
||||
import React from "react";
|
||||
import Fallback from "../../Components/Fallback";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const Status = () => {
|
||||
return <div>Status</div>;
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div className="status">
|
||||
<Fallback
|
||||
title="status page"
|
||||
checks={[
|
||||
"Share your uptime publicly",
|
||||
"Keep your users informed about incidents",
|
||||
"Build trust with your customers",
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Status;
|
||||
@@ -1,10 +0,0 @@
|
||||
const pathMap = {
|
||||
"/monitors": "Monitors",
|
||||
"/incidents": "Incidents",
|
||||
"/status": "Status Pages",
|
||||
"/integrations": "Integrations",
|
||||
"/maintenance": "maintenance",
|
||||
"/settings": "Settings",
|
||||
};
|
||||
|
||||
export default pathMap;
|
||||
@@ -26,6 +26,7 @@ const otherColorsWhite = "#fff";
|
||||
const otherColorsGraishWhiteLight = "#f2f2f2";
|
||||
const otherColorsStrongBlue = "#f2f2f2";
|
||||
const otherColorsSlateGray = "#667085";
|
||||
const otherColorsFillGray = "#f4f4f4";
|
||||
|
||||
//box shadow
|
||||
const shadow =
|
||||
@@ -74,6 +75,7 @@ const theme = createTheme({
|
||||
graishWhiteLight: otherColorsGraishWhiteLight,
|
||||
strongBlue: otherColorsStrongBlue,
|
||||
slateGray: otherColorsSlateGray,
|
||||
fillGray: otherColorsFillGray,
|
||||
},
|
||||
},
|
||||
font: {
|
||||
@@ -116,6 +118,24 @@ const theme = createTheme({
|
||||
border: "#fec84b",
|
||||
},
|
||||
},
|
||||
//add more as needed
|
||||
label: {
|
||||
up: {
|
||||
borderColor: sectionBorder,
|
||||
bgColor: "#ecfdf3",
|
||||
dotColor: "#17B26A",
|
||||
},
|
||||
down: {
|
||||
borderColor: sectionBorder,
|
||||
bgColor: "#f9eced",
|
||||
dotColor: labelRed,
|
||||
},
|
||||
"cannot resolve": {
|
||||
borderColor: sectionBorder,
|
||||
bgColor: "#f2f4f7",
|
||||
dotColor: labelGray,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default theme;
|
||||
@@ -0,0 +1,59 @@
|
||||
export const formatDuration = (ms) => {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
let dateStr = "";
|
||||
|
||||
days && (dateStr += `${days}d `);
|
||||
hours && (dateStr += `${hours % 24}h `);
|
||||
minutes && (dateStr += `${minutes % 60}m `);
|
||||
seconds && (dateStr += `${seconds % 60}s `);
|
||||
|
||||
dateStr === "" && (dateStr = "0s");
|
||||
|
||||
return dateStr;
|
||||
};
|
||||
|
||||
export const formatDurationRounded = (ms) => {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
let time = "";
|
||||
if (days > 0) {
|
||||
time += `${days} day${days !== 1 ? "s" : ""}`;
|
||||
return time;
|
||||
}
|
||||
if (hours > 0) {
|
||||
time += `${hours} hour${hours !== 1 ? "s" : ""}`;
|
||||
return time;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
time += `${minutes} minute${minutes !== 1 ? "s" : ""}`;
|
||||
return time;
|
||||
}
|
||||
if (seconds > 0) {
|
||||
time += `${seconds} second${seconds !== 1 ? "s" : ""}`;
|
||||
return time;
|
||||
}
|
||||
|
||||
return time;
|
||||
};
|
||||
|
||||
export const formatDate = (date) => {
|
||||
const options = {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
hour12: true,
|
||||
};
|
||||
|
||||
// Return the date using the specified options
|
||||
return date.toLocaleString("en-US", options);
|
||||
};
|
||||
@@ -43,8 +43,8 @@ const passwordSchema = joi
|
||||
});
|
||||
|
||||
const credentials = joi.object({
|
||||
firstname: nameSchema,
|
||||
lastname: nameSchema,
|
||||
firstName: nameSchema,
|
||||
lastName: nameSchema,
|
||||
email: joi
|
||||
.string()
|
||||
.trim()
|
||||
@@ -68,26 +68,28 @@ const credentials = joi.object({
|
||||
}
|
||||
return value;
|
||||
}),
|
||||
role: joi.string().messages({
|
||||
"string.empty": "Role is required",
|
||||
}),
|
||||
role: joi.array(),
|
||||
});
|
||||
|
||||
const createMonitorValidation = joi.object({
|
||||
const monitorValidation = joi.object({
|
||||
url: joi
|
||||
.string()
|
||||
.uri({ allowRelative: true })
|
||||
.trim()
|
||||
.messages({ "string.empty": "*This field is required." }),
|
||||
.messages({
|
||||
"string.empty": "This field is required.",
|
||||
"string.uri": "The URL you provided is not valid.",
|
||||
}),
|
||||
name: joi.string().trim().max(50).allow("").messages({
|
||||
"string.max": "*This field should not exceed the 50 characters limit.",
|
||||
"string.max": "This field should not exceed the 50 characters limit.",
|
||||
}),
|
||||
type: joi
|
||||
.string()
|
||||
.trim()
|
||||
.messages({ "string.empty": "*This field is required." }),
|
||||
frequency: joi.number().messages({
|
||||
"number.base": "*Frequency must be a number.",
|
||||
"any.required": "*Frequency is required.",
|
||||
.messages({ "string.empty": "This field is required." }),
|
||||
interval: joi.number().messages({
|
||||
"number.base": "Frequency must be a number.",
|
||||
"any.required": "Frequency is required.",
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -106,4 +108,4 @@ const imageValidation = joi.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export { credentials, imageValidation, createMonitorValidation };
|
||||
export { credentials, imageValidation, monitorValidation };
|
||||
|
Before Width: | Height: | Size: 170 B |
|
Before Width: | Height: | Size: 171 B |
|
Before Width: | Height: | Size: 633 B |
|
Before Width: | Height: | Size: 744 B |
|
Before Width: | Height: | Size: 897 B |
|
Before Width: | Height: | Size: 375 B |
|
Before Width: | Height: | Size: 495 B |
|
Before Width: | Height: | Size: 761 B |
|
Before Width: | Height: | Size: 863 B |
|
Before Width: | Height: | Size: 718 B |
|
Before Width: | Height: | Size: 878 B |
|
Before Width: | Height: | Size: 462 B |
@@ -2,4 +2,4 @@
|
||||
<rect width="353" height="52.4582" fill="white"/>
|
||||
<path d="M1.67676 4.59633H11.2078C12.5021 4.59633 13.767 4.74621 15.0025 5.04598C16.238 5.34574 17.3411 5.84534 18.3119 6.54478C19.2826 7.21093 20.0622 8.09355 20.6505 9.19267C21.2683 10.2918 21.5771 11.6574 21.5771 13.2894C21.5771 15.3544 21.077 17.0697 20.0769 18.4353C19.1061 19.7676 17.7824 20.7168 16.1056 21.283V21.383C17.1058 21.4829 18.0324 21.7826 18.8855 22.2822C19.768 22.7485 20.5181 23.3647 21.1359 24.1308C21.783 24.8968 22.2831 25.7962 22.6361 26.8286C22.9891 27.8278 23.1656 28.9102 23.1656 30.076C23.1656 32.0412 22.7978 33.6564 22.0625 34.9222C21.3565 36.1879 20.4152 37.2036 19.2385 37.9697C18.0913 38.7024 16.7822 39.2187 15.3114 39.5185C13.87 39.8183 12.4285 39.9682 10.9871 39.9682H1.67676V4.59633ZM5.38326 19.5844H10.5018C11.3843 19.5844 12.2373 19.5178 13.061 19.3846C13.9141 19.2513 14.6789 18.9849 15.3555 18.5852C16.0321 18.1522 16.5763 17.5693 16.9881 16.8366C17.4 16.0705 17.6059 15.0713 17.6059 13.839C17.6059 12.7398 17.4147 11.8572 17.0323 11.1911C16.6498 10.4916 16.135 9.94207 15.4879 9.54239C14.8701 9.14271 14.1494 8.87625 13.3258 8.74303C12.5315 8.57649 11.7225 8.49323 10.8989 8.49323H5.38326V19.5844ZM5.38326 36.0713H11.649C12.5609 36.0713 13.4581 35.9713 14.3406 35.7715C15.2525 35.5384 16.0615 35.1887 16.7675 34.7223C17.5029 34.2227 18.0913 33.59 18.5325 32.8238C18.9738 32.0245 19.1944 31.0585 19.1944 29.9262C19.1944 28.6939 18.9738 27.6612 18.5325 26.8286C18.1207 25.996 17.5618 25.3298 16.8558 24.8302C16.1792 24.3306 15.3849 23.9809 14.473 23.781C13.5905 23.5812 12.6786 23.4813 11.7373 23.4813H5.38326V36.0713ZM29.1616 2.19824H32.6033V39.9682H29.1616V2.19824ZM57.4558 34.4226C57.4558 35.4885 57.4854 36.4877 57.5441 37.4202C57.603 38.3527 57.6323 39.202 57.6323 39.9682H54.3671C54.3671 39.3354 54.3525 38.7024 54.323 38.0697C54.2936 37.4369 54.2788 36.7874 54.2788 36.1212H54.1906C53.9554 36.6875 53.617 37.2371 53.1757 37.7699C52.764 38.3027 52.2639 38.7858 51.6755 39.2187C51.0873 39.6184 50.4254 39.9349 49.6898 40.168C48.9545 40.4345 48.1602 40.5677 47.3071 40.5677C45.954 40.5677 44.7774 40.3346 43.7771 39.8682C42.8063 39.4019 41.9975 38.7524 41.3502 37.9198C40.7031 37.0872 40.2177 36.088 39.8941 34.9222C39.6 33.7231 39.4528 32.4242 39.4528 31.0253V16.5868H42.8946V30.6256C42.8946 32.5908 43.2771 34.1395 44.0418 35.2719C44.8067 36.4042 45.9981 36.9705 47.616 36.9705C48.7339 36.9705 49.6898 36.754 50.4841 36.3211C51.3079 35.8882 51.9698 35.2719 52.4697 34.4725C52.9992 33.6732 53.3818 32.7239 53.617 31.6248C53.8817 30.5257 54.0141 29.3099 54.0141 27.9777V16.5868H57.4558V34.4226ZM82.4158 35.7215C81.1803 37.5201 79.8272 38.7858 78.3563 39.5185C76.8856 40.218 75.1793 40.5677 73.2378 40.5677C71.62 40.5677 70.1638 40.2512 68.8694 39.6184C67.5752 38.9522 66.4721 38.0697 65.56 36.9705C64.6775 35.8714 63.9863 34.5724 63.4862 33.0736C63.0156 31.5748 62.7802 29.9761 62.7802 28.2775C62.7802 26.4789 63.0449 24.8302 63.5744 23.3314C64.1039 21.7993 64.8395 20.5003 65.7807 19.4345C66.7221 18.3354 67.84 17.4861 69.1342 16.8865C70.4286 16.287 71.8406 15.9873 73.3702 15.9873C74.8117 15.9873 76.1355 16.2704 77.3414 16.8366C78.5476 17.3695 79.5772 18.1688 80.4302 19.2347C81.3127 20.2672 81.9893 21.5495 82.4599 23.0816C82.9307 24.5804 83.1659 26.2958 83.1659 28.2275V29.4765H66.4867C66.5456 30.4757 66.7514 31.4417 67.1044 32.3742C67.487 33.2735 67.9724 34.0728 68.5605 34.7723C69.149 35.4385 69.8402 35.9713 70.6344 36.371C71.4287 36.7707 72.2966 36.9705 73.2378 36.9705C74.738 36.9705 76.0031 36.6708 77.0326 36.0713C78.0622 35.4717 78.9888 34.5892 79.8124 33.4234L82.4158 35.7215ZM79.4594 26.1791C79.4007 24.1807 78.8271 22.582 77.7386 21.383C76.6502 20.1839 75.15 19.5844 73.2378 19.5844C71.3259 19.5844 69.7815 20.1839 68.6047 21.383C67.4281 22.582 66.7221 24.1807 66.4867 26.1791H79.4594ZM85.4898 4.59633H89.5934L96.6975 33.9729H96.7858L104.464 4.59633H108.611L116.245 33.9729H116.333L123.57 4.59633H127.453L118.275 39.9682H114.259L106.537 9.99204H106.449L98.6831 39.9682H94.6236L85.4898 4.59633ZM129.076 19.5844C130.135 18.352 131.385 17.4528 132.826 16.8865C134.297 16.287 135.783 15.9873 137.283 15.9873C140.342 15.9873 142.548 16.8033 143.902 18.4353C145.255 20.0673 145.931 22.5487 145.931 25.8794V35.9214C145.931 36.5876 145.961 37.287 146.02 38.0197C146.078 38.7191 146.152 39.3686 146.24 39.9682H142.931C142.813 39.4353 142.74 38.8523 142.71 38.2195C142.71 37.5533 142.71 36.9705 142.71 36.4709H142.622C141.946 37.67 141.033 38.6524 139.886 39.4186C138.768 40.1847 137.415 40.5677 135.827 40.5677C134.768 40.5677 133.768 40.4178 132.826 40.118C131.914 39.8183 131.105 39.3854 130.399 38.8191C129.723 38.2195 129.179 37.4869 128.767 36.6208C128.355 35.7548 128.149 34.7556 128.149 33.6232C128.149 31.6915 128.59 30.176 129.473 29.0768C130.355 27.9777 131.444 27.1616 132.738 26.6288C134.062 26.096 135.474 25.7627 136.974 25.6296C138.504 25.4964 139.901 25.4297 141.166 25.4297H142.49V24.7303C142.49 23.0316 142.034 21.7493 141.122 20.8834C140.239 20.0174 138.989 19.5844 137.371 19.5844C136.253 19.5844 135.15 19.8009 134.062 20.2339Line truncated
|
||||
<path d="M234.357 26.4788C234.357 28.4105 234.136 30.2425 233.695 31.9744C233.282 33.7062 232.605 35.2385 231.665 36.5707C230.752 37.8697 229.547 38.9188 228.047 39.7182C226.547 40.4843 224.722 40.8673 222.575 40.8673C220.427 40.8673 218.604 40.4843 217.103 39.7182C215.603 38.9188 214.382 37.8697 213.441 36.5707C212.529 35.2385 211.852 33.7062 211.411 31.9744C210.999 30.2425 210.793 28.4105 210.793 26.4788V4.59619H214.5V25.8792C214.5 27.3113 214.632 28.6937 214.897 30.0259C215.162 31.3581 215.603 32.5407 216.221 33.5731C216.839 34.6055 217.662 35.4384 218.692 36.0711C219.721 36.6706 221.015 36.9704 222.575 36.9704C224.133 36.9704 225.428 36.6706 226.458 36.0711C227.486 35.4384 228.312 34.6055 228.929 33.5731C229.547 32.5407 229.988 31.3581 230.253 30.0259C230.518 28.6937 230.65 27.3113 230.65 25.8792V4.59619H234.357V26.4788ZM241.2 16.5866H244.642V19.9839H244.73C245.67 18.7183 246.76 17.7357 247.996 17.0363C249.26 16.3368 250.643 15.9871 252.143 15.9871C253.761 15.9871 255.232 16.3035 256.556 16.9364C257.908 17.5359 259.056 18.3852 259.998 19.4843C260.938 20.5835 261.674 21.8824 262.204 23.3812C262.733 24.88 262.998 26.512 262.998 28.2773C262.998 30.0427 262.733 31.6746 262.204 33.1734C261.674 34.6722 260.938 35.9712 259.998 37.0703C259.084 38.1694 257.997 39.0355 256.732 39.6682C255.466 40.2678 254.114 40.5675 252.673 40.5675C250.82 40.5675 249.187 40.1179 247.775 39.2186C246.392 38.2861 245.377 37.1535 244.73 35.8213H244.642V51.3589H241.2V16.5866ZM244.642 28.2773C244.642 29.5096 244.803 30.6587 245.128 31.7246C245.481 32.757 245.979 33.673 246.628 34.4724C247.274 35.2385 248.04 35.8545 248.922 36.3209C249.833 36.7538 250.848 36.9704 251.967 36.9704C253.083 36.9704 254.085 36.7538 254.967 36.3209C255.879 35.8545 256.657 35.2385 257.306 34.4724C257.952 33.673 258.438 32.757 258.762 31.7246C259.115 30.6587 259.292 29.5096 259.292 28.2773C259.292 27.0451 259.115 25.9125 258.762 24.88C258.438 23.8142 257.952 22.8983 257.306 22.1322C256.657 21.3329 255.879 20.7167 254.967 20.2837C254.085 19.8174 253.083 19.5842 251.967 19.5842C250.848 19.5842 249.833 19.8174 248.922 20.2837C248.04 20.7167 247.274 21.3329 246.628 22.1322C245.979 22.8983 245.481 23.8142 245.128 24.88C244.803 25.9125 244.642 27.0451 244.642 28.2773ZM279.647 19.884H273.557V31.5747C273.557 32.3408 273.573 33.0568 273.601 33.723C273.632 34.3557 273.749 34.922 273.954 35.4216C274.162 35.888 274.471 36.271 274.881 36.5707C275.294 36.8373 275.883 36.9704 276.646 36.9704C277.147 36.9704 277.661 36.9037 278.19 36.7706C278.72 36.6374 279.221 36.4376 279.691 36.171L279.823 39.7182C279.236 40.018 278.574 40.2345 277.837 40.3677C277.131 40.5008 276.456 40.5675 275.808 40.5675C274.572 40.5675 273.588 40.3844 272.851 40.018C272.117 39.6515 271.543 39.1354 271.13 38.4692C270.72 37.803 270.44 36.9871 270.292 36.0212C270.175 35.0219 270.116 33.9228 270.116 32.7238V19.884H265.615V16.5866H270.116V9.94193H273.557V16.5866H279.647V19.884ZM284.216 16.5866H287.657V39.968H284.216V16.5866ZM283.421 7.44392C283.421 6.67786 283.658 6.01174 284.127 5.44551C284.628 4.87929 285.231 4.59619 285.937 4.59619C286.643 4.59619 287.232 4.87929 287.702 5.44551C288.202 6.01174 288.452 6.67786 288.452 7.44392C288.452 8.27658 288.216 8.95939 287.746 9.49229C287.276 10.0252 286.671 10.2917 285.937 10.2917C285.202 10.2917 284.597 10.0252 284.127 9.49229C283.658 8.95939 283.421 8.27658 283.421 7.44392ZM294.508 22.1322C294.508 21.0664 294.477 20.0672 294.42 19.1346C294.36 18.202 294.331 17.3527 294.331 16.5866H297.597C297.597 17.2195 297.61 17.8523 297.641 18.4851C297.669 19.1179 297.685 19.7674 297.685 20.4336H297.773C298.007 19.8673 298.331 19.3178 298.744 18.7849C299.185 18.252 299.699 17.7857 300.288 17.386C300.875 16.953 301.537 16.6199 302.274 16.3868C303.009 16.1203 303.803 15.9871 304.657 15.9871C305.773 15.9871 306.7 16.1537 307.436 16.4867C308.2 16.7865 308.833 17.1695 309.334 17.6358C309.832 18.0688 310.216 18.5517 310.481 19.0846C310.775 19.5842 311.011 20.0339 311.187 20.4336C311.95 19.0347 312.833 17.9522 313.835 17.1862C314.834 16.3868 316.129 15.9871 317.718 15.9871C319.187 15.9871 320.422 16.2203 321.424 16.6866C322.424 17.1195 323.233 17.769 323.851 18.635C324.469 19.4677 324.91 20.5002 325.175 21.7325C325.439 22.9316 325.572 24.2805 325.572 25.7793V39.968H322.13V25.9792C322.13 25.1798 322.055 24.3971 321.909 23.631C321.762 22.865 321.497 22.1822 321.115 21.5827C320.731 20.9831 320.217 20.5002 319.571 20.1338C318.922 19.7674 318.099 19.5842 317.1 19.5842C316.363 19.5842 315.672 19.7508 315.026 20.0839C314.377 20.4169 313.804 20.8832 313.305 21.4827C312.833 22.0823 312.451 22.815 312.158 23.681C311.893 24.5136 311.761 25.4463 311.761 26.4788V39.968H308.319V26.7286C308.319 24.1306 308.01 22.2988 307.392 21.2329C306.775 20.1338 305.76 19.5842 304.348 19.5842C303.229 19.5842 302.258 19.8008 301.435 20.2337C300.641 20.6667 299.979 21.2829 299.45 22.0823C298.949 22.8816 298.567 23.8309 298.303 24.93C298.067 26.0291 297.95 27.2449 297.95 28.5771V39.968H294.508V22.1322ZM350.527 35.72Line truncated
|
||||
</svg>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg width="256" height="170" viewBox="0 0 256 170" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.390244" y="0.390244" width="255.22" height="168.585" rx="7.41463" fill="white"/>
|
||||
<rect x="0.390244" y="0.390244" width="255.22" height="168.585" rx="7.41463" stroke="#EBEBEB" stroke-width="0.780488"/>
|
||||
<rect x="17.9512" y="16.3901" width="220.098" height="54.6341" rx="3.12195" fill="#EFF2FF"/>
|
||||
<rect x="17.9512" y="78.8293" width="85.0732" height="9.36585" rx="3.12195" fill="#D9D9D9"/>
|
||||
<rect x="17.9512" y="93.6584" width="220.098" height="9.36585" rx="3.12195" fill="#F3F3F3"/>
|
||||
<rect x="17.9512" y="108.488" width="220.098" height="9.36585" rx="3.12195" fill="#F3F3F3"/>
|
||||
<g filter="url(#filter0_d_28_7483)">
|
||||
<g clip-path="url(#clip0_28_7483)">
|
||||
<rect x="174.049" y="129.561" width="64" height="20.2927" rx="3.12195" fill="#E0F2FE"/>
|
||||
</g>
|
||||
<rect x="174.439" y="129.951" width="63.2195" height="19.5122" rx="2.73171" stroke="#D5D9EB" stroke-width="0.780488"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_28_7483" x="172.049" y="128.561" width="68" height="24.2927" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_28_7483"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_28_7483" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_28_7483">
|
||||
<rect x="174.049" y="129.561" width="64" height="20.2927" rx="3.12195" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -1,9 +0,0 @@
|
||||
<svg width="256" height="136" viewBox="0 0 256 136" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.390244" y="0.390244" width="255.22" height="135.22" rx="7.41463" fill="white"/>
|
||||
<rect x="0.390244" y="0.390244" width="255.22" height="135.22" rx="7.41463" stroke="#EBEBEB" stroke-width="0.780488"/>
|
||||
<rect x="17.9512" y="16.3901" width="220.098" height="54.6341" rx="3.12195" fill="#EFF2FF"/>
|
||||
<rect x="17.9512" y="78.8293" width="85.0732" height="9.36585" rx="3.12195" fill="#D9D9D9"/>
|
||||
<rect x="111" y="79" width="127" height="9" rx="3.12195" fill="#F3F3F3"/>
|
||||
<rect width="85" height="9" rx="3.12195" transform="matrix(-1 0 0 1 238 98)" fill="#D9D9D9"/>
|
||||
<rect width="127" height="9" rx="3.12195" transform="matrix(-1 0 0 1 145 98)" fill="#F3F3F3"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 766 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.24935 10.0001L8.74935 12.5001L13.7493 7.50008M18.3327 10.0001C18.3327 14.6025 14.6017 18.3334 9.99935 18.3334C5.39698 18.3334 1.66602 14.6025 1.66602 10.0001C1.66602 5.39771 5.39698 1.66675 9.99935 1.66675C14.6017 1.66675 18.3327 5.39771 18.3327 10.0001Z" stroke="#1570EF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 454 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 6C0 2.68629 2.68629 0 6 0H14C17.3137 0 20 2.68629 20 6V14C20 17.3137 17.3137 20 14 20H6C2.68629 20 0 17.3137 0 14V6Z" fill="#1570EF"/>
|
||||
<path d="M14.6668 6.5L8.25016 12.9167L5.3335 10" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 380 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.5 6C0.5 2.96243 2.96243 0.5 6 0.5H14C17.0376 0.5 19.5 2.96243 19.5 6V14C19.5 17.0376 17.0376 19.5 14 19.5H6C2.96243 19.5 0.5 17.0376 0.5 14V6Z" stroke="#848b97"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 278 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.00004 5.33333V8M8.00004 10.6667H8.00671M14.6667 8C14.6667 11.6819 11.6819 14.6667 8.00004 14.6667C4.31814 14.6667 1.33337 11.6819 1.33337 8C1.33337 4.3181 4.31814 1.33333 8.00004 1.33333C11.6819 1.33333 14.6667 4.3181 14.6667 8Z" stroke="#F04438" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 434 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 8V12M12 16H12.01M2 8.52274V15.4773C2 15.7218 2 15.8441 2.02763 15.9592C2.05213 16.0613 2.09253 16.1588 2.14736 16.2483C2.2092 16.3492 2.29568 16.4357 2.46863 16.6086L7.39137 21.5314C7.56432 21.7043 7.6508 21.7908 7.75172 21.8526C7.84119 21.9075 7.93873 21.9479 8.04077 21.9724C8.15586 22 8.27815 22 8.52274 22H15.4773C15.7218 22 15.8441 22 15.9592 21.9724C16.0613 21.9479 16.1588 21.9075 16.2483 21.8526C16.3492 21.7908 16.4357 21.7043 16.6086 21.5314L21.5314 16.6086C21.7043 16.4357 21.7908 16.3492 21.8526 16.2483C21.9075 16.1588 21.9479 16.0613 21.9724 15.9592C22 15.8441 22 15.7218 22 15.4773V8.52274C22 8.27815 22 8.15586 21.9724 8.04077C21.9479 7.93873 21.9075 7.84119 21.8526 7.75172C21.7908 7.6508 21.7043 7.56432 21.5314 7.39137L16.6086 2.46863C16.4357 2.29568 16.3492 2.2092 16.2483 2.14736C16.1588 2.09253 16.0613 2.05213 15.9592 2.02763C15.8441 2 15.7218 2 15.4773 2H8.52274C8.27815 2 8.15586 2 8.04077 2.02763C7.93873 2.05213 7.84119 2.09253 7.75172 2.14736C7.6508 2.2092 7.56432 2.29568 7.39137 2.46863L2.46863 7.39137C2.29568 7.56432 2.2092 7.6508 2.14736 7.75172C2.09253 7.84119 2.05213 7.93873 2.02763 8.04077C2 8.15586 2 8.27815 2 8.52274Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0002 13C10.4297 13.5741 10.9776 14.0491 11.6067 14.3929C12.2359 14.7367 12.9317 14.9411 13.6468 14.9923C14.362 15.0435 15.0798 14.9403 15.7515 14.6897C16.4233 14.4392 17.0333 14.047 17.5402 13.54L20.5402 10.54C21.451 9.59695 21.955 8.33394 21.9436 7.02296C21.9322 5.71198 21.4063 4.45791 20.4793 3.53087C19.5523 2.60383 18.2982 2.07799 16.9872 2.0666C15.6762 2.0552 14.4132 2.55918 13.4702 3.46997L11.7502 5.17997M14.0002 11C13.5707 10.4258 13.0228 9.95078 12.3936 9.60703C11.7645 9.26327 11.0687 9.05885 10.3535 9.00763C9.63841 8.95641 8.92061 9.0596 8.24885 9.31018C7.5771 9.56077 6.96709 9.9529 6.4602 10.46L3.4602 13.46C2.54941 14.403 2.04544 15.666 2.05683 16.977C2.06822 18.288 2.59407 19.542 3.52111 20.4691C4.44815 21.3961 5.70221 21.9219 7.01319 21.9333C8.32418 21.9447 9.58719 21.4408 10.5302 20.53L12.2402 18.82" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 6V12L16 14M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 326 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 11C4 7.13401 7.13401 4 11 4M15.4999 6.5L10.9999 11M21 11C21 16.5228 16.5228 21 11 21C5.47715 21 1 16.5228 1 11C1 5.47715 5.47715 1 11 1C16.5228 1 21 5.47715 21 11ZM12 11C12 11.5523 11.5523 12 11 12C10.4477 12 10 11.5523 10 11C10 10.4477 10.4477 10 11 10C11.5523 10 12 10.4477 12 11Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 484 B |
@@ -1,4 +1,4 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 11.4C10.3255 11.4 11.4 10.3255 11.4 9C11.4 7.67452 10.3255 6.6 9 6.6C7.67452 6.6 6.6 7.67452 6.6 9C6.6 10.3255 7.67452 11.4 9 11.4Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14.3818 11.1818C14.285 11.4012 14.2561 11.6445 14.2989 11.8804C14.3417 12.1164 14.4542 12.3341 14.6218 12.5055L14.6655 12.5491C14.8007 12.6842 14.908 12.8446 14.9812 13.0212C15.0544 13.1978 15.0921 13.387 15.0921 13.5782C15.0921 13.7693 15.0544 13.9586 14.9812 14.1352C14.908 14.3118 14.8007 14.4722 14.6655 14.6073C14.5304 14.7425 14.3699 14.8498 14.1934 14.923C14.0168 14.9962 13.8275 15.0339 13.6364 15.0339C13.4452 15.0339 13.2559 14.9962 13.0794 14.923C12.9028 14.8498 12.7424 14.7425 12.6073 14.6073L12.5636 14.5636C12.3922 14.396 12.1745 14.2835 11.9386 14.2407C11.7027 14.1979 11.4594 14.2268 11.24 14.3236C11.0249 14.4158 10.8414 14.5689 10.7122 14.764C10.583 14.9591 10.5137 15.1878 10.5127 15.4218V15.5455C10.5127 15.9312 10.3595 16.3012 10.0867 16.574C9.81392 16.8468 9.44395 17 9.05818 17C8.67241 17 8.30244 16.8468 8.02966 16.574C7.75688 16.3012 7.60364 15.9312 7.60364 15.5455V15.48C7.59801 15.2393 7.52009 15.0058 7.38001 14.81C7.23993 14.6141 7.04417 14.4649 6.81818 14.3818C6.59882 14.285 6.3555 14.2561 6.11957 14.2989C5.88365 14.3417 5.66595 14.4542 5.49455 14.6218L5.45091 14.6655C5.31582 14.8007 5.1554 14.908 4.97882 14.9812C4.80224 15.0544 4.61297 15.0921 4.42182 15.0921C4.23067 15.0921 4.04139 15.0544 3.86481 14.9812C3.68824 14.908 3.52782 14.8007 3.39273 14.6655C3.25749 14.5304 3.1502 14.3699 3.077 14.1934C3.00381 14.0168 2.96613 13.8275 2.96613 13.6364C2.96613 13.4452 3.00381 13.2559 3.077 13.0794C3.1502 12.9028 3.25749 12.7424 3.39273 12.6073L3.43636 12.5636C3.60403 12.3922 3.7165 12.1745 3.75928 11.9386C3.80205 11.7027 3.77317 11.4594 3.67636 11.24C3.58417 11.0249 3.4311 10.8414 3.23597 10.7122C3.04085 10.583 2.81221 10.5137 2.57818 10.5127H2.45455C2.06878 10.5127 1.69881 10.3595 1.42603 10.0867C1.15325 9.81392 1 9.44395 1 9.05818C1 8.67241 1.15325 8.30244 1.42603 8.02966C1.69881 7.75688 2.06878 7.60364 2.45455 7.60364H2.52C2.76072 7.59801 2.99419 7.52009 3.19004 7.38001C3.38589 7.23993 3.53507 7.04417 3.61818 6.81818C3.71499 6.59882 3.74387 6.3555 3.70109 6.11957C3.65832 5.88365 3.54585 5.66595 3.37818 5.49455L3.33455 5.45091C3.19931 5.31582 3.09202 5.1554 3.01882 4.97882C2.94562 4.80224 2.90795 4.61297 2.90795 4.42182C2.90795 4.23067 2.94562 4.04139 3.01882 3.86481C3.09202 3.68824 3.19931 3.52782 3.33455 3.39273C3.46963 3.25749 3.63005 3.1502 3.80663 3.077C3.98321 3.00381 4.17249 2.96613 4.36364 2.96613C4.55479 2.96613 4.74406 3.00381 4.92064 3.077C5.09722 3.1502 5.25764 3.25749 5.39273 3.39273L5.43636 3.43636C5.60777 3.60403 5.82547 3.7165 6.06139 3.75928C6.29731 3.80205 6.54064 3.77317 6.76 3.67636H6.81818C7.03329 3.58417 7.21674 3.4311 7.34596 3.23597C7.47518 3.04085 7.54452 2.81221 7.54545 2.57818V2.45455C7.54545 2.06878 7.6987 1.69881 7.97148 1.42603C8.24426 1.15325 8.61423 1 9 1C9.38577 1 9.75574 1.15325 10.0285 1.42603C10.3013 1.69881 10.4545 2.06878 10.4545 2.45455V2.52C10.4555 2.75403 10.5248 2.98267 10.654 3.17779C10.7833 3.37291 10.9667 3.52599 11.1818 3.61818C11.4012 3.71499 11.6445 3.74387 11.8804 3.70109C12.1164 3.65832 12.3341 3.54585 12.5055 3.37818L12.5491 3.33455C12.6842 3.19931 12.8446 3.09202 13.0212 3.01882C13.1978 2.94562 13.387 2.90795 13.5782 2.90795C13.7693 2.90795 13.9586 2.94562 14.1352 3.01882C14.3118 3.09202 14.4722 3.19931 14.6073 3.33455C14.7425 3.46963 14.8498 3.63005 14.923 3.80663C14.9962 3.98321 15.0339 4.17249 15.0339 4.36364C15.0339 4.55479 14.9962 4.74406 14.923 4.92064C14.8498 5.09722 14.7425 5.25764 14.6073 5.39273L14.5636 5.43636C14.396 5.60777 14.2835 5.82547 14.2407 6.06139C14.1979 6.29731 14.2268 6.54064 14.3236 6.76V6.81818C14.4158 7.03329 14.5689 7.21674 14.764 7.34596C14.9591 7.47518 15.1878 7.54452 15.4218 7.54545H15.5455C15.9312 7.54545 16.3012 7.6987 16.574 7.97148C16.8468 8.24426 17 8.61423 17 9C17 9.38577 16.8468 9.75574 16.574 10.0285C16.3012 10.3013 15.9312 10.4545 15.5455 10.4545H15.48C15.246 10.4555 15.0173 10.5248 14.8222 10.654C14.6271 10.7833 14.474 10.9667 14.3818 11.1818Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 14C12.6569 14 14 12.6569 14 11C14 9.34315 12.6569 8 11 8C9.34315 8 8 9.34315 8 11C8 12.6569 9.34315 14 11 14Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M17.7273 13.7273C17.6063 14.0015 17.5702 14.3056 17.6236 14.6005C17.6771 14.8954 17.8177 15.1676 18.0273 15.3818L18.0818 15.4364C18.2509 15.6052 18.385 15.8057 18.4765 16.0265C18.568 16.2472 18.6151 16.4838 18.6151 16.7227C18.6151 16.9617 18.568 17.1983 18.4765 17.419C18.385 17.6397 18.2509 17.8402 18.0818 18.0091C17.913 18.1781 17.7124 18.3122 17.4917 18.4037C17.271 18.4952 17.0344 18.5423 16.7955 18.5423C16.5565 18.5423 16.3199 18.4952 16.0992 18.4037C15.8785 18.3122 15.678 18.1781 15.5091 18.0091L15.4545 17.9545C15.2403 17.745 14.9682 17.6044 14.6733 17.5509C14.3784 17.4974 14.0742 17.5335 13.8 17.6545C13.5311 17.7698 13.3018 17.9611 13.1403 18.205C12.9788 18.4489 12.8921 18.7347 12.8909 19.0273V19.1818C12.8909 19.664 12.6994 20.1265 12.3584 20.4675C12.0174 20.8084 11.5549 21 11.0727 21C10.5905 21 10.1281 20.8084 9.78708 20.4675C9.4461 20.1265 9.25455 19.664 9.25455 19.1818V19.1C9.24751 18.7991 9.15011 18.5073 8.97501 18.2625C8.79991 18.0176 8.55521 17.8312 8.27273 17.7273C7.99853 17.6063 7.69437 17.5702 7.39947 17.6236C7.10456 17.6771 6.83244 17.8177 6.61818 18.0273L6.56364 18.0818C6.39478 18.2509 6.19425 18.385 5.97353 18.4765C5.7528 18.568 5.51621 18.6151 5.27727 18.6151C5.03834 18.6151 4.80174 18.568 4.58102 18.4765C4.36029 18.385 4.15977 18.2509 3.99091 18.0818C3.82186 17.913 3.68775 17.7124 3.59626 17.4917C3.50476 17.271 3.45766 17.0344 3.45766 16.7955C3.45766 16.5565 3.50476 16.3199 3.59626 16.0992C3.68775 15.8785 3.82186 15.678 3.99091 15.5091L4.04545 15.4545C4.25503 15.2403 4.39562 14.9682 4.4491 14.6733C4.50257 14.3784 4.46647 14.0742 4.34545 13.8C4.23022 13.5311 4.03887 13.3018 3.79497 13.1403C3.55107 12.9788 3.26526 12.8921 2.97273 12.8909H2.81818C2.33597 12.8909 1.87351 12.6994 1.53253 12.3584C1.19156 12.0174 1 11.5549 1 11.0727C1 10.5905 1.19156 10.1281 1.53253 9.78708C1.87351 9.4461 2.33597 9.25455 2.81818 9.25455H2.9C3.2009 9.24751 3.49273 9.15011 3.73754 8.97501C3.98236 8.79991 4.16883 8.55521 4.27273 8.27273C4.39374 7.99853 4.42984 7.69437 4.37637 7.39947C4.3229 7.10456 4.18231 6.83244 3.97273 6.61818L3.91818 6.56364C3.74913 6.39478 3.61503 6.19425 3.52353 5.97353C3.43203 5.7528 3.38493 5.51621 3.38493 5.27727C3.38493 5.03834 3.43203 4.80174 3.52353 4.58102C3.61503 4.36029 3.74913 4.15977 3.91818 3.99091C4.08704 3.82186 4.28757 3.68775 4.50829 3.59626C4.72901 3.50476 4.96561 3.45766 5.20455 3.45766C5.44348 3.45766 5.68008 3.50476 5.9008 3.59626C6.12152 3.68775 6.32205 3.82186 6.49091 3.99091L6.54545 4.04545C6.75971 4.25503 7.03183 4.39562 7.32674 4.4491C7.62164 4.50257 7.9258 4.46647 8.2 4.34545H8.27273C8.54161 4.23022 8.77093 4.03887 8.93245 3.79497C9.09397 3.55107 9.18065 3.26526 9.18182 2.97273V2.81818C9.18182 2.33597 9.37338 1.87351 9.71435 1.53253C10.0553 1.19156 10.5178 1 11 1C11.4822 1 11.9447 1.19156 12.2856 1.53253C12.6266 1.87351 12.8182 2.33597 12.8182 2.81818V2.9C12.8193 3.19253 12.906 3.47834 13.0676 3.72224C13.2291 3.96614 13.4584 4.15749 13.7273 4.27273C14.0015 4.39374 14.3056 4.42984 14.6005 4.37637C14.8954 4.3229 15.1676 4.18231 15.3818 3.97273L15.4364 3.91818C15.6052 3.74913 15.8057 3.61503 16.0265 3.52353C16.2472 3.43203 16.4838 3.38493 16.7227 3.38493C16.9617 3.38493 17.1983 3.43203 17.419 3.52353C17.6397 3.61503 17.8402 3.74913 18.0091 3.91818C18.1781 4.08704 18.3122 4.28757 18.4037 4.50829C18.4952 4.72901 18.5423 4.96561 18.5423 5.20455C18.5423 5.44348 18.4952 5.68008 18.4037 5.9008C18.3122 6.12152 18.1781 6.32205 18.0091 6.49091L17.9545 6.54545C17.745 6.75971 17.6044 7.03183 17.5509 7.32674C17.4974 7.62164 17.5335 7.9258 17.6545 8.2V8.27273C17.7698 8.54161 17.9611 8.77093 18.205 8.93245C18.4489 9.09397 18.7347 9.18065 19.0273 9.18182H19.1818C19.664 9.18182 20.1265 9.37338 20.4675 9.71435C20.8084 10.0553 21 10.5178 21 11C21 11.4822 20.8084 11.9447 20.4675 12.2856C20.1265 12.6266 19.664 12.8182 19.1818 12.8182H19.1C18.8075 12.8193 18.5217 12.906 18.2778 13.0676C18.0339 13.2291 17.8425 13.4584 17.7273 13.7273Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |