diff --git a/CommonServer/API/StatusPageAPI.ts b/CommonServer/API/StatusPageAPI.ts index c45ed3ea6e..94249f4460 100644 --- a/CommonServer/API/StatusPageAPI.ts +++ b/CommonServer/API/StatusPageAPI.ts @@ -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 = + 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 diff --git a/CommonUI/src/Components/Graphs/DayUptimeGraph.tsx b/CommonUI/src/Components/Graphs/DayUptimeGraph.tsx index b4b5b7615d..868e76c8d0 100644 --- a/CommonUI/src/Components/Graphs/DayUptimeGraph.tsx +++ b/CommonUI/src/Components/Graphs/DayUptimeGraph.tsx @@ -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; defaultLabel: string; height?: number | undefined; + barColorRules?: Array | undefined; + downtimeEventStatusIds?: Array | undefined; } const DayUptimeGraph: FunctionComponent = ( @@ -42,14 +51,17 @@ const DayUptimeGraph: FunctionComponent = ( 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 = ( 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 = ( } 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 = ( 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) { diff --git a/CommonUI/src/Components/MonitorGraphs/Uptime.tsx b/CommonUI/src/Components/MonitorGraphs/Uptime.tsx index 6b91308fbe..6bf546f4a3 100644 --- a/CommonUI/src/Components/MonitorGraphs/Uptime.tsx +++ b/CommonUI/src/Components/MonitorGraphs/Uptime.tsx @@ -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 | undefined; + downtimeMonitorStatus: Array | undefined; } const MonitorUptimeGraph: FunctionComponent = ( @@ -31,12 +35,28 @@ const MonitorUptimeGraph: FunctionComponent = ( ): ReactElement => { const [events, setEvents] = useState>([]); + const [barColorRules, setBarColorRules] = useState([]); + useEffect(() => { const eventList: Array = 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 ; } @@ -59,6 +79,10 @@ const MonitorUptimeGraph: FunctionComponent = ( events={events} defaultLabel={'Operational'} height={props.height} + barColorRules={barColorRules} + downtimeEventStatusIds={props.downtimeMonitorStatus?.map((status: MonitorStatus)=>{ + return status.id!; + }) || []} /> ); }; diff --git a/CommonUI/src/Components/MonitorGraphs/UptimeUtil.ts b/CommonUI/src/Components/MonitorGraphs/UptimeUtil.ts index 65f3a66159..268c34f31b 100644 --- a/CommonUI/src/Components/MonitorGraphs/UptimeUtil.ts +++ b/CommonUI/src/Components/MonitorGraphs/UptimeUtil.ts @@ -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, }); } } diff --git a/Dashboard/src/Components/MonitorStatus/MonitorStatusElement.tsx b/Dashboard/src/Components/MonitorStatus/MonitorStatusElement.tsx new file mode 100644 index 0000000000..211752bdeb --- /dev/null +++ b/Dashboard/src/Components/MonitorStatus/MonitorStatusElement.tsx @@ -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 = ( + props: ComponentProps +): ReactElement => { + return {props.monitorStatus.name}; +}; + +export default TeamElement; diff --git a/Dashboard/src/Components/MonitorStatus/MonitorStatusesElement.tsx b/Dashboard/src/Components/MonitorStatus/MonitorStatusesElement.tsx new file mode 100644 index 0000000000..fb6540ddd2 --- /dev/null +++ b/Dashboard/src/Components/MonitorStatus/MonitorStatusesElement.tsx @@ -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; + onNavigateComplete?: (() => void) | undefined; +} + +const MonitorStatusesElement: FunctionComponent = ( + props: ComponentProps +): ReactElement => { + if (!props.monitorStatuses || props.monitorStatuses.length === 0) { + return

No monitor status attached.

; + } + + return ( +
+ {props.monitorStatuses.map( + (monitorStatus: MonitorStatus, i: number) => { + return ( + + + {i !== props.monitorStatuses.length - 1 && ( + + )} + + ); + } + )} +
+ ); +}; + +export default MonitorStatusesElement; diff --git a/Dashboard/src/Pages/StatusPages/View/OverviewPageBranding.tsx b/Dashboard/src/Pages/StatusPages/View/OverviewPageBranding.tsx index 88c0e46897..0b46cd4f01 100644 --- a/Dashboard/src/Pages/StatusPages/View/OverviewPageBranding.tsx +++ b/Dashboard/src/Pages/StatusPages/View/OverviewPageBranding.tsx @@ -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 = ( props: PageComponentProps @@ -120,22 +122,6 @@ const StatusPageDelete: FunctionComponent = ( 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 = ( ]} /> + + 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 ( + ) || [] + } + /> + ); + } + + return <>; + }, + }, + ], + modelId: modelId, + }} + /> + name="Status Page > Branding > Default Bar Color" cardProps={{ diff --git a/Model/Models/StatusPage.ts b/Model/Models/StatusPage.ts index 79b4ed1cbd..024d35e1c1 100755 --- a/Model/Models/StatusPage.ts +++ b/Model/Models/StatusPage.ts @@ -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 = undefined; } diff --git a/Model/Models/StatusPageHistoryChartBarColorRule.ts b/Model/Models/StatusPageHistoryChartBarColorRule.ts index bad21a7c5c..3d1a91670b 100644 --- a/Model/Models/StatusPageHistoryChartBarColorRule.ts +++ b/Model/Models/StatusPageHistoryChartBarColorRule.ts @@ -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 = undefined; - @ColumnAccessControl({ create: [ Permission.ProjectOwner, diff --git a/StatusPage/src/Components/Monitor/MonitorOverview.tsx b/StatusPage/src/Components/Monitor/MonitorOverview.tsx index 6d1773d32c..756a252e6e 100644 --- a/StatusPage/src/Components/Monitor/MonitorOverview.tsx +++ b/StatusPage/src/Components/Monitor/MonitorOverview.tsx @@ -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; + statusPageHistoryChartBarColorRules: Array; + downtimeMonitorStatus: Array; } const MonitorOverview: FunctionComponent = ( @@ -116,6 +119,10 @@ const MonitorOverview: FunctionComponent = (
= ( props: PageComponentProps @@ -97,6 +98,12 @@ const Overview: FunctionComponent = ( const [monitorStatuses, setMonitorStatuses] = useState< Array >([]); + + const [ + statusPageHistoryChartBarColorRules, + setStatusPageHistoryChartBarColorRules, + ] = useState>([]); + const [statusPageResources, setStatusPageResources] = useState< Array >([]); @@ -176,6 +183,15 @@ const Overview: FunctionComponent = ( (data['incidentPublicNotes'] as JSONArray) || [], IncidentPublicNote ); + + const statusPageHistoryChartBarColorRules: Array = + BaseModel.fromJSONArray( + (data[ + 'statusPageHistoryChartBarColorRules' + ] as JSONArray) || [], + StatusPageHistoryChartBarColorRule + ); + const activeIncidents: Array = BaseModel.fromJSONArray( (data['activeIncidents'] as JSONArray) || [], Incident @@ -231,6 +247,10 @@ const Overview: FunctionComponent = ( setMonitorsInGroup(monitorsInGroup); setMonitorGroupCurrentStatuses(monitorGroupCurrentStatuses); + setStatusPageHistoryChartBarColorRules( + statusPageHistoryChartBarColorRules + ); + // save data. set() setScheduledMaintenanceEventsPublicNotes( scheduledMaintenanceEventsPublicNotes @@ -391,6 +411,10 @@ const Overview: FunctionComponent = ( resource.monitor?.name || '' } + statusPageHistoryChartBarColorRules={ + statusPageHistoryChartBarColorRules + } + downtimeMonitorStatus={statusPage?.downtimeMonitorStatuses || []} description={resource.displayDescription || ''} tooltip={resource.displayTooltip || ''} currentStatus={currentStatus} @@ -453,6 +477,9 @@ const Overview: FunctionComponent = ( resource.uptimePercentPrecision || UptimePrecision.ONE_DECIMAL } + statusPageHistoryChartBarColorRules={ + statusPageHistoryChartBarColorRules + } description={resource.displayDescription || ''} tooltip={resource.displayTooltip || ''} currentStatus={currentStatus} @@ -481,6 +508,7 @@ const Overview: FunctionComponent = ( } ); })} + downtimeMonitorStatus={statusPage?.downtimeMonitorStatuses || []} startDate={startDate} endDate={endDate} showHistoryChart={resource.showStatusHistoryChart}