diff --git a/components/UptimeDot/UptimeDot.js b/components/UptimeDot/UptimeDot.js index c6e1c83..5543081 100644 --- a/components/UptimeDot/UptimeDot.js +++ b/components/UptimeDot/UptimeDot.js @@ -10,10 +10,12 @@ const DEFAULT_THRESHOLD = 5; const state = (timeserie, threshold) => { if (timeserie.missingDataPoint) return "missing"; - return Object.values(timeserie.values).reduce((a, b) => Math.max(a, b)) > - threshold - ? "down" - : "up"; + const maxDowntime = Object.values(timeserie.values).reduce((a, b) => + Math.max(a, b) + ); + if (maxDowntime === 1440) return "down"; + if (maxDowntime > threshold) return "partial"; + return "up"; }; export const downtimeSummary = (timeserie, threshold) => { @@ -41,6 +43,7 @@ const UptimeDot = ({ timeserie, threshold = DEFAULT_THRESHOLD }) => { const stateColor = { up: "bg-green-500", down: "bg-red-500", + partial: "bg-yellow-500", missing: "bg-gray-200", }; diff --git a/components/UptimeDot/UptimeDot.test.js b/components/UptimeDot/UptimeDot.test.js index 391332f..9bc01d1 100644 --- a/components/UptimeDot/UptimeDot.test.js +++ b/components/UptimeDot/UptimeDot.test.js @@ -41,7 +41,7 @@ describe("UptimeDot", () => { test("dot is red when down and above threshold", () => { const { container } = build({ timeserie: down, threshold: 0 }); const dot = container.querySelector("div"); - expect(dot.classList).toContain("bg-red-500"); + expect(dot.classList).toContain("bg-yellow-500"); }); test("dot is green when down and under threshold", () => { diff --git a/components/UptimeMonitor/UptimeMonitor.js b/components/UptimeMonitor/UptimeMonitor.js index b4efad5..fa0a78e 100644 --- a/components/UptimeMonitor/UptimeMonitor.js +++ b/components/UptimeMonitor/UptimeMonitor.js @@ -1,9 +1,16 @@ import React from "react"; import PropTypes from "prop-types"; +import Tippy from "@tippyjs/react"; + import OutagesOverlay from "../OutagesOverlay"; import UptimeDots from "../UptimeDots"; +import { + timeseriesByDay as groupTimeseriesByDay, + roundDecimal, +} from "../../utils"; + export const LoadingDot = () => { return (
{ ); }; +export const calculateUptime = (timeseries, regions) => { + const timeseriesByDay = groupTimeseriesByDay(timeseries, regions); + const timeSeriesLast30Days = timeseriesByDay + .slice(-30, timeseriesByDay.length - 1) + .filter((item) => item.missingDataPoint === false); + const minutesPerDay = 1440.0; + + const downtimePerRegion = []; + + regions.map((region) => { + const downtimeInMinutes = timeSeriesLast30Days.reduce((acc, item) => { + return acc + item.values[region]; + }, 0); + + const uptimePercentage = + 100 - + roundDecimal( + (100.0 / (minutesPerDay * timeSeriesLast30Days.length)) * + downtimeInMinutes + ); + + downtimePerRegion.push({ + region: region, + minutes: downtimeInMinutes, + percentage: uptimePercentage, + }); + }); + + return downtimePerRegion; +}; + +export const averageDowntimeOverRegions = (downtimePerRegion) => { + const average = + Object.values(downtimePerRegion).reduce((acc, item) => { + return (acc += item.percentage); + }, 0) / Object.keys(downtimePerRegion).length; + return roundDecimal(average).toFixed(2); +}; + const UptimeMonitor = ({ uptimeMonitor, threshold }) => { const [overlayOpen, setOverlayOpen] = React.useState(false); const [loading, setLoading] = React.useState(true); @@ -45,6 +91,11 @@ const UptimeMonitor = ({ uptimeMonitor, threshold }) => { return () => (mounted = false); }, [uptimeMonitor.id, uptimeMonitor.endpoint]); + const calculatedUptime = calculateUptime( + monitor.timeseries, + uptimeMonitor.regions + ); + return ( <>
@@ -54,7 +105,21 @@ const UptimeMonitor = ({ uptimeMonitor, threshold }) => { className="c_h-heading focus:outline-none" onClick={() => setOverlayOpen(true)} > - {uptimeMonitor.title} + {uptimeMonitor.title}  + {!loading && ( + ( +
+ {region.percentage}% in {region.region} +
+ ))} + > + + ({averageDowntimeOverRegions(calculatedUptime)} % uptime) + +
+ )}

diff --git a/components/UptimeMonitor/__snapshots__/UptimeMonitor.test.js.snap b/components/UptimeMonitor/__snapshots__/UptimeMonitor.test.js.snap index 1c2221a..c525e5f 100644 --- a/components/UptimeMonitor/__snapshots__/UptimeMonitor.test.js.snap +++ b/components/UptimeMonitor/__snapshots__/UptimeMonitor.test.js.snap @@ -14,6 +14,7 @@ exports[`UptimeMonitor renders without errors 1`] = ` class="c_h-heading focus:outline-none" > homepage +  

Always Down +  

blog +  

homepage +  

{ }; export const timeseriesByDay = (timeseries, expectedRegions) => { + if (timeseries === undefined) return []; return sortedTimeseries(timeseries.slice()).reduce((group, timeserie) => { const startOfDay = dayjs(timeserie.timestamp).startOf("day").utc().format(); @@ -67,3 +68,7 @@ export const fillMissingDataPoints = (timeseries, expectedDays) => { } return sortedTimeseries(timeseries); }; + +export const roundDecimal = (number) => { + return parseInt(number.toFixed(3) * 100) / 100; +}; diff --git a/utils/index.test.js b/utils/index.test.js index 8217c2e..603a44a 100644 --- a/utils/index.test.js +++ b/utils/index.test.js @@ -4,6 +4,7 @@ import { timeseriesByDay, sortedTimeseries, fillMissingDataPoints, + roundDecimal, } from "./index"; import homepageMock from "../mocks/monitors/homepage.json"; @@ -120,3 +121,21 @@ describe("#fillMissingDataPoints", () => { expect(withFilledMissingPoints[1].missingDataPoint).toBeFalsy(); }); }); + +describe("#roundDecimal", () => { + it("returns a round number if round number is given", () => { + expect(roundDecimal(100)).toEqual(100); + expect(roundDecimal(50)).toEqual(50); + }); + + it("rounds a number with two decimals", () => { + expect(roundDecimal(100.12)).toEqual(100.12); + expect(roundDecimal(50.34)).toEqual(50.34); + }); + + it("rounds number with more decimals", () => { + expect(roundDecimal(100.122342342344234)).toEqual(100.12); + expect(roundDecimal(50.3499999)).toEqual(50.35); + expect(roundDecimal(99.9075)).toEqual(99.9); + }); +});