Add new fields for current status and uptime precision in StatusPageGroup

This commit is contained in:
Simon Larsen 2024-09-20 12:56:50 +01:00
parent 6c73747564
commit d79d17e230
No known key found for this signature in database
GPG Key ID: 96C5DCA24769DBCA
5 changed files with 237 additions and 84 deletions

View File

@ -541,6 +541,9 @@ export default class StatusPageAPI extends BaseAPI<
order: true,
description: true,
isExpandedByDefault: true,
showCurrentStatus: true,
showUptimePercent: true,
uptimePercentPrecision: true,
},
sort: {
order: SortOrder.Ascending,

View File

@ -1,18 +1,29 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1726831037585 implements MigrationInterface {
public name = 'MigrationName1726831037585'
public name = "MigrationName1726831037585";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "StatusPageGroup" ADD "showCurrentStatus" boolean NOT NULL DEFAULT true`);
await queryRunner.query(`ALTER TABLE "StatusPageGroup" ADD "showUptimePercent" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "StatusPageGroup" ADD "uptimePercentPrecision" character varying`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "StatusPageGroup" DROP COLUMN "uptimePercentPrecision"`);
await queryRunner.query(`ALTER TABLE "StatusPageGroup" DROP COLUMN "showUptimePercent"`);
await queryRunner.query(`ALTER TABLE "StatusPageGroup" DROP COLUMN "showCurrentStatus"`);
}
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "StatusPageGroup" ADD "showCurrentStatus" boolean NOT NULL DEFAULT true`,
);
await queryRunner.query(
`ALTER TABLE "StatusPageGroup" ADD "showUptimePercent" boolean NOT NULL DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "StatusPageGroup" ADD "uptimePercentPrecision" character varying`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "StatusPageGroup" DROP COLUMN "uptimePercentPrecision"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPageGroup" DROP COLUMN "showUptimePercent"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPageGroup" DROP COLUMN "showCurrentStatus"`,
);
}
}

View File

@ -125,5 +125,5 @@ export default [
MigrationName1725901024444,
MigrationName1725975175669,
MigrationName1725976810107,
MigrationName1726831037585
MigrationName1726831037585,
];

View File

