implement adjustments and merge with develop
This commit is contained in:
1 parent
1cb730764a
commit
c8bf571f7a
17 files changed
+373
-26
No files matched your search
@@ -87,11 +87,17 @@ const Fallback = ({
|
||||
sx={{
|
||||
display: "flex",
|
||||
borderStyle: "dashed",
|
||||
height: { sm: "50vh", md: "60vh" },
|
||||
width: { md: "45%", lg: "40%" },
|
||||
minWidth: theme.spacing(200),
|
||||
maxHeight: theme.spacing(350),
|
||||
maxWidth: theme.spacing(450),
|
||||
height: {
|
||||
xs: theme.spacing(200),
|
||||
sm: theme.spacing(300),
|
||||
md: theme.spacing(300),
|
||||
},
|
||||
width: {
|
||||
xs: theme.spacing(100),
|
||||
sm: theme.spacing(210),
|
||||
md: theme.spacing(250),
|
||||
},
|
||||
padding: theme.spacing(10),
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
@@ -116,10 +122,11 @@ const Fallback = ({
|
||||
</Box>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
maxWidth={"300px"}
|
||||
maxWidth={theme.spacing(180)}
|
||||
zIndex={1}
|
||||
>
|
||||
<Typography
|
||||
alignSelf="center"
|
||||
component="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
|
||||
@@ -214,7 +214,7 @@ const PasswordPanel = () => {
|
||||
<TextInput
|
||||
type="password"
|
||||
id="edit-confirm-password"
|
||||
placeholder={t("confirmPassword", "Confirm password")}
|
||||
placeholder={t("confirmPassword")}
|
||||
autoComplete="new-password"
|
||||
value={localData.confirm}
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -122,4 +122,30 @@ const useFetchChecksByMonitor = ({
|
||||
return [checks, checksCount, isLoading, networkError];
|
||||
};
|
||||
|
||||
export { useFetchChecksByMonitor, useFetchChecksTeam };
|
||||
const useFetchChecksSummaryByTeamId = () => {
|
||||
const [summary, setSummary] = useState(undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSummary = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const res = await networkService.getChecksAndSummaryByTeamId();
|
||||
setSummary(res.data.data);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({ body: error.message });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchSummary();
|
||||
}, []);
|
||||
|
||||
return [summary, isLoading, networkError];
|
||||
};
|
||||
|
||||
export { useFetchChecksByMonitor, useFetchChecksTeam, useFetchChecksSummaryByTeamId };
|
||||
@@ -385,9 +385,9 @@ const useAddDemoMonitors = () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.addDemoMonitors();
|
||||
createToast({ body: t("settingsDemoMonitorsAdded") });
|
||||
createToast({ body: t("monitorHooks.successAddDemoMonitors") });
|
||||
} catch (error) {
|
||||
createToast({ body: t("settingsFailedToAddDemoMonitors") });
|
||||
createToast({ body: t("monitorHooks.failureAddDemoMonitors") });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -87,13 +87,13 @@ const OptionsHeader = ({
|
||||
>
|
||||
{t("incidentsOptionsHeaderFilterCannotResolve")}
|
||||
</Button>
|
||||
{/* <Button
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(filter === "resolved").toString()}
|
||||
onClick={() => setFilter("resolved")}
|
||||
>
|
||||
{t("incidentsOptionsHeaderFilterResolved")}
|
||||
</Button> */}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<Stack {...stackStyles}>
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import Background from "../../../../assets/Images/background-grid.svg?react";
|
||||
import MonitorHeartOutlinedIcon from "@mui/icons-material/MonitorHeartOutlined";
|
||||
import TaskAltOutlinedIcon from "@mui/icons-material/TaskAltOutlined";
|
||||
import CancelOutlinedIcon from "@mui/icons-material/CancelOutlined";
|
||||
import WarningAmberRoundedIcon from "@mui/icons-material/WarningAmberRounded";
|
||||
|
||||
const StatusBox = ({ title, value, status }) => {
|
||||
const theme = useTheme();
|
||||
let sharedStyles = {
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
opacity: 0.5,
|
||||
"& svg path": { stroke: theme.palette.primary.contrastTextTertiary },
|
||||
};
|
||||
|
||||
let color;
|
||||
let icon;
|
||||
if (status === "up") {
|
||||
color = theme.palette.success.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<TaskAltOutlinedIcon fontSize="small" />
|
||||
</Box>
|
||||
);
|
||||
} else if (status === "down") {
|
||||
color = theme.palette.error.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<CancelOutlinedIcon fontSize="small" />
|
||||
</Box>
|
||||
);
|
||||
} else if (status === "paused") {
|
||||
color = theme.palette.warning.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<WarningAmberRoundedIcon fontSize="small" />
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
color = theme.palette.accent.main;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<MonitorHeartOutlinedIcon fontSize="small" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
flex={1}
|
||||
border={1}
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
p={theme.spacing(8)}
|
||||
overflow="hidden"
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
top="-10%"
|
||||
left="5%"
|
||||
>
|
||||
<Background />
|
||||
</Box>
|
||||
<Stack direction="column">
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
mb={theme.spacing(8)}
|
||||
>
|
||||
<Typography
|
||||
variant={"h2"}
|
||||
textTransform="uppercase"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{icon}
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
fontSize={theme.typography.h1.fontSize}
|
||||
fontWeight={600}
|
||||
color={color}
|
||||
gap={theme.spacing(1)}
|
||||
>
|
||||
{value}
|
||||
|
||||
<Typography
|
||||
fontSize={theme.typography.label.fontSize}
|
||||
fontWeight={300}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
sx={{
|
||||
opacity: 0.3,
|
||||
}}
|
||||
>
|
||||
#
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
StatusBox.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
status: PropTypes.string,
|
||||
};
|
||||
|
||||
export default StatusBox;
|
||||
@@ -0,0 +1,46 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Stack } from "@mui/material";
|
||||
import StatusBox from "./StatusBox";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
|
||||
const StatusBoxes = ({ isLoading, summary }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
if (isLoading) return <SkeletonLayout shouldRender={isLoading} />;
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(12)}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<StatusBox
|
||||
title={t("incidentsOptionsHeaderTotalIncidents")}
|
||||
value={summary?.totalChecks || 0}
|
||||
/>
|
||||
<StatusBox
|
||||
title={t("incidentsOptionsHeaderFilterResolved")}
|
||||
status="up"
|
||||
value={summary?.resolvedChecks || 0}
|
||||
/>
|
||||
<StatusBox
|
||||
title={t("incidentsOptionsHeaderFilterCannotResolve")}
|
||||
status="paused"
|
||||
value={summary?.cannotResolveChecks || 0}
|
||||
/>
|
||||
<StatusBox
|
||||
title={t("incidentsOptionsHeaderFilterDown")}
|
||||
status="down"
|
||||
value={summary?.downChecks || 0}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
StatusBoxes.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
summary: PropTypes.object,
|
||||
};
|
||||
|
||||
export default StatusBoxes;
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Skeleton, Stack } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(12)}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -4,10 +4,12 @@ import Breadcrumbs from "../../Components/Breadcrumbs";
|
||||
import GenericFallback from "../../Components/GenericFallback";
|
||||
import IncidentTable from "./Components/IncidentTable";
|
||||
import OptionsHeader from "./Components/OptionsHeader";
|
||||
import StatusBoxes from "./Components/StatusBoxes";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks";
|
||||
import { useFetchChecksSummaryByTeamId } from "../../Hooks/checkHooks";
|
||||
import { useState, useEffect } from "react";
|
||||
import NetworkError from "../../Components/GenericFallback/NetworkError";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -31,6 +33,8 @@ const Incidents = () => {
|
||||
//Utils
|
||||
const theme = useTheme();
|
||||
const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({});
|
||||
const [summary, isLoadingSummary, networkErrorSummary] =
|
||||
useFetchChecksSummaryByTeamId();
|
||||
const { monitorId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -51,7 +55,7 @@ const Incidents = () => {
|
||||
setMonitorLookup(monitorLookup);
|
||||
}, [monitors]);
|
||||
|
||||
if (networkError) {
|
||||
if (networkError || networkErrorSummary) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<NetworkError />
|
||||
@@ -62,6 +66,10 @@ const Incidents = () => {
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<StatusBoxes
|
||||
isLoading={isLoadingSummary}
|
||||
summary={summary}
|
||||
/>
|
||||
<OptionsHeader
|
||||
shouldRender={!isLoading}
|
||||
monitors={monitorLookup}
|
||||
|
||||
@@ -87,10 +87,7 @@ const SettingsEmail = ({
|
||||
!emailConfig.systemEmailPassword
|
||||
) {
|
||||
createToast({
|
||||
body: t(
|
||||
"settingsEmailRequiredFields",
|
||||
"Email address, host, port and password are required"
|
||||
),
|
||||
body: t("settingsPage.emailSettings.toastEmailRequiredFieldsError"),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -19,21 +19,21 @@ const StatusBox = ({ title, value, status }) => {
|
||||
if (status === "up") {
|
||||
color = theme.palette.success.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: 8 }}>
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(4) }}>
|
||||
<Arrow />
|
||||
</Box>
|
||||
);
|
||||
} else if (status === "down") {
|
||||
color = theme.palette.error.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, transform: "rotate(180deg)", top: 5 }}>
|
||||
<Box sx={{ ...sharedStyles, transform: "rotate(180deg)", top: theme.spacing(2) }}>
|
||||
<Arrow />
|
||||
</Box>
|
||||
);
|
||||
} else if (status === "paused") {
|
||||
color = theme.palette.warning.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: 12, right: 12 }}>
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<ClockSnooze />
|
||||
</Box>
|
||||
);
|
||||
@@ -75,15 +75,15 @@ const StatusBox = ({ title, value, status }) => {
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
fontSize={36}
|
||||
fontSize={theme.typography.h1.fontSize}
|
||||
fontWeight={600}
|
||||
color={color}
|
||||
gap="2px"
|
||||
gap={theme.spacing(1)}
|
||||
>
|
||||
{value}
|
||||
|
||||
<Typography
|
||||
fontSize={20}
|
||||
fontSize={theme.typography.label.fontSize}
|
||||
fontWeight={300}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
sx={{
|
||||
|
||||
@@ -601,6 +601,18 @@ class NetworkService {
|
||||
return this.axiosInstance.get(`/checks/team?${params.toString()}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Get summary of checks by team ID
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
|
||||
*/
|
||||
getChecksAndSummaryByTeamId = async () => {
|
||||
return this.axiosInstance.get(`/checks/team/summary`);
|
||||
};
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Update the status of a check
|
||||
|
||||
@@ -197,12 +197,14 @@
|
||||
"basicInformation": "Basic Information",
|
||||
"bulkImport": {
|
||||
"fallbackPage": "Import a file to upload a list of servers in bulk",
|
||||
"invalidFileType": "Invalid file type",
|
||||
"noFileSelected": "No file selected",
|
||||
"parsingFailed": "Parsing failed",
|
||||
"selectFile": "Select File",
|
||||
"selectFileDescription": "You can download our <template>template</template> or <sample>sample</sample>",
|
||||
"selectFileTips": "Select CSV file to upload",
|
||||
"title": "Bulk Import",
|
||||
"uploadFailed": "Upload failed",
|
||||
"uploadSuccess": "Monitors created successfully!",
|
||||
"validationFailed": "Validation failed"
|
||||
},
|
||||
@@ -226,6 +228,7 @@
|
||||
"commonSaving": "Saving...",
|
||||
"companyName": "Company name",
|
||||
"configure": "Configure",
|
||||
"confirmPassword": "Confirm password",
|
||||
"cores": "Cores",
|
||||
"country": "COUNTRY",
|
||||
"cpu": "CPU",
|
||||
@@ -385,7 +388,8 @@
|
||||
"incidentsOptionsHeader": "Incidents for:",
|
||||
"incidentsOptionsHeaderFilterAll": "All",
|
||||
"incidentsOptionsHeaderFilterBy": "Filter by:",
|
||||
"incidentsOptionsHeaderFilterCannotResolve": "Cannot resolve",
|
||||
"incidentsOptionsHeaderTotalIncidents": "Total Incidents",
|
||||
"incidentsOptionsHeaderFilterCannotResolve": "Cannot Resolve",
|
||||
"incidentsOptionsHeaderFilterDown": "Down",
|
||||
"incidentsOptionsHeaderFilterResolved": "Resolved",
|
||||
"incidentsOptionsHeaderLastDay": "Last day",
|
||||
@@ -488,6 +492,11 @@
|
||||
"title": "Export/Import"
|
||||
},
|
||||
"monitorDisplayName": "Monitor display name",
|
||||
"monitorHooks": {
|
||||
"failureAddDemoMonitors": "Failed to add demo monitors",
|
||||
"successAddDemoMonitors": "Successfully added demo monitors"
|
||||
},
|
||||
|
||||
"monitorState": {
|
||||
"active": "Active",
|
||||
"paused": "Paused",
|
||||
@@ -661,12 +670,14 @@
|
||||
"selectAll": "Select all",
|
||||
"sendTestNotifications": "Send test notifications",
|
||||
"seperateEmails": "You can separate multiple emails with a comma",
|
||||
"settingsAppearance": "Appearance",
|
||||
"settingsDisabled": "Disabled",
|
||||
"settingsDisplayTimezone": "Display timezone",
|
||||
"settingsEmailUser": "Email user - Username for authentication, overrides email address if specified",
|
||||
"settingsFailedToAddDemoMonitors": "Failed to add demo monitors",
|
||||
"settingsFailedToClearStats": "Failed to clear stats",
|
||||
"settingsFailedToDeleteMonitors": "Failed to delete all monitors",
|
||||
"settingsFailedToSave": "Failed to save settings",
|
||||
"settingsGeneralSettings": "General settings",
|
||||
"settingsMonitorsDeleted": "Successfully deleted all monitors",
|
||||
"settingsPage": {
|
||||
"aboutSettings": {
|
||||
@@ -698,7 +709,8 @@
|
||||
"labelUser": "Email user - Username for authentication, overrides email address if specified",
|
||||
"linkTransport": "See specifications here",
|
||||
"placeholderUser": "Leave empty if not required",
|
||||
"title": "Email"
|
||||
"title": "Email",
|
||||
"toastEmailRequiredFieldsError": "Email address, host, port and password are required"
|
||||
},
|
||||
"pageSpeedSettings": {
|
||||
"description": "Enter your Google PageSpeed API key to enable Google PageSpeed monitoring. Click Reset to update the key.",
|
||||
|
||||
@@ -111,6 +111,19 @@ class CheckController {
|
||||
}
|
||||
};
|
||||
|
||||
getChecksSummaryByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
const { teamId } = req.user;
|
||||
const summary = await this.db.getChecksSummaryByTeamId({ teamId });
|
||||
return res.success({
|
||||
msg: this.stringService.checkGetSummary,
|
||||
data: summary,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getChecksSummaryByTeamId"));
|
||||
}
|
||||
};
|
||||
|
||||
ackCheck = async (req, res, next) => {
|
||||
try {
|
||||
await ackCheckBodyValidation.validateAsync(req.body);
|
||||
|
||||
@@ -5,6 +5,7 @@ import PageSpeedCheck from "../../models/PageSpeedCheck.js";
|
||||
import User from "../../models/User.js";
|
||||
import logger from "../../../utils/logger.js";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { buildChecksSummaryByTeamIdPipeline } from "./checkModuleQueries.js";
|
||||
|
||||
const SERVICE_NAME = "checkModule";
|
||||
const dateRangeLookup = {
|
||||
@@ -304,6 +305,29 @@ const ackAllChecks = async (monitorId, teamId, ack, path) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get checks and summary by team ID
|
||||
* @async
|
||||
* @param {string} teamId
|
||||
* @returns {Promise<Object>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getChecksSummaryByTeamId = async ({ teamId }) => {
|
||||
try {
|
||||
const matchStage = {
|
||||
teamId: new ObjectId(teamId),
|
||||
};
|
||||
const checks = await Check.aggregate(
|
||||
buildChecksSummaryByTeamIdPipeline({ matchStage })
|
||||
);
|
||||
return checks[0].summary;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getChecksSummaryByTeamId";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete all checks for a monitor
|
||||
* @async
|
||||
@@ -387,6 +411,7 @@ export {
|
||||
getChecksByTeam,
|
||||
ackCheck,
|
||||
ackAllChecks,
|
||||
getChecksSummaryByTeamId,
|
||||
deleteChecks,
|
||||
deleteChecksByTeamId,
|
||||
updateChecksTTL,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
const buildChecksSummaryByTeamIdPipeline = ({ matchStage }) => {
|
||||
return [
|
||||
{ $match: matchStage },
|
||||
{
|
||||
$facet: {
|
||||
summary: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalChecks: { $sum: { $cond: [{ $eq: ["$status", false] }, 1, 0] } },
|
||||
resolvedChecks: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$ack", true] }, 1, 0],
|
||||
},
|
||||
},
|
||||
downChecks: {
|
||||
$sum: {
|
||||
$cond: [
|
||||
{ $and: [{ $eq: ["$ack", false] }, { $eq: ["$status", false] }] },
|
||||
1,
|
||||
0,
|
||||
],
|
||||
},
|
||||
},
|
||||
cannotResolveChecks: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$statusCode", 5000] }, 1, 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
summary: { $arrayElemAt: ["$summary", 0] },
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export { buildChecksSummaryByTeamIdPipeline };
|
||||
@@ -14,6 +14,7 @@ class CheckRoutes {
|
||||
|
||||
initRoutes() {
|
||||
this.router.get("/team", this.checkController.getChecksByTeam);
|
||||
this.router.get("/team/summary", this.checkController.getChecksSummaryByTeamId);
|
||||
this.router.delete(
|
||||
"/team",
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
|
||||
Reference in new issue
Block a user