Add eventStatusId to UptimeUtil and MonitorOverview

This commit is contained in:
Simon Larsen
2024-01-26 13:52:54 +00:00
parent cd9a72aa36
commit b86cfd3bb8
11 changed files with 318 additions and 75 deletions

View File

@@ -72,6 +72,8 @@ import ScheduledMaintenanceStateService from '../Services/ScheduledMaintenanceSt
import BaseModel from 'Common/Models/BaseModel';
import CommonAPI from './CommonAPI';
import Phone from 'Common/Types/Phone';
import StatusPageHistoryChartBarColorRule from 'Model/Models/StatusPageHistoryChartBarColorRule';
import StatusPageHistoryChartBarColorRuleService from '../Services/StatusPageHistoryChartBarColorRuleService';
export default class StatusPageAPI extends BaseAPI<
StatusPage,
@@ -504,6 +506,9 @@ export default class StatusPageAPI extends BaseAPI<
overviewPageDescription: true,
showIncidentLabelsOnStatusPage: true,
showScheduledEventLabelsOnStatusPage: true,
downtimeMonitorStatuses: {
_id: true,
}
},
props: {
isRoot: true,
@@ -1059,12 +1064,40 @@ export default class StatusPageAPI extends BaseAPI<
);
}
// get all status page bar chart rules
const statusPageHistoryChartBarColorRules: Array<StatusPageHistoryChartBarColorRule> =
await StatusPageHistoryChartBarColorRuleService.findBy({
query: {
statusPageId: objectId,
},
select: {
_id: true,
barColor: true,
order: true,
statusPageId: true,
uptimePercentGreaterThanOrEqualTo: true,
},
sort: {
order: SortOrder.Ascending,
},
skip: 0,
limit: LIMIT_PER_PROJECT,
props: {
isRoot: true,
},
});
const response: JSONObject = {
scheduledMaintenanceEventsPublicNotes:
BaseModel.toJSONArray(
scheduledMaintenanceEventsPublicNotes,
ScheduledMaintenancePublicNote
),
statusPageHistoryChartBarColorRules:
BaseModel.toJSONArray(
statusPageHistoryChartBarColorRules,
StatusPageHistoryChartBarColorRule
),
scheduledMaintenanceEvents: BaseModel.toJSONArray(
scheduledMaintenanceEvents,
ScheduledMaintenance

View File

@@ -9,6 +9,7 @@ import React, {
useState,
} from 'react';
import Tooltip from '../Tooltip/Tooltip';
import ObjectID from 'Common/Types/ObjectID';
export interface Event {
startDate: Date;
@@ -16,6 +17,12 @@ export interface Event {
label: string;
priority: number;
color: Color;
eventStatusId: ObjectID; // this is the id of the event status. for example, monitor status id.
}
export interface BarChartRule {
barColor: Color;
uptimePercentGreaterThanOrEqualTo: number;
}
export interface ComponentProps {
@@ -24,6 +31,8 @@ export interface ComponentProps {
events: Array<Event>;
defaultLabel: string;
height?: number | undefined;
barColorRules?: Array<BarChartRule> | undefined;
downtimeEventStatusIds?: Array<ObjectID> | undefined;
}
const DayUptimeGraph: FunctionComponent<ComponentProps> = (
@@ -42,14 +51,17 @@ const DayUptimeGraph: FunctionComponent<ComponentProps> = (
const getUptimeBar: Function = (dayNumber: number): ReactElement => {
let color: Color = Green;
const todaysDay: Date = OneUptimeDate.getSomeDaysAfterDate(
props.startDate,
dayNumber
);
let toolTipText: string = `${OneUptimeDate.getDateAsLocalFormattedString(
todaysDay,
true
)}`;
const startOfTheDay: Date = OneUptimeDate.getStartOfDay(todaysDay);
const endOfTheDay: Date = OneUptimeDate.getEndOfDay(todaysDay);
@@ -121,11 +133,11 @@ const DayUptimeGraph: FunctionComponent<ComponentProps> = (
endDate
);
if (!secondsOfEvent[event.label]) {
secondsOfEvent[event.label] = 0;
if (!secondsOfEvent[event.eventStatusId.toString()]) {
secondsOfEvent[event.eventStatusId.toString()] = 0;
}
secondsOfEvent[event.label] += seconds;
secondsOfEvent[event.eventStatusId.toString()] += seconds;
// set bar color.
if (currentPriority <= event.priority) {
@@ -135,6 +147,10 @@ const DayUptimeGraph: FunctionComponent<ComponentProps> = (
}
let hasText: boolean = false;
let totalUptimeInSecondsInDayBasedOnBarRules: number =
OneUptimeDate.getSecondsBetweenDates(startOfTheDay, endOfTheDay);
for (const key in secondsOfEvent) {
if (todaysEvents.length === 1) {
break;
@@ -144,6 +160,41 @@ const DayUptimeGraph: FunctionComponent<ComponentProps> = (
toolTipText += `, ${key} for ${OneUptimeDate.secondsToFormattedFriendlyTimeString(
secondsOfEvent[key] || 0
)}`;
// TODO: Add rules here.
const eventStatusId: string = key;
const isDowntimeEvent: boolean = Boolean(
props.downtimeEventStatusIds?.find((id: ObjectID) => {
return id.toString() === eventStatusId;
})
);
if (isDowntimeEvent) {
// remove the seconds from total uptime.
const secondsOfDowntime: number = secondsOfEvent[key] || 0;
totalUptimeInSecondsInDayBasedOnBarRules -= secondsOfDowntime;
}
}
// now check bar rules and finalize the color of the bar.
const totalSecondsForTheDay: number =
OneUptimeDate.getSecondsBetweenDates(startOfTheDay, endOfTheDay);
const uptimePercentForTheDay: number =
(totalUptimeInSecondsInDayBasedOnBarRules / totalSecondsForTheDay) *
100;
for (const rules of props.barColorRules || []) {
if (
uptimePercentForTheDay >=
rules.uptimePercentGreaterThanOrEqualTo
) {
color = rules.barColor;
break;
}
}
if (todaysEvents.length === 1) {

View File

@@ -7,10 +7,12 @@ import React, {
import MonitorStatusTimeline from 'Model/Models/MonitorStatusTimeline';
import ComponentLoader from '../ComponentLoader/ComponentLoader';
import DayUptimeGraph, { Event } from '../Graphs/DayUptimeGraph';
import DayUptimeGraph, { BarChartRule, Event } from '../Graphs/DayUptimeGraph';
import ErrorMessage from '../ErrorMessage/ErrorMessage';
import ObjectID from 'Common/Types/ObjectID';
import UptimeUtil from './UptimeUtil';
import StatusPageHistoryChartBarColorRule from 'Model/Models/StatusPageHistoryChartBarColorRule';
import MonitorStatus from 'Model/Models/MonitorStatus';
export interface MonitorEvent extends Event {
monitorId: ObjectID;
@@ -24,6 +26,8 @@ export interface ComponentProps {
onRefreshClick?: (() => void) | undefined;
error?: string | undefined;
height?: number | undefined;
barColorRules?: Array<StatusPageHistoryChartBarColorRule> | undefined;
downtimeMonitorStatus: Array<MonitorStatus> | undefined;
}
const MonitorUptimeGraph: FunctionComponent<ComponentProps> = (
@@ -31,12 +35,28 @@ const MonitorUptimeGraph: FunctionComponent<ComponentProps> = (
): ReactElement => {
const [events, setEvents] = useState<Array<Event>>([]);
const [barColorRules, setBarColorRules] = useState<BarChartRule[]>([]);
useEffect(() => {
const eventList: Array<Event> =
UptimeUtil.getNonOverlappingMonitorEvents(props.items);
setEvents(eventList);
}, [props.items]);
useEffect(() => {
if (props.barColorRules) {
setBarColorRules(
props.barColorRules.map((rule) => {
return {
barColor: rule.barColor!,
uptimePercentGreaterThanOrEqualTo:
rule.uptimePercentGreaterThanOrEqualTo!,
};
})
);
}
}, [props.barColorRules]);
if (props.isLoading) {
return <ComponentLoader />;
}
@@ -59,6 +79,10 @@ const MonitorUptimeGraph: FunctionComponent<ComponentProps> = (
events={events}
defaultLabel={'Operational'}
height={props.height}
barColorRules={barColorRules}
downtimeEventStatusIds={props.downtimeMonitorStatus?.map((status: MonitorStatus)=>{
return status.id!;
}) || []}
/>
);
};

View File

@@ -53,6 +53,7 @@ export default class UptimeUtil {
priority: monitorEvents[i]?.monitorStatus?.priority || 0,
color: monitorEvents[i]?.monitorStatus?.color || Green,
monitorId: monitorEvents[i]!.monitorId!,
eventStatusId: monitorEvents[i]!.monitorStatus?.id!,
});
}
@@ -120,6 +121,7 @@ export default class UptimeUtil {
label: tempLastEvent.label,
priority: tempLastEvent.priority,
color: tempLastEvent.color,
eventStatusId: tempLastEvent.eventStatusId,
});
}
}

View File

@@ -0,0 +1,15 @@
import React, { FunctionComponent, ReactElement } from 'react';
import MonitorStatus from 'Model/Models/MonitorStatus';
export interface ComponentProps {
monitorStatus: MonitorStatus;
onNavigateComplete?: (() => void) | undefined;
}
const TeamElement: FunctionComponent<ComponentProps> = (
props: ComponentProps
): ReactElement => {
return <span>{props.monitorStatus.name}</span>;
};
export default TeamElement;

View File

@@ -0,0 +1,38 @@
import MonitorStatus from 'Model/Models/MonitorStatus';
import React, { FunctionComponent, ReactElement } from 'react';
import MonitorStatusElement from './MonitorStatusElement';
export interface ComponentProps {
monitorStatuses: Array<MonitorStatus>;
onNavigateComplete?: (() => void) | undefined;
}
const MonitorStatusesElement: FunctionComponent<ComponentProps> = (
props: ComponentProps
): ReactElement => {
if (!props.monitorStatuses || props.monitorStatuses.length === 0) {
return <p>No monitor status attached.</p>;
}
return (
<div>
{props.monitorStatuses.map(
(monitorStatus: MonitorStatus, i: number) => {
return (
<span key={i}>
<MonitorStatusElement
monitorStatus={monitorStatus}
onNavigateComplete={props.onNavigateComplete}
/>
{i !== props.monitorStatuses.length - 1 && (
<span>,&nbsp;</span>
)}
</span>
);
}
)}
</div>
);
};
export default MonitorStatusesElement;

View File

@@ -12,6 +12,8 @@ import SortOrder from 'Common/Types/BaseDatabase/SortOrder';
import DashboardNavigation from '../../../Utils/Navigation';
import BadDataException from 'Common/Types/Exception/BadDataException';
import MonitorStatus from 'Model/Models/MonitorStatus';
import { JSONObject } from 'Common/Types/JSON';
import MonitorStatuesElement from '../../../Components/MonitorStatus/MonitorStatusesElement';
const StatusPageDelete: FunctionComponent<PageComponentProps> = (
props: PageComponentProps
@@ -120,22 +122,6 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
required: true,
placeholder: 'No color set',
},
{
field: {
downtimeMonitorStatuses: true,
},
title: 'These monitor statuses are considered as downtime',
description:
'These monitor statuses will be considered as downtime.',
fieldType: FormFieldSchemaType.MultiSelectDropdown,
dropdownModal: {
type: MonitorStatus,
labelField: 'name',
valueField: '_id',
},
required: true,
placeholder: 'Select monitor statuses',
},
]}
showRefreshButton={true}
showFilterButton={true}
@@ -160,6 +146,71 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
]}
/>
<CardModelDetail<StatusPage>
name="Status Page > Branding > Downtime Monitor Statuses"
cardProps={{
title: 'Downtime Monitor Statuses',
description:
'These monitor statuses are be considered as down when we calculate uptime %.',
}}
isEditable={true}
editButtonText={'Edit Statuses'}
formFields={[
{
field: {
downtimeMonitorStatuses: true,
},
title: 'These monitor statuses are considered as down',
description:
'These monitor statuses are be considered as down when we calculate uptime %.',
fieldType: FormFieldSchemaType.MultiSelectDropdown,
dropdownModal: {
type: MonitorStatus,
labelField: 'name',
valueField: '_id',
},
required: true,
placeholder: 'Select monitor statuses',
},
]}
modelDetailProps={{
showDetailsInNumberOfColumns: 1,
modelType: StatusPage,
id: 'default-bar-color',
fields: [
{
field: {
downtimeMonitorStatuses: {
_id: true,
name: true,
color: true,
},
},
title: 'Downtime Monitor Statuses',
description:
'These monitor statuses are be considered as down when we calculate uptime %',
fieldType: FieldType.EntityArray,
getElement: (item: JSONObject): ReactElement => {
if (item['downtimeMonitorStatuses']) {
return (
<MonitorStatuesElement
monitorStatuses={
(item[
'downtimeMonitorStatuses'
] as Array<MonitorStatus>) || []
}
/>
);
}
return <></>;
},
},
],
modelId: modelId,
}}
/>
<CardModelDetail<StatusPage>
name="Status Page > Branding > Default Bar Color"
cardProps={{

View File

@@ -36,6 +36,7 @@ import ColumnBillingAccessControl from 'Common/Types/Database/AccessControl/Colu
import { PlanSelect } from 'Common/Types/Billing/SubscriptionPlan';
import ProjectCallSMSConfig from './ProjectCallSMSConfig';
import Color from 'Common/Types/Color';
import MonitorStatus from './MonitorStatus';
@EnableDocumentation()
@AccessControlColumn('labels')
@@ -1599,4 +1600,51 @@ export default class StatusPage extends BaseModel {
transformer: Color.getDatabaseTransformer(),
})
public defaultBarColor?: Color = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CanCreateProjectStatusPage,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CanReadProjectStatusPage,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CanEditProjectStatusPage,
],
})
@TableColumn({
required: false,
type: TableColumnType.EntityArray,
modelType: MonitorStatus,
title: 'Downtime Monitor Statuses',
description:
'List of monitors statuses that are considered as "down" for this status page.',
})
@ManyToMany(
() => {
return MonitorStatus;
},
{ eager: false }
)
@JoinTable({
name: 'StatusPageDownMonitorStatus',
inverseJoinColumn: {
name: 'monitorStatusId',
referencedColumnName: '_id',
},
joinColumn: {
name: 'statusPageId',
referencedColumnName: '_id',
},
})
public downtimeMonitorStatuses?: Array<MonitorStatus> = undefined;
}

View File

@@ -1,12 +1,4 @@
import {
Column,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
} from 'typeorm';
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import BaseModel from 'Common/Models/BaseModel';
import User from './User';
import Project from './Project';
@@ -213,52 +205,6 @@ export default class StatusPageHistoryChartBarColorRule extends BaseModel {
})
public statusPageId?: ObjectID = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CanCreateStatusPageHistoryChartBarColorRule,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CanReadStatusPageHistoryChartBarColorRule,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CanEditStatusPageHistoryChartBarColorRule,
],
})
@TableColumn({
required: false,
type: TableColumnType.EntityArray,
modelType: MonitorStatus,
title: 'Downtime Monitor Statuses',
description: 'List of monitors statuses that are considered as "down"',
})
@ManyToMany(
() => {
return MonitorStatus;
},
{ eager: false }
)
@JoinTable({
name: 'IncidentMonitorStatus',
inverseJoinColumn: {
name: 'monitorStatusId',
referencedColumnName: '_id',
},
joinColumn: {
name: 'StatusPageHistoryChartBarColorRuleRuleId',
referencedColumnName: '_id',
},
})
public downtimeMonitorStatuses?: Array<MonitorStatus> = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -9,6 +9,7 @@ import IconProp from 'Common/Types/Icon/IconProp';
import MarkdownViewer from 'CommonUI/src/Components/Markdown.tsx/LazyMarkdownViewer';
import { UptimePrecision } from 'Model/Models/StatusPageResource';
import UptimeUtil from 'CommonUI/src/Components/MonitorGraphs/UptimeUtil';
import StatusPageHistoryChartBarColorRule from 'Model/Models/StatusPageHistoryChartBarColorRule';
export interface ComponentProps {
monitorName: string;
@@ -25,6 +26,8 @@ export interface ComponentProps {
showUptimePercent: boolean;
uptimePrecision?: UptimePrecision | undefined;
monitorStatuses: Array<MonitorStatus>;
statusPageHistoryChartBarColorRules: Array<StatusPageHistoryChartBarColorRule>;
downtimeMonitorStatus: Array<MonitorStatus>;
}
const MonitorOverview: FunctionComponent<ComponentProps> = (
@@ -116,6 +119,10 @@ const MonitorOverview: FunctionComponent<ComponentProps> = (
<div>
<MonitorUptimeGraph
error={undefined}
barColorRules={
props.statusPageHistoryChartBarColorRules
}
downtimeMonitorStatus={props.downtimeMonitorStatus}
items={props.monitorStatusTimeline || []}
startDate={props.startDate}
endDate={props.endDate}

View File

@@ -53,6 +53,7 @@ import StatusPageUtil from '../../Utils/StatusPage';
import HTTPErrorResponse from 'Common/Types/API/HTTPErrorResponse';
import { STATUS_PAGE_API_URL } from '../../Utils/Config';
import Section from '../../Components/Section/Section';
import StatusPageHistoryChartBarColorRule from 'Model/Models/StatusPageHistoryChartBarColorRule';
const Overview: FunctionComponent<PageComponentProps> = (
props: PageComponentProps
@@ -97,6 +98,12 @@ const Overview: FunctionComponent<PageComponentProps> = (
const [monitorStatuses, setMonitorStatuses] = useState<
Array<MonitorStatus>
>([]);
const [
statusPageHistoryChartBarColorRules,
setStatusPageHistoryChartBarColorRules,
] = useState<Array<StatusPageHistoryChartBarColorRule>>([]);
const [statusPageResources, setStatusPageResources] = useState<
Array<StatusPageResource>
>([]);
@@ -176,6 +183,15 @@ const Overview: FunctionComponent<PageComponentProps> = (
(data['incidentPublicNotes'] as JSONArray) || [],
IncidentPublicNote
);
const statusPageHistoryChartBarColorRules: Array<StatusPageHistoryChartBarColorRule> =
BaseModel.fromJSONArray(
(data[
'statusPageHistoryChartBarColorRules'
] as JSONArray) || [],
StatusPageHistoryChartBarColorRule
);
const activeIncidents: Array<Incident> = BaseModel.fromJSONArray(
(data['activeIncidents'] as JSONArray) || [],
Incident
@@ -231,6 +247,10 @@ const Overview: FunctionComponent<PageComponentProps> = (
setMonitorsInGroup(monitorsInGroup);
setMonitorGroupCurrentStatuses(monitorGroupCurrentStatuses);
setStatusPageHistoryChartBarColorRules(
statusPageHistoryChartBarColorRules
);
// save data. set()
setScheduledMaintenanceEventsPublicNotes(
scheduledMaintenanceEventsPublicNotes
@@ -391,6 +411,10 @@ const Overview: FunctionComponent<PageComponentProps> = (
resource.monitor?.name ||
''
}
statusPageHistoryChartBarColorRules={
statusPageHistoryChartBarColorRules
}
downtimeMonitorStatus={statusPage?.downtimeMonitorStatuses || []}
description={resource.displayDescription || ''}
tooltip={resource.displayTooltip || ''}
currentStatus={currentStatus}
@@ -453,6 +477,9 @@ const Overview: FunctionComponent<PageComponentProps> = (
resource.uptimePercentPrecision ||
UptimePrecision.ONE_DECIMAL
}
statusPageHistoryChartBarColorRules={
statusPageHistoryChartBarColorRules
}
description={resource.displayDescription || ''}
tooltip={resource.displayTooltip || ''}
currentStatus={currentStatus}
@@ -481,6 +508,7 @@ const Overview: FunctionComponent<PageComponentProps> = (
}
);
})}
downtimeMonitorStatus={statusPage?.downtimeMonitorStatuses || []}
startDate={startDate}
endDate={endDate}
showHistoryChart={resource.showStatusHistoryChart}