@ -9,6 +9,9 @@ import FieldType from "Common/UI/Components/Types/FieldType";
import Navigation from "Common/UI/Utils/Navigation";
import StatusPageGroup from "Common/Models/DatabaseModels/StatusPageGroup";
import React, { Fragment, FunctionComponent, ReactElement } from "react";
import UptimePrecision from "Common/Types/StatusPage/UptimePrecision";
import DropdownUtil from "Common/UI/Utils/Dropdown";
import FormValues from "Common/UI/Components/Forms/Types/FormValues";
const StatusPageDelete: FunctionComponent<PageComponentProps> = (
props: PageComponentProps,
@ -48,6 +51,16 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
"Here are different groups for your status page resources.",
}}
noItemsMessage={"No status page group created for this status page."}
formSteps={[
{
title: "Group Details",
id: "group-details",
},
{
title: "Advanced",
id: "advanced",
},
]}
formFields={[
{
field: {
@ -57,6 +70,7 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: "Resource Group Name",
stepId: "group-details",
},
{
field: {
@ -65,6 +79,7 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
title: "Group Description",
fieldType: FormFieldSchemaType.Markdown,
required: false,
stepId: "group-details",
},
{
field: {
@ -73,6 +88,46 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
title: "Expand on Status Page by Default",
fieldType: FormFieldSchemaType.Toggle,
required: false,
stepId: "group-details",
},
{
field: {
showCurrentStatus: true,
},
title: "Show Current Group Status",
fieldType: FormFieldSchemaType.Toggle,
required: false,
defaultValue: true,
description:
"Current Status will be shown beside this group on your status page.",
stepId: "advanced",
},
{
field: {
showUptimePercent: true,
},
title: "Show Uptime %",
fieldType: FormFieldSchemaType.Toggle,
required: false,
defaultValue: false,
description:
"Show uptime percentage for the past 90 days beside this group on your status page.",
stepId: "advanced",
},
{
field: {
uptimePercentPrecision: true,
},
stepId: "advanced",
fieldType: FormFieldSchemaType.Dropdown,
dropdownOptions:
DropdownUtil.getDropdownOptionsFromEnum(UptimePrecision),
showIf: (item: FormValues<StatusPageGroup>): boolean => {
return Boolean(item.showUptimePercent);
},
title: "Select Uptime Precision",
defaultValue: UptimePrecision.ONE_DECIMAL,
required: true,
},
]}
showRefreshButton={true}

View File

@ -293,6 +293,118 @@ const Overview: FunctionComponent<PageComponentProps> = (
StatusPageUtil.isPrivateStatusPage(),
]);
type GetMonitorStatusTimelineForResourceFunction = (data: {
resource: StatusPageResource;
}) => Array<MonitorStatusTimeline>;
const getMonitorStatusTimelineForResource: GetMonitorStatusTimelineForResourceFunction =
(data: { resource: StatusPageResource }): Array<MonitorStatusTimeline> => {
return [...monitorStatusTimelines].filter(
(timeline: MonitorStatusTimeline) => {
// check monitor if first.
if (data.resource.monitorId) {
return (
timeline.monitorId?.toString() ===
data.resource.monitorId?.toString()
);
}
if (data.resource.monitorGroupId) {
const monitorsInThisGroup: Array<ObjectID> | undefined =
monitorsInGroup[data.resource.monitorGroupId?.toString() || ""];
if (!monitorsInThisGroup) {
return false;
}
return monitorsInThisGroup.find((monitorId: ObjectID) => {
return monitorId.toString() === timeline.monitorId?.toString();
});
}
return false;
},
);
};
type GetCurrentGroupStatusElementFunction = (data: {
group: StatusPageGroup;
}) => ReactElement;
const getCurrentGroupStatusElement: GetCurrentGroupStatusElementFunction =
(data: { group: StatusPageGroup }): ReactElement => {
const currentStatus: MonitorStatus = getCurrentGroupStatus(data.group);
const resourcesInGroup: Array<StatusPageResource> = getResourcesInGroup(
data.group,
);
const monitorStatusTimelines: Array<MonitorStatusTimeline> = [];
for (const resource of resourcesInGroup) {
// get monitor status timeline.
const monitorStatusTimelines: Array<MonitorStatusTimeline> =
getMonitorStatusTimelineForResource({
resource: resource,
});
// add to the monitor status timelines.
monitorStatusTimelines.push(...monitorStatusTimelines);
}
const downtimeMonitorStatuses: Array<MonitorStatus> =
statusPage?.downtimeMonitorStatuses || [];
// if the current status is operational then show uptime Percent.
let precision: UptimePrecision = UptimePrecision.ONE_DECIMAL;
if (data.group.uptimePercentPrecision) {
precision = data.group.uptimePercentPrecision;
}
if (
!downtimeMonitorStatuses.find((downtimeStatus: MonitorStatus) => {
return currentStatus.id?.toString() === downtimeStatus.id?.toString();
}) &&
data.group.showUptimePercent
) {
const uptimePercent: number = UptimeUtil.calculateUptimePercentage(
monitorStatusTimelines,
precision,
downtimeMonitorStatuses,
);
return (
<div
className="font-medium"
style={{
color: currentStatus?.color?.toString() || Green.toString(),
}}
>
{uptimePercent}% uptime
</div>
);
}
if (data.group.showCurrentStatus) {
return (
<div
className=""
style={{
color: currentStatus?.color?.toString() || Green.toString(),
}}
>
{currentStatus?.name || "Operational"}
</div>
);
}
return <></>;
};
type GetOverallMonitorStatusFunction = (
statusPageResources: Array<StatusPageResource>,
monitorStatuses: Array<MonitorStatus>,
@ -416,14 +528,9 @@ const Overview: FunctionComponent<PageComponentProps> = (
uptimePrecision={
resource.uptimePercentPrecision || UptimePrecision.ONE_DECIMAL
}
monitorStatusTimeline={[...monitorStatusTimelines].filter(
(timeline: MonitorStatusTimeline) => {
return (
timeline.monitorId?.toString() ===
resource.monitorId?.toString()
);
},
)}
monitorStatusTimeline={getMonitorStatusTimelineForResource({
resource: resource,
})}
startDate={startDate}
endDate={endDate}
showHistoryChart={resource.showStatusHistoryChart}
@ -468,22 +575,9 @@ const Overview: FunctionComponent<PageComponentProps> = (
description={resource.displayDescription || ""}
tooltip={resource.displayTooltip || ""}
currentStatus={currentStatus}
monitorStatusTimeline={[...monitorStatusTimelines].filter(
(timeline: MonitorStatusTimeline) => {
const monitorsInThisGroup: Array<ObjectID> | undefined =
monitorsInGroup[resource.monitorGroupId?.toString() || ""];
if (!monitorsInThisGroup) {
return false;
}
return monitorsInThisGroup.find((monitorId: ObjectID) => {
return (
monitorId.toString() === timeline.monitorId?.toString()
);
});
},
)}
monitorStatusTimeline={getMonitorStatusTimelineForResource({
resource: resource,
})}
downtimeMonitorStatuses={
statusPage?.downtimeMonitorStatuses || []
}
@ -598,62 +692,52 @@ const Overview: FunctionComponent<PageComponentProps> = (
return groups;
};
type GetRightAccordionElementFunction = (
type GetResourcesInGroupFunction = (
group: StatusPageGroup,
) => ReactElement;
) => Array<StatusPageResource>;
const getRightAccordionElement: GetRightAccordionElementFunction = (
const getResourcesInGroup: GetResourcesInGroupFunction = (
group: StatusPageGroup,
): ReactElement => {
): Array<StatusPageResource> => {
return statusPageResources.filter((resource: StatusPageResource) => {
return resource.statusPageGroupId?.toString() === group._id?.toString();
});
};
type GetCurrentGroupStatus = (group: StatusPageGroup) => MonitorStatus;
const getCurrentGroupStatus: GetCurrentGroupStatus = (
group: StatusPageGroup,
): MonitorStatus => {
let currentStatus: MonitorStatus = new MonitorStatus();
currentStatus.name = "Operational";
currentStatus.color = Green;
let hasResource: boolean = false;
for (const resource of statusPageResources) {
const resourcesInGroup: Array<StatusPageResource> =
getResourcesInGroup(group);
for (const resource of resourcesInGroup) {
const currentMonitorStatus: MonitorStatus | undefined =
monitorStatuses.find((status: MonitorStatus) => {
return (
status._id?.toString() ===
resource.monitor?.currentMonitorStatusId?.toString()
);
});
if (
(resource.statusPageGroupId &&
resource.statusPageGroupId.toString() &&
group &&
group._id?.toString() &&
group._id?.toString() === resource.statusPageGroupId.toString()) ||
(!resource.statusPageGroupId && !group)
(currentStatus &&
currentStatus.priority &&
currentMonitorStatus?.priority &&
currentMonitorStatus?.priority > currentStatus.priority) ||
!currentStatus ||
!currentStatus.priority
) {
hasResource = true;
const currentMonitorStatus: MonitorStatus | undefined =
monitorStatuses.find((status: MonitorStatus) => {
return (
status._id?.toString() ===
resource.monitor?.currentMonitorStatusId?.toString()
);
});
if (
(currentStatus &&
currentStatus.priority &&
currentMonitorStatus?.priority &&
currentMonitorStatus?.priority > currentStatus.priority) ||
!currentStatus ||
!currentStatus.priority
) {
currentStatus = currentMonitorStatus!;
}
currentStatus = currentMonitorStatus!;
}
}
if (hasResource) {
return (
<div
className="bold font16"
style={{
color: currentStatus?.color?.toString() || Green.toString(),
}}
>
{currentStatus?.name || "Operational"}
</div>
);
}
return <></>;
return currentStatus;
};
const activeIncidentsInIncidentGroup: Array<IncidentGroup> =
@ -759,9 +843,9 @@ const Overview: FunctionComponent<PageComponentProps> = (
return (
<Accordion
key={i}
rightElement={getRightAccordionElement(
resourceGroup,
)}
rightElement={getCurrentGroupStatusElement({
group: resourceGroup,
})}
isInitiallyExpanded={
resourceGroup.isExpandedByDefault
